Unverified Commit f77ae41f authored by Tasso Evangelista's avatar Tasso Evangelista Committed by GitHub

perf: Modal (#349)

parent 24da5e75
......@@ -2,28 +2,53 @@ import React from 'react';
import { Box } from '../src';
export function PropsVariationSection({ component: Component, common = {}, xAxis = {}, yAxis = {} }) {
return <Box is='table' marginBlock='x16' marginInline='auto' style={{ borderCollapse: 'collapse' }}>
<Box is='thead'>
<Box is='tr'>
<Box is='th' />
{Object.keys(xAxis).map((xVariation, key) =>
<Box key={key} is='th' color='hint' fontScale='c1'>{xVariation}</Box>)}
</Box>
</Box>
<Box is='tbody'>
{Object.entries(yAxis).map(([yVariation, yProps], y) => (
<Box key={y} is='tr'>
<Box is='th' color='hint' fontScale='c1'>{yVariation}</Box>
{Object.values(xAxis).map((xProps, x) => <Box key={x} is='td' margin='none' paddingBlock='x8' paddingInline='x16'>
<Box display='flex' alignItems='center' justifyContent='center'>
<Component {...common} {...xProps} {...yProps} />
export function PropsVariationSection({
component: Component,
common = {},
xAxis = {},
yAxis = {},
}) {
return (
<Box
is='table'
marginBlock='x16'
marginInline='auto'
style={{ borderCollapse: 'collapse' }}
>
<Box is='thead'>
<Box is='tr'>
<Box is='th' />
{Object.keys(xAxis).map((xVariation, key) => (
<Box key={key} is='th' color='hint' fontScale='c1'>
{xVariation}
</Box>
</Box>)}
))}
</Box>
))}
</Box>
<Box is='tbody'>
{Object.entries(yAxis).map(([yVariation, yProps], y) => (
<Box key={y} is='tr'>
<Box is='th' color='hint' fontScale='c1'>
{yVariation}
</Box>
{Object.values(xAxis).map((xProps, x) => (
<Box
key={x}
is='td'
margin='none'
paddingBlock='x8'
paddingInline='x16'
>
<Box display='flex' alignItems='center' justifyContent='center'>
<Component {...common} {...xProps} {...yProps} />
</Box>
</Box>
))}
</Box>
))}
</Box>
</Box>
</Box>;
);
}
export const exampleAvatar = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAA
......
......@@ -4,14 +4,13 @@ module.exports = {
addons: [
'@storybook/addon-actions',
'@storybook/addon-backgrounds',
'@storybook/addon-controls',
'@storybook/addon-docs',
'@storybook/addon-knobs',
'@storybook/addon-viewport',
...process.env.NODE_ENV === 'production' ? ['@storybook/addon-jest'] : [],
],
stories: [
'../src/**/*.stories.{mdx,js}',
...(process.env.NODE_ENV === 'production' ? ['@storybook/addon-jest'] : []),
],
stories: ['../src/**/*.stories.{mdx,js}'],
webpackFinal: (config) => {
config.module.rules.push({
test: /\.scss$/,
......
......@@ -7,30 +7,33 @@ import '@rocket.chat/icons/dist/rocketchat.css';
import '@rocket.chat/fuselage-polyfills';
addParameters({
background: {
grid: {
cellSize: 4,
},
},
docs: {
container: DocsContainer,
page: DocsPage,
},
grid: {
cellSize: 4,
},
options: {
showRoots: true,
storySort: ([, a], [, b]) =>
a.kind.localeCompare(b.kind),
storySort: ([, a], [, b]) => a.kind.localeCompare(b.kind),
},
viewport: {
viewports: Object.entries(breakpointTokens).reduce((obj, [name, { minViewportWidth }]) => ({
...obj,
[name]: {
name,
styles: {
width: `${ minViewportWidth }px`,
height: '90%',
viewports: Object.entries(breakpointTokens).reduce(
(obj, [name, { minViewportWidth }]) => ({
...obj,
[name]: {
name,
styles: {
width: `${minViewportWidth}px`,
height: '90%',
},
type: 'desktop',
},
type: 'desktop',
},
}), {}),
}),
{}
),
},
});
......
declare module '@rocket.chat/fuselage' {
import { css } from '@rocket.chat/css-in-js';
import type { css } from '@rocket.chat/css-in-js';
import {
AllHTMLAttributes,
Context,
......@@ -226,19 +227,21 @@ declare module '@rocket.chat/fuselage' {
};
type ModalProps = BoxProps;
type ModalHeaderProps = BoxProps;
type ModalTitleProps = BoxProps;
type ModalBackdropProps = BoxProps;
type ModalCloseProps = BoxProps;
type ModalContentProps = BoxProps;
type ModalFooterProps = BoxProps;
type ModalBackdropProps = BoxProps;
type ModalHeaderProps = BoxProps;
type ModalThumbProps = BoxProps;
type ModalTitleProps = BoxProps;
export const Modal: ForwardRefExoticComponent<ModalProps> & {
Header: ForwardRefExoticComponent<ModalHeaderProps>;
Title: ForwardRefExoticComponent<ModalTitleProps>;
Backdrop: ForwardRefExoticComponent<ModalBackdropProps>;
Close: ForwardRefExoticComponent<ModalCloseProps>;
Content: ForwardRefExoticComponent<ModalContentProps>;
Footer: ForwardRefExoticComponent<ModalFooterProps>;
Backdrop: ForwardRefExoticComponent<ModalBackdropProps>;
Header: ForwardRefExoticComponent<ModalHeaderProps>;
Thumb: ForwardRefExoticComponent<ModalThumbProps>;
Title: ForwardRefExoticComponent<ModalTitleProps>;
};
type NumberInputProps = BoxProps;
......
......@@ -67,6 +67,7 @@
"@rocket.chat/fuselage-polyfills": "^0.19.0",
"@storybook/addon-actions": "^6.0.21",
"@storybook/addon-backgrounds": "^6.0.21",
"@storybook/addon-controls": "^6.1.10",
"@storybook/addon-docs": "^6.0.21",
"@storybook/addon-jest": "^6.0.21",
"@storybook/addon-knobs": "^6.0.21",
......
import React from 'react';
import { Box } from '../Box';
export const Modal = React.forwardRef(({ children, ...props }, ref) => (
<Box is='dialog' rcx-modal {...props}>
<Box ref={ref} rcx-modal__inner elevation='2'>
{children}
</Box>
</Box>
));
import React from 'react';
import ReactDOM from 'react-dom';
import { Modal } from '../..';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<Modal />, div);
ReactDOM.unmountComponentAtNode(div);
});
import React from 'react';
import { ButtonGroup, Button, Modal } from '../..';
export default {
title: 'Containers/Modal',
component: Modal,
parameters: {
jest: ['Modal/Modal.spec.js'],
},
};
export const _Modal = () => (
<Modal>
<Modal.Header>
<Modal.Thumb url='data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==' />
<Modal.Title>Modal Header</Modal.Title>
<Modal.Close />
</Modal.Header>
<Modal.Content>Modal Body</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button>Cancel</Button>
<Button primary>Submit</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>
);
import { Meta, Canvas, ArgsTable, Story } from '@storybook/addon-docs/blocks';
import { ButtonGroup, Button, Modal } from '../..';
<Meta title='Containers/Modal' parameters={{ jest: ['Modal/spec'] }} />
# Modal
<Canvas>
<Story name='Default'>
<Modal>
<Modal.Header>
<Modal.Thumb url='data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==' />
<Modal.Title>Modal Header</Modal.Title>
<Modal.Close/>
</Modal.Header>
<Modal.Content>
Modal Body
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button>Cancel</Button>
<Button primary>Submit</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>
</Story>
</Canvas>
<ArgsTable of={Modal} />
## Modal.Header
<ArgsTable of={Modal.Header} />
## Modal.Thumb
<ArgsTable of={Modal.Thumb} />
## Modal.Title
<ArgsTable of={Modal.Title} />
## Modal.Close
<ArgsTable of={Modal.Close} />
## Modal.Content
<ArgsTable of={Modal.Content} />
## Modal.Footer
<ArgsTable of={Modal.Footer} />
......@@ -5,34 +5,68 @@
.rcx-modal {
position: static;
width: lengths.size(full);
max-height: lengths.size(full);
display: flex;
width: 100%;
max-height: 100%;
margin: auto;
background: none;
&__inner {
min-width: lengths.size(none);
display: flex;
flex-direction: column;
flex-grow: 1;
min-width: 0;
padding: 0;
color: colors.foreground(default);
border-radius: 2px;
background-color: colors.surface();
@include typography.use-font-scale(p1);
}
&__header {
margin: 32px;
}
&__header-inner {
display: flex;
flex-wrap: nowrap;
align-items: center;
margin: -8px;
}
&__title {
@include typography.use-text-ellipsis;
flex-grow: 1;
flex-shrink: 1;
white-space: nowrap;
color: colors.foreground('default');
@include typography.use-font-scale(h1);
}
&__backdrop {
position: fixed;
z-index: 100;
inset: lengths.inset(none);
top: 0;
bottom: 0;
left: 0;
right: 0;
inset: 0;
background-color: colors.neutral(800, $alpha: 0.5);
}
&__footer {
margin: 32px;
}
&__content-wrapper {
margin-inline: 32px;
}
@include on-breakpoint(sm) {
max-width: lengths.size(640);
padding: lengths.padding(16);
......
import React from 'react';
import { Box } from '../Box';
export const ModalBackdrop = (props) => <Box rcx-modal__backdrop {...props} />;
import React from 'react';
import { Button } from '../Button';
import { Icon } from '../Icon';
export const ModalClose = (props) => (
<Button small ghost flexShrink={0} {...props}>
<Icon name='cross' size='x24' />
</Button>
);
import React from 'react';
import { Box, Scrollable } from '../Box';
export const ModalContent = ({ children, onScrollContent, ...props }) => (
<Scrollable vertical onScrollContent={onScrollContent}>
<Box rcx-modal__content>
<Box rcx-modal__content-wrapper {...props}>
{children}
</Box>
</Box>
</Scrollable>
);
import React from 'react';
import { Box } from '../Box';
export const ModalFooter = ({ children }) => (
<Box rcx-modal__footer>{children}</Box>
);
import React from 'react';
import { Box } from '../Box';
import Margins from '../Margins';
export const ModalHeader = ({ children, ...props }) => (
<Box rcx-modal__header is='header' {...props}>
<Box rcx-modal__header-inner>
<Margins all='x8'>{children}</Margins>
</Box>
</Box>
);
import React from 'react';
import { Avatar } from '../Avatar';
import { Box } from '../Box';
export const ModalThumb = (props) => (
<Box>
<Avatar size='x32' {...props} />
</Box>
);
import React from 'react';
import { Box } from '../Box';
export const ModalTitle = ({ children, ...props }) => (
<Box rcx-modal__title {...props}>
{children}
</Box>
);
import React, { useState, createContext, useContext } from 'react';
import ReactDOM from 'react-dom';
import { AnimatedVisibility, Box } from '../Box';
export const ModalContext = createContext();
export const useModalStack = () => useContext(ModalContext);
const createModalRoot = () => {
const node = document.createElement('div');
document.querySelector('body').appendChild(node);
return node;
};
export const ModalPortal = ({ children, rootElement = createModalRoot() }) =>
ReactDOM.createPortal(children, rootElement);
export const ModalBackdrop = (props) => <Box rcx-modal__backdrop {...props} />;
export function ModalContainer() {
const { stack } = useModalStack();
return (
<ModalPortal>
{stack.size > 0 && <ModalBackdrop />}
{Array.from(stack.entries()).map(([key, element], i, array) => (
<AnimatedVisibility
key={key}
children={element}
visibility={
i === array.length - 1
? AnimatedVisibility.VISIBLE
: AnimatedVisibility.HIDDEN
}
/>
))}
</ModalPortal>
);
}
export function ModalStack({ children }) {
const [stack, setStack] = useState(new Map());
const open = ({ id, ...data }) => {
setStack(new Map([id, data]));
};
const update = ({ id, ...data }) => {
setStack(new Map([id, data]));
};
const push = ({ id, ...data }) => {
setStack(new Map(stack.set(id, data)));
};
const pop = () => {
if (!stack.size) {
return;
}
const key = Array.from(stack.keys()).pop();
stack.delete(key);
setStack(new Map(stack));
};
const close = () => {
setStack(new Map());
};
return (
<ModalContext.Provider value={{ stack, open, push, pop, close, update }}>
{children}
<ModalContainer />
</ModalContext.Provider>
);
}
import React from 'react';
import { Modal } from './Modal';
import { ModalBackdrop } from './ModalBackdrop';
import { ModalClose } from './ModalClose';
import { ModalContent } from './ModalContent';
import { ModalFooter } from './ModalFooter';
import { ModalHeader } from './ModalHeader';
import { ModalThumb } from './ModalThumb';
import { ModalTitle } from './ModalTitle';
import { Avatar } from '../Avatar';
import { Box, Flex, Scrollable } from '../Box';
import { Button } from '../Button';
import { Icon } from '../Icon';
import Margins from '../Margins';
import { Tile } from '../Tile';
import {
ModalBackdrop,
ModalContainer,
ModalPortal,
ModalStack,
useModalStack,
} from './Stack';
export const Modal = React.forwardRef(({ children, ...props }, ref) => (
<Flex.Container>
<Box is='dialog' rcx-modal {...props}>
<Flex.Container direction='column'>