@agregio-solutions/design-system 1.90.1 → 1.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/design-system.cjs +9 -5
- package/dist/design-system.js +14 -6
- package/dist/packages/components/Accordion/doc.md +342 -0
- package/dist/packages/components/Badge/doc.md +192 -0
- package/dist/packages/components/Breadcrumbs/doc.md +332 -0
- package/dist/packages/components/Button/doc.md +425 -0
- package/dist/packages/components/Calendar/doc.md +465 -0
- package/dist/packages/components/ChartLegend/doc.md +151 -0
- package/dist/packages/components/ChartTooltip/doc.md +124 -0
- package/dist/packages/components/Checkbox/doc.md +329 -0
- package/dist/packages/components/CheckboxGroup/doc.md +242 -0
- package/dist/packages/components/Chip/doc.md +99 -0
- package/dist/packages/components/Combobox/Combobox.d.ts +8 -0
- package/dist/packages/components/Combobox/doc.md +680 -0
- package/dist/packages/components/DataTable/doc.md +1124 -0
- package/dist/packages/components/DatePicker/doc.md +579 -0
- package/dist/packages/components/DateRangePicker/doc.md +638 -0
- package/dist/packages/components/Drawer/doc.md +338 -0
- package/dist/packages/components/Dropdown/Dropdown.d.ts +4 -0
- package/dist/packages/components/Dropdown/doc.md +205 -0
- package/dist/packages/components/EmptyState/doc.md +101 -0
- package/dist/packages/components/FileUpload/doc.md +449 -0
- package/dist/packages/components/Filter/doc.md +196 -0
- package/dist/packages/components/Header/doc.md +373 -0
- package/dist/packages/components/I18nProvider/doc.md +187 -0
- package/dist/packages/components/Icon/doc.md +63 -0
- package/dist/packages/components/Label/doc.md +60 -0
- package/dist/packages/components/LinearProgressBar/doc.md +148 -0
- package/dist/packages/components/Link/doc.md +206 -0
- package/dist/packages/components/List/doc.md +481 -0
- package/dist/packages/components/Loader/doc.md +53 -0
- package/dist/packages/components/Menu/Menu.d.ts +5 -1
- package/dist/packages/components/Menu/doc.md +231 -0
- package/dist/packages/components/Message/doc.md +166 -0
- package/dist/packages/components/Modal/doc.md +289 -0
- package/dist/packages/components/Navigation/doc.md +992 -0
- package/dist/packages/components/NavigationItem/doc.md +167 -0
- package/dist/packages/components/NotificationCard/doc.md +206 -0
- package/dist/packages/components/Notifications/doc.md +240 -0
- package/dist/packages/components/NumberField/doc.md +582 -0
- package/dist/packages/components/PageLayout/doc.md +651 -0
- package/dist/packages/components/Pagination/doc.md +227 -0
- package/dist/packages/components/Popover/doc.md +245 -0
- package/dist/packages/components/Radio/doc.md +370 -0
- package/dist/packages/components/RouterProvider/doc.md +64 -0
- package/dist/packages/components/SearchBar/doc.md +504 -0
- package/dist/packages/components/SegmentedControl/doc.md +398 -0
- package/dist/packages/components/Select/Select.d.ts +4 -0
- package/dist/packages/components/Select/doc.md +1133 -0
- package/dist/packages/components/Skeleton/doc.md +129 -0
- package/dist/packages/components/Slider/doc.md +362 -0
- package/dist/packages/components/Stepper/doc.md +104 -0
- package/dist/packages/components/Switch/doc.md +296 -0
- package/dist/packages/components/Tabs/doc.md +295 -0
- package/dist/packages/components/Tag/doc.md +81 -0
- package/dist/packages/components/TextInput/doc.md +490 -0
- package/dist/packages/components/TimeField/doc.md +353 -0
- package/dist/packages/components/Timeline/doc.md +1046 -0
- package/dist/packages/components/Toaster/doc.md +263 -0
- package/dist/packages/components/ToggleButton/doc.md +108 -0
- package/dist/packages/components/ToggleButtonGroup/doc.md +307 -0
- package/dist/packages/components/Tooltip/doc.md +206 -0
- package/dist/packages/components/YearMonthPicker/YearMonthPicker.d.ts +8 -0
- package/dist/packages/components/YearMonthPicker/doc.md +638 -0
- package/dist/public_docs/components.md +68 -0
- package/dist/public_docs/index.md +30 -0
- package/dist/public_docs/tokens.md +121 -0
- package/package.json +3 -2
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Menu
|
|
2
|
+
|
|
3
|
+
## Props
|
|
4
|
+
|
|
5
|
+
The complete Props documentation with JS doc for this component is available at this path:
|
|
6
|
+
|
|
7
|
+
node_modules/@agregio-solutions/design-system/dist/packages/components/Menu/Menu.d.ts
|
|
8
|
+
|
|
9
|
+
## Example usage
|
|
10
|
+
|
|
11
|
+
Here are the Storybook Stories.
|
|
12
|
+
|
|
13
|
+
Base stories:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
17
|
+
import Menu from "./Menu";
|
|
18
|
+
import MenuItem from "../MenuItem/MenuItem";
|
|
19
|
+
|
|
20
|
+
const meta: Meta<typeof Menu> = {
|
|
21
|
+
component: Menu,
|
|
22
|
+
parameters: {
|
|
23
|
+
layout: "centered",
|
|
24
|
+
},
|
|
25
|
+
argTypes: {
|
|
26
|
+
children: { control: false },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
export default meta;
|
|
30
|
+
|
|
31
|
+
type Story = StoryObj<typeof meta>;
|
|
32
|
+
|
|
33
|
+
export const Playground: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
buttonProps: {
|
|
36
|
+
iconLeft: "menu",
|
|
37
|
+
mode: "secondary",
|
|
38
|
+
},
|
|
39
|
+
children: (
|
|
40
|
+
<>
|
|
41
|
+
<MenuItem onClick={() => alert("open")} text="Open" />
|
|
42
|
+
<MenuItem onClick={() => alert("rename")} text="Rename…" />
|
|
43
|
+
<MenuItem onClick={() => alert("duplicate")} text="Duplicate" />
|
|
44
|
+
<MenuItem onClick={() => alert("share")} text="Share…" />
|
|
45
|
+
<MenuItem
|
|
46
|
+
onClick={() => alert("disabled")}
|
|
47
|
+
text="Disabled"
|
|
48
|
+
isDisabled
|
|
49
|
+
/>
|
|
50
|
+
</>
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const OpenByDefault: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
...Playground.args,
|
|
58
|
+
defaultOpen: true,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## How to test this component
|
|
64
|
+
|
|
65
|
+
Here are some more advanced stories with more testing coverage and examples that you can read to understand how to test this component.
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
69
|
+
import { expect, userEvent, within, screen } from "storybook/test";
|
|
70
|
+
import Menu from "../Menu";
|
|
71
|
+
|
|
72
|
+
import * as MenuStories from "../Menu.stories";
|
|
73
|
+
import MenuItem from "@packages/components/MenuItem/MenuItem";
|
|
74
|
+
import { useState } from "react";
|
|
75
|
+
|
|
76
|
+
const meta: Meta<typeof Menu> = {
|
|
77
|
+
component: Menu,
|
|
78
|
+
...MenuStories.default,
|
|
79
|
+
parameters: {
|
|
80
|
+
...MenuStories.default.parameters,
|
|
81
|
+
chromatic: { disableSnapshot: true },
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
export default meta;
|
|
85
|
+
|
|
86
|
+
type Story = StoryObj<typeof meta>;
|
|
87
|
+
|
|
88
|
+
export const ExampleUsage: Story = {
|
|
89
|
+
render: () => {
|
|
90
|
+
const Parent = () => {
|
|
91
|
+
const [selectedItem, setSelectedItem] = useState("");
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div>
|
|
95
|
+
<p>The selected item is: {selectedItem}</p>
|
|
96
|
+
|
|
97
|
+
<Menu
|
|
98
|
+
buttonProps={{
|
|
99
|
+
iconLeft: "menu",
|
|
100
|
+
"aria-label": "Menu",
|
|
101
|
+
mode: "secondary",
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<MenuItem text="Item 1" onClick={() => setSelectedItem("Item 1")} />
|
|
105
|
+
<MenuItem text="Item 2" onClick={() => setSelectedItem("Item 2")} />
|
|
106
|
+
<MenuItem text="Item 3" onClick={() => setSelectedItem("Item 3")} />
|
|
107
|
+
<MenuItem
|
|
108
|
+
text="Disabled item"
|
|
109
|
+
onClick={() => setSelectedItem("Item disabled")}
|
|
110
|
+
isDisabled
|
|
111
|
+
/>
|
|
112
|
+
</Menu>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return <Parent />;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
export const ControlledOpenState: Story = {
|
|
121
|
+
render: () => {
|
|
122
|
+
const Parent = () => {
|
|
123
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Menu
|
|
127
|
+
isOpen={isOpen}
|
|
128
|
+
onOpenChange={setIsOpen}
|
|
129
|
+
buttonProps={{
|
|
130
|
+
iconLeft: "menu",
|
|
131
|
+
"aria-label": "Menu",
|
|
132
|
+
mode: "secondary",
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<MenuItem text="Item 1" />
|
|
136
|
+
<MenuItem text="Item 2" />
|
|
137
|
+
</Menu>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return <Parent />;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const TestShouldOpenAndClickOnItem: Story = {
|
|
146
|
+
render: ExampleUsage.render,
|
|
147
|
+
play: async ({ canvasElement }) => {
|
|
148
|
+
const canvas = within(canvasElement);
|
|
149
|
+
const user = userEvent.setup();
|
|
150
|
+
await user.click(canvas.getByLabelText("Menu"));
|
|
151
|
+
await screen.findByText("Item 1");
|
|
152
|
+
await user.click(screen.getByText("Item 1"));
|
|
153
|
+
expect(
|
|
154
|
+
canvas.getByText("The selected item is: Item 1"),
|
|
155
|
+
).toBeInTheDocument();
|
|
156
|
+
await expect(canvas.queryByText("Item 1")).not.toBeInTheDocument();
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const TestShouldNotOpenAndClickOnDisabledItem: Story = {
|
|
161
|
+
render: ExampleUsage.render,
|
|
162
|
+
play: async ({ canvasElement }) => {
|
|
163
|
+
const canvas = within(canvasElement);
|
|
164
|
+
const user = userEvent.setup();
|
|
165
|
+
await user.click(canvas.getByLabelText("Menu"));
|
|
166
|
+
await screen.findByText("Disabled item");
|
|
167
|
+
await user.click(screen.getByText("Disabled item"));
|
|
168
|
+
expect(
|
|
169
|
+
canvas.queryByText("The selected item is: Disabled item"),
|
|
170
|
+
).not.toBeInTheDocument();
|
|
171
|
+
await screen.findByText("Disabled item");
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Developer notes
|
|
177
|
+
|
|
178
|
+
Here are the notes available for the developer on the built Storybook, you can read them to understand the component and how to use it.
|
|
179
|
+
|
|
180
|
+
```mdx
|
|
181
|
+
import {
|
|
182
|
+
Canvas,
|
|
183
|
+
Meta,
|
|
184
|
+
Stories,
|
|
185
|
+
Controls,
|
|
186
|
+
Source,
|
|
187
|
+
} from "@storybook/addon-docs/blocks";
|
|
188
|
+
|
|
189
|
+
import * as Menu from "./Menu.stories";
|
|
190
|
+
import * as MenuTests from "./tests/Menu.stories";
|
|
191
|
+
|
|
192
|
+
<Meta of={Menu} />
|
|
193
|
+
|
|
194
|
+
# Menu
|
|
195
|
+
|
|
196
|
+
A menu displays a list of actions or options that a user can choose.
|
|
197
|
+
|
|
198
|
+
## Example usage
|
|
199
|
+
|
|
200
|
+
<Source of={MenuTests.ExampleUsage} type="code" dark />
|
|
201
|
+
|
|
202
|
+
## Features
|
|
203
|
+
|
|
204
|
+
There is no native element to implement a menu in HTML that is widely supported.
|
|
205
|
+
Menu and MenuItem help achieve accessible menu components that are nicely styled and easy to use.
|
|
206
|
+
|
|
207
|
+
- **Keyboard navigation** – Menu items can be navigated using the arrow keys, along with page up/down, home/end, etc. Typeahead, auto scrolling, and disabled items are supported as well.
|
|
208
|
+
- **Trigger interactions** – Menus can be triggered by pressing with a mouse or touch, or optionally, with a long press interaction. The arrow keys also open the menu with a keyboard, automatically focusing the first or last item accordingly.
|
|
209
|
+
- **Accessible** – Follows the ARIA menu pattern, with keyboard shortcut elements within each item for improved screen reader announcement.
|
|
210
|
+
|
|
211
|
+
## Controlled open state
|
|
212
|
+
|
|
213
|
+
The open state of the menu can be controlled via the `defaultOpen` and `isOpen` props.
|
|
214
|
+
|
|
215
|
+
<Source of={MenuTests.ControlledOpenState} type="code" dark />
|
|
216
|
+
|
|
217
|
+
Alternatively, you can use the `defaultOpen` if you just want the menu to be
|
|
218
|
+
open by default (without having to manage the open state yourself).
|
|
219
|
+
|
|
220
|
+
## How to test this component?
|
|
221
|
+
|
|
222
|
+
It is really simple to test a menu. Here is an example:
|
|
223
|
+
|
|
224
|
+
<Source of={MenuTests.TestShouldOpenAndClickOnItem} type="code" dark />
|
|
225
|
+
|
|
226
|
+
## Playground & Props
|
|
227
|
+
|
|
228
|
+
<Canvas of={Menu.Playground} />
|
|
229
|
+
|
|
230
|
+
<Controls of={Menu.Playground} />
|
|
231
|
+
```
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Message
|
|
2
|
+
|
|
3
|
+
## Props
|
|
4
|
+
|
|
5
|
+
The complete Props documentation with JS doc for this component is available at this path:
|
|
6
|
+
|
|
7
|
+
node_modules/@agregio-solutions/design-system/dist/packages/components/Message/Message.d.ts
|
|
8
|
+
|
|
9
|
+
## Example usage
|
|
10
|
+
|
|
11
|
+
Here are the Storybook Stories.
|
|
12
|
+
|
|
13
|
+
Base stories:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
17
|
+
import { expect, fn, userEvent, within } from "storybook/test";
|
|
18
|
+
import Message from "./Message";
|
|
19
|
+
import { I18nProvider } from "react-aria-components";
|
|
20
|
+
import { STORYBOOK_VIEWPORTS } from "@internal/test-utils-storybook/test-utils-storybook";
|
|
21
|
+
|
|
22
|
+
const meta: Meta<typeof Message> = {
|
|
23
|
+
component: Message,
|
|
24
|
+
tags: ["autodocs"],
|
|
25
|
+
parameters: {
|
|
26
|
+
layout: "centered",
|
|
27
|
+
},
|
|
28
|
+
argTypes: {
|
|
29
|
+
children: { control: { type: "text" } },
|
|
30
|
+
},
|
|
31
|
+
decorators: [
|
|
32
|
+
(Story) => (
|
|
33
|
+
<I18nProvider locale="en">
|
|
34
|
+
<Story />
|
|
35
|
+
</I18nProvider>
|
|
36
|
+
),
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
export default meta;
|
|
40
|
+
|
|
41
|
+
type Story = StoryObj<typeof meta>;
|
|
42
|
+
|
|
43
|
+
export const Playground: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
nature: "negative",
|
|
46
|
+
onClose: fn(),
|
|
47
|
+
actions: [
|
|
48
|
+
{ text: "Primary Action", mode: "primary" },
|
|
49
|
+
{ text: "Secondary Action", mode: "secondary" },
|
|
50
|
+
],
|
|
51
|
+
title: "The Title",
|
|
52
|
+
children: "Some error message",
|
|
53
|
+
},
|
|
54
|
+
play: async ({ canvasElement, args }) => {
|
|
55
|
+
const canvas = within(canvasElement);
|
|
56
|
+
const user = userEvent.setup();
|
|
57
|
+
await canvas.findByText(args.title as string);
|
|
58
|
+
await canvas.findByText(args.children as string);
|
|
59
|
+
await canvas.findByText("Primary Action");
|
|
60
|
+
await canvas.findByText("Secondary Action");
|
|
61
|
+
await user.click(await canvas.findByLabelText("Close alert"));
|
|
62
|
+
await expect(args.onClose).toHaveBeenCalledTimes(1);
|
|
63
|
+
await user.click(await canvas.findByText(args.children as string)); // Just to avoid the close button being focused in Chromatic snapshot
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const Positive: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
...Playground.args,
|
|
70
|
+
nature: "positive",
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const Warning: Story = {
|
|
75
|
+
args: {
|
|
76
|
+
...Playground.args,
|
|
77
|
+
nature: "warning",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const Informative: Story = {
|
|
82
|
+
args: {
|
|
83
|
+
...Playground.args,
|
|
84
|
+
nature: "informative",
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const Negative: Story = {
|
|
89
|
+
args: {
|
|
90
|
+
...Playground.args,
|
|
91
|
+
nature: "negative",
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const Minimal: Story = {
|
|
96
|
+
args: {
|
|
97
|
+
children: "Some error message",
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const PixelPerfect: Story = {
|
|
102
|
+
args: {
|
|
103
|
+
onClose: fn(),
|
|
104
|
+
title: "[Insert title]",
|
|
105
|
+
actions: [
|
|
106
|
+
{ text: "[Insert name]", mode: "primary" },
|
|
107
|
+
{ text: "[Insert name]", mode: "secondary" },
|
|
108
|
+
{ text: "[Insert name]", mode: "secondary" },
|
|
109
|
+
],
|
|
110
|
+
children: "[Insert description]",
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const StressTestOverflow: Story = {
|
|
115
|
+
decorators: [
|
|
116
|
+
(Story) => (
|
|
117
|
+
<div style={{ width: "800px" }}>
|
|
118
|
+
<Story />
|
|
119
|
+
</div>
|
|
120
|
+
),
|
|
121
|
+
],
|
|
122
|
+
args: {
|
|
123
|
+
...PixelPerfect.args,
|
|
124
|
+
children:
|
|
125
|
+
"qsdhqjshdjhqskjdhjkqsdhqkshdjkqhsdhqjksdhjkqhsdkjhqjksdhkjqsjkhdkjqhsdkjhqkdsjdsqqsdhqjshdjhqskjdhjkqsdhqkshdjkqhsdhqjksdhjkqhsdkjhqjksdhkjqsjkhdkjqhsdkjhqkdsjdsq",
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const Mobile: Story = {
|
|
130
|
+
parameters: {
|
|
131
|
+
...STORYBOOK_VIEWPORTS,
|
|
132
|
+
layout: "padded",
|
|
133
|
+
},
|
|
134
|
+
globals: {
|
|
135
|
+
viewport: { value: "iphone6", isRotated: false },
|
|
136
|
+
},
|
|
137
|
+
args: {
|
|
138
|
+
...PixelPerfect.args,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const FullWidth: Story = {
|
|
143
|
+
parameters: {
|
|
144
|
+
layout: "padded",
|
|
145
|
+
},
|
|
146
|
+
args: {
|
|
147
|
+
...PixelPerfect.args,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const AutoHide: Story = {
|
|
152
|
+
args: {
|
|
153
|
+
...Playground.args,
|
|
154
|
+
autoHide: true,
|
|
155
|
+
},
|
|
156
|
+
play: async ({ canvasElement, args }) => {
|
|
157
|
+
const canvas = within(canvasElement);
|
|
158
|
+
const user = userEvent.setup();
|
|
159
|
+
await canvas.findByText(args.children as string);
|
|
160
|
+
await user.click(await canvas.findByLabelText("Close alert"));
|
|
161
|
+
await expect(
|
|
162
|
+
canvas.queryByText(args.children as string),
|
|
163
|
+
).not.toBeInTheDocument();
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
```
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Modal
|
|
2
|
+
|
|
3
|
+
## Props
|
|
4
|
+
|
|
5
|
+
The complete Props documentation with JS doc for this component is available at this path:
|
|
6
|
+
|
|
7
|
+
node_modules/@agregio-solutions/design-system/dist/packages/components/Modal/Modal.d.ts
|
|
8
|
+
|
|
9
|
+
## Example usage
|
|
10
|
+
|
|
11
|
+
Here are the Storybook Stories.
|
|
12
|
+
|
|
13
|
+
Base stories:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
17
|
+
import Modal, { ModalActions, ModalContent, Props } from "./Modal";
|
|
18
|
+
import { screen } from "storybook/test";
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import Button from "@components/Button/Button";
|
|
21
|
+
import { expectNotPresent } from "@internal/test-utils-storybook/test-utils-storybook";
|
|
22
|
+
|
|
23
|
+
const meta: Meta<typeof Modal> = {
|
|
24
|
+
component: Modal,
|
|
25
|
+
argTypes: {
|
|
26
|
+
children: { control: false },
|
|
27
|
+
},
|
|
28
|
+
parameters: {
|
|
29
|
+
layout: "none",
|
|
30
|
+
},
|
|
31
|
+
render: (args) => {
|
|
32
|
+
function ParentComponent(props: Props) {
|
|
33
|
+
const [isOpen, setIsOpen] = useState(props.isOpen);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
style={{
|
|
38
|
+
display: "flex",
|
|
39
|
+
alignItems: "center",
|
|
40
|
+
justifyContent: "center",
|
|
41
|
+
height: "100vh",
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<Modal {...props} isOpen={isOpen} onOpenChange={setIsOpen}>
|
|
45
|
+
<ModalContent>{props.children}</ModalContent>
|
|
46
|
+
|
|
47
|
+
<ModalActions>
|
|
48
|
+
<Button
|
|
49
|
+
onClick={() => setIsOpen(false)}
|
|
50
|
+
text="Close"
|
|
51
|
+
mode="secondary"
|
|
52
|
+
/>
|
|
53
|
+
<Button onClick={() => setIsOpen(false)} text="Apply" />
|
|
54
|
+
</ModalActions>
|
|
55
|
+
</Modal>
|
|
56
|
+
|
|
57
|
+
<div style={{ textAlign: "center" }}>
|
|
58
|
+
<Button onClick={() => setIsOpen(true)} text="Open Modal" />
|
|
59
|
+
<div style={{ marginTop: 16 }}>
|
|
60
|
+
The Modal is {isOpen ? "open" : "closed"}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return <ParentComponent {...args}>{args.children}</ParentComponent>;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
export default meta;
|
|
71
|
+
|
|
72
|
+
export const Playground: StoryObj<typeof Modal> = {
|
|
73
|
+
args: {
|
|
74
|
+
title: "[Insert Modal title]",
|
|
75
|
+
children: <div>The content</div>,
|
|
76
|
+
isOpen: true,
|
|
77
|
+
},
|
|
78
|
+
play: async ({ args }) => {
|
|
79
|
+
await screen.findByText(args.title);
|
|
80
|
+
await screen.findByText("The content");
|
|
81
|
+
await screen.findByText("Close");
|
|
82
|
+
await screen.findByLabelText("Close");
|
|
83
|
+
await expectNotPresent(() => screen.queryByText("Submit"));
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const OverflowingContent: StoryObj<typeof Modal> = {
|
|
88
|
+
args: {
|
|
89
|
+
...Playground.args,
|
|
90
|
+
children: "The content. ".repeat(400),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const CustomWidth: StoryObj<typeof Modal> = {
|
|
95
|
+
args: {
|
|
96
|
+
...Playground.args,
|
|
97
|
+
width: "auto",
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## How to test this component
|
|
103
|
+
|
|
104
|
+
Here are some more advanced stories with more testing coverage and examples that you can read to understand how to test this component.
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
108
|
+
import Modal, { ModalActions, ModalContent } from "../Modal";
|
|
109
|
+
import { userEvent, within, screen } from "storybook/test";
|
|
110
|
+
import { useState } from "react";
|
|
111
|
+
import Button from "@components/Button/Button";
|
|
112
|
+
import { expectNotPresent } from "@internal/test-utils-storybook/test-utils-storybook";
|
|
113
|
+
import { Playground } from "../Modal.stories";
|
|
114
|
+
import baseMeta from "../Modal.stories";
|
|
115
|
+
import { useForm } from "react-hook-form";
|
|
116
|
+
import TextInput from "@components/TextInput/TextInput";
|
|
117
|
+
import { ModalForm } from "../Modal.styled";
|
|
118
|
+
|
|
119
|
+
const meta: Meta<typeof Modal> = {
|
|
120
|
+
component: Modal,
|
|
121
|
+
parameters: {
|
|
122
|
+
layout: "none",
|
|
123
|
+
chromatic: { disableSnapshot: true },
|
|
124
|
+
},
|
|
125
|
+
render: baseMeta.render,
|
|
126
|
+
};
|
|
127
|
+
export default meta;
|
|
128
|
+
|
|
129
|
+
export const TestCloseButtonNextTitleShouldCloseModal: StoryObj<typeof Modal> =
|
|
130
|
+
{
|
|
131
|
+
args: {
|
|
132
|
+
...Playground.args,
|
|
133
|
+
},
|
|
134
|
+
play: async ({ canvasElement, args }) => {
|
|
135
|
+
const canvas = within(canvasElement);
|
|
136
|
+
const user = userEvent.setup({ delay: 50 });
|
|
137
|
+
await canvas.findByText("The Modal is open");
|
|
138
|
+
await screen.findByText(args.title);
|
|
139
|
+
await user.click(screen.getByLabelText("Close"));
|
|
140
|
+
await canvas.findByText("The Modal is closed");
|
|
141
|
+
await expectNotPresent(() => screen.queryByText(args.title));
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const TestFooterCloseButtonShouldCloseModal: StoryObj<typeof Modal> = {
|
|
146
|
+
args: {
|
|
147
|
+
...Playground.args,
|
|
148
|
+
},
|
|
149
|
+
play: async ({ canvasElement, args }) => {
|
|
150
|
+
const canvas = within(canvasElement);
|
|
151
|
+
const user = userEvent.setup({ delay: 50 });
|
|
152
|
+
await canvas.findByText("The Modal is open");
|
|
153
|
+
await screen.findByText(args.title);
|
|
154
|
+
await user.click(screen.getByText("Close"));
|
|
155
|
+
await canvas.findByText("The Modal is closed");
|
|
156
|
+
await expectNotPresent(() => screen.queryByText(args.title));
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const ExampleUsage: StoryObj<typeof Modal> = {
|
|
161
|
+
render: () => {
|
|
162
|
+
function ParentComponent() {
|
|
163
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div>
|
|
167
|
+
<div>
|
|
168
|
+
<Button onClick={() => setIsOpen(true)} text="Open modal" />
|
|
169
|
+
</div>
|
|
170
|
+
<Modal isOpen={isOpen} onOpenChange={setIsOpen} title="Modal title">
|
|
171
|
+
<ModalContent>Modal content</ModalContent>
|
|
172
|
+
<ModalActions>
|
|
173
|
+
<Button
|
|
174
|
+
onClick={() => setIsOpen(false)}
|
|
175
|
+
text="Close"
|
|
176
|
+
mode="secondary"
|
|
177
|
+
/>
|
|
178
|
+
<Button onClick={() => setIsOpen(false)} text="Apply" />
|
|
179
|
+
</ModalActions>
|
|
180
|
+
</Modal>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return <ParentComponent />;
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const ExampleWithReactHookForm: StoryObj<typeof Modal> = {
|
|
190
|
+
render: () => {
|
|
191
|
+
type Form = {
|
|
192
|
+
name: string;
|
|
193
|
+
email: string;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
type FormProps = {
|
|
197
|
+
onClose: () => void;
|
|
198
|
+
onSubmit: (values: Form) => void;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
function Form({ onClose, onSubmit }: FormProps) {
|
|
202
|
+
const form = useForm<Form>();
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<ModalForm
|
|
206
|
+
onSubmit={form.handleSubmit((data: Form) => {
|
|
207
|
+
onSubmit(data);
|
|
208
|
+
})}
|
|
209
|
+
>
|
|
210
|
+
<ModalContent>
|
|
211
|
+
<TextInput
|
|
212
|
+
{...form.register("name", { required: "Name is required" })}
|
|
213
|
+
id="name"
|
|
214
|
+
label="Name"
|
|
215
|
+
errorHelperText={form.formState.errors.name?.message}
|
|
216
|
+
/>
|
|
217
|
+
</ModalContent>
|
|
218
|
+
<ModalActions>
|
|
219
|
+
<Button onClick={onClose} text="Close" mode="secondary" />
|
|
220
|
+
<Button type="submit" text="Create user" />
|
|
221
|
+
</ModalActions>
|
|
222
|
+
</ModalForm>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function ParentComponent() {
|
|
227
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div>
|
|
231
|
+
<Button onClick={() => setIsOpen(true)} text="Open form" />
|
|
232
|
+
|
|
233
|
+
<Modal
|
|
234
|
+
isOpen={isOpen}
|
|
235
|
+
onOpenChange={setIsOpen}
|
|
236
|
+
title="Creating a new user"
|
|
237
|
+
>
|
|
238
|
+
<Form
|
|
239
|
+
onClose={() => setIsOpen(false)}
|
|
240
|
+
onSubmit={(values) => {
|
|
241
|
+
// API call for example
|
|
242
|
+
console.log(values); // eslint-disable-line no-console
|
|
243
|
+
setIsOpen(false);
|
|
244
|
+
}}
|
|
245
|
+
/>
|
|
246
|
+
</Modal>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return <ParentComponent />;
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Developer notes
|
|
257
|
+
|
|
258
|
+
Here are the notes available for the developer on the built Storybook, you can read them to understand the component and how to use it.
|
|
259
|
+
|
|
260
|
+
```mdx
|
|
261
|
+
import { Meta, Controls, Source } from "@storybook/addon-docs/blocks";
|
|
262
|
+
|
|
263
|
+
import * as Modal from "./Modal.stories";
|
|
264
|
+
import * as ModalTests from "./tests/Modal.stories";
|
|
265
|
+
|
|
266
|
+
<Meta of={Modal} />
|
|
267
|
+
|
|
268
|
+
# Modal
|
|
269
|
+
|
|
270
|
+
The HTML `<dialog>` element can be used to build modals.
|
|
271
|
+
However, it is not yet widely supported across browsers, and building fully accessible custom dialogs from scratch is very difficult and error prone.
|
|
272
|
+
Modal helps achieve accessible modals.
|
|
273
|
+
|
|
274
|
+
- **Accessible** – Content outside the model is hidden from assistive technologies while it is open. The modal closes when interacting outside, or pressing the Escape key.
|
|
275
|
+
- **Focus management** – Focus is moved into the modal on mount, and restored to the trigger element on unmount. While open, focus is contained within the modal, preventing the user from tabbing outside.
|
|
276
|
+
- **Scroll locking** – Scrolling the page behind the modal is prevented while it is open, including in mobile browsers.
|
|
277
|
+
|
|
278
|
+
## Example usage
|
|
279
|
+
|
|
280
|
+
<Source of={ModalTests.ExampleUsage} type="code" dark />
|
|
281
|
+
|
|
282
|
+
## Example with a form
|
|
283
|
+
|
|
284
|
+
<Source of={ModalTests.ExampleWithReactHookForm} type="code" dark />
|
|
285
|
+
|
|
286
|
+
## Props
|
|
287
|
+
|
|
288
|
+
<Controls of={Modal.Playground} />
|
|
289
|
+
```
|