Unverified Commit 8b634833 authored by Tasso Evangelista's avatar Tasso Evangelista Committed by GitHub
Browse files

perf: Keyed children (#231)

parent 184e8ed2
...@@ -46,7 +46,8 @@ ...@@ -46,7 +46,8 @@
"@rocket.chat/css-in-js": "^0.9.0", "@rocket.chat/css-in-js": "^0.9.0",
"@rocket.chat/fuselage-tokens": "^0.9.0", "@rocket.chat/fuselage-tokens": "^0.9.0",
"@rocket.chat/icons": "^0.9.0", "@rocket.chat/icons": "^0.9.0",
"invariant": "^2.2.4" "invariant": "^2.2.4",
"react-keyed-flatten-children": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.4.5", "@babel/core": "^7.4.5",
......
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { useContext, createContext } from 'react'; import React, { useContext, createContext } from 'react';
import flattenChildren from 'react-keyed-flatten-children';
import { Box } from '../Box'; import { Box } from '../Box';
import { sizePropType } from '../../styles/props/layout'; import { sizePropType } from '../../styles/props/layout';
...@@ -48,7 +49,7 @@ Avatar.propTypes = { ...@@ -48,7 +49,7 @@ Avatar.propTypes = {
const AvatarStack = ({ children, ...props }) => const AvatarStack = ({ children, ...props }) =>
<Box rcx-avatar-stack {...props}> <Box rcx-avatar-stack {...props}>
{React.Children.toArray(children).reverse()} {flattenChildren(children).reverse()}
</Box>; </Box>;
Avatar.Stack = AvatarStack; Avatar.Stack = AvatarStack;
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { createElement, forwardRef, memo, useContext } from 'react'; import React, { createElement, forwardRef, memo, useContext } from 'react';
import { mergeProps } from '../../helpers/mergeProps';
import { marginPropType, paddingPropType, mapSpaceProps } from '../../styles/props/spaces';
import { colorPropType, mapColorProps } from '../../styles/props/colors';
import { fontFamilyPropType, fontScalePropType, mapTypographyProps } from '../../styles/props/typography';
import { sizePropType, mapLayoutProps } from '../../styles/props/layout';
import { insetPropType, mapPositionProps } from '../../styles/props/position';
import { PropsContext } from './PropsContext'; import { PropsContext } from './PropsContext';
import { useStyleSheet } from './useStyleSheet'; import { useStyleSheet } from './useStyleSheet';
import { mergeProps } from './mergeProps'; import { mapSpecialProps } from '../../styles/props/special';
import { marginPropType, paddingPropType } from '../../styles/props/spaces'; import { mapClassNames } from './mapClassNames';
import { colorPropType } from '../../styles/props/colors'; import { mapFlexBoxProps } from '../../styles/props/flexBox';
import { fontFamilyPropType, fontScalePropType } from '../../styles/props/typography'; import { mapBorderProps } from '../../styles/props/borders';
import { sizePropType } from '../../styles/props/layout';
import { insetPropType } from '../../styles/props/position'; const transforms = [
mapBorderProps,
mapColorProps,
mapFlexBoxProps,
mapLayoutProps,
mapPositionProps,
mapSpaceProps,
mapTypographyProps,
mapSpecialProps,
mapClassNames,
];
export const Box = memo(forwardRef(function Box(props, ref) { export const Box = memo(forwardRef(function Box(props, ref) {
useStyleSheet(); useStyleSheet();
const contextProps = useContext(PropsContext); const contextProps = useContext(PropsContext);
const { is, ...mergedProps } = mergeProps(props, contextProps, ref); const { is, ...mergedProps } = mergeProps(contextProps, { ...props, 'rcx-box': true, ref }, transforms);
const children = createElement(is || 'div', mergedProps); const children = createElement(is || 'div', mergedProps);
......
import { toClassName } from '@rocket.chat/css-in-js';
import { mapClassNames } from './mapClassNames';
import { mapSpaceProps } from '../../styles/props/spaces';
import { mapLayoutProps } from '../../styles/props/layout';
import { mapFlexBoxProps } from '../../styles/props/flexBox';
import { mapColorProps } from '../../styles/props/colors';
import { mapTypographyProps } from '../../styles/props/typography';
import { mapBorderProps } from '../../styles/props/borders';
import { mapPositionProps } from '../../styles/props/position';
import { mapSpecialProps } from '../../styles/props/special';
export const mergeProps = (props, contextProps, ref) => {
const initialProps = {
ref,
...contextProps,
...props,
className: [
'rcx-box',
...Array.isArray(contextProps?.className) ? contextProps?.className : [contextProps?.className],
...Array.isArray(props.className) ? props.className : [props.className],
],
style: {
...contextProps?.style,
...props.style,
},
};
const mergedProps = [
mapSpecialProps,
mapClassNames,
mapSpaceProps,
mapLayoutProps,
mapColorProps,
mapTypographyProps,
mapFlexBoxProps,
mapBorderProps,
mapPositionProps,
].reduce((props, transform) => transform(props), initialProps);
mergedProps.className = Array.from(
new Set(
mergedProps.className.map(toClassName).filter(Boolean),
),
).join(' ');
return mergedProps;
};
import { css } from '@rocket.chat/css-in-js'; import { css } from '@rocket.chat/css-in-js';
import { Meta, Preview, Props, Story } from '@storybook/addon-docs/blocks'; import { Meta, Preview, Props, Story } from '@storybook/addon-docs/blocks';
import flattenChildren from 'react-keyed-flatten-children';
import { Box, Button } from '../..'; import { Box, Button } from '../..';
<Meta title='Box/Props' parameters={{ jest: ['Box/spec'] }} /> <Meta title='Box/Props' parameters={{ jest: ['Box/spec'] }} />
...@@ -32,7 +33,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -32,7 +33,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Margins' name='Margins'
decorators={[ decorators={[
(fn) => <Box display='flex' flexWrap='wrap' alignItems='center'> (fn) => <Box display='flex' flexWrap='wrap' alignItems='center'>
{React.Children.map(fn().props.children, (child) => <Box bg='neutral-200' m='x16'> {flattenChildren(fn().props.children).map((child) => <Box key={child.key} bg='neutral-200' m='x16'>
{React.cloneElement(child, { bg:'primary-200' }, <Box bg='neutral-500' size='x16' />)} {React.cloneElement(child, { bg:'primary-200' }, <Box bg='neutral-500' size='x16' />)}
</Box>)} </Box>)}
</Box> </Box>
...@@ -64,7 +65,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -64,7 +65,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Paddings' name='Paddings'
decorators={[ decorators={[
(fn) => <Box display='flex' flexWrap='wrap' alignItems='center'> (fn) => <Box display='flex' flexWrap='wrap' alignItems='center'>
{React.Children.map(fn().props.children, (child) => <Box bg='neutral-200' m='x16'> {flattenChildren(fn().props.children).map((child) => <Box key={child.key} bg='neutral-200' m='x16'>
{React.cloneElement(child, { bg:'primary-200' }, <Box bg='neutral-500' size='x16' />)} {React.cloneElement(child, { bg:'primary-200' }, <Box bg='neutral-500' size='x16' />)}
</Box>)} </Box>)}
</Box> </Box>
...@@ -98,7 +99,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -98,7 +99,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Colors' name='Colors'
decorators={[ decorators={[
(fn) => <Box display='flex' flexWrap='wrap' alignItems='center' overflow='hidden'> (fn) => <Box display='flex' flexWrap='wrap' alignItems='center' overflow='hidden'>
{React.Children.map(fn().props.children, (child) => {flattenChildren(fn().props.children).map((child) =>
React.cloneElement(child, { bg: 'neutral-200', m: 'x4', p: 'x4' }, child.props.color))} React.cloneElement(child, { bg: 'neutral-200', m: 'x4', p: 'x4' }, child.props.color))}
</Box> </Box>
]} ]}
...@@ -141,7 +142,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -141,7 +142,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Background colors' name='Background colors'
decorators={[ decorators={[
(fn) => <Box display='flex' flexWrap='wrap' alignItems='center' overflow='hidden'> (fn) => <Box display='flex' flexWrap='wrap' alignItems='center' overflow='hidden'>
{React.Children.map(fn().props.children, (child) => React.cloneElement(child, { m: 'x4', size: 'x32' }))} {flattenChildren(fn().props.children).map((child) => React.cloneElement(child, { m: 'x4', size: 'x32' }))}
</Box> </Box>
]} ]}
> >
...@@ -364,7 +365,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -364,7 +365,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Widths' name='Widths'
decorators={[ decorators={[
(fn) => <Box display='flex' flexWrap='wrap' alignItems='center'> (fn) => <Box display='flex' flexWrap='wrap' alignItems='center'>
{React.Children.map(fn().props.children, (child) => {flattenChildren(fn().props.children).map((child) =>
React.cloneElement(child, { bg:'primary-200', h: 'x32', m: 'x4' }))} React.cloneElement(child, { bg:'primary-200', h: 'x32', m: 'x4' }))}
</Box> </Box>
]} ]}
...@@ -385,7 +386,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -385,7 +386,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Heights' name='Heights'
decorators={[ decorators={[
(fn) => <Box display='flex' flexWrap='wrap' alignItems='center'> (fn) => <Box display='flex' flexWrap='wrap' alignItems='center'>
{React.Children.map(fn().props.children, (child) => {flattenChildren(fn().props.children).map((child) =>
React.cloneElement(child, { bg:'primary-200', w: 'x32', m: 'x4' }))} React.cloneElement(child, { bg:'primary-200', w: 'x32', m: 'x4' }))}
</Box> </Box>
]} ]}
...@@ -406,7 +407,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -406,7 +407,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Sizes' name='Sizes'
decorators={[ decorators={[
(fn) => <Box display='flex' flexWrap='wrap' alignItems='center'> (fn) => <Box display='flex' flexWrap='wrap' alignItems='center'>
{React.Children.map(fn().props.children, (child) => {flattenChildren(fn().props.children).map((child) =>
React.cloneElement(child, { bg:'primary-200', m: 'x4' }))} React.cloneElement(child, { bg:'primary-200', m: 'x4' }))}
</Box> </Box>
]} ]}
...@@ -426,7 +427,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -426,7 +427,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Display' name='Display'
decorators={[ decorators={[
(fn) => <Box> (fn) => <Box>
{React.Children.map(fn().props.children, (child) => {flattenChildren(fn().props.children).map((child) =>
React.cloneElement(child, { children: child.props.display }))} React.cloneElement(child, { children: child.props.display }))}
</Box> </Box>
]} ]}
...@@ -450,7 +451,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -450,7 +451,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Vertical alignment' name='Vertical alignment'
decorators={[ decorators={[
(fn) => <Box> (fn) => <Box>
{React.Children.map(fn().props.children, (child) => {flattenChildren(fn().props.children).map((child) =>
React.cloneElement(child, { display: 'inline', children: child.props.verticalAlign }))} React.cloneElement(child, { display: 'inline', children: child.props.verticalAlign }))}
</Box> </Box>
]} ]}
...@@ -476,7 +477,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -476,7 +477,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Borders' name='Borders'
decorators={[ decorators={[
(fn) => <Box display='flex' flexWrap='wrap' alignItems='center'> (fn) => <Box display='flex' flexWrap='wrap' alignItems='center'>
{React.Children.map(fn().props.children, (child) => {flattenChildren(fn().props.children).map((child) =>
React.cloneElement(child, { bg:'primary-200', size: 'x32', m: 'x16' }))} React.cloneElement(child, { bg:'primary-200', size: 'x32', m: 'x16' }))}
</Box> </Box>
]} ]}
...@@ -521,7 +522,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -521,7 +522,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Border radii' name='Border radii'
decorators={[ decorators={[
(fn) => <Box display='flex' flexWrap='wrap' alignItems='center'> (fn) => <Box display='flex' flexWrap='wrap' alignItems='center'>
{React.Children.map(fn().props.children, (child) => {flattenChildren(fn().props.children).map((child) =>
React.cloneElement(child, { bg:'primary-200', size: 'x32', m: 'x16' }))} React.cloneElement(child, { bg:'primary-200', size: 'x32', m: 'x16' }))}
</Box> </Box>
]} ]}
...@@ -545,7 +546,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -545,7 +546,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Position' name='Position'
decorators={[ decorators={[
(fn) => <Box display='flex' flexWrap='wrap' alignItems='center'> (fn) => <Box display='flex' flexWrap='wrap' alignItems='center'>
{React.Children.map(fn().props.children, (child) => {flattenChildren(fn().props.children).map((child) =>
React.cloneElement(child, { bg:'primary-200', size: 'x32', m: 'x16' }))} React.cloneElement(child, { bg:'primary-200', size: 'x32', m: 'x16' }))}
</Box> </Box>
]} ]}
...@@ -567,7 +568,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -567,7 +568,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Z-index' name='Z-index'
decorators={[ decorators={[
(fn) => <Box display='flex' flexWrap='wrap' alignItems='center'> (fn) => <Box display='flex' flexWrap='wrap' alignItems='center'>
{React.Children.map(fn().props.children, (child) => {flattenChildren(fn().props.children).map((child) =>
React.cloneElement(child, { bg: 'primary-200', borderWidth: 'x4', size: 'x32', m: 'neg-x2' }))} React.cloneElement(child, { bg: 'primary-200', borderWidth: 'x4', size: 'x32', m: 'neg-x2' }))}
</Box> </Box>
]} ]}
...@@ -588,9 +589,10 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -588,9 +589,10 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Inset' name='Inset'
decorators={[ decorators={[
(fn) => <Box display='flex' flexWrap='wrap' alignItems='center'> (fn) => <Box display='flex' flexWrap='wrap' alignItems='center'>
{React.Children.map(fn().props.children, (child) => <Box position='relative' bg='neutral-200' m='x16' size='x64'> {flattenChildren(fn().props.children).map((child) =>
{React.cloneElement(child, { bg:'primary-200', position: 'absolute', minSize: 'x16' })} <Box key={child.key} position='relative' bg='neutral-200' m='x16' size='x64'>
</Box>)} {React.cloneElement(child, { bg:'primary-200', position: 'absolute', minSize: 'x16' })}
</Box>)}
</Box> </Box>
]} ]}
> >
...@@ -620,7 +622,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -620,7 +622,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Elevation' name='Elevation'
decorators={[ decorators={[
(fn) => <Box display='flex' flexWrap='wrap' alignItems='center'> (fn) => <Box display='flex' flexWrap='wrap' alignItems='center'>
{React.Children.map(fn().props.children, (child) => {flattenChildren(fn().props.children).map((child) =>
React.cloneElement(child, { bg: 'primary-100', m: 'x16', size: 'x64' }))} React.cloneElement(child, { bg: 'primary-100', m: 'x16', size: 'x64' }))}
</Box> </Box>
]} ]}
...@@ -640,7 +642,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO ...@@ -640,7 +642,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Invisible' name='Invisible'
decorators={[ decorators={[
(fn) => <Box display='flex' flexWrap='wrap' alignItems='center'> (fn) => <Box display='flex' flexWrap='wrap' alignItems='center'>
{React.Children.map(fn(), (child) => <Box bg='neutral-200' p='x16'> {flattenChildren(fn()).map((child) => <Box key={child.key} bg='neutral-200' p='x16'>
{React.cloneElement(child, { bg:'primary-200', size: 'x16' })} {React.cloneElement(child, { bg:'primary-200', size: 'x16' })}
</Box>)} </Box>)}
</Box> </Box>
......
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { Box, PropsProvider } from '../Box'; import { Box } from '../Box';
import { patchChildren } from '../../helpers/patchChildren';
export function ButtonGroup({ export function ButtonGroup({
align, align,
...@@ -20,9 +21,7 @@ export function ButtonGroup({ ...@@ -20,9 +21,7 @@ export function ButtonGroup({
role='group' role='group'
{...props} {...props}
> >
<PropsProvider children={children} fn={({ className }) => ({ {patchChildren(children, { className: 'rcx-button-group__item' })}
className: [className, 'rcx-button-group__item'],
})} />
</Box>; </Box>;
} }
......
...@@ -8,53 +8,19 @@ ...@@ -8,53 +8,19 @@
align-items: center; align-items: center;
& > .rcx-button-group__item {
margin-inline: lengths.margin(8);
&:first-child {
margin-inline-start: lengths.margin(none);
}
&:last-child {
margin-inline-end: lengths.margin(none);
}
}
&--wrap { &--wrap {
flex-wrap: wrap; flex-wrap: wrap;
margin-block-end: lengths.margin(-16); margin-block-end: lengths.margin(-16);
& > .rcx-button-group__item {
margin-block-end: lengths.margin(16);
margin-inline-start: lengths.margin(none);
margin-inline-end: lengths.margin(16);
}
} }
&--stretch { &--stretch {
justify-content: stretch; justify-content: stretch;
align-items: stretch;
& > .rcx-button-group__item {
flex-grow: 1;
}
} }
&--vertical { &--vertical {
flex-direction: column; flex-direction: column;
& > .rcx-button-group__item {
margin-block-end: lengths.margin(16);
margin-inline: lengths.margin(none);
&:last-child {
margin-block-end: lengths.margin(none);
}
}
}
&--vertical#{&}--stretch {
align-items: stretch;
} }
&--align-start { &--align-start {
...@@ -69,3 +35,40 @@ ...@@ -69,3 +35,40 @@
justify-content: flex-end; justify-content: flex-end;
} }
} }
.rcx-button-group__item {
.rcx-button-group > & {
margin-inline: lengths.margin(8);
&:first-child {
margin-inline-start: lengths.margin(none);
}
&:last-child {
margin-inline-end: lengths.margin(none);
}
}
.rcx-button-group--wrap > & {
margin-block-end: lengths.margin(16);
margin-inline-start: lengths.margin(none);
margin-inline-end: lengths.margin(16);
}
.rcx-button-group--stretch > & {
flex-grow: 1;
}
.rcx-button-group--vertical > & {
margin-block: lengths.margin(8);
margin-inline: lengths.margin(none);
&:first-child {
margin-block-start: lengths.margin(none);
}
&:last-child {
margin-block-end: lengths.margin(none);
}
}
}
import React, { useMemo } from 'react'; import React from 'react';
import { patchChildren } from '../../helpers/patchChildren';
import { Box } from '../Box'; import { Box } from '../Box';
export function FieldGroup({ export function FieldGroup({ children, ...props }) {
children, return <Box is='fieldset' rcx-field-group role='group' {...props}>
...props {patchChildren(children, { className: 'rcx-field-group__item' })}
}) { </Box>;
const wrappedChildren = useMemo(() =>
React.Children.map(children, (child, index) =>
<Box key={index} rcx-field-group__item children={child} />), [children]);
return <Box is='fieldset' rcx-field-group children={wrappedChildren} role='group' {...props} />;
} }
...@@ -10,11 +10,13 @@ ...@@ -10,11 +10,13 @@
} }
.rcx-field-group__item { .rcx-field-group__item {
flex: 0 0 auto; .rcx-field-group > & {
flex: 0 0 auto;
margin-block-end: lengths.margin(24); width: lengths.size(full);
}
&:last-child { & + & {
margin-block-end: lengths.margin(none); margin-block-start: lengths.margin(24);
} }
} }
import { css } from '@rocket.chat/css-in-js'; import { css } from '@rocket.chat/css-in-js';
import { memoize } from './memoize'; import { cssSupports } from './cssSupports';
export const cssSupports = memoize((value) => typeof window !== 'undefined' && window.CSS && window.CSS.supports(value));
export const createLogicalProperties = ({ export const createLogicalProperties = ({
blockPropertyName, blockPropertyName,
......
import { toClassName } from '@rocket.chat/css-in-js';
export const mergeProps = (targetProps, sourceProps, transforms = []) => {
const initialProps = {
...targetProps,
...sourceProps,
className: [
...Array.isArray(targetProps?.className) ? targetProps?.className : [targetProps?.className],
...Array.isArray(sourceProps?.className) ? sourceProps?.className : [sourceProps?.className],
],
style: {
...targetProps?.style,
...sourceProps?.style,
},
};
const mergedProps = transforms.reduce((props, transform) => transform(props), initialProps);
mergedProps.className = Array.from(
new Set(
mergedProps.className.map(toClassName).filter(Boolean),
),
).join(' ');
return mergedProps;
};
import flattenChildren from 'react-keyed-flatten-children';