From 995e88d278114b6c83edf097dac7b0d57cdae3e8 Mon Sep 17 00:00:00 2001 From: Mikyo King Date: Fri, 12 May 2023 15:15:40 -0600 Subject: [PATCH] feat: layout flex --- src/empty/graphics/EmptyDocuments.tsx | 4 +- src/index.tsx | 1 + src/layout/Flex.tsx | 105 +++++++++++++++++++++++++ src/layout/index.tsx | 1 + src/provider/GlobalStyles.tsx | 86 +++++++++++++++++++- src/types/core.ts | 1 + src/types/locale.ts | 1 + src/types/style.ts | 95 ++++++++++++++++++++++ src/utils/index.ts | 1 + src/utils/styleProps.ts | 109 +++++++++++++++++++++++--- tsconfig.json | 2 + 11 files changed, 393 insertions(+), 13 deletions(-) create mode 100644 src/layout/Flex.tsx create mode 100644 src/layout/index.tsx create mode 100644 src/types/locale.ts diff --git a/src/empty/graphics/EmptyDocuments.tsx b/src/empty/graphics/EmptyDocuments.tsx index 7d9087d5..0d22d950 100644 --- a/src/empty/graphics/EmptyDocuments.tsx +++ b/src/empty/graphics/EmptyDocuments.tsx @@ -1,18 +1,20 @@ import React, { useId } from 'react'; import { css } from '@emotion/react'; import { StyleProps } from '../../types'; +import { useStyleProps } from '../../utils'; export const EmptyDocuments = (props: StyleProps) => { // Create a unique ID so that more than one gradient def can exist // TODO - try to hoist the gradient defs to be global const id = useId(); const linearGradientIdPrefix = `#${id}-linear-gradient`; + const { styleProps } = useStyleProps(props); return ( { // /** custom content, defaults to 'the snozzberries taste like snozzberries' */ diff --git a/src/layout/Flex.tsx b/src/layout/Flex.tsx new file mode 100644 index 00000000..6d31c40a --- /dev/null +++ b/src/layout/Flex.tsx @@ -0,0 +1,105 @@ +import { + StyleHandlers, + classNames, + passthroughStyle, + responsiveDimensionValue, + useDOMRef, + useStyleProps, +} from '../utils'; +import { DOMProps, DOMRef, FlexStyleProps } from '../types'; +import { filterDOMProps } from '@react-aria/utils'; +import React, { forwardRef, ReactNode } from 'react'; +import { css } from '@emotion/react'; + +export interface FlexProps extends DOMProps, FlexStyleProps { + /** Children of the flex container. */ + children: ReactNode; +} + +const flexCSS = css` + display: flex; +`; + +const flexStyleProps: StyleHandlers = { + direction: ['flexDirection', passthroughStyle], + wrap: ['flexWrap', flexWrapValue], + justifyContent: ['justifyContent', flexAlignValue], + alignItems: ['alignItems', flexAlignValue], + alignContent: ['alignContent', flexAlignValue], +}; + +function Flex(props: FlexProps, ref: DOMRef) { + let { children, ...otherProps } = props; + + let matchedBreakpoints = ['base']; + let { styleProps } = useStyleProps(otherProps); + let { styleProps: flexStyle } = useStyleProps(otherProps, flexStyleProps); + let domRef = useDOMRef(ref); + + // If no gaps, or native support exists, then we only need to render a single div. + let style = { + ...styleProps.style, + ...flexStyle.style, + }; + + if (props.gap != null) { + style.gap = responsiveDimensionValue(props.gap, matchedBreakpoints); + } + + if (props.columnGap != null) { + style.columnGap = responsiveDimensionValue( + props.columnGap, + matchedBreakpoints + ); + } + + if (props.rowGap != null) { + style.rowGap = responsiveDimensionValue(props.rowGap, matchedBreakpoints); + } + + return ( +
+ {children} +
+ ); +} + +/** + * Normalize 'start' and 'end' alignment values to 'flex-start' and 'flex-end' + * in flex containers for browser compatibility. + */ +function flexAlignValue(value) { + if (value === 'start') { + return 'flex-start'; + } + + if (value === 'end') { + return 'flex-end'; + } + + return value; +} + +/** + * Takes a boolean and translates it to flex wrap or nowrap. + */ +function flexWrapValue(value: boolean | 'wrap' | 'nowrap') { + if (typeof value === 'boolean') { + return value ? 'wrap' : 'nowrap'; + } + + return value; +} + +/** + * A layout container using flexbox. Provides Spectrum dimension values, and supports the gap + * property to define consistent spacing between items. + */ +const _Flex = forwardRef(Flex); +export { _Flex as Flex }; diff --git a/src/layout/index.tsx b/src/layout/index.tsx new file mode 100644 index 00000000..7cf460b0 --- /dev/null +++ b/src/layout/index.tsx @@ -0,0 +1 @@ +export * from './Flex'; diff --git a/src/provider/GlobalStyles.tsx b/src/provider/GlobalStyles.tsx index 6943d5b9..2de692dd 100644 --- a/src/provider/GlobalStyles.tsx +++ b/src/provider/GlobalStyles.tsx @@ -1,5 +1,65 @@ import { Global, css } from '@emotion/react'; +/** + * Medium size root CSS variables + */ +export const mediumRootCSS = css` + :root { + --ac-global-dimension-scale-factor: 1; + --ac-global-dimension-size-0: 0px; + --ac-global-dimension-size-10: 1px; + --ac-global-dimension-size-25: 2px; + --ac-global-dimension-size-30: 2px; + --ac-global-dimension-size-40: 3px; + --ac-global-dimension-size-50: 4px; + --ac-global-dimension-size-65: 5px; + --ac-global-dimension-size-75: 6px; + --ac-global-dimension-size-85: 7px; + --ac-global-dimension-size-100: 8px; + --ac-global-dimension-size-115: 9px; + --ac-global-dimension-size-125: 10px; + --ac-global-dimension-size-130: 11px; + --ac-global-dimension-size-150: 12px; + --ac-global-dimension-size-160: 13px; + --ac-global-dimension-size-175: 14px; + --ac-global-dimension-size-185: 15px; + --ac-global-dimension-size-200: 16px; + --ac-global-dimension-size-225: 18px; + --ac-global-dimension-size-250: 20px; + --ac-global-dimension-size-275: 22px; + --ac-global-dimension-size-300: 24px; + --ac-global-dimension-size-325: 26px; + --ac-global-dimension-size-350: 28px; + --ac-global-dimension-size-400: 32px; + --ac-global-dimension-size-450: 36px; + --ac-global-dimension-size-500: 40px; + --ac-global-dimension-size-550: 44px; + --ac-global-dimension-size-600: 48px; + --ac-global-dimension-size-650: 52px; + --ac-global-dimension-size-675: 54px; + --ac-global-dimension-size-700: 56px; + --ac-global-dimension-size-750: 60px; + --ac-global-dimension-size-800: 64px; + --ac-global-dimension-size-900: 72px; + --ac-global-dimension-size-1000: 80px; + --ac-global-dimension-size-1125: 90px; + --ac-global-dimension-size-1200: 96px; + --ac-global-dimension-size-1250: 100px; + --ac-global-dimension-size-1600: 128px; + --ac-global-dimension-size-1700: 136px; + --ac-global-dimension-size-1800: 144px; + --ac-global-dimension-size-2000: 160px; + --ac-global-dimension-size-2400: 192px; + --ac-global-dimension-size-2500: 200px; + --ac-global-dimension-size-3000: 240px; + --ac-global-dimension-size-3400: 272px; + --ac-global-dimension-size-3600: 288px; + --ac-global-dimension-size-4600: 368px; + --ac-global-dimension-size-5000: 400px; + --ac-global-dimension-size-6000: 480px; + } +`; + export const globalCSS = css` :root { --ac-global-dimension-static-size-0: 0px; @@ -85,9 +145,33 @@ export const globalCSS = css` --ac-global-rounding-small: var(--ac-global-dimension-static-size-50); --ac-global-rounding-medium: var(--ac-global-dimension-static-size-100); + + --ac-alias-border-size-thin: var(--ac-global-dimension-static-size-10); + --ac-alias-border-size-thick: var(--ac-global-dimension-static-size-25); + --ac-alias-border-size-thicker: var(--ac-global-dimension-static-size-50); + --ac-alias-border-size-thickest: var(--ac-global-dimension-static-size-100); + --ac-alias-border-offset-thin: var(--ac-global-dimension-static-size-25); + --ac-alias-border-offset-thick: var(--ac-global-dimension-static-size-50); + --ac-alias-border-offset-thicker: var( + --ac-global-dimension-static-size-100 + ); + --ac-alias-border-offset-thickest: var( + --ac-global-dimension-static-size-200 + ); + --ac-alias-grid-baseline: var(--ac-global-dimension-static-size-100); + --ac-alias-grid-gutter-xsmall: var(--ac-global-dimension-static-size-200); + --ac-alias-grid-gutter-small: var(--ac-global-dimension-static-size-300); + --ac-alias-grid-gutter-medium: var(--ac-global-dimension-static-size-400); + --ac-alias-grid-gutter-large: var(--ac-global-dimension-static-size-500); + --ac-alias-grid-gutter-xlarge: var(--ac-global-dimension-static-size-600); + --ac-alias-grid-margin-xsmall: var(--ac-global-dimension-static-size-200); + --ac-alias-grid-margin-small: var(--ac-global-dimension-static-size-300); + --ac-alias-grid-margin-medium: var(--ac-global-dimension-static-size-400); + --ac-alias-grid-margin-large: var(--ac-global-dimension-static-size-500); + --ac-alias-grid-margin-xlarge: var(--ac-global-dimension-static-size-600); } `; export function GlobalStyles() { - return ; + return ; } diff --git a/src/types/core.ts b/src/types/core.ts index 20728d86..f5fdf10c 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -92,6 +92,7 @@ export type DimensionValue = export type BorderRadiusValue = 'small' | 'medium'; export type BorderColorValue = 'light' | 'dark'; +export type BorderSizeValue = 'thin' | 'thick' | 'thicker' | 'thickest'; export type BackgroundColorValue = 'light' | 'dark'; export type ColorValue = diff --git a/src/types/locale.ts b/src/types/locale.ts new file mode 100644 index 00000000..ea77bd26 --- /dev/null +++ b/src/types/locale.ts @@ -0,0 +1 @@ +export type Direction = 'ltr' | 'rtl'; diff --git a/src/types/style.ts b/src/types/style.ts index b052043b..ba07f8ff 100644 --- a/src/types/style.ts +++ b/src/types/style.ts @@ -9,6 +9,25 @@ export interface StyleProps { /** Sets the CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. Only use as a **last resort**. **/ className?: string; + /** The margin for all four sides of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/margin). */ + margin?: Responsive; + /** The margin for the logical start side of the element, depending on layout direction. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/margin-inline-start). */ + marginStart?: Responsive; + /** The margin for the logical end side of an element, depending on layout direction. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/margin-inline-end). */ + marginEnd?: Responsive; + // /** The margin for the left side of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/margin-left). Consider using `marginStart` instead for RTL support. */ + // marginLeft?: Responsive, + // /** The margin for the right side of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/margin-left). Consider using `marginEnd` instead for RTL support. */ + // marginRight?: Responsive, + /** The margin for the top side of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/margin-top). */ + marginTop?: Responsive; + /** The margin for the bottom side of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/margin-bottom). */ + marginBottom?: Responsive; + /** The margin for both the left and right sides of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/margin). */ + marginX?: Responsive; + /** The margin for both the top and bottom sides of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/margin). */ + marginY?: Responsive; + /** The width of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/width). */ width?: DimensionValue; /** The height of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/height). */ @@ -21,6 +40,82 @@ export interface StyleProps { maxWidth?: string | number; /** The maximum height of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/max-height). */ maxHeight?: string | number; + + /** When used in a flex layout, specifies how the element will grow or shrink to fit the space available. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/flex). */ + flex?: Responsive; + /** When used in a flex layout, specifies how the element will grow to fit the space available. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-grow). */ + flexGrow?: Responsive; + /** When used in a flex layout, specifies how the element will shrink to fit the space available. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-shrink). */ + flexShrink?: Responsive; + /** When used in a flex layout, specifies the initial main size of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis). */ + flexBasis?: Responsive; + /** Specifies how the element is justified inside a flex or grid container. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-self). */ + justifySelf?: Responsive< + | 'auto' + | 'normal' + | 'start' + | 'end' + | 'flex-start' + | 'flex-end' + | 'self-start' + | 'self-end' + | 'center' + | 'left' + | 'right' + | 'stretch' + >; // ... + /** Overrides the `alignItems` property of a flex or grid container. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-self). */ + alignSelf?: Responsive< + | 'auto' + | 'normal' + | 'start' + | 'end' + | 'center' + | 'flex-start' + | 'flex-end' + | 'self-start' + | 'self-end' + | 'stretch' + >; + /** The layout order for the element within a flex or grid container. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/order). */ + order?: Responsive; + + /** When used in a grid layout, specifies the named grid area that the element should be placed in within the grid. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-area). */ + gridArea?: Responsive; + /** When used in a grid layout, specifies the column the element should be placed in within the grid. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-column). */ + gridColumn?: Responsive; + /** When used in a grid layout, specifies the row the element should be placed in within the grid. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-row). */ + gridRow?: Responsive; + /** When used in a grid layout, specifies the starting column to span within the grid. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-column-start). */ + gridColumnStart?: Responsive; + /** When used in a grid layout, specifies the ending column to span within the grid. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-column-end). */ + gridColumnEnd?: Responsive; + /** When used in a grid layout, specifies the starting row to span within the grid. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-row-start). */ + gridRowStart?: Responsive; + /** When used in a grid layout, specifies the ending row to span within the grid. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-row-end). */ + gridRowEnd?: Responsive; + + /** Specifies how the element is positioned. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/position). */ + position?: Responsive< + 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky' + >; + /** The stacking order for the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/z-index). */ + zIndex?: Responsive; + /** The top position for the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/top). */ + top?: Responsive; + /** The bottom position for the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/bottom). */ + bottom?: Responsive; + /** The logical start position for the element, depending on layout direction. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/inset-inline-start). */ + start?: Responsive; + /** The logical end position for the element, depending on layout direction. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/inset-inline-end). */ + end?: Responsive; + /** The left position for the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/left). Consider using `start` instead for RTL support. */ + left?: Responsive; + /** The right position for the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/right). Consider using `start` instead for RTL support. */ + right?: Responsive; + + /** Hides the element. */ + isHidden?: Responsive; } // These support more properties than specific arize components diff --git a/src/utils/index.ts b/src/utils/index.ts index da83d0c6..326845e3 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ export * from './classNames'; export * from './useDOMRef'; export * from './getWrappedElement'; +export * from './styleProps'; diff --git a/src/utils/styleProps.ts b/src/utils/styleProps.ts index 1e537342..8b2923fc 100644 --- a/src/utils/styleProps.ts +++ b/src/utils/styleProps.ts @@ -1,18 +1,22 @@ -import { CSSProperties } from 'react'; +import { CSSProperties, HTMLAttributes } from 'react'; import { BackgroundColorValue, BorderColorValue, BorderRadiusValue, + BorderSizeValue, ColorValue, DimensionValue, Direction, Responsive, + ResponsiveProp, + StyleProps, ViewStyleProps, } from '../types'; +import { useLocale } from '@react-aria/i18n'; type Breakpoint = 'base' | 'S' | 'M' | 'L' | string; type StyleName = string | string[] | ((dir: Direction) => string); -type StyleHandler = (value: any) => string; +type StyleHandler = (value: any) => string | undefined; export interface StyleHandlers { [key: string]: [StyleName, StyleHandler]; } @@ -152,6 +156,14 @@ export function dimensionValue(value: DimensionValue) { return `var(--ac-global-dimension-${value})`; } +export function responsiveDimensionValue( + value: Responsive, + matchedBreakpoints: Breakpoint[] +) { + value = getResponsiveProp(value, matchedBreakpoints); + return dimensionValue(value); +} + export function convertStyleProps( props: ViewStyleProps, handlers: StyleHandlers, @@ -170,20 +182,24 @@ export function convertStyleProps( name = name(direction); } - let prop = getResponsiveProp(props[key], matchedBreakpoints); + let prop = getResponsiveProp( + props[key as keyof ViewStyleProps], + matchedBreakpoints + ); let value = convert(prop); if (Array.isArray(name)) { for (let k of name) { - style[k] = value; + (style as any)[k] = value; } } else { - style[name] = value; + (style as any)[name] = value; } } for (let prop in borderStyleProps) { - if (style[prop]) { - style[borderStyleProps[prop]] = 'solid'; + if (style[prop as keyof typeof borderStyleProps]) { + (style as any)[borderStyleProps[prop as keyof typeof borderStyleProps]] = + 'solid'; style.boxSizing = 'border-box'; } } @@ -220,14 +236,34 @@ function borderColorValue(value: BorderColorValue) { )})`; } -// function borderSizeValue(value: BorderSizeValue) { -// return `var(--ac-alias-border-size-${value})`; -// } +function borderSizeValue(value: BorderSizeValue) { + return `var(--ac-alias-border-size-${value})`; +} + +export function passthroughStyle(value) { + return value; +} function borderRadiusValue(value: BorderRadiusValue) { return `var(--ac-alias-border-radius-${value})`; } +function hiddenValue(value: boolean) { + return value ? 'none' : undefined; +} + +function anyValue(value: any) { + return value; +} + +function flexValue(value: boolean | number | string) { + if (typeof value === 'boolean') { + return value ? '1' : undefined; + } + + return '' + value; +} + export function getResponsiveProp( prop: Responsive, matchedBreakpoints: Breakpoint[] @@ -239,7 +275,58 @@ export function getResponsiveProp( return prop[breakpoint]; } } - return (prop as ResponsiveProp).base; + return (prop as ResponsiveProp).base as T; } return prop as T; } + +type StylePropsOptions = { + matchedBreakpoints?: Breakpoint[]; +}; + +export function useStyleProps( + props: T, + handlers: StyleHandlers = baseStyleProps, + options: StylePropsOptions = {} +) { + let { ...otherProps } = props; + let { direction } = useLocale(); + let { matchedBreakpoints = ['base'] } = options; + let styles = convertStyleProps( + props, + handlers, + direction, + matchedBreakpoints + ); + let style = { ...styles }; + + // @ts-ignore + if (otherProps.className) { + console.warn( + 'The className prop is unsafe and is unsupported in Arize Components. ' + + 'Please use style props with Spectrum variables, or UNSAFE_className if you absolutely must do something custom. ' + + 'Note that this may break in future versions due to DOM structure changes.' + ); + } + + // @ts-ignore + if (otherProps.style) { + console.warn( + 'The style prop is unsafe and is unsupported in React Arize Components. ' + + 'Please use style props with Spectrum variables, or UNSAFE_style if you absolutely must do something custom. ' + + 'Note that this may break in future versions due to DOM structure changes.' + ); + } + + let styleProps: HTMLAttributes = { + style, + }; + + if (getResponsiveProp(props.isHidden, matchedBreakpoints)) { + styleProps.hidden = true; + } + + return { + styleProps, + }; +} diff --git a/tsconfig.json b/tsconfig.json index 8f023c77..b851ec78 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,6 +33,8 @@ "forceConsistentCasingInFileNames": true, // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` "noEmit": true, + // we can explicitly declare `any`, but we don't want to infer `any` + "noImplicitAny": false, "types": ["@emotion/react/types/css-prop"] } }