From 43ec63bfd16c6c4337e5611c4e09c60d92c0adf0 Mon Sep 17 00:00:00 2001 From: Mikyo King Date: Mon, 15 May 2023 11:28:23 -0600 Subject: [PATCH] fteat: layout flex (#125) * WIP * feat: flex * feat: layout flex * feat: Flex and View * clean up a few things * remove aliases * add to the gallery * v0.9.25-0 * support colors on backgrounds * v0.9.25-1 --- package.json | 2 +- src/empty/graphics/EmptyDocuments.tsx | 4 +- src/index.tsx | 1 + src/layout/Flex.tsx | 105 +++++++++ src/layout/index.tsx | 1 + src/provider/GlobalStyles.tsx | 89 +++++++- src/types/core.ts | 17 +- src/types/index.ts | 1 + src/types/locale.ts | 1 + src/types/style.ts | 259 ++++++++++++++++++++- src/utils/index.ts | 1 + src/utils/styleProps.ts | 315 +++++++++++++++++++++++++- src/view/View.tsx | 46 +--- stories/Flex.stories.tsx | 30 +++ stories/Gallery.stories.tsx | 36 +++ stories/View.stories.tsx | 2 + tsconfig.json | 2 + 17 files changed, 862 insertions(+), 50 deletions(-) create mode 100644 src/layout/Flex.tsx create mode 100644 src/layout/index.tsx create mode 100644 src/types/locale.ts create mode 100644 stories/Flex.stories.tsx diff --git a/package.json b/package.json index 91fecad8..4f3b7a3c 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.9.24", + "version": "0.9.25-1", "license": "MIT", "main": "dist/index.js", "typings": "dist/index.d.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..108a7d01 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; @@ -80,14 +140,41 @@ export const globalCSS = css` --ac-global-background-color-dark: var(--ac-global-color-gray-900); --ac-global-background-color-danger: var(--ac-global-color-danger); + --ac-global-border-color-default: var(--ac-global-color-gray-100); --ac-global-border-color-light: var(--ac-global-color-gray-100); --ac-global-border-color-dark: var(--ac-global-color-gray-400); --ac-global-rounding-small: var(--ac-global-dimension-static-size-50); --ac-global-rounding-medium: var(--ac-global-dimension-static-size-100); + + --ac-global-border-size-thin: var(--ac-global-dimension-static-size-10); + --ac-global-border-size-thick: var(--ac-global-dimension-static-size-25); + --ac-global-border-size-thicker: var(--ac-global-dimension-static-size-50); + --ac-global-border-size-thickest: var( + --ac-global-dimension-static-size-100 + ); + --ac-global-border-offset-thin: var(--ac-global-dimension-static-size-25); + --ac-global-border-offset-thick: var(--ac-global-dimension-static-size-50); + --ac-global-border-offset-thicker: var( + --ac-global-dimension-static-size-100 + ); + --ac-global-border-offset-thickest: var( + --ac-global-dimension-static-size-200 + ); + --ac-global-grid-baseline: var(--ac-global-dimension-static-size-100); + --ac-global-grid-gutter-xsmall: var(--ac-global-dimension-static-size-200); + --ac-global-grid-gutter-small: var(--ac-global-dimension-static-size-300); + --ac-global-grid-gutter-medium: var(--ac-global-dimension-static-size-400); + --ac-global-grid-gutter-large: var(--ac-global-dimension-static-size-500); + --ac-global-grid-gutter-xlarge: var(--ac-global-dimension-static-size-600); + --ac-global-grid-margin-xsmall: var(--ac-global-dimension-static-size-200); + --ac-global-grid-margin-small: var(--ac-global-dimension-static-size-300); + --ac-global-grid-margin-medium: var(--ac-global-dimension-static-size-400); + --ac-global-grid-margin-large: var(--ac-global-dimension-static-size-500); + --ac-global-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 a6996c85..d9e25954 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -91,7 +91,18 @@ export type DimensionValue = | (string & {}); export type BorderRadiusValue = 'small' | 'medium'; +export type BorderColorValue = 'default' | 'light' | 'dark'; +export type BorderSizeValue = 'thin' | 'thick' | 'thicker' | 'thickest'; +export type BackgroundColorValue = 'light' | 'dark' | ColorValue; -export type BorderColorValue = 'light' | 'dark'; - -export type BackgroundColorValue = 'light' | 'dark'; +export type ColorValue = + | 'gray-100' + | 'gray-200' + | 'gray-300' + | 'gray-400' + | 'gray-500' + | 'gray-600' + | 'gray-700' + | 'gray-800' + | 'gray-900' + | 'danger'; diff --git a/src/types/index.ts b/src/types/index.ts index 6a144e97..49a6b4fe 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -15,3 +15,4 @@ export * from './button'; export * from './style'; export * from './progress'; export * from './style'; +export * from './locale'; 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 6ddc7c6f..7d774ac9 100644 --- a/src/types/style.ts +++ b/src/types/style.ts @@ -1,9 +1,34 @@ -import { DimensionValue } from './core'; +import { + BackgroundColorValue, + BorderColorValue, + BorderRadiusValue, + BorderSizeValue, + DimensionValue, +} from './core'; 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). */ @@ -16,6 +41,225 @@ 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 +// but still based on arize global/alias variables. +export interface ViewStyleProps extends StyleProps { + /** The background color for the element. */ + backgroundColor?: Responsive; + + /** The width of the element's border on all four sides. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-width). */ + borderWidth?: Responsive; + /** The width of the border on the logical start side, depending on the layout direction. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-inline-start-width). */ + borderStartWidth?: Responsive; + /** The width of the border on the logical end side, depending on the layout direction. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-inline-end-width). */ + borderEndWidth?: Responsive; + borderLeftWidth?: BorderSizeValue; + borderRightWidth?: BorderSizeValue; + /** The width of the top border. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-top-width). */ + borderTopWidth?: Responsive; + /** The width of the bottom border. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-bottom-width). */ + borderBottomWidth?: Responsive; + /** The width of the left and right borders. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-width). */ + borderXWidth?: Responsive; + /** The width of the top and bottom borders. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-width). */ + borderYWidth?: Responsive; + + /** The color of the element's border on all four sides. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-color). */ + borderColor?: Responsive; + /** The color of the border on the logical start side, depending on the layout direction. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-inline-start-color). */ + borderStartColor?: Responsive; + /** The color of the border on the logical end side, depending on the layout direction. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-inline-end-color). */ + borderEndColor?: Responsive; + // borderLeftColor?: BorderColorValue, + // borderRightColor?: BorderColorValue, + /** The color of the top border. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-top-color). */ + borderTopColor?: Responsive; + /** The color of the bottom border. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-bottom-color). */ + borderBottomColor?: Responsive; + /** The color of the left and right borders. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-color). */ + borderXColor?: Responsive; + /** The color of the top and bottom borders. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-width). */ + borderYColor?: Responsive; + + /** The border radius on all four sides of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius). */ + borderRadius?: Responsive; + /** The border radius for the top start corner of the element, depending on the layout direction. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-start-start-radius). */ + borderTopStartRadius?: Responsive; + /** The border radius for the top end corner of the element, depending on the layout direction. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-start-end-radius). */ + borderTopEndRadius?: Responsive; + /** The border radius for the bottom start corner of the element, depending on the layout direction. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-end-start-radius). */ + borderBottomStartRadius?: Responsive; + /** The border radius for the bottom end corner of the element, depending on the layout direction. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/border-end-end-radius). */ + borderBottomEndRadius?: Responsive; + // borderTopLeftRadius?: BorderRadiusValue, + // borderTopRightRadius?: BorderRadiusValue, + // borderBottomLeftRadius?: BorderRadiusValue, + // borderBottomRightRadius?: BorderRadiusValue, + + /** The padding for all four sides of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/padding). */ + padding?: Responsive; + /** The padding for the logical start side of the element, depending on layout direction. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/padding-inline-start). */ + paddingStart?: Responsive; + /** The padding for the logical end side of an element, depending on layout direction. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/padding-inline-end). */ + paddingEnd?: Responsive; + // paddingLeft?: Responsive, + // paddingRight?: Responsive, + /** The padding for the top side of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/padding-top). */ + paddingTop?: Responsive; + /** The padding for the bottom side of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/padding-bottom). */ + paddingBottom?: Responsive; + /** The padding for both the left and right sides of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/padding). */ + paddingX?: Responsive; + /** The padding for both the top and bottom sides of the element. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/padding). */ + paddingY?: Responsive; + + /** Species what to do when the element's content is too long to fit its size. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/overflow). */ + overflow?: Responsive; + // ... + // shadows? + // transforms? +} + +export interface BoxAlignmentStyleProps { + /** + * The distribution of space around items along the main axis. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content). + * @default 'stretch' + */ + justifyContent?: Responsive< + | 'start' + | 'end' + | 'center' + | 'left' + | 'right' + | 'space-between' + | 'space-around' + | 'space-evenly' + | 'stretch' + | 'baseline' + | 'first baseline' + | 'last baseline' + | 'safe center' + | 'unsafe center' + >; + /** + * The distribution of space around child items along the cross axis. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content). + * @default 'start' + */ + alignContent?: Responsive< + | 'start' + | 'end' + | 'center' + | 'space-between' + | 'space-around' + | 'space-evenly' + | 'stretch' + | 'baseline' + | 'first baseline' + | 'last baseline' + | 'safe center' + | 'unsafe center' + >; + /** + * The alignment of children within their container. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items). + * @default 'stretch' + */ + alignItems?: Responsive< + | 'start' + | 'end' + | 'center' + | 'stretch' + | 'self-start' + | 'self-end' + | 'baseline' + | 'first baseline' + | 'last baseline' + | 'safe center' + | 'unsafe center' + >; + /** The space to display between both rows and columns. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/gap). */ + gap?: Responsive; + /** The space to display between columns. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/column-gap). */ + columnGap?: Responsive; + /** The space to display between rows. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/row-gap). */ + rowGap?: Responsive; } export type ResponsiveProp = { @@ -26,4 +270,17 @@ export type ResponsiveProp = { [custom: string]: T | undefined; }; +export interface FlexStyleProps extends BoxAlignmentStyleProps, StyleProps { + /** + * The direction in which to layout children. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction). + * @default 'row' + */ + direction?: Responsive<'row' | 'column' | 'row-reverse' | 'column-reverse'>; + /** + * Whether to wrap items onto multiple lines. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-wrap). + * @default false + */ + wrap?: Responsive; +} + export type Responsive = T | ResponsiveProp; 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 a8c27b53..ae130646 100644 --- a/src/utils/styleProps.ts +++ b/src/utils/styleProps.ts @@ -1,4 +1,137 @@ -import { DimensionValue } from '../types'; +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 | undefined; +export interface StyleHandlers { + [key: string]: [StyleName, StyleHandler]; +} + +export const baseStyleProps: StyleHandlers = { + margin: ['margin', dimensionValue], + marginStart: [rtl('marginLeft', 'marginRight'), dimensionValue], + marginEnd: [rtl('marginRight', 'marginLeft'), dimensionValue], + // marginLeft: ['marginLeft', dimensionValue], + // marginRight: ['marginRight', dimensionValue], + marginTop: ['marginTop', dimensionValue], + marginBottom: ['marginBottom', dimensionValue], + marginX: [['marginLeft', 'marginRight'], dimensionValue], + marginY: [['marginTop', 'marginBottom'], dimensionValue], + width: ['width', dimensionValue], + height: ['height', dimensionValue], + minWidth: ['minWidth', dimensionValue], + minHeight: ['minHeight', dimensionValue], + maxWidth: ['maxWidth', dimensionValue], + maxHeight: ['maxHeight', dimensionValue], + isHidden: ['display', hiddenValue], + alignSelf: ['alignSelf', passthroughStyle], + justifySelf: ['justifySelf', passthroughStyle], + position: ['position', anyValue], + zIndex: ['zIndex', anyValue], + top: ['top', dimensionValue], + bottom: ['bottom', dimensionValue], + start: [rtl('left', 'right'), dimensionValue], + end: [rtl('right', 'left'), dimensionValue], + left: ['left', dimensionValue], + right: ['right', dimensionValue], + order: ['order', anyValue], + flex: ['flex', flexValue], + flexGrow: ['flexGrow', passthroughStyle], + flexShrink: ['flexShrink', passthroughStyle], + flexBasis: ['flexBasis', passthroughStyle], + gridArea: ['gridArea', passthroughStyle], + gridColumn: ['gridColumn', passthroughStyle], + gridColumnEnd: ['gridColumnEnd', passthroughStyle], + gridColumnStart: ['gridColumnStart', passthroughStyle], + gridRow: ['gridRow', passthroughStyle], + gridRowEnd: ['gridRowEnd', passthroughStyle], + gridRowStart: ['gridRowStart', passthroughStyle], +}; + +export const viewStyleProps: StyleHandlers = { + ...baseStyleProps, + backgroundColor: ['backgroundColor', backgroundColorValue], + borderWidth: ['borderWidth', borderSizeValue], + borderStartWidth: [ + rtl('borderLeftWidth', 'borderRightWidth'), + borderSizeValue, + ], + borderEndWidth: [rtl('borderRightWidth', 'borderLeftWidth'), borderSizeValue], + borderLeftWidth: ['borderLeftWidth', borderSizeValue], + borderRightWidth: ['borderRightWidth', borderSizeValue], + borderTopWidth: ['borderTopWidth', borderSizeValue], + borderBottomWidth: ['borderBottomWidth', borderSizeValue], + borderXWidth: [['borderLeftWidth', 'borderRightWidth'], borderSizeValue], + borderYWidth: [['borderTopWidth', 'borderBottomWidth'], borderSizeValue], + borderColor: ['borderColor', borderColorValue], + borderStartColor: [ + rtl('borderLeftColor', 'borderRightColor'), + borderColorValue, + ], + borderEndColor: [ + rtl('borderRightColor', 'borderLeftColor'), + borderColorValue, + ], + borderLeftColor: ['borderLeftColor', borderColorValue], + borderRightColor: ['borderRightColor', borderColorValue], + borderTopColor: ['borderTopColor', borderColorValue], + borderBottomColor: ['borderBottomColor', borderColorValue], + borderXColor: [['borderLeftColor', 'borderRightColor'], borderColorValue], + borderYColor: [['borderTopColor', 'borderBottomColor'], borderColorValue], + borderRadius: ['borderRadius', borderRadiusValue], + borderTopStartRadius: [ + rtl('borderTopLeftRadius', 'borderTopRightRadius'), + borderRadiusValue, + ], + borderTopEndRadius: [ + rtl('borderTopRightRadius', 'borderTopLeftRadius'), + borderRadiusValue, + ], + borderBottomStartRadius: [ + rtl('borderBottomLeftRadius', 'borderBottomRightRadius'), + borderRadiusValue, + ], + borderBottomEndRadius: [ + rtl('borderBottomRightRadius', 'borderBottomLeftRadius'), + borderRadiusValue, + ], + borderTopLeftRadius: ['borderTopLeftRadius', borderRadiusValue], + borderTopRightRadius: ['borderTopRightRadius', borderRadiusValue], + borderBottomLeftRadius: ['borderBottomLeftRadius', borderRadiusValue], + borderBottomRightRadius: ['borderBottomRightRadius', borderRadiusValue], + padding: ['padding', dimensionValue], + paddingStart: [rtl('paddingLeft', 'paddingRight'), dimensionValue], + paddingEnd: [rtl('paddingRight', 'paddingLeft'), dimensionValue], + paddingLeft: ['paddingLeft', dimensionValue], + paddingRight: ['paddingRight', dimensionValue], + paddingTop: ['paddingTop', dimensionValue], + paddingBottom: ['paddingBottom', dimensionValue], + paddingX: [['paddingLeft', 'paddingRight'], dimensionValue], + paddingY: [['paddingTop', 'paddingBottom'], dimensionValue], + overflow: ['overflow', passthroughStyle], +}; + +const borderStyleProps = { + borderWidth: 'borderStyle', + borderLeftWidth: 'borderLeftStyle', + borderRightWidth: 'borderRightStyle', + borderTopWidth: 'borderTopStyle', + borderBottomWidth: 'borderBottomStyle', +}; const UNIT_RE = /(%|px|em|rem|vw|vh|auto|cm|mm|in|pt|pc|ex|ch|rem|vmin|vmax|fr)$/; const FUNC_RE = /^\s*\w+\(/; @@ -14,11 +147,183 @@ export function dimensionValue(value: DimensionValue) { } if (FUNC_RE.test(value)) { - return value.replace( - AC_VARIABLE_RE, - 'var(--ac-global-dimension-$&, var(--spectrum-alias-$&))' - ); + return value.replace(AC_VARIABLE_RE, 'var(--ac-global-dimension-$&)'); } 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, + direction: Direction, + matchedBreakpoints: Breakpoint[] +) { + let style: CSSProperties = {}; + for (let key in props) { + let styleProp = handlers[key]; + if (!styleProp || props[key] == null) { + continue; + } + + let [name, convert] = styleProp; + if (typeof name === 'function') { + name = name(direction); + } + + let prop = getResponsiveProp( + props[key as keyof ViewStyleProps], + matchedBreakpoints + ); + let value = convert(prop); + if (Array.isArray(name)) { + for (let k of name) { + (style as any)[k] = value; + } + } else { + (style as any)[name] = value; + } + } + + for (let prop in borderStyleProps) { + if (style[prop as keyof typeof borderStyleProps]) { + (style as any)[borderStyleProps[prop as keyof typeof borderStyleProps]] = + 'solid'; + style.boxSizing = 'border-box'; + } + } + + return style; +} + +function rtl(ltr: string, rtl: string) { + return (direction: Direction) => (direction === 'rtl' ? rtl : ltr); +} + +type ColorType = 'default' | 'background' | 'border' | 'icon' | 'status'; +function colorValue(value: ColorValue, type: ColorType = 'default') { + // TODO actually support semantic colors + return `var(--ac-global-color-${value}, var(--ac-semantic-${value}-color-${type}))`; +} + +function backgroundColorValue(value: BackgroundColorValue) { + return `var(--ac-global-background-color-${value}, ${colorValue( + value as ColorValue, + 'background' + )})`; +} + +function borderColorValue(value: BorderColorValue) { + // TODO support default color + if (value === 'default') { + return 'var(--ac-global-border-color)'; + } + + return `var(--ac-global-border-color-${value}, ${colorValue( + value as ColorValue, + 'border' + )})`; +} + +function borderSizeValue(value: BorderSizeValue) { + return `var(--ac-global-border-size-${value})`; +} + +export function passthroughStyle(value) { + return value; +} + +function borderRadiusValue(value: BorderRadiusValue) { + return `var(--ac-global-rounding-${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[] +): T { + if (prop && typeof prop === 'object' && !Array.isArray(prop)) { + for (let i = 0; i < matchedBreakpoints.length; i++) { + let breakpoint = matchedBreakpoints[i]; + if (prop[breakpoint] != null) { + return prop[breakpoint]; + } + } + 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/src/view/View.tsx b/src/view/View.tsx index 86b19d14..c341eefd 100644 --- a/src/view/View.tsx +++ b/src/view/View.tsx @@ -1,16 +1,11 @@ import React, { JSXElementConstructor, ReactNode, forwardRef } from 'react'; import { css } from '@emotion/react'; -import { - BackgroundColorValue, - BorderColorValue, - BorderRadiusValue, - DOMRef, - DimensionValue, -} from '../types'; +import { DOMProps, DOMRef, ViewStyleProps } from '../types'; import { useDOMRef } from '../utils'; -import { dimensionValue } from '../utils/styleProps'; +import { useStyleProps, viewStyleProps } from '../utils'; +import { filterDOMProps } from '@react-aria/utils'; -export interface ViewProps { +export interface ViewProps extends ViewStyleProps, DOMProps { /** * The children to be displayed in the View. */ @@ -20,49 +15,24 @@ export interface ViewProps { * @default 'div' */ elementType?: string | JSXElementConstructor; - padding?: Extract< - DimensionValue, - 'static-size-50' | 'static-size-100' | 'static-size-200' - >; - borderRadius?: BorderRadiusValue; - borderColor?: BorderColorValue; - backgroundColor?: BackgroundColorValue; - width?: DimensionValue; - height?: DimensionValue; - id?: string; } function View(props: ViewProps, ref: DOMRef) { const { children, elementType: ElementType = 'div', - padding, - borderRadius, - borderColor, - backgroundColor, - width, - height, id, + ...otherProps } = props; + const { styleProps } = useStyleProps(props, viewStyleProps); const domRef = useDOMRef(ref); return ( ( + + Column + + + + + + Row + + + + + + +); diff --git a/stories/Gallery.stories.tsx b/stories/Gallery.stories.tsx index 5c2df5be..f68ce497 100644 --- a/stories/Gallery.stories.tsx +++ b/stories/Gallery.stories.tsx @@ -17,6 +17,7 @@ import { View, Heading, ButtonGroup, + Flex, } from '../src'; import { Icon, Icons, SearchOutline } from '../src/icon'; // @ts-ignore @@ -204,6 +205,41 @@ const Template: Story = args => { `} /> + + + + chart image + + + + Statistic + 437 + + + + {holder} diff --git a/stories/View.stories.tsx b/stories/View.stories.tsx index 528cd92b..1ee52234 100644 --- a/stories/View.stories.tsx +++ b/stories/View.stories.tsx @@ -113,6 +113,7 @@ export const Gallery = args => { {