reshaped 3.3.13 → 3.4.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/bundle.css +1 -1
  3. package/dist/bundle.d.ts +3 -0
  4. package/dist/bundle.js +11 -11
  5. package/dist/components/Autocomplete/Autocomplete.js +34 -24
  6. package/dist/components/Autocomplete/Autocomplete.types.d.ts +1 -1
  7. package/dist/components/Autocomplete/tests/Autocomplete.stories.js +8 -1
  8. package/dist/components/Carousel/Carousel.js +2 -1
  9. package/dist/components/Carousel/Carousel.types.d.ts +1 -0
  10. package/dist/components/ContextMenu/ContextMenu.js +2 -2
  11. package/dist/components/ContextMenu/tests/ContextMenu.test.stories.js +1 -1
  12. package/dist/components/DropdownMenu/DropdownMenu.js +6 -1
  13. package/dist/components/DropdownMenu/tests/DropdownMenu.test.stories.js +2 -2
  14. package/dist/components/Popover/Popover.js +1 -1
  15. package/dist/components/Popover/tests/Popover.test.stories.js +4 -4
  16. package/dist/components/ProgressIndicator/ProgressIndicator.d.ts +3 -0
  17. package/dist/components/ProgressIndicator/ProgressIndicator.js +101 -0
  18. package/dist/components/ProgressIndicator/ProgressIndicator.module.css +1 -0
  19. package/dist/components/ProgressIndicator/ProgressIndicator.types.d.ts +9 -0
  20. package/dist/components/ProgressIndicator/ProgressIndicator.types.js +1 -0
  21. package/dist/components/ProgressIndicator/index.d.ts +2 -0
  22. package/dist/components/ProgressIndicator/index.js +1 -0
  23. package/dist/components/ProgressIndicator/tests/ProgressIndicator.stories.d.ts +19 -0
  24. package/dist/components/ProgressIndicator/tests/ProgressIndicator.stories.js +85 -0
  25. package/dist/components/Reshaped/Reshaped.js +4 -5
  26. package/dist/components/TextField/TextField.module.css +1 -1
  27. package/dist/components/Toast/tests/Toast.stories.js +22 -7
  28. package/dist/components/Tooltip/tests/Tooltip.test.stories.js +1 -1
  29. package/dist/components/_private/Flyout/Flyout.module.css +1 -1
  30. package/dist/components/_private/Flyout/Flyout.types.d.ts +14 -3
  31. package/dist/components/_private/Flyout/FlyoutContent.js +37 -7
  32. package/dist/components/_private/Flyout/FlyoutControlled.js +12 -11
  33. package/dist/components/_private/Flyout/FlyoutUncontrolled.js +3 -5
  34. package/dist/components/_private/Flyout/index.d.ts +1 -1
  35. package/dist/components/_private/Flyout/tests/Flyout.stories.d.ts +79 -13
  36. package/dist/components/_private/Flyout/tests/Flyout.stories.js +526 -280
  37. package/dist/components/_private/Flyout/useFlyout.js +9 -3
  38. package/dist/components/_private/Flyout/utilities/isFullyVisible.d.ts +3 -1
  39. package/dist/components/_private/Flyout/utilities/isFullyVisible.js +2 -2
  40. package/dist/hooks/_private/usePrevious.d.ts +2 -0
  41. package/dist/hooks/_private/usePrevious.js +17 -0
  42. package/dist/hooks/_private/useSingletonEnvironment.d.ts +1 -1
  43. package/dist/hooks/_private/useSingletonEnvironment.js +1 -1
  44. package/dist/hooks/_private/useSingletonKeyboardMode.d.ts +13 -2
  45. package/dist/hooks/_private/useSingletonKeyboardMode.js +48 -15
  46. package/dist/hooks/tests/useKeyboardMode.stories.d.ts +6 -0
  47. package/dist/hooks/tests/useKeyboardMode.stories.js +37 -0
  48. package/dist/hooks/useKeyboardMode.d.ts +7 -0
  49. package/dist/hooks/useKeyboardMode.js +13 -0
  50. package/dist/index.d.ts +3 -0
  51. package/dist/index.js +2 -0
  52. package/dist/utilities/a11y/index.d.ts +1 -1
  53. package/dist/utilities/a11y/index.js +1 -1
  54. package/dist/utilities/a11y/keyboardMode.d.ts +2 -2
  55. package/dist/utilities/a11y/keyboardMode.js +2 -2
  56. package/dist/utilities/dom/find.d.ts +4 -1
  57. package/dist/utilities/dom/find.js +4 -4
  58. package/dist/utilities/scroll/lockStandard.js +1 -1
  59. package/package.json +3 -4
  60. package/dist/components/_private/Flyout/tests/Flyout.test.stories.d.ts +0 -28
  61. package/dist/components/_private/Flyout/tests/Flyout.test.stories.js +0 -205
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { createRoot } from "react-dom/client";
3
+ import { userEvent, waitFor, within, expect, fn } from "@storybook/test";
3
4
  import { Example } from "../../../../utilities/storybook/index.js";
4
5
  import Reshaped from "../../../Reshaped/index.js";
5
6
  import View from "../../../View/index.js";
@@ -7,177 +8,442 @@ import Theme from "../../../Theme/index.js";
7
8
  import Button from "../../../Button/index.js";
