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 @@
"@rocket.chat/css-in-js": "^0.9.0",
"@rocket.chat/fuselage-tokens": "^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": {
"@babel/core": "^7.4.5",
......
import PropTypes from 'prop-types';
import React, { useContext, createContext } from 'react';
import flattenChildren from 'react-keyed-flatten-children';
import { Box } from '../Box';
import { sizePropType } from '../../styles/props/layout';
......@@ -48,7 +49,7 @@ Avatar.propTypes = {
const AvatarStack = ({ children, ...props }) =>
<Box rcx-avatar-stack {...props}>
{React.Children.toArray(children).reverse()}
{flattenChildren(children).reverse()}
</Box>;
Avatar.Stack = AvatarStack;
import PropTypes from 'prop-types';
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 { useStyleSheet } from './useStyleSheet';
import { mergeProps } from './mergeProps';
import { marginPropType, paddingPropType } from '../../styles/props/spaces';
import { colorPropType } from '../../styles/props/colors';
import { fontFamilyPropType, fontScalePropType } from '../../styles/props/typography';
import { sizePropType } from '../../styles/props/layout';
import { insetPropType } from '../../styles/props/position';
import { mapSpecialProps } from '../../styles/props/special';
import { mapClassNames } from './mapClassNames';
import { mapFlexBoxProps } from '../../styles/props/flexBox';
import { mapBorderProps } from '../../styles/props/borders';
const transforms = [
mapBorderProps,
mapColorProps,
mapFlexBoxProps,
mapLayoutProps,
mapPositionProps,
mapSpaceProps,
mapTypographyProps,
mapSpecialProps,
mapClassNames,
];
export const Box = memo(forwardRef(function Box(props, ref) {
useStyleSheet();
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);
......
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 { Meta, Preview, Props, Story } from '@storybook/addon-docs/blocks';
import flattenChildren from 'react-keyed-flatten-children';
import { Box, Button } from '../..';
<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
name='Margins'
decorators={[
(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' />)}
</Box>)}
</Box>
......@@ -64,7 +65,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Paddings'
decorators={[
(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' />)}
</Box>)}
</Box>
......@@ -98,7 +99,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Colors'
decorators={[
(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))}
</Box>
]}
......@@ -141,7 +142,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Background colors'
decorators={[
(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>
]}
>
......@@ -364,7 +365,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Widths'
decorators={[
(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' }))}
</Box>
]}
......@@ -385,7 +386,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Heights'
decorators={[
(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' }))}
</Box>
]}
......@@ -406,7 +407,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Sizes'
decorators={[
(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' }))}
</Box>
]}
......@@ -426,7 +427,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Display'
decorators={[
(fn) => <Box>
{React.Children.map(fn().props.children, (child) =>
{flattenChildren(fn().props.children).map((child) =>
React.cloneElement(child, { children: child.props.display }))}
</Box>
]}
......@@ -450,7 +451,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Vertical alignment'
decorators={[
(fn) => <Box>
{React.Children.map(fn().props.children, (child) =>
{flattenChildren(fn().props.children).map((child) =>
React.cloneElement(child, { display: 'inline', children: child.props.verticalAlign }))}
</Box>
]}
......@@ -476,7 +477,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Borders'
decorators={[
(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' }))}
</Box>
]}
......@@ -521,7 +522,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Border radii'
decorators={[
(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' }))}
</Box>
]}
......@@ -545,7 +546,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Position'
decorators={[
(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' }))}
</Box>
]}
......@@ -567,7 +568,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Z-index'
decorators={[
(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' }))}
</Box>
]}
......@@ -588,9 +589,10 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Inset'
decorators={[
(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'>
{React.cloneElement(child, { bg:'primary-200', position: 'absolute', minSize: 'x16' })}
</Box>)}
{flattenChildren(fn().props.children).map((child) =>
<Box key={child.key} position='relative' bg='neutral-200' m='x16' size='x64'>
{React.cloneElement(child, { bg:'primary-200', position: 'absolute', minSize: 'x16' })}
</Box>)}
</Box>
]}
>
......@@ -620,7 +622,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Elevation'
decorators={[
(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' }))}
</Box>
]}
......@@ -640,7 +642,7 @@ The `is` prop allows `Box` to render any component capable of handling common DO
name='Invisible'
decorators={[
(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' })}
</Box>)}
</Box>
......
import PropTypes from 'prop-types';
import React from 'react';
import { Box, PropsProvider } from '../Box';
import { Box } from '../Box';
import { patchChildren } from '../../helpers/patchChildren';
export function ButtonGroup({
align,
......@@ -20,9 +21,7 @@ export function ButtonGroup({
role='group'
{...props}
>
<PropsProvider children={children} fn={({ className }) => ({
className: [className, 'rcx-button-group__item'],
})} />
{patchChildren(children, { className: 'rcx-button-group__item' })}
</Box>;
}
......
......@@ -8,53 +8,19 @@
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 {
flex-wrap: wrap;
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 {
justify-content: stretch;
& > .rcx-button-group__item {
flex-grow: 1;
}
align-items: stretch;
}
&--vertical {
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 {
......@@ -69,3 +35,40 @@
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';
export function FieldGroup({
children,
...props
}) {
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} />;
export function FieldGroup({ children, ...props }) {
return <Box is='fieldset' rcx-field-group role='group' {...props}>
{patchChildren(children, { className: 'rcx-field-group__item' })}
</Box>;
}
......@@ -10,11 +10,13 @@
}
.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 { memoize } from './memoize';
export const cssSupports = memoize((value) => typeof window !== 'undefined' && window.CSS && window.CSS.supports(value));
import { cssSupports } from './cssSupports';
export const createLogicalProperties = ({
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';
import { cloneElement } from 'react';
import { mergeProps } from './mergeProps';
import { shallowEqual } from './shallowEqual';
export const patchChildren = (children, props, transforms) => {
let dirty = false;
const newChildren = flattenChildren(children).map((child) => {
const mergedProps = mergeProps(child.props, props, transforms);
if (shallowEqual(child.props, mergedProps)) {
return child;
}
dirty = true;
return cloneElement(child, mergedProps);
});
if (dirty) {
return newChildren;
}
return children;
};
const isObject = (value) => typeof value === 'object' && value !== null;
const compareObjects = (a, b) => {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) {
return false;
}
return !keysA.some((key) => !b.hasOwnProperty(key) || a[key] !== b[key]);
};
export const shallowEqual = (a, b) => {
if (a === b) {
return true;
}
if ([a, b].every(Number.isNaN)) {
return true;
}
if (![a, b].every(isObject)) {
return false;
}
return compareObjects(a, b);
};
......@@ -14587,11 +14587,18 @@ react-inspector@^4.0.0:
is-dom "^1.0.9"
prop-types "^15.6.1"
 
react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1:
react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
 
react-keyed-flatten-children@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/react-keyed-flatten-children/-/react-keyed-flatten-children-1.2.0.tgz#44c741f9f5acca1a7294c3f7a11f1d31c417c1e1"
integrity sha512-gJoD3br3tK59+AOKlzb+7G39YtkSC4gWjQjDbWOmHjW7pxgYomnRBemKXuNBafnPYSKpjp7evOlxtGEHwiP11g==
dependencies:
react-is "^16.8.6"
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment