Skip to content

Commit 6694b06

Browse files
Elena RashkovanLena Rashkovan
andauthored
feat(dialog): add experimental dialog (#490)
* feat(modal): add experimental modal component * feat(backdrop): add experimental backdrop component * feat(dialog): add experimental dialog component * chore: update stylelint rule --------- Co-authored-by: Lena Rashkovan <[email protected]>
1 parent 153f857 commit 6694b06

File tree

8 files changed

+249
-20
lines changed

8 files changed

+249
-20
lines changed

.storybook/preview.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useLayoutEffect } from 'react';
22
import { I18nProvider } from 'react-aria-components';
33
import { Preview } from '@storybook/react';
44
import { themes } from '@storybook/theming';
@@ -44,12 +44,14 @@ export const withTheme = (Story, context) => {
4444
const { theme } = context.globals;
4545
const { Colors, font } = THEMES[theme];
4646

47+
useLayoutEffect(() => {
48+
document.body.style.fontFamily = font;
49+
}, []);
50+
4751
return (
4852
<>
4953
<Colors />
50-
<div style={{ fontFamily: font }}>
51-
<Story {...context} />
52-
</div>
54+
<Story {...context} />
5355
</>
5456
);
5557
};
@@ -106,13 +108,13 @@ export const preview: Preview = {
106108
container: props => {
107109
const scheme = useDarkMode() ? DarkTheme : LightTheme;
108110
const globals = props.context.store.globals.get();
109-
const { Colors, font } = THEMES[globals.theme];
111+
const { Colors } = THEMES[globals.theme];
110112

111113
return (
112-
<div style={{ fontFamily: font }}>
114+
<>
113115
<Colors />
114116
<DocsContainer {...props} theme={scheme} />
115-
</div>
117+
</>
116118
);
117119
},
118120
toc: {

.stylelintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"rules": {
1111
"declaration-empty-line-before": null,
1212
"declaration-property-unit-whitelist": {
13-
"/.*/": ["rem", "deg", "fr", "ms", "%", "px"]
13+
"/.*/": ["rem", "deg", "fr", "ms", "%", "px", "vw"]
1414
},
1515
"declaration-property-value-blacklist": {
1616
"/.*/": ["(\\d+[1]+px|[^1]+px)"]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import styled from 'styled-components';
2+
import { ModalOverlayProps, ModalOverlay } from 'react-aria-components';
3+
import { getSemanticHslValue } from '../../../essentials/experimental';
4+
import { Elevation } from '../../../essentials';
5+
6+
type BackdropProps = ModalOverlayProps;
7+
8+
const Backdrop = styled(ModalOverlay)`
9+
position: fixed;
10+
top: 0;
11+
left: 0;
12+
width: 100vw;
13+
height: var(--visual-viewport-height);
14+
background: hsla(${getSemanticHslValue('on-surface')}, 60%);
15+
display: flex;
16+
align-items: center;
17+
justify-content: center;
18+
z-index: ${Elevation.DIMMING};
19+
20+
&[data-entering] {
21+
animation: backdrop-fade 200ms;
22+
}
23+
24+
&[data-exiting] {
25+
animation: backdrop-fade 150ms reverse ease-in;
26+
}
27+
28+
@keyframes backdrop-fade {
29+
from {
30+
opacity: 0;
31+
}
32+
to {
33+
opacity: 1;
34+
}
35+
}
36+
`;
37+
38+
export { Backdrop, BackdropProps };
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React, { ReactElement, ReactNode } from 'react';
2+
import { Heading } from 'react-aria-components';
3+
import styled from 'styled-components';
4+
import { Text, textStyles } from '../Text/Text';
5+
import { Modal } from '../Modal/Modal';
6+
import { Backdrop, BackdropProps } from '../Backdrop/Backdrop';
7+
import { getSemanticValue } from '../../../essentials/experimental';
8+
9+
const Card = styled.div`
10+
display: grid;
11+
gap: 0.5rem;
12+
`;
13+
14+
const ButtonsWrapper = styled.div`
15+
padding-top: 1.5rem;
16+
display: flex;
17+
flex-direction: row;
18+
justify-content: flex-end;
19+
gap: 1rem;
20+
`;
21+
22+
const StyledModal = styled(Modal)`
23+
width: 30rem;
24+
`;
25+
26+
const HeadlineText = styled(Heading)`
27+
margin: 0;
28+
${textStyles.variants.headline}
29+
`;
30+
31+
const SubtitleText = styled(Text)`
32+
color: ${getSemanticValue('on-surface-variant')};
33+
`;
34+
35+
interface DialogProps extends Omit<BackdropProps, 'isDismissable' | 'isKeyboardDismissDisabled'> {
36+
role?: 'dialog' | 'alertdialog';
37+
headline: ReactNode;
38+
subtitle: ReactNode;
39+
dismissButton: ReactNode;
40+
actionButton: ReactNode;
41+
}
42+
43+
const Dialog = ({
44+
role = 'dialog',
45+
headline,
46+
subtitle,
47+
dismissButton,
48+
actionButton,
49+
...props
50+
}: DialogProps): ReactElement => (
51+
<Backdrop {...props} isDismissable={false} isKeyboardDismissDisabled>
52+
<StyledModal role={role}>
53+
<Card>
54+
<HeadlineText slot="title">{headline}</HeadlineText>
55+
56+
<SubtitleText as="p" variant="body1">
57+
{subtitle}
58+
</SubtitleText>
59+
60+
<ButtonsWrapper>
61+
{dismissButton}
62+
{actionButton}
63+
</ButtonsWrapper>
64+
</Card>
65+
</StyledModal>
66+
</Backdrop>
67+
);
68+
69+
export { Dialog, DialogProps };
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React, { useState } from 'react';
2+
import { DialogTrigger } from 'react-aria-components';
3+
import { StoryObj, Meta } from '@storybook/react';
4+
import { action } from '@storybook/addon-actions';
5+
import { Dialog } from '../Dialog';
6+
import { Button } from '../../Button/Button';
7+
import { WarningIcon } from '../../../../icons';
8+
9+
const meta: Meta = {
10+
title: 'Experimental/Components/Dialog',
11+
component: Dialog,
12+
parameters: {
13+
layout: 'centered'
14+
}
15+
};
16+
17+
export default meta;
18+
19+
type Story = StoryObj<typeof Dialog>;
20+
21+
export const Default: Story = {
22+
render: () => {
23+
const [isOpen, setIsOpen] = useState(false);
24+
25+
return (
26+
<>
27+
<Button onPress={() => setIsOpen(true)}>Open a dialog</Button>
28+
<Dialog
29+
isOpen={isOpen}
30+
onOpenChange={setIsOpen}
31+
headline="Are you sure?"
32+
subtitle="This action cannot be undone"
33+
dismissButton={
34+
<Button emphasis="secondary" onPress={() => setIsOpen(false)}>
35+
Cancel
36+
</Button>
37+
}
38+
actionButton={
39+
<Button
40+
onPress={() => {
41+
action('Action')();
42+
setIsOpen(false);
43+
}}
44+
>
45+
<WarningIcon /> I do not care, do it
46+
</Button>
47+
}
48+
/>
49+
</>
50+
);
51+
}
52+
};
53+
54+
export const Alert: Story = {
55+
render: () => {
56+
const [isOpen, setIsOpen] = useState(false);
57+
58+
return (
59+
<>
60+
<Button onPress={() => setIsOpen(true)}>Trigger alert</Button>
61+
<Dialog
62+
isOpen={isOpen}
63+
onOpenChange={setIsOpen}
64+
role="alertdialog"
65+
headline="Error"
66+
subtitle="We could not do the action"
67+
dismissButton={
68+
<Button emphasis="secondary" onPress={() => setIsOpen(false)}>
69+
Dismiss
70+
</Button>
71+
}
72+
actionButton={
73+
<Button
74+
onPress={() => {
75+
action('Action retry')();
76+
setIsOpen(false);
77+
}}
78+
>
79+
Try again
80+
</Button>
81+
}
82+
/>
83+
</>
84+
);
85+
}
86+
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from 'react';
2+
import styled from 'styled-components';
3+
import { Dialog, DialogProps, Modal as BaseModal } from 'react-aria-components';
4+
import { getSemanticValue } from '../../../essentials/experimental';
5+
6+
const ModalCard = styled(BaseModal)`
7+
padding: 2rem;
8+
border-radius: 1.5rem;
9+
background: ${getSemanticValue('surface')};
10+
color: ${getSemanticValue('on-surface')};
11+
outline: none;
12+
13+
&[data-entering] {
14+
animation: modal-zoom 300ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
15+
}
16+
17+
@keyframes modal-zoom {
18+
from {
19+
transform: scale(0.8);
20+
}
21+
to {
22+
transform: scale(1);
23+
}
24+
}
25+
`;
26+
27+
const StyledDialog = styled(Dialog)`
28+
outline: none;
29+
`;
30+
31+
type ModalProps = Pick<DialogProps, 'children' | 'role'>;
32+
33+
const Modal = React.forwardRef<HTMLDivElement, ModalProps>((props, ref) => (
34+
<ModalCard ref={ref}>
35+
<StyledDialog {...props} />
36+
</ModalCard>
37+
));
38+
39+
export { Modal, ModalProps };

src/components/experimental/Text/Text.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import React from 'react';
2-
import { Text as BaseText, TextContext, useContextProps, TextProps as BaseTextProps } from 'react-aria-components';
1+
import { Text as BaseText, TextProps as BaseTextProps } from 'react-aria-components';
32
import styled from 'styled-components';
43
import { compose, ResponsiveValue, variant } from 'styled-system';
54
import { theme } from '../../../essentials/experimental';
65

76
interface TextProps extends BaseTextProps {
8-
as?: React.ElementType;
97
variant?: ResponsiveValue<'display' | 'headline' | 'title1' | 'title2' | 'body1' | 'body2' | 'label1' | 'label2'>;
108
}
119

@@ -64,19 +62,15 @@ export const textStyles = {
6462
}
6563
};
6664

67-
const variantStyles = variant(textStyles);
68-
69-
const StyledText = styled(BaseText).attrs({ theme })<TextProps>`
65+
const Text = styled(BaseText)<TextProps>`
7066
color: inherit;
7167
margin: 0;
7268
73-
${compose(variantStyles)}
69+
${compose(variant(textStyles))}
7470
`;
7571

76-
const Text = React.forwardRef((textProps: TextProps, forwardedRef: React.ForwardedRef<HTMLElement>) => {
77-
const [props, ref] = useContextProps(textProps, forwardedRef, TextContext);
78-
79-
return <StyledText {...props} variant={textProps.variant || 'body1'} ref={ref} />;
80-
});
72+
Text.defaultProps = {
73+
variant: 'body1'
74+
};
8175

8276
export { Text, TextProps };

src/components/experimental/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export { Chip } from './Chip/Chip';
44
export { ComboBox } from './ComboBox/ComboBox';
55
export { DateField } from './DateField/DateField';
66
export { DatePicker } from './DatePicker/DatePicker';
7+
export { Dialog } from './Dialog/Dialog';
78
export { Divider } from './Divider/Divider';
89
export { IconButton } from './IconButton/IconButton';
910
export { Label } from './Label/Label';

0 commit comments

Comments
 (0)