diff --git a/slang/src/Box/Box.tsx b/slang/src/Box/Box.tsx index bd0da4e..b55680d 100644 --- a/slang/src/Box/Box.tsx +++ b/slang/src/Box/Box.tsx @@ -3,17 +3,17 @@ import { ResponsifyComponentProps, PropsWithAs, produceComponentClassesPropsGetter, + separateComponentProps, } from "../utils"; import "./Box.css"; -import { boxConfig, ResponsiveProps } from "./props"; +import { boxConfig, propKeys, ResponsiveProps } from "./props"; -export interface BaseBoxProps< +export type BaseBoxProps< Breakpoint extends string = "tablet" | "desktop", Colors extends string = "foreground" | "background" -> extends ResponsifyComponentProps, Breakpoint> { - // Stretch - stretch?: boolean; -} +> = ResponsifyComponentProps, Breakpoint>; + +const componentKeys = [...propKeys, "at"]; const getCssClasses = produceComponentClassesPropsGetter< BaseBoxProps, @@ -21,22 +21,23 @@ const getCssClasses = produceComponentClassesPropsGetter< >(boxConfig); export default function BoxComponent( - { as, ...props }: PropsWithAs>, + props: PropsWithAs>, ref: Ref, ) { + const { as, ...otherProps } = props; const Component = as || "div"; - const [css, classes] = getCssClasses(props); + + const [componentProps, elementProps] = separateComponentProps( + otherProps, + componentKeys, + ); + const [css, classes] = getCssClasses(componentProps); return ( [] = [ { key: "flow", @@ -126,7 +155,7 @@ export const boxConfig: ComponentConfig[] = [ key: "display", defaultValue: "grid", cssFromVariable: (v) => `display: ${v};`, - propValueToCssValue: (n: string | boolean) => { + propValueToCssValue: (n) => { if (typeof n === "boolean") { return n ? "grid" : "none"; } @@ -143,6 +172,7 @@ export const boxConfig: ComponentConfig[] = [ defaultValue: "100%", cssFromVariable: (v) => `height: ${v};`, propValueToCssValue: (height) => { + if (typeof height === "undefined") return height; if (typeof height === "boolean") { return height ? "100%" : "auto"; } @@ -150,7 +180,7 @@ export const boxConfig: ComponentConfig[] = [ }, }, { - key: "hmin", + key: "root", defaultValue: "100%", cssFromVariable: (v) => `min-height: ${v};`, propValueToCssValue: (min) => { @@ -160,6 +190,16 @@ export const boxConfig: ComponentConfig[] = [ } return min ?? "none"; }, + iosSafariPatch: { + cssFromVariable: (v) => `min-height: ${v};`, + propValueToCssValue: (min) => { + if (typeof min === "undefined") return min; + if (typeof min === "boolean") { + return min ? "-webkit-fill-available" : "none"; + } + return min ?? "none"; + }, + }, }, { key: "background", diff --git a/slang/src/Type/Type.tsx b/slang/src/Type/Type.tsx index 322e440..14e3dfa 100644 --- a/slang/src/Type/Type.tsx +++ b/slang/src/Type/Type.tsx @@ -1,35 +1,42 @@ -import React, { ReactNode, Ref } from "react"; +import React, { Ref } from "react"; import "./Type.scss"; import { produceComponentClassesPropsGetter, PropsWithAs, ResponsifyComponentProps, + separateComponentProps, } from "../utils"; -import { typeConfig, ResponsiveProps } from "./props"; +import { typeConfig, ResponsiveProps, propKeys } from "./props"; export interface BaseTypeProps< Breakpoint extends string = "tablet" | "desktop", Colors extends string = "foreground" | "background" > extends ResponsifyComponentProps, Breakpoint> { size?: number; - children?: ReactNode; } +const componentKeys = [...propKeys, "at", "size"]; + const getCssClasses = produceComponentClassesPropsGetter< BaseTypeProps, ResponsiveProps >(typeConfig); export default function TypeComponent( - { as, ...props }: PropsWithAs, "p">, + props: PropsWithAs, "p">, ref: Ref, ) { + const { as, ...otherProps } = props; const Component = as || "p"; - const [css, classes] = getCssClasses(props); + const [componentProps, elementProps] = separateComponentProps( + otherProps, + componentKeys, + ); + const [css, classes] = getCssClasses(componentProps); return ( [] = [ { key: "weight", diff --git a/slang/src/constants.ts b/slang/src/constants.ts new file mode 100644 index 0000000..1f48641 --- /dev/null +++ b/slang/src/constants.ts @@ -0,0 +1 @@ +export const SAFARI_PATCH_KEY = "safari"; diff --git a/slang/src/createResponsiveCss.ts b/slang/src/createResponsiveCss.ts index d3b3e48..e8adcaf 100644 --- a/slang/src/createResponsiveCss.ts +++ b/slang/src/createResponsiveCss.ts @@ -1,22 +1,39 @@ import { SlangConfig, defaultConfig, componentClassesConfig } from "./config"; +import { SAFARI_PATCH_KEY } from "./constants"; export function createResponsiveCss(userConfig?: Partial): string { - let breakpoints: string[] = [""]; + let cssLines: string[] = [""]; const config = { ...defaultConfig, ...userConfig }; + // Loop over each component's responsive property list for (const [className, configList] of Object.entries( componentClassesConfig, )) { + // Loop over each property for (const prop of configList) { - breakpoints = breakpoints.concat( + cssLines = cssLines.concat( `.${className}.${prop.key.toString()} {`, prop.cssFromVariable(`var(--${prop.key.toString()})`), `}`, ); + + // Check for safari patch and do the same + if ("iosSafariPatch" in prop) { + cssLines = cssLines.concat( + "@supports (-webkit-touch-callout: none) {", + `.${className}.${prop.key.toString()}-${SAFARI_PATCH_KEY} {`, + prop.cssFromVariable( + `var(--${prop.key.toString()}-${SAFARI_PATCH_KEY})`, + ), + "}", + "}", + ); + } } + // Loop over each breakpoint for (const [breakpoint, size] of Object.entries(config.breakpoints)) { - breakpoints = breakpoints.concat([ + cssLines = cssLines.concat([ `@media(min-width: ${size}px) {`, ...configList.reduce( (acc, p) => @@ -32,5 +49,5 @@ export function createResponsiveCss(userConfig?: Partial): string { } } - return breakpoints.join("\n"); + return cssLines.join("\n"); } diff --git a/slang/src/generate.ts b/slang/src/generate.ts index 0977b95..b8041a2 100644 --- a/slang/src/generate.ts +++ b/slang/src/generate.ts @@ -58,7 +58,8 @@ if (watch === "-w") { function getComponentCode(userConfig: Partial) { const config = mergeDefault(userConfig); - return `import { + return `/* Do not edit this file directly */ + import { BoxComponent, TypeComponent, forwardRefWithAs, @@ -83,7 +84,7 @@ type Colors = export type BoxProps = PropsWithAs>; export type TypeProps = PropsWithAs>; const Box = forwardRefWithAs(BoxComponent); -const Type = forwardRefWithAs(TypeComponent); +const Type = forwardRefWithAs(TypeComponent); export { Box, Type }; `; } diff --git a/slang/src/index.css b/slang/src/index.css index e5fb348..beab18f 100644 --- a/slang/src/index.css +++ b/slang/src/index.css @@ -5,12 +5,22 @@ padding: 0; } +/* Cascade heights for Next.js and Create-React-App */ html, body, #__next, #root { height: 100%; - height: -webkit-fill-available; + /* height: -webkit-fill-available; */ +} + +@supports (-webkit-touch-callout: none) { + html, + body, + #__next, + #root { + height: -webkit-fill-available; + } } html { diff --git a/slang/src/utils.ts b/slang/src/utils.ts index 8a6e7f5..5e6289b 100644 --- a/slang/src/utils.ts +++ b/slang/src/utils.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ import React, { CSSProperties } from "react"; +import { SAFARI_PATCH_KEY } from "./constants"; export type As = React.ElementType; @@ -50,6 +51,19 @@ type ResponsiveComponentProperty = { * into the component, into the value that will be assed to the custom property */ propValueToCssValue?: (x: keyof X) => string | undefined; + + /** + * Declares that a patch needs to be made to make ios safari behavior consistent + * + * Adds custom property in the form {key}-{breakpoint}-safari: value + * + * The presence of this key causes the classes and custom prop to be rendered + * This variable takes an object expecting {cssFromVariable, propValueToCssValue} + */ + iosSafariPatch?: { + cssFromVariable: (customProperty: string) => string; + propValueToCssValue?: (x: keyof X) => string | undefined; + }; }; export type ComponentConfig = { @@ -83,14 +97,16 @@ export function produceComponentClassesPropsGetter( ): [CSSProperties, string[]] { const [props, classes] = toReduce.reduce<[CSSProperties, string[]]>( (acc, prop) => { - const [p, c] = getIndividualChildCssProp( - { - ...prop, - node, - }, - ); - acc[0] = { ...acc[0], ...p }; - acc[1] = acc[1].concat(c); + const [properties, classes] = getIndividualChildCssProp< + ComponentProps, + Breakpoint, + X + >({ + ...prop, + node, + }); + acc[0] = { ...acc[0], ...properties }; + acc[1] = acc[1].concat(classes); return acc; }, [{}, []], @@ -111,6 +127,7 @@ function getIndividualChildCssProp< propValueToCssValue = (z: any) => z, node, + iosSafariPatch, }: ComponentConfig & { node: ResponsifyComponentProps; }): [Record, string[]] { @@ -129,7 +146,16 @@ function getIndividualChildCssProp< properties[`--${key}`] = last; } - // ~~need all breakpoints~~ + if (iosSafariPatch && iosSafariPatch.propValueToCssValue) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const safariValue = iosSafariPatch.propValueToCssValue(node?.[key] as any); + if (safariValue) { + classes.push(`${key.toString()}-${SAFARI_PATCH_KEY}`); + properties[`--${key.toString()}-${SAFARI_PATCH_KEY}`] = safariValue; + } + } + // no longer should need all breakpoints, because // going to use compiled classes, to make the breakpoints // take effect @@ -152,3 +178,13 @@ function getIndividualChildCssProp< } return [properties, classes]; } + +export function separateComponentProps(obj: T, componentKeys: string[]) { + return Object.keys(obj).reduce( + (acc, key) => + componentKeys.includes(key) + ? [{ ...acc[0], [key]: obj[key] }, acc[1]] + : [acc[0], { ...acc[1], [key]: obj[key] }], + [{}, {}], + ); +}