From f43f82af6362afcb5d0c0d85cfc2560d441e3ff9 Mon Sep 17 00:00:00 2001 From: richbachman Date: Thu, 9 Jan 2020 16:20:00 -0700 Subject: [PATCH] feat(grid): create grid component --- .../paste-core/utilities/grid/package.json | 51 +- .../utilities/grid/rollup.config.js | 34 ++ .../paste-core/utilities/grid/src/Column.tsx | 138 +++++ .../paste-core/utilities/grid/src/Grid.tsx | 78 +++ .../paste-core/utilities/grid/src/index.tsx | 2 + .../utilities/grid/stories/index.stories.tsx | 541 ++++++++++++++++++ .../utilities/grid/tsconfig.build.json | 27 + .../paste-core/utilities/grid/tsconfig.json | 4 + 8 files changed, 873 insertions(+), 2 deletions(-) create mode 100644 packages/paste-core/utilities/grid/rollup.config.js create mode 100644 packages/paste-core/utilities/grid/src/Column.tsx create mode 100644 packages/paste-core/utilities/grid/src/Grid.tsx create mode 100644 packages/paste-core/utilities/grid/src/index.tsx create mode 100644 packages/paste-core/utilities/grid/stories/index.stories.tsx create mode 100644 packages/paste-core/utilities/grid/tsconfig.build.json create mode 100644 packages/paste-core/utilities/grid/tsconfig.json diff --git a/packages/paste-core/utilities/grid/package.json b/packages/paste-core/utilities/grid/package.json index ae63bdacd0..550f08e6dc 100644 --- a/packages/paste-core/utilities/grid/package.json +++ b/packages/paste-core/utilities/grid/package.json @@ -2,6 +2,53 @@ "name": "@twilio-paste/grid", "version": "0.0.2", "category": "layout", - "status": "backlog", - "private": true + "status": "beta", + "description": "A flex based horizontal grid system used to build layouts.", + "author": "Twilio Inc.", + "license": "MIT", + "main:dev": "src/index.tsx", + "main": "dist/index.js", + "module": "dist/index.es.js", + "types": "dist/index.d.ts", + "sideEffects": false, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "yarn clean && yarn compile", + "build:dev": "yarn clean && yarn compile:dev", + "clean": "rm -rf ./dist && rm -rf tsconfig.build.tsbuildinfo && rm -rf .rpt2_cache", + "compile": "rollup -c --environment NODE_ENV:production", + "compile:dev": "rollup -c --environment NODE_ENV:development", + "prepublishOnly": "yarn build", + "type-check": "tsc --noEmit" + }, + "peerDependencies": { + "@twilio-paste/box": "^2.1.1", + "@twilio-paste/flex": "^0.1.1", + "@twilio-paste/style-props": "^0.1.1", + "@twilio-paste/theme": "^3.0.0", + "@twilio-paste/theme-tokens": "^3.0.0", + "prop-types": "^15.7.2", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "styled-system": "^5.1.2" + }, + "devDependencies": { + "@twilio-paste/box": "^2.1.1", + "@twilio-paste/flex": "^0.1.1", + "@twilio-paste/style-props": "^0.1.1", + "@twilio-paste/theme": "^3.0.0", + "@twilio-paste/theme-tokens": "^3.0.0", + "rollup": "^1.16.2", + "rollup-plugin-babel": "^4.3.3", + "rollup-plugin-commonjs": "^10.0.1", + "rollup-plugin-node-resolve": "^5.1.0", + "rollup-plugin-terser": "^5.0.0", + "rollup-plugin-typescript2": "^0.21.2", + "typescript": "^3.5.2" + } } diff --git a/packages/paste-core/utilities/grid/rollup.config.js b/packages/paste-core/utilities/grid/rollup.config.js new file mode 100644 index 0000000000..ce1cb4a3bb --- /dev/null +++ b/packages/paste-core/utilities/grid/rollup.config.js @@ -0,0 +1,34 @@ +import typescript from 'rollup-plugin-typescript2'; +import babel from 'rollup-plugin-babel'; +import resolve from 'rollup-plugin-node-resolve'; +import commonjs from 'rollup-plugin-commonjs'; +import {terser} from 'rollup-plugin-terser'; +import pkg from './package.json'; + +export default { + input: pkg['main:dev'], + output: [ + { + file: pkg.main, + format: 'cjs', + }, + { + file: pkg.module, + format: 'esm', + }, + ], + external: [...Object.keys(pkg.peerDependencies || {})], + plugins: [ + resolve(), + commonjs(), + typescript({ + clean: true, + typescript: require('typescript'), + tsconfig: './tsconfig.build.json', + }), + babel({ + exclude: 'node_modules/**', + }), + process.env.NODE_ENV === 'production' ? terser() : null, + ], +}; diff --git a/packages/paste-core/utilities/grid/src/Column.tsx b/packages/paste-core/utilities/grid/src/Column.tsx new file mode 100644 index 0000000000..688da2a771 --- /dev/null +++ b/packages/paste-core/utilities/grid/src/Column.tsx @@ -0,0 +1,138 @@ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +import {compose, layout, space, flexbox, ResponsiveValue} from 'styled-system'; +import {FlexboxProps, LayoutProps, PaddingProps, Space} from '@twilio-paste/style-props'; +import {FlexProps, Vertical} from '@twilio-paste/flex'; + +type ColumnMinWidth = ResponsiveValue<'100%' | '0'>; +type ColumnWidthSpan = ResponsiveValue; +type ColumnOffsetOptions = number; +export type ColumnOffset = ResponsiveValue; +type ColumnSpanOptions = number; +export type ColumnSpan = ResponsiveValue; + +interface ColumnStyles extends Omit, FlexboxProps, PaddingProps { + marginLeft?: ResponsiveValue; + minWidth?: ColumnMinWidth; + width?: ColumnWidthSpan; +} + +export interface ColumnProps extends Omit, ColumnStyles { + count?: number; + gutter?: Space; + offset?: ColumnOffset; + span?: ColumnSpan; + vertical?: Vertical; +} + +const getVertical = (vertical: Vertical): ColumnMinWidth => { + if (Array.isArray(vertical)) { + return (vertical as Vertical[]).map((value: Vertical) => { + if (typeof value === 'boolean') { + return value === true ? '100%' : '0'; + } + return null; + }); + } + + if (vertical) { + return '100%'; + } + + return '0'; +}; + +const getSpan = ({count, span}: ColumnProps): ColumnWidthSpan => { + if (Array.isArray(span) && count) { + return (span as ColumnSpanOptions[]).map((value: ColumnSpanOptions) => { + return `${(value / 12) * 100}%`; + }); + } + + if (typeof span === 'number' && count && count <= 12) { + return `${(span / 12) * 100}%`; + } + + if (count !== undefined) { + return `${(1 / count) * 100}%`; + } + + return `${(1 / 12) * 100}%`; +}; + +const getOffset = (offset: ColumnOffset): ResponsiveValue => { + if (Array.isArray(offset)) { + return (offset as ColumnOffsetOptions[]).map((value: ColumnOffsetOptions) => { + return `${(value / 12) * 100}%`; + }); + } + + return `${((offset as ColumnOffsetOptions) / 12) * 100}%`; +}; + +const getColumnStyles = (props: ColumnProps): ColumnStyles => { + const columnStyles: ColumnStyles = { + width: getSpan(props), + }; + + if (props.gutter) { + columnStyles.paddingLeft = props.gutter; + columnStyles.paddingRight = props.gutter; + } + + if (!props.offset) { + columnStyles.flexGrow = 1; + columnStyles.flexShrink = 1; + columnStyles.flexBasis = 'auto'; + } + + if (props.offset) { + columnStyles.marginLeft = getOffset(props.offset); + } + + if (props.vertical && !props.offset) { + columnStyles.minWidth = getVertical(props.vertical); + columnStyles.marginLeft = 'space0'; + } + + return columnStyles; +}; + +const StyledColumn = styled.div( + compose( + space, + layout, + flexbox + ) +) as React.FC; + +const Column: React.FC = ({children, count, gutter, offset, span, vertical}) => { + const ColumnStyles = React.useMemo(() => getColumnStyles({count, gutter, offset, span, vertical}), [ + count, + gutter, + offset, + span, + vertical, + ]); + return ( + + {children} + + ); +}; + +Column.propTypes = { + offset: PropTypes.oneOfType([ + PropTypes.oneOfType([PropTypes.number]), + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number])), + ] as any), + span: PropTypes.oneOfType([ + PropTypes.oneOfType([PropTypes.number]), + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number])), + ] as any), +}; + +Column.displayName = 'Column'; + +export {Column}; diff --git a/packages/paste-core/utilities/grid/src/Grid.tsx b/packages/paste-core/utilities/grid/src/Grid.tsx new file mode 100644 index 0000000000..ded627656d --- /dev/null +++ b/packages/paste-core/utilities/grid/src/Grid.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; +import {useTheme} from '@twilio-paste/theme'; +import {ThemeShape} from '@twilio-paste/theme-tokens'; +import {Margin, MarginProps, Space, SpaceOptions, SpaceProps} from '@twilio-paste/style-props'; +import {safelySpreadBoxProps} from '@twilio-paste/box'; +import {Flex, Vertical} from '@twilio-paste/flex'; + +export interface GridProps extends SpaceProps { + children: NonNullable; + gutter?: Space; + vertical?: Vertical; +} + +const getNegativeMargin = (theme: ThemeShape, gutter?: Space): Margin => { + if (Array.isArray(gutter)) { + return (gutter as SpaceOptions[]).map((value: SpaceOptions) => { + return `-${theme.space[value]}` as SpaceOptions; + }); + } + + if (gutter) { + return `-${theme.space[gutter as SpaceOptions]}` as SpaceOptions; + } + + return 'auto'; +}; + +const getGridStyles = (theme: ThemeShape, gutter?: Space): MarginProps => { + const marginStyles: MarginProps = { + marginLeft: getNegativeMargin(theme, gutter), + marginRight: getNegativeMargin(theme, gutter), + }; + + return marginStyles; +}; + +const Grid: React.FC = ({children, gutter, marginTop, marginBottom, vertical, ...props}) => { + const GridColumns = React.useMemo( + () => + React.Children.map(children, child => + React.isValidElement(child) + ? React.cloneElement(child, {count: React.Children.count(children), gutter, vertical}) + : child + ), + [children] + ); + + const GridStyles = React.useMemo(() => getGridStyles(useTheme(), gutter), [gutter]); + + return ( + + {GridColumns} + + ); +}; + +Grid.propTypes = { + children: PropTypes.node.isRequired, + vertical: PropTypes.oneOfType([ + PropTypes.oneOfType([PropTypes.bool]), + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.bool])), + ] as any), +}; + +Grid.defaultProps = { + vertical: false, +}; + +Grid.displayName = 'Grid'; + +export {Grid}; diff --git a/packages/paste-core/utilities/grid/src/index.tsx b/packages/paste-core/utilities/grid/src/index.tsx new file mode 100644 index 0000000000..b50e8c09d0 --- /dev/null +++ b/packages/paste-core/utilities/grid/src/index.tsx @@ -0,0 +1,2 @@ +export * from './Grid'; +export * from './Column'; diff --git a/packages/paste-core/utilities/grid/stories/index.stories.tsx b/packages/paste-core/utilities/grid/stories/index.stories.tsx new file mode 100644 index 0000000000..65dd661e00 --- /dev/null +++ b/packages/paste-core/utilities/grid/stories/index.stories.tsx @@ -0,0 +1,541 @@ +import * as React from 'react'; +import {storiesOf} from '@storybook/react'; +import {withKnobs, select} from '@storybook/addon-knobs'; +import {DefaultTheme, ThemeShape} from '@twilio-paste/theme-tokens'; +import {Box} from '@twilio-paste/box'; +import {Grid, Column} from '../src'; + +const spaceOptions = Object.keys(DefaultTheme.space); + +storiesOf('Utilities|Grid', module) + .addDecorator(withKnobs) + .add('Grid - 12 Column and Gutter Support', () => { + const gutterValue = select('gutter', spaceOptions, 'space0') as keyof ThemeShape['space']; + return ( + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 7 + + + + + 8 + + + + + 9 + + + + + 10 + + + + + 11 + + + + + 12 + + + + ); + }) + .add('Grid - 1 column', () => { + return ( + + + + 1 + + + + ); + }) + .add('Grid - 2 column', () => { + return ( + + + + 1 + + + + + 2 + + + + ); + }) + .add('Grid - 3 Column', () => { + return ( + + + + 1 + + + + + 2 + + + + + 3 + + + + ); + }) + .add('Grid - 4 Column', () => { + return ( + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + ); + }) + .add('Grid - 6 Column', () => { + return ( + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + ); + }) + .add('Grid - 5, 5, and 2 Column', () => { + return ( + + + + 5 + + + + + 5 + + + + + 2 + + + + ); + }) + .add('Grid - 6 and 6 Column', () => { + return ( + + + + 6 + + + + + 6 + + + + ); + }) + .add('Grid - 8 and 4 Column', () => { + return ( + + + + 8 + + + + + 4 + + + + ); + }) + .add('Grid - 9 and 3 Column', () => { + return ( + + + + 9 + + + + + 3 + + + + ); + }) + .add('Grid - 10 and 2 Column', () => { + return ( + + + + 10 + + + + + 2 + + + + ); + }) + .add('Grid - 11 and 1 Column', () => { + return ( + + + + 11 + + + + + 1 + + + + ); + }) + .add('Grid - 2 Column and Single Columns', () => { + return ( + + + + 2 + + + + + 1 + + + + + 1 + + + + + 1 + + + + + 1 + + + + + 1 + + + + + 1 + + + + + 1 + + + + + 1 + + + + + 1 + + + + + 1 + + + + ); + }) + .add('Grid - 8 Column and 4 Single Columns', () => { + return ( + + + + 8 + + + + + 1 + + + + + 1 + + + + + 1 + + + + + 1 + + + + ); + }) + .add('Grid - 8 Column Span with Offset', () => { + return ( + + + + 8 + + + + ); + }) + .add('Grid - 2 Columns with Offset', () => { + return ( + + + + 6 + + + + + 4 + + + + ); + }) + .add('Grid - Right Offset Column', () => { + return ( + + + + 4 + + + + + 4 + + + + ); + }) + .add('Grid - Multiple Offset Columns', () => { + return ( + + + + 2 + + + + + 2 + + + + ); + }) + .add('Grid - 2 Column Sidebar', () => { + return ( + + + + 3 + + + + + 9 + + + + ); + }) + .add('Grid - 3 Column Wider Center Column', () => { + return ( + + + + 3 + + + + + 9 + + + + + 3 + + + + ); + }) + .add('Grid - 2 Column Responsive', () => { + return ( + + + + Responsive Column Size + + + + + Responsive Column Size + + + + ); + }) + .add('Grid - 2 Column Responsive Stack', () => { + return ( + + + + Responsive Column Size + + + + + Responsive Column Size + + + + ); + }) + .add('Grid - Nested Column', () => { + return ( + + + + 1 + + + + + + + + Nested Column 1 + + + + + Nested Column 2 + + + + + + + + 3 + + + + ); + }); diff --git a/packages/paste-core/utilities/grid/tsconfig.build.json b/packages/paste-core/utilities/grid/tsconfig.build.json new file mode 100644 index 0000000000..8b64519366 --- /dev/null +++ b/packages/paste-core/utilities/grid/tsconfig.build.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": [ + "src/**/*" + ], + "references": [ + { + "path": "../box" + }, + { + "path": "../flex" + }, + { + "path": "../../../paste-style-props" + }, + { + "path": "../../../paste-theme" + }, + { + "path": "../../../paste-theme-tokens" + } + ] +} \ No newline at end of file diff --git a/packages/paste-core/utilities/grid/tsconfig.json b/packages/paste-core/utilities/grid/tsconfig.json new file mode 100644 index 0000000000..20378db15e --- /dev/null +++ b/packages/paste-core/utilities/grid/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../../tsconfig.json", + "include": ["src/**/*"] +}