8
9
  import Flyout from "../index.js";
9
10
  import TextField from "../../../TextField/index.js";
11
+ import MenuItem from "../../../MenuItem/index.js";
12
+ import { sleep } from "../../../../utilities/helpers.js";
10
13
  export default { title: "Internal/Flyout" };
14
+ /**
15
+ * Unit
16
+ * - groupTimeouts
17
+ * - id
18
+ * - contentClassName
19
+ * - contentAttributes
20
+ * - content attributes
21
+ * - content className
22
+ */
23
+ const Content = (props) => (<div style={{
24
+ background: "var(--rs-color-background-elevation-overlay)",
25
+ padding: "var(--rs-unit-x4)",
26
+ height: props.height ?? 150,
27
+ minWidth: props.width === false ? undefined : props.width || 160,
28
+ borderRadius: "var(--rs-radius-medium)",
29
+ border: "1px solid var(--rs-color-border-neutral-faded)",
30
+ boxSizing: "border-box",
31
+ }}>
32
+ {props.children || "Content"}
33
+ </div>);
11
34
  const Demo = (props) => {
12
- const { position = "bottom-start", children, ...rest } = props;
13
- return (<Flyout triggerType="click" position={position} {...rest}>
35
+ const { position = "bottom-start", children, contentHeight, contentWidth, ...rest } = props;
36
+ return (<Flyout position={position} {...rest}>
14
37
  <Flyout.Trigger>
15
38
  {(attributes) => <Button attributes={attributes}>{position}</Button>}
16
39
  </Flyout.Trigger>
17
40
  <Flyout.Content>
18
- <div style={{
19
- background: "var(--rs-color-background-elevation-overlay)",
20
- padding: "var(--rs-unit-x4)",
21
- height: 150,
22
- minWidth: 160,
23
- borderRadius: "var(--rs-radius-medium)",
24
- border: "1px solid var(--rs-color-border-neutral-faded)",
25
- boxSizing: "border-box",
26
- }}>
27
- {children || "Content"}
28
- </div>
41
+ <Content height={contentHeight} width={contentWidth}>
42
+ {children}
43
+ </Content>
29
44
  </Flyout.Content>
30
45
  </Flyout>);
31
46
  };
32
- export const position = () => (<div style={{ paddingTop: 200 }}>
33
- <View gap={3} direction="row">
34
- <Demo position="bottom-start"/>
35
- <Demo position="bottom-end"/>
36
- <Demo position="bottom"/>
47
+ export const position = {
48
+ name: "position",
49
+ render: () => {
50
+ return (<View gap={4} padding={50} align="center" justify="center">
51
+ <View gap={4} direction="row">
52
+ <Demo position="top-start"/>
53
+ <Demo position="top"/>
54
+ <Demo position="top-end"/>
55
+ </View>
37
56
 
38
- <Demo position="top-start"/>
39
- <Demo position="top-end"/>
40
- <Demo position="top"/>
57
+ <View gap={4} direction="row">
58
+ <Demo position="end-top"/>
59
+ <Demo position="end"/>
60
+ <Demo position="end-bottom"/>
61
+ </View>
41
62
 
42
- <Demo position="end"/>
43
- <Demo position="end-top"/>
44
- <Demo position="end-bottom"/>
63
+ <View gap={4} direction="row">
64
+ <Demo position="start-top"/>
65
+ <Demo position="start"/>
66
+ <Demo position="start-bottom"/>
67
+ </View>
45
68
 
46
- <Demo position="start"/>
47
- <Demo position="start-top"/>
48
- <Demo position="start-bottom"/>
49
- </View>
50
- </div>);
51
- export const dynamicPosition = () => (<div style={{ position: "absolute", top: 0, left: "50%" }}>
52
- <Demo position="top"/>
53
- </div>);
54
- export const originCoordinates = () => {
55
- const [coordinates, setCoordinates] = React.useState(null);
56
- return (<Example>
57
- <Example.Item>
58
- <View height={25} width={25} attributes={{
59
- onContextMenu: (e) => {
60
- e.preventDefault();
61
- setCoordinates({ x: e.clientX, y: e.clientY });
62
- },
63
- }} backgroundColor="neutral-faded" borderRadius="medium"/>
64
- <br /> <br />
65
- <Demo position="top" originCoordinates={coordinates} active={!!coordinates} onClose={() => setCoordinates(null)}/>
66
- </Example.Item>
67
- </Example>);
69
+ <View gap={4} direction="row">
70
+ <Demo position="bottom-start"/>
71
+ <Demo position="bottom"/>
72
+ <Demo position="bottom-end" defaultActive/>
73
+ </View>
74
+ </View>);
75
+ },
68
76
  };
69
- export const modes = () => (<Example>
70
- <Example.Item title="dialog click">
71
- <Demo position="bottom-start" trapFocusMode="dialog">
72
- <button type="button">Item 1</button>
73
- <button type="button">Item 2</button>
74
- <button type="button">Close</button>
75
- </Demo>
76
- </Example.Item>
77
-
78
- <Example.Item title="action-menu click">
79
- <Demo position="bottom-start" trapFocusMode="action-menu">
80
- <button type="button">Item 1</button>
81
- <button type="button">Item 2</button>
82
- <button type="button">Close</button>
83
- </Demo>
84
- </Example.Item>
77
+ export const defaultActive = {
78
+ name: "defaultActive, uncontrolled",
79
+ args: {
80
+ handleOpen: fn(),
81
+ handleClose: fn(),
82
+ },
83
+ render: (args) => (<Flyout onOpen={args.handleOpen} onClose={args.handleClose} defaultActive>
84
+ <Flyout.Trigger>
85
+ {(attributes) => <Button attributes={attributes}>Trigger</Button>}
86
+ </Flyout.Trigger>
87
+ <Flyout.Content>
88
+ <Content />
89
+ </Flyout.Content>
90
+ </Flyout>),
91
+ play: async ({ canvasElement, args }) => {
92
+ const canvas = within(canvasElement.ownerDocument.body);
93
+ const trigger = canvas.getAllByRole("button")[0];
94
+ let item = canvas.getByText("Content");
95
+ await sleep(500);
96
+ await userEvent.click(document.body);
97
+ await waitFor(() => {
98
+ expect(args.handleClose).toHaveBeenCalledTimes(1);
99
+ expect(args.handleClose).toHaveBeenCalledWith({ reason: "outside-click" });
100
+ expect(item).not.toBeInTheDocument();
101
+ });
102
+ await userEvent.click(trigger);
103
+ await waitFor(() => {
104
+ expect(args.handleOpen).toHaveBeenCalledTimes(1);
105
+ expect(args.handleOpen).toHaveBeenCalledWith();
106
+ });
107
+ item = canvas.getByText("Content");
108
+ expect(item).toBeInTheDocument();
109
+ },
110
+ };
111
+ export const active = {
112
+ name: "active, controlled",
113
+ args: {
114
+ handleOpen: fn(),
115
+ handleClose: fn(),
116
+ },
117
+ render: (args) => (<Flyout onOpen={args.handleOpen} onClose={args.handleClose} active>
118
+ <Flyout.Trigger>
119
+ {(attributes) => <Button attributes={attributes}>Trigger</Button>}
120
+ </Flyout.Trigger>
121
+ <Flyout.Content>
122
+ <Content />
123
+ </Flyout.Content>
124
+ </Flyout>),
125
+ play: async ({ canvasElement, args }) => {
126
+ const canvas = within(canvasElement.ownerDocument.body);
127
+ const item = canvas.getByText("Content");
128
+ await userEvent.click(document.body);
129
+ await waitFor(() => {
130
+ expect(args.handleClose).toHaveBeenCalledTimes(1);
131
+ expect(args.handleClose).toHaveBeenCalledWith({ reason: "outside-click" });
132
+ });
133
+ expect(item).toBeInTheDocument();
134
+ },
135
+ };
136
+ export const activeFalse = {
137
+ name: "active: false, controlled",
138
+ args: {
139
+ handleOpen: fn(),
140
+ handleClose: fn(),
141
+ },
142
+ render: (args) => (<Flyout onOpen={args.handleOpen} onClose={args.handleClose} active={false}>
143
+ <Flyout.Trigger>
144
+ {(attributes) => <Button attributes={attributes}>Trigger</Button>}
145
+ </Flyout.Trigger>
146
+ <Flyout.Content>
147
+ <Content />
148
+ </Flyout.Content>
149
+ </Flyout>),
150
+ play: async ({ canvasElement, args }) => {
151
+ const canvas = within(canvasElement.ownerDocument.body);
152
+ const trigger = canvas.getAllByRole("button")[0];
153
+ await userEvent.click(trigger);
154
+ await waitFor(() => {
155
+ expect(args.handleOpen).toHaveBeenCalledTimes(1);
156
+ expect(args.handleOpen).toHaveBeenCalledWith();
157
+ });
158
+ const item = canvas.queryByText("Content");
159
+ expect(item).not.toBeInTheDocument();
160
+ },
161
+ };
162
+ export const modes = {
163
+ name: "triggerType, trapFocusMode",
164
+ render: () => {
165
+ return (<Example>
166
+ <Example.Item title={[
167
+ "triggerType: click, trapFocusMode: dialog",
168
+ "tab navigation, completely traps the focus inside",
169
+ ]}>
170
+ <Demo position="bottom-start" trapFocusMode="dialog">
171
+ <View direction="row" gap={2}>
172
+ <Button onClick={() => { }}>Action 1</Button>
173
+ <Button onClick={() => { }}>Action 2</Button>
174
+ <Button onClick={() => { }}>Action 3</Button>
175
+ </View>
176
+ </Demo>
177
+ </Example.Item>
85
178
 
86
- <Example.Item title="content-menu click">
87
- <Demo position="bottom-start" trapFocusMode="content-menu">
88
- <button type="button">Item 1</button>
89
- <button type="button">Item 2</button>
90
- <button type="button">Close</button>
91
- </Demo>
92
- </Example.Item>
179
+ <Example.Item title={[
180
+ "triggerType: click, trapFocusMode: action-menu",
181
+ "arrow navigation, tab closes the content",
182
+ ]}>
183
+ <Demo position="bottom-start" trapFocusMode="action-menu">
184
+ <View direction="row" gap={2}>
185
+ <Button onClick={() => { }}>Action 1</Button>
186
+ <Button onClick={() => { }}>Action 2</Button>
187
+ <Button onClick={() => { }}>Action 3</Button>
188
+ </View>
189
+ </Demo>
190
+ </Example.Item>
93
191
 
94
- <Example.Item title="dialog hover">
95
- <Demo position="bottom-start" trapFocusMode="dialog" triggerType="hover">
96
- <button type="button">Item 1</button>
97
- <button type="button">Item 2</button>
98
- <button type="button">Close</button>
99
- </Demo>
100
- </Example.Item>
192
+ <Example.Item title={[
193
+ "triggerType: click, trapFocusMode: content-menu",
194
+ "tab navigation, simulates natural focus order for navigation menus",
195
+ ]}>
196
+ <Demo position="bottom-start" trapFocusMode="content-menu">
197
+ <View direction="row" gap={2}>
198
+ <Button onClick={() => { }}>Action 1</Button>
199
+ <Button onClick={() => { }}>Action 2</Button>
200
+ <Button onClick={() => { }}>Action 3</Button>
201
+ </View>
202
+ </Demo>
203
+ </Example.Item>
101
204
 
102
- <Example.Item title="action-menu hover">
103
- <Demo position="bottom-start" trapFocusMode="action-menu" triggerType="hover">
104
- <button type="button">Item 1</button>
105
- <button type="button">Item 2</button>
106
- <button type="button">Close</button>
107
- </Demo>
108
- </Example.Item>
205
+ <Example.Item title="triggerType: hover, trapFocusMode: dialog">
206
+ <Demo position="bottom-start" trapFocusMode="dialog" triggerType="hover">
207
+ <View direction="row" gap={2}>
208
+ <Button onClick={() => { }}>Action 1</Button>
209
+ <Button onClick={() => { }}>Action 2</Button>
210
+ <Button onClick={() => { }}>Action 3</Button>
211
+ </View>
212
+ </Demo>
213
+ </Example.Item>
109
214
 
110
- <Example.Item title="content-menu hover without buttons">
111
- <Demo position="bottom-start" trapFocusMode="content-menu" triggerType="hover">
112
- <div style={{ height: 50, width: 50, background: "tomato" }}/>
113
- </Demo>
114
- </Example.Item>
215
+ <Example.Item title="triggerType: hover, trapFocusMode: action-menu">
216
+ <Demo position="bottom-start" trapFocusMode="action-menu" triggerType="hover">
217
+ <View direction="row" gap={2}>
218
+ <Button onClick={() => { }}>Action 1</Button>
219
+ <Button onClick={() => { }}>Action 2</Button>
220
+ <Button onClick={() => { }}>Action 3</Button>
221
+ </View>
222
+ </Demo>
223
+ </Example.Item>
115
224
 
116
- <Example.Item title="content-menu hover">
117
- <Demo position="bottom-start" trapFocusMode="content-menu" triggerType="hover">
118
- <button type="button">Item 1</button>
119
- <button type="button">Item 2</button>
120
- <button type="button">Close</button>
121
- </Demo>
122
- </Example.Item>
123
- </Example>);
124
- export const width = () => (<Example>
125
- <Example.Item title="width: 300px">
126
- <Demo width="300px" position="bottom"/>
127
- </Example.Item>
128
- <Example.Item title="width: trigger">
129
- <Flyout triggerType="click" width="trigger" position="bottom">
130
- <Flyout.Trigger>
131
- {(attributes) => <Button attributes={attributes}>Trigger with long text</Button>}
132
- </Flyout.Trigger>
133
- <Flyout.Content>
134
- <div style={{
135
- background: "var(--rs-color-background-elevation-overlay)",
136
- padding: "var(--rs-unit-x4)",
137
- borderRadius: "var(--rs-radius-medium)",
138
- border: "1px solid var(--rs-color-border-neutral-faded)",
139
- boxSizing: "border-box",
140
- }}></div>
141
- </Flyout.Content>
142
- </Flyout>
143
- </Example.Item>
144
- </Example>);
145
- export const offset = () => (<Example>
146
- <Example.Item title="contentGap: x10">
147
- <Demo contentGap={10}/>
148
- </Example.Item>
149
- <Example.Item title="contentShift: x10">
150
- <Demo contentShift={10}/>
151
- </Example.Item>
152
- </Example>);
153
- export const disableFlags = () => (<Example>
154
- <Example.Item title="disableContentHover">
155
- <Demo triggerType="hover" disableContentHover>
156
- Content
157
- </Demo>
158
- </Example.Item>
225
+ <Example.Item title="triggerType: hover, trapFocusMode: content-menu">
226
+ <Demo position="bottom-start" trapFocusMode="content-menu" triggerType="hover">
227
+ <View direction="row" gap={2}>
228
+ <Button onClick={() => { }}>Action 1</Button>
229
+ <Button onClick={() => { }}>Action 2</Button>
230
+ <Button onClick={() => { }}>Action 3</Button>
231
+ </View>
232
+ </Demo>
233
+ </Example.Item>
159
234
 
160
- <Example.Item title="disableCloseOnOutsideClick">
161
- <Demo disableCloseOnOutsideClick>Content</Demo>
162
- </Example.Item>
235
+ <Example.Item title={[
236
+ "triggerType: hover, trapFocusMode: content-menu, no focusable elements inside",
237
+ "keeps the focus on trigger",
238
+ ]}>
239
+ <Demo position="bottom-start" trapFocusMode="content-menu" triggerType="hover"/>
240
+ </Example.Item>
163
241
 
164
- <Example.Item title="disableHideAnimation">
165
- <Demo disableHideAnimation>Content</Demo>
166
- </Example.Item>
167
- </Example>);
168
- export const initialFocus = () => {
169
- const initialFocusRef = React.useRef(null);
170
- return (<Example>
171
- <Example.Item title="focuses input on open">
172
- <Demo initialFocusRef={initialFocusRef}>
173
- <View gap={4}>
174
- <Button onClick={() => { }}>Click me</Button>
175
- <TextField name="foo" inputAttributes={{ ref: initialFocusRef }}/>
242
+ <Example.Item title={[
243
+ "triggerType: focus, trapFocusMode: selection-menu",
244
+ "keeps real focus on trigger and simulates arrow key item selection focus on the content",
245
+ ]}>
246
+ <Demo position="bottom-start" trapFocusMode="selection-menu" triggerType="focus">
247
+ <View gap={1}>
248
+ <MenuItem onClick={() => { }} roundedCorners>
249
+ Action 1
250
+ </MenuItem>
251
+ <MenuItem onClick={() => { }} roundedCorners>
252
+ Action 2
253
+ </MenuItem>
254
+ <MenuItem onClick={() => { }} roundedCorners>
255
+ Action 3
256
+ </MenuItem>
257
+ </View>
258
+ </Demo>
259
+ </Example.Item>
260
+ </Example>);
261
+ },
262
+ };
263
+ export const positionFallbacks = {
264
+ name: "fallbackPositions",
265
+ render: () => {
266
+ return (<Example>
267
+ <Example.Item title="position: top, no fallbacks passed">
268
+ <View justify="center" align="center">
269
+ <Demo position="top"/>
176
270
  </View>
177
- </Demo>
271
+ </Example.Item>
272
+ <Example.Item title="position: top, fallbackPositions: [start]">
273
+ <View justify="center" align="center">
274
+ <Demo position="top" fallbackPositions={["start"]} contentHeight={200} defaultActive/>
275
+ </View>
276
+ </Example.Item>
277
+ <Example.Item title="position: top, fallbackPositions: false">
278
+ <View justify="center" align="center">
279
+ <Demo position="top" fallbackPositions={false} contentHeight={400}/>
280
+ </View>
281
+ </Example.Item>
282
+ </Example>);
283
+ },
284
+ };
285
+ export const originCoordinates = {
286
+ name: "originCoordinates",
287
+ render: () => {
288
+ return (<View gap={4} direction="row">
289
+ <Demo position="bottom-start" originCoordinates={{ x: 150, y: 150 }} defaultActive/>
290
+ </View>);
291
+ },
292
+ };
293
+ export const width = {
294
+ name: "width",
295
+ render: () => (<Example>
296
+ <Example.Item title="width: 300px">
297
+ <Demo width="300px" position="bottom"/>
178
298
  </Example.Item>
179
- </Example>);
299
+
300
+ <Example.Item title="width: trigger">
301
+ <Demo width="trigger" contentWidth={false} defaultActive/>
302
+ </Example.Item>
303
+ </Example>),
304
+ };
305
+ export const contentGap = {
306
+ name: "contentGap",
307
+ render: () => <Demo contentGap={10} defaultActive/>,
308
+ };
309
+ export const contentShift = {
310
+ name: "contentShift",
311
+ render: () => <Demo contentShift={10} defaultActive/>,
180
312
  };
313
+ export const disableContentHover = {
314
+ name: "disableContentHover",
315
+ render: () => <Demo triggerType="hover" disableContentHover/>,
316
+ };
317
+ export const disableCloseOnOutsideClick = {
318
+ name: "disableCloseOnOutsideClick",
319
+ render: () => <Demo disableCloseOnOutsideClick/>,
320
+ play: async ({ canvasElement }) => {
321
+ const canvas = within(canvasElement.ownerDocument.body);
322
+ const trigger = canvas.getAllByRole("button")[0];
323
+ await userEvent.click(trigger);
324
+ await waitFor(() => {
325
+ const content = canvas.getByText("Content");
326
+ expect(content).toBeVisible();
327
+ });
328
+ await userEvent.click(document.body);
329
+ await sleep(500);
330
+ const content = canvas.getByText("Content");
331
+ expect(content).toBeVisible();
332
+ },
333
+ };
334
+ export const disableHideAnimation = {
335
+ name: "disableHideAnimation",
336
+ render: () => <Demo disableHideAnimation defaultActive/>,
337
+ };
338
+ export const disabled = {
339
+ name: "disabled",
340
+ args: {
341
+ handleOpen: fn(),
342
+ },
343
+ render: () => (<Flyout disabled>
344
+ <Flyout.Trigger>
345
+ {(attributes) => <Button attributes={attributes}>Trigger</Button>}
346
+ </Flyout.Trigger>
347
+ <Flyout.Content>
348
+ <Content />
349
+ </Flyout.Content>
350
+ </Flyout>),
351
+ play: async ({ canvasElement, args }) => {
352
+ const canvas = within(canvasElement.ownerDocument.body);
353
+ const button = canvas.getAllByRole("button")[0];
354
+ await userEvent.click(button);
355
+ expect(args.handleOpen).toHaveBeenCalledTimes(0);
356
+ },
357
+ };
358
+ export const containerRef = {
359
+ name: "containerRef",
360
+ render: () => {
361
+ const portalRef = React.useRef(null);
362
+ return (<View backgroundColor="neutral-faded" borderRadius="small" height={80} attributes={{ ref: portalRef, "data-testid": "container" }} justify="end" align="start" padding={4}>
363
+ <Demo containerRef={portalRef} defaultActive position="bottom-start"/>
364
+ </View>);
365
+ },
366
+ play: async ({ canvasElement }) => {
367
+ const canvas = within(canvasElement.ownerDocument.body);
368
+ const containerEl = canvas.getByTestId("container");
369
+ const contentEl = canvas.getByText("Content");
370
+ expect(containerEl).toContainElement(contentEl);
371
+ },
372
+ };
373
+ export const initialFocusRef = {
374
+ name: "initialFocusRef",
375
+ render: () => {
376
+ const initialFocusRef = React.useRef(null);
377
+ return (<Demo initialFocusRef={initialFocusRef}>
378
+ <View gap={4}>
379
+ <Button onClick={() => { }}>Action 1</Button>
380
+ <TextField name="foo" inputAttributes={{ ref: initialFocusRef }}/>
381
+ </View>
382
+ </Demo>);
383
+ },
384
+ play: async ({ canvasElement }) => {
385
+ const canvas = within(canvasElement.ownerDocument.body);
386
+ const trigger = canvas.getAllByRole("button")[0];
387
+ await userEvent.click(trigger);
388
+ await waitFor(() => {
389
+ const input = canvas.getByRole("textbox");
390
+ expect(input).toBe(document.activeElement);
391
+ });
392
+ },
393
+ };
394
+ export const instanceRef = {
395
+ name: "instanceRef",
396
+ args: {
397
+ handleOpen: fn(),
398
+ handleClose: fn(),
399
+ },
400
+ render: (args) => {
401
+ const flyoutRef = React.useRef(null);
402
+ return (<View direction="row" gap={4}>
403
+ <Demo instanceRef={flyoutRef} disableCloseOnOutsideClick onOpen={args.handleOpen} onClose={args.handleClose}/>
404
+
405
+ <Button onClick={() => flyoutRef.current?.open()}>Open</Button>
406
+ <Button onClick={() => flyoutRef.current?.close()}>Close</Button>
407
+ </View>);
408
+ },
409
+ play: async ({ canvasElement, args }) => {
410
+ const canvas = within(canvasElement.ownerDocument.body);
411
+ const openTrigger = canvas.getAllByRole("button")[1];
412
+ const closeTrigger = canvas.getAllByRole("button")[2];
413
+ await userEvent.click(openTrigger);
414
+ await waitFor(() => {
415
+ expect(args.handleOpen).toHaveBeenCalledTimes(1);
416
+ expect(args.handleOpen).toHaveBeenCalledWith();
417
+ });
418
+ await sleep(500);
419
+ await userEvent.click(closeTrigger);
420
+ await waitFor(() => {
421
+ expect(args.handleClose).toHaveBeenCalledTimes(1);
422
+ expect(args.handleClose).toHaveBeenCalledWith({});
423
+ });
424
+ },
425
+ };
426
+ export const contentAttributes = {
427
+ name: "content: className, attributes",
428
+ render: () => {
429
+ return (<Flyout position="bottom" defaultActive>
430
+ <Flyout.Trigger>
431
+ {(attributes) => <Button attributes={attributes}>`Trigger</Button>}
432
+ </Flyout.Trigger>
433
+ <Flyout.Content attributes={{ "data-testid": "test-id" }} className="test-classname">
434
+ <Content />
435
+ </Flyout.Content>
436
+ </Flyout>);
437
+ },
438
+ play: async ({ canvasElement }) => {
439
+ const canvas = within(canvasElement.ownerDocument.body);
440
+ const content = canvas.getByTestId("test-id");
441
+ expect(content).toHaveClass("test-classname");
442
+ },
443
+ };
444
+ /*
445
+ * Test edge cases
446
+ */
181
447
  class CustomElement extends window.HTMLElement {
182
448
  constructor() {
183
449
  super();
@@ -185,152 +451,132 @@ class CustomElement extends window.HTMLElement {
185
451
  if (!this.shadowRoot)
186
452
  return;
187
453
  const node = (<Reshaped>
188
- <Flyout active>
189
- <Flyout.Trigger>{(attributes) => <button {...attributes}>Open</button>}</Flyout.Trigger>
190
- <Flyout.Content>Content</Flyout.Content>
191
- </Flyout>
454
+ <Demo defaultActive>
455
+ Content
456
+ <div id="test-id"/>
457
+ </Demo>
192
458
  </Reshaped>);
193
459
  const root = createRoot(this.shadowRoot);
194
460
  root.render(node);
195
461
  }
196
462
  }
197
- if (!window.customElements.get("custom-element")) {
198
- window.customElements.define("custom-element", CustomElement);
463
+ if (!window.customElements.get("custom-element-flyout")) {
464
+ window.customElements.define("custom-element-flyout", CustomElement);
199
465
  }
200
- export const customPortalTarget = () => {
201
- const portalRef = React.useRef(null);
202
- return (<Example>
203
- <Example.Item title="Custom containerRef">
204
- <View padding={4} paddingInline={40} height={50} overflow="auto" backgroundColor="neutral-faded" borderRadius="small" attributes={{ ref: portalRef }}>
205
- <Flyout position="bottom-end" active>
206
- <Flyout.Trigger>{(attributes) => <button {...attributes}>Open</button>}</Flyout.Trigger>
466
+ export const testShadowDom = {
467
+ name: "test: shadow dom",
468
+ // @ts-ignore
469
+ render: () => <custom-element-flyout />,
470
+ };
471
+ export const testInsideFixed = {
472
+ name: "test: inside position fixed",
473
+ render: () => (<React.Fragment>
474
+ <View position="fixed" insetTop={2} insetStart={2} insetEnd={2} backgroundColor="elevation-overlay" borderColor="neutral-faded" borderRadius="small" padding={4} zIndex={10} attributes={{ "data-testid": "container" }}>
475
+ <Demo defaultActive/>
476
+ </View>
477
+ <View paddingTop={18} gap={4}>
478
+ <View height={200} backgroundColor="neutral-faded" borderRadius="small"/>
479
+ <View height={200} backgroundColor="neutral-faded" borderRadius="small"/>
480
+ </View>
481
+ </React.Fragment>),
482
+ play: ({ canvas }) => {
483
+ const container = canvas.getByTestId("container");
484
+ const content = canvas.getByText("Content");
485
+ expect(container).toContainElement(content);
486
+ },
487
+ };
488
+ export const testInsideSticky = {
489
+ name: "test: inside position sticky",
490
+ render: () => (<React.Fragment>
491
+ <View position="sticky" insetTop={4} insetStart={0} insetEnd={0} backgroundColor="elevation-overlay" borderColor="neutral-faded" borderRadius="small" padding={4} zIndex={10} attributes={{ "data-testid": "container" }}>
492
+ <Demo defaultActive/>
493
+ </View>
494
+ <View gap={4} paddingTop={2}>
495
+ <View height={200} backgroundColor="neutral-faded" borderRadius="small"/>
496
+ <View height={200} backgroundColor="neutral-faded" borderRadius="small"/>
497
+ </View>
498
+ </React.Fragment>),
499
+ play: ({ canvas }) => {
500
+ const container = canvas.getByTestId("container");
501
+ const content = canvas.getByText("Content");
502
+ expect(container).toContainElement(content);
503
+ },
504
+ };
505
+ export const testInsideScrollable = {
506
+ name: "test: inside scrollable",
507
+ render: () => {
508
+ const containerRef = React.useRef(null);
509
+ return (<View padding={50}>
510
+ <View height={30} overflow="auto" backgroundColor="neutral-faded" borderRadius="small">
511
+ <View height={50} attributes={{ ref: containerRef }} padding={4} paddingBottom={30}>
512
+ <Demo position="start"/>
513
+ </View>
514
+ </View>
515
+ </View>);
516
+ },
517
+ };
518
+ export const testDynamicBounds = {
519
+ name: "test: auto position update",
520
+ render: () => {
521
+ const [left, setLeft] = React.useState(50);
522
+ const [top, setTop] = React.useState(50);
523
+ const [size, setSize] = React.useState("medium");
524
+ const flyoutRef = React.useRef(null);
525
+ React.useEffect(() => {
526
+ flyoutRef.current?.updatePosition();
527
+ }, [left, top]);
528
+ return (<View gap={4}>
529
+ <View direction="row" gap={2}>
530
+ <Button onClick={() => setLeft((prev) => prev - 10)}>Left</Button>
531
+ <Button onClick={() => setLeft((prev) => prev + 10)}>Right</Button>
532
+ <Button onClick={() => setTop((prev) => prev - 10)}>Up</Button>
533
+ <Button onClick={() => setTop((prev) => prev + 10)}>Down</Button>
534
+ <Button onClick={() => {
535
+ setLeft(50);
536
+ setTop(50);
537
+ }}>
538
+ Center
539
+ </Button>
540
+ <Button onClick={() => setSize("large")}>Large button</Button>
541
+ <Button onClick={() => setSize("medium")}>Small button</Button>
542
+ </View>
543
+ <View height={100}>
544
+ <Flyout position="bottom" instanceRef={flyoutRef} disableCloseOnOutsideClick>
545
+ <Flyout.Trigger>
546
+ {(attributes) => (<div style={{ position: "absolute", left: `${left}%`, top: `${top}%` }}>
547
+ <Button color="primary" attributes={attributes} size={size}>
548
+ Open
549
+ </Button>
550
+ </div>)}
551
+ </Flyout.Trigger>
207
552
  <Flyout.Content>
208
- <div style={{
209
- background: "var(--rs-color-background-elevation-overlay)",
210
- padding: "var(--rs-unit-x4)",
211
- height: 200,
212
- width: 160,
213
- borderRadius: "var(--rs-radius-medium)",
214
- border: "1px solid var(--rs-color-border-neutral-faded)",
215
- boxSizing: "border-box",
216
- }}>
217
- {"Content"}
218
- </div>
553
+ <Content />
219
554
  </Flyout.Content>
220
555
  </Flyout>
221
- <div style={{ height: 1000 }}/>
222
556
  </View>
223
- </Example.Item>
224
- <Example.Item title="Shadow dom">
225
- {/* @ts-ignore */}
226
- <custom-element />
227
- </Example.Item>
228
- </Example>);
557
+ </View>);
558
+ },
229
559
  };
230
- export const testInsideFixed = () => (<Example>
231
- <Example.Item title="should move the content on page scroll">
232
- <View position="fixed" insetTop={20} insetStart={0} insetEnd={0} backgroundColor="neutral-faded" padding={4}>
233
- <Flyout triggerType="click" position="bottom-start">
234
- <Flyout.Trigger>{(attributes) => <button {...attributes}>Foo</button>}</Flyout.Trigger>
235
- <Flyout.Content>
236
- <div style={{
237
- background: "var(--rs-color-background-elevation-overlay)",
238
- padding: "var(--rs-unit-x4)",
239
- height: 100,
240
- width: 160,
241
- borderRadius: "var(--rs-radius-medium)",
242
- border: "1px solid var(--rs-color-border-neutral-faded)",
243
- boxSizing: "border-box",
244
- }}>
245
- {"Content"}
246
- </div>
247
- </Flyout.Content>
248
- </Flyout>
249
- </View>
250
- <div style={{ height: 2000 }}/>
251
- </Example.Item>
252
- </Example>);
253
- export const testDynamicBounds = () => {
254
- const [left, setLeft] = React.useState(50);
255
- const [top, setTop] = React.useState(50);
256
- const [size, setSize] = React.useState("medium");
257
- const flyoutRef = React.useRef(null);
258
- React.useEffect(() => {
259
- flyoutRef.current?.updatePosition();
260
- }, [left, top]);
261
- return (<View gap={4}>
262
- <View direction="row" gap={2}>
263
- <Button onClick={() => setLeft((prev) => prev - 10)}>Left</Button>
264
- <Button onClick={() => setLeft((prev) => prev + 10)}>Right</Button>
265
- <Button onClick={() => setTop((prev) => prev - 10)}>Up</Button>
266
- <Button onClick={() => setTop((prev) => prev + 10)}>Down</Button>
267
- <Button onClick={() => {
268
- setLeft(50);
269
- setTop(50);
270
- }}>
271
- Center
272
- </Button>
273
- <Button onClick={() => setSize("large")}>Large button</Button>
274
- <Button onClick={() => setSize("medium")}>Small button</Button>
275
- </View>
276
- <View height={100}>
277
- <Flyout position="bottom" instanceRef={flyoutRef} disableCloseOnOutsideClick fallbackPositions={["top", "bottom"]}>
560
+ export const testScopedTheming = {
561
+ name: "test: content uses scope theme",
562
+ render: () => (<View gap={3} align="start">
563
+ <Button color="primary">Reshaped button</Button>
564
+ <Theme name="slate">
565
+ <Flyout triggerType="click" active position="bottom-start">
278
566
  <Flyout.Trigger>
279
- {(attributes) => (<div style={{ position: "absolute", left: `${left}%`, top: `${top}%` }}>
280
- <Button color="primary" attributes={attributes} size={size}>
281
- Open
282
- </Button>
283
- </div>)}
567
+ {(attributes) => (<Button color="primary" attributes={attributes}>
568
+ Slate button
569
+ </Button>)}
284
570
  </Flyout.Trigger>
285
571
  <Flyout.Content>
286
- <div style={{
287
- background: "var(--rs-color-background-elevation-overlay)",
288
- padding: "var(--rs-unit-x4)",
289
- height: 100,
290
- minWidth: 160,
291
- borderRadius: "var(--rs-radius-medium)",
292
- border: "1px solid var(--rs-color-border-neutral-faded)",
293
- boxSizing: "border-box",
294
- }}>
295
- Content
296
- </div>
572
+ <Content>
573
+ <View gap={1}>
574
+ <View.Item>Portal content, rendered in body</View.Item>
575
+ <Button color="primary">Slate button</Button>
576
+ </View>
577
+ </Content>
297
578
  </Flyout.Content>
298
579
  </Flyout>
299
- </View>
300
- </View>);
580
+ </Theme>
581
+ </View>),
301
582
  };
302
- export const testDisableOutsideClick = () => {
303
- return (<Example>
304
- <Example.Item title="opening second flyout shouldn't block the first one from closing">
305
- <View direction="row" gap={4}>
306
- <Demo disableCloseOnOutsideClick/>
307
- <Demo />
308
- </View>
309
- </Example.Item>
310
- </Example>);
311
- };
312
- export const testScopedTheming = () => (<View gap={3} align="start">
313
- <Button color="primary">Reshaped button</Button>
314
- <Theme name="slate">
315
- <Flyout triggerType="click" active position="bottom-start">
316
- <Flyout.Trigger>
317
- {(attributes) => (<Button color="primary" attributes={attributes}>
318
- Slate button
319
- </Button>)}
320
- </Flyout.Trigger>
321
- <Flyout.Content>
322
- <div style={{
323
- background: "var(--rs-color-background-elevation-overlay)",
324
- padding: 8,
325
- border: "1px solid var(--rs-color-border-neutral-faded)",
326
- boxSizing: "border-box",
327
- }}>
328
- <View gap={1}>
329
- <View.Item>Portal content, rendered in body</View.Item>
330
- <Button color="primary">Slate button</Button>
331
- </View>
332
- </div>
333
- </Flyout.Content>
334
- </Flyout>
335
- </Theme>
336
- </View>);