reshaped 3.5.3 → 3.6.0-canary.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/CHANGELOG.md +11 -21
- package/dist/bundle.css +1 -1
- package/dist/bundle.d.ts +36 -31
- package/dist/bundle.js +10 -10
- package/dist/components/Autocomplete/tests/Autocomplete.stories.js +0 -1
- package/dist/components/Button/Button.types.d.ts +1 -1
- package/dist/components/Card/Card.d.ts +1 -1
- package/dist/components/Checkbox/Checkbox.module.css +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.js +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.types.d.ts +1 -1
- package/dist/components/DropdownMenu/tests/DropdownMenu.stories.d.ts +1 -1
- package/dist/components/DropdownMenu/tests/DropdownMenu.test.stories.d.ts +1 -1
- package/dist/components/Flyout/Flyout.constants.d.ts +6 -0
- package/dist/components/Flyout/Flyout.constants.js +19 -0
- package/dist/components/{_private/Flyout → Flyout}/Flyout.types.d.ts +3 -3
- package/dist/components/{_private/Flyout → Flyout}/FlyoutContent.js +25 -20
- package/dist/components/{_private/Flyout → Flyout}/FlyoutControlled.js +9 -9
- package/dist/components/{_private/Flyout → Flyout}/tests/Flyout.stories.d.ts +6 -4
- package/dist/components/{_private/Flyout → Flyout}/tests/Flyout.stories.js +128 -118
- package/dist/components/{_private/Flyout → Flyout}/useFlyout.d.ts +1 -1
- package/dist/components/Flyout/useFlyout.js +116 -0
- package/dist/components/Flyout/utilities/calculatePosition.d.ts +30 -0
- package/dist/components/Flyout/utilities/calculatePosition.js +129 -0
- package/dist/components/Flyout/utilities/flyout.d.ts +11 -0
- package/dist/components/Flyout/utilities/flyout.js +79 -0
- package/dist/components/Flyout/utilities/isFullyVisible.d.ts +10 -0
- package/dist/components/Flyout/utilities/isFullyVisible.js +24 -0
- package/dist/components/Link/Link.d.ts +1 -1
- package/dist/components/Popover/Popover.d.ts +1 -1
- package/dist/components/Popover/Popover.js +1 -1
- package/dist/components/Popover/Popover.types.d.ts +2 -1
- package/dist/components/Popover/tests/Popover.stories.d.ts +2 -2
- package/dist/components/Popover/tests/Popover.test.stories.d.ts +2 -2
- package/dist/components/Radio/Radio.module.css +1 -1
- package/dist/components/ScrollArea/ScrollArea.js +6 -3
- package/dist/components/ScrollArea/tests/ScrollArea.stories.d.ts +4 -13
- package/dist/components/ScrollArea/tests/ScrollArea.stories.js +30 -129
- package/dist/components/ScrollArea/tests/ScrollArea.test.stories.d.ts +23 -0
- package/dist/components/ScrollArea/tests/ScrollArea.test.stories.js +66 -0
- package/dist/components/Tabs/TabsContext.d.ts +2 -2
- package/dist/components/Tooltip/Tooltip.js +1 -1
- package/dist/components/Tooltip/Tooltip.types.d.ts +1 -1
- package/dist/config/tailwind.d.ts +1 -1
- package/dist/hooks/useIsomorphicLayoutEffect.d.ts +1 -1
- package/dist/index.d.ts +36 -31
- package/dist/index.js +20 -16
- package/dist/utilities/dom/find.d.ts +6 -9
- package/dist/utilities/dom/find.js +17 -15
- package/dist/utilities/dom/index.d.ts +1 -1
- package/dist/utilities/dom/index.js +1 -1
- package/dist/utilities/scroll/lock.js +4 -3
- package/package.json +10 -9
- package/CHANGELOG-old.md +0 -14
- package/dist/components/_private/Flyout/Flyout.constants.d.ts +0 -3
- package/dist/components/_private/Flyout/Flyout.constants.js +0 -3
- package/dist/components/_private/Flyout/useFlyout.js +0 -211
- package/dist/components/_private/Flyout/utilities/calculatePosition.d.ts +0 -19
- package/dist/components/_private/Flyout/utilities/calculatePosition.js +0 -102
- package/dist/components/_private/Flyout/utilities/isFullyVisible.d.ts +0 -8
- package/dist/components/_private/Flyout/utilities/isFullyVisible.js +0 -16
- /package/dist/components/{_private/Flyout → Flyout}/Flyout.context.d.ts +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/Flyout.context.js +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/Flyout.d.ts +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/Flyout.js +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/Flyout.module.css +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/Flyout.types.js +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/FlyoutContent.d.ts +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/FlyoutControlled.d.ts +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/FlyoutTrigger.d.ts +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/FlyoutTrigger.js +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/FlyoutUncontrolled.d.ts +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/FlyoutUncontrolled.js +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/index.d.ts +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/index.js +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/utilities/cooldown.d.ts +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/utilities/cooldown.js +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/utilities/getPositionFallbacks.d.ts +0 -0
- /package/dist/components/{_private/Flyout → Flyout}/utilities/getPositionFallbacks.js +0 -0
@@ -1,20 +1,21 @@
|
|
1
1
|
import React from "react";
|
2
2
|
import { createRoot } from "react-dom/client";
|
3
3
|
import { userEvent, waitFor, within, expect, fn } from "@storybook/test";
|
4
|
-
import { Example } from "
|
5
|
-
import Reshaped from "
|
6
|
-
import View from "
|
7
|
-
import Theme from "
|
8
|
-
import Button from "
|
4
|
+
import { Example } from "../../../utilities/storybook/index.js";
|
5
|
+
import Reshaped from "../../Reshaped/index.js";
|
6
|
+
import View from "../../View/index.js";
|
7
|
+
import Theme from "../../Theme/index.js";
|
8
|
+
import Button from "../../Button/index.js";
|
9
9
|
import Flyout from "../index.js";
|
10
|
-
import TextField from "
|
11
|
-
import
|
12
|
-
import
|
13
|
-
|
10
|
+
import TextField from "../../TextField/index.js";
|
11
|
+
import Select from "../../Select/index.js";
|
12
|
+
import Switch from "../../Switch/index.js";
|
13
|
+
import { sleep } from "../../../utilities/helpers.js";
|
14
|
+
export default { title: "Utility components/Flyout" };
|
14
15
|
const Content = (props) => (<div style={{
|
15
16
|
background: "var(--rs-color-background-elevation-overlay)",
|
16
17
|
padding: "var(--rs-unit-x4)",
|
17
|
-
height: props.height
|
18
|
+
height: props.height === false ? undefined : props.height || 150,
|
18
19
|
minWidth: props.width === false ? undefined : props.width || 160,
|
19
20
|
borderRadius: "var(--rs-radius-medium)",
|
20
21
|
border: "1px solid var(--rs-color-border-neutral-faded)",
|
@@ -23,10 +24,10 @@ const Content = (props) => (<div style={{
|
|
23
24
|
{props.children || "Content"}
|
24
25
|
</div>);
|
25
26
|
const Demo = (props) => {
|
26
|
-
const { position = "bottom-start", children, contentHeight, contentWidth, ...rest } = props;
|
27
|
+
const { position = "bottom-start", children, text, contentHeight, contentWidth, ...rest } = props;
|
27
28
|
return (<Flyout position={position} {...rest}>
|
28
29
|
<Flyout.Trigger>
|
29
|
-
{(attributes) => <Button attributes={attributes}>{position}</Button>}
|
30
|
+
{(attributes) => <Button attributes={attributes}>{text || position}</Button>}
|
30
31
|
</Flyout.Trigger>
|
31
32
|
<Flyout.Content>
|
32
33
|
<Content height={contentHeight} width={contentWidth}>
|
@@ -38,9 +39,9 @@ const Demo = (props) => {
|
|
38
39
|
export const position = {
|
39
40
|
name: "position",
|
40
41
|
render: () => {
|
41
|
-
return (<View gap={4} padding={50} align="center" justify="center">
|
42
|
+
return (<View gap={4} padding={50} align="center" justify="center" height="120vh" width="120%">
|
42
43
|
<View gap={4} direction="row">
|
43
|
-
<Demo position="top-start"/>
|
44
|
+
<Demo position="top-start" defaultActive/>
|
44
45
|
<Demo position="top"/>
|
45
46
|
<Demo position="top-end"/>
|
46
47
|
</View>
|
@@ -60,7 +61,7 @@ export const position = {
|
|
60
61
|
<View gap={4} direction="row">
|
61
62
|
<Demo position="bottom-start"/>
|
62
63
|
<Demo position="bottom"/>
|
63
|
-
<Demo position="bottom-end"
|
64
|
+
<Demo position="bottom-end"/>
|
64
65
|
</View>
|
65
66
|
</View>);
|
66
67
|
},
|
@@ -150,103 +151,47 @@ export const activeFalse = {
|
|
150
151
|
expect(item).not.toBeInTheDocument();
|
151
152
|
},
|
152
153
|
};
|
154
|
+
const modeContent = (<View direction="row" gap={2}>
|
155
|
+
<Button onClick={() => { }}>Action 1</Button>
|
156
|
+
<Button onClick={() => { }}>Action 2</Button>
|
157
|
+
<Button onClick={() => { }}>Action 3</Button>
|
158
|
+
</View>);
|
153
159
|
export const modes = {
|
154
160
|
name: "triggerType, trapFocusMode",
|
155
161
|
render: () => {
|
156
162
|
return (<Example>
|
157
|
-
<Example.Item title=
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
<
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
"arrow navigation, tab closes the content",
|
173
|
-
]}>
|
174
|
-
<Demo position="bottom-start" trapFocusMode="action-menu">
|
175
|
-
<View direction="row" gap={2}>
|
176
|
-
<Button onClick={() => { }}>Action 1</Button>
|
177
|
-
<Button onClick={() => { }}>Action 2</Button>
|
178
|
-
<Button onClick={() => { }}>Action 3</Button>
|
179
|
-
</View>
|
180
|
-
</Demo>
|
181
|
-
</Example.Item>
|
182
|
-
|
183
|
-
<Example.Item title={[
|
184
|
-
"triggerType: click, trapFocusMode: content-menu",
|
185
|
-
"tab navigation, simulates natural focus order for navigation menus",
|
186
|
-
]}>
|
187
|
-
<Demo position="bottom-start" trapFocusMode="content-menu">
|
188
|
-
<View direction="row" gap={2}>
|
189
|
-
<Button onClick={() => { }}>Action 1</Button>
|
190
|
-
<Button onClick={() => { }}>Action 2</Button>
|
191
|
-
<Button onClick={() => { }}>Action 3</Button>
|
192
|
-
</View>
|
193
|
-
</Demo>
|
194
|
-
</Example.Item>
|
195
|
-
|
196
|
-
<Example.Item title="triggerType: hover, trapFocusMode: dialog">
|
197
|
-
<Demo position="bottom-start" trapFocusMode="dialog" triggerType="hover">
|
198
|
-
<View direction="row" gap={2}>
|
199
|
-
<Button onClick={() => { }}>Action 1</Button>
|
200
|
-
<Button onClick={() => { }}>Action 2</Button>
|
201
|
-
<Button onClick={() => { }}>Action 3</Button>
|
202
|
-
</View>
|
203
|
-
</Demo>
|
204
|
-
</Example.Item>
|
205
|
-
|
206
|
-
<Example.Item title="triggerType: hover, trapFocusMode: action-menu">
|
207
|
-
<Demo position="bottom-start" trapFocusMode="action-menu" triggerType="hover">
|
208
|
-
<View direction="row" gap={2}>
|
209
|
-
<Button onClick={() => { }}>Action 1</Button>
|
210
|
-
<Button onClick={() => { }}>Action 2</Button>
|
211
|
-
<Button onClick={() => { }}>Action 3</Button>
|
212
|
-
</View>
|
213
|
-
</Demo>
|
214
|
-
</Example.Item>
|
215
|
-
|
216
|
-
<Example.Item title="triggerType: hover, trapFocusMode: content-menu">
|
217
|
-
<Demo position="bottom-start" trapFocusMode="content-menu" triggerType="hover">
|
218
|
-
<View direction="row" gap={2}>
|
219
|
-
<Button onClick={() => { }}>Action 1</Button>
|
220
|
-
<Button onClick={() => { }}>Action 2</Button>
|
221
|
-
<Button onClick={() => { }}>Action 3</Button>
|
222
|
-
</View>
|
223
|
-
</Demo>
|
224
|
-
</Example.Item>
|
225
|
-
|
226
|
-
<Example.Item title={[
|
227
|
-
"triggerType: hover, trapFocusMode: content-menu, no focusable elements inside",
|
228
|
-
"keeps the focus on trigger",
|
229
|
-
]}>
|
230
|
-
<Demo position="bottom-start" trapFocusMode="content-menu" triggerType="hover"/>
|
163
|
+
<Example.Item title="triggerType: click">
|
164
|
+
<View direction="row" gap={4}>
|
165
|
+
<Demo position="bottom-start" trapFocusMode="dialog" text="dialog">
|
166
|
+
{modeContent}
|
167
|
+
</Demo>
|
168
|
+
<Demo position="bottom-start" trapFocusMode="action-menu" text="action-menu">
|
169
|
+
{modeContent}
|
170
|
+
</Demo>
|
171
|
+
<Demo position="bottom-start" trapFocusMode="action-bar" text="action-bar">
|
172
|
+
{modeContent}
|
173
|
+
</Demo>
|
174
|
+
<Demo position="bottom-start" trapFocusMode="content-menu" text="content-menu">
|
175
|
+
{modeContent}
|
176
|
+
</Demo>
|
177
|
+
</View>
|
231
178
|
</Example.Item>
|
232
179
|
|
233
|
-
<Example.Item title=
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
<
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
</View>
|
249
|
-
</Demo>
|
180
|
+
<Example.Item title="triggerType: hover">
|
181
|
+
<View direction="row" gap={4}>
|
182
|
+
<Demo position="bottom-start" trapFocusMode="dialog" triggerType="hover" text="dialog">
|
183
|
+
{modeContent}
|
184
|
+
</Demo>
|
185
|
+
<Demo position="bottom-start" trapFocusMode="action-menu" triggerType="hover" text="action-menu">
|
186
|
+
{modeContent}
|
187
|
+
</Demo>
|
188
|
+
<Demo position="bottom-start" trapFocusMode="action-bar" triggerType="hover" text="action-bar">
|
189
|
+
{modeContent}
|
190
|
+
</Demo>
|
191
|
+
<Demo position="bottom-start" trapFocusMode="content-menu" triggerType="hover" text="content-menu">
|
192
|
+
{modeContent}
|
193
|
+
</Demo>
|
194
|
+
</View>
|
250
195
|
</Example.Item>
|
251
196
|
</Example>);
|
252
197
|
},
|
@@ -255,7 +200,7 @@ export const positionFallbacks = {
|
|
255
200
|
name: "fallbackPositions",
|
256
201
|
render: () => {
|
257
202
|
return (<Example>
|
258
|
-
<Example.Item title="position: top,
|
203
|
+
<Example.Item title="position: top, default fallbacks">
|
259
204
|
<View justify="center" align="center">
|
260
205
|
<Demo position="top"/>
|
261
206
|
</View>
|
@@ -304,6 +249,7 @@ export const contentShift = {
|
|
304
249
|
export const disableContentHover = {
|
305
250
|
name: "disableContentHover",
|
306
251
|
render: () => <Demo triggerType="hover" disableContentHover/>,
|
252
|
+
// Can't trigger real mouse move from trigger to content in play function, so testing it manually
|
307
253
|
};
|
308
254
|
export const disableCloseOnOutsideClick = {
|
309
255
|
name: "disableCloseOnOutsideClick",
|
@@ -350,14 +296,26 @@ export const containerRef = {
|
|
350
296
|
name: "containerRef",
|
351
297
|
render: () => {
|
352
298
|
const portalRef = React.useRef(null);
|
353
|
-
|
354
|
-
|
299
|
+
const portalRef2 = React.useRef(null);
|
300
|
+
const portalRef3 = React.useRef(null);
|
301
|
+
return (<View gap={4} direction="row">
|
302
|
+
<View grow backgroundColor="neutral-faded" borderRadius="small" height={80} attributes={{ ref: portalRef, "data-testid": "container" }} justify="end" align="start" padding={4}>
|
303
|
+
<Demo containerRef={portalRef} position="bottom-start" defaultActive/>
|
304
|
+
</View>
|
305
|
+
<View grow backgroundColor="neutral-faded" borderRadius="small" height={80} attributes={{ ref: portalRef2 }} justify="start" align="end" padding={4}>
|
306
|
+
<Demo containerRef={portalRef2} position="top-end"/>
|
307
|
+
</View>
|
308
|
+
<View width={50} backgroundColor="neutral-faded" borderRadius="small" height={80} attributes={{ ref: portalRef3 }} padding={4} overflow="auto">
|
309
|
+
<View height={120} width="120%" justify="center" align="center">
|
310
|
+
<Demo containerRef={portalRef3} position="bottom-end"/>
|
311
|
+
</View>
|
312
|
+
</View>
|
355
313
|
</View>);
|
356
314
|
},
|
357
315
|
play: async ({ canvasElement }) => {
|
358
316
|
const canvas = within(canvasElement.ownerDocument.body);
|
359
317
|
const containerEl = canvas.getByTestId("container");
|
360
|
-
const contentEl = canvas.
|
318
|
+
const contentEl = canvas.getAllByText("Content")[0];
|
361
319
|
expect(containerEl).toContainElement(contentEl);
|
362
320
|
},
|
363
321
|
};
|
@@ -419,7 +377,7 @@ export const contentAttributes = {
|
|
419
377
|
render: () => {
|
420
378
|
return (<Flyout position="bottom" defaultActive>
|
421
379
|
<Flyout.Trigger>
|
422
|
-
{(attributes) => <Button attributes={attributes}
|
380
|
+
{(attributes) => <Button attributes={attributes}>Trigger</Button>}
|
423
381
|
</Flyout.Trigger>
|
424
382
|
<Flyout.Content attributes={{ "data-testid": "test-id" }} className="test-classname">
|
425
383
|
<Content />
|
@@ -462,8 +420,8 @@ export const testShadowDom = {
|
|
462
420
|
export const testInsideFixed = {
|
463
421
|
name: "test: inside position fixed",
|
464
422
|
render: () => (<React.Fragment>
|
465
|
-
<View position="fixed"
|
466
|
-
<Demo defaultActive/>
|
423
|
+
<View position="fixed" insetBottom={2} insetStart={2} insetEnd={2} backgroundColor="elevation-overlay" borderColor="neutral-faded" borderRadius="small" padding={4} zIndex={10} attributes={{ "data-testid": "container" }}>
|
424
|
+
<Demo defaultActive position="top-start"/>
|
467
425
|
</View>
|
468
426
|
<View paddingTop={18} gap={4}>
|
469
427
|
<View height={200} backgroundColor="neutral-faded" borderRadius="small"/>
|
@@ -532,7 +490,7 @@ export const testDynamicBounds = {
|
|
532
490
|
<Button onClick={() => setSize("medium")}>Small button</Button>
|
533
491
|
</View>
|
534
492
|
<View height={100}>
|
535
|
-
<Flyout position="bottom" instanceRef={flyoutRef} disableCloseOnOutsideClick>
|
493
|
+
<Flyout position="bottom" instanceRef={flyoutRef} disableCloseOnOutsideClick defaultActive>
|
536
494
|
<Flyout.Trigger>
|
537
495
|
{(attributes) => (<div style={{ position: "absolute", left: `${left}%`, top: `${top}%` }}>
|
538
496
|
<Button color="primary" attributes={attributes} size={size}>
|
@@ -551,19 +509,19 @@ export const testDynamicBounds = {
|
|
551
509
|
export const testScopedTheming = {
|
552
510
|
name: "test: content uses scope theme",
|
553
511
|
render: () => (<View gap={3} align="start">
|
554
|
-
<Button color="primary">
|
555
|
-
<Theme name="
|
512
|
+
<Button color="primary">Slate button</Button>
|
513
|
+
<Theme name="reshaped">
|
556
514
|
<Flyout triggerType="click" active position="bottom-start">
|
557
515
|
<Flyout.Trigger>
|
558
516
|
{(attributes) => (<Button color="primary" attributes={attributes}>
|
559
|
-
|
517
|
+
Reshaped button
|
560
518
|
</Button>)}
|
561
519
|
</Flyout.Trigger>
|
562
520
|
<Flyout.Content>
|
563
521
|
<Content>
|
564
522
|
<View gap={1}>
|
565
523
|
<View.Item>Portal content, rendered in body</View.Item>
|
566
|
-
<Button color="primary">
|
524
|
+
<Button color="primary">Reshaped button</Button>
|
567
525
|
</View>
|
568
526
|
</Content>
|
569
527
|
</Flyout.Content>
|
@@ -571,3 +529,55 @@ export const testScopedTheming = {
|
|
571
529
|
</Theme>
|
572
530
|
</View>),
|
573
531
|
};
|
532
|
+
export const testWithoutFocusable = {
|
533
|
+
name: "test: without focusable content",
|
534
|
+
render: () => <Demo position="bottom-start"/>,
|
535
|
+
play: async ({ canvasElement }) => {
|
536
|
+
const canvas = within(canvasElement.ownerDocument.body);
|
537
|
+
const trigger = canvas.getAllByRole("button")[0];
|
538
|
+
await userEvent.click(trigger);
|
539
|
+
await waitFor(() => {
|
540
|
+
const content = canvas.getByText("Content");
|
541
|
+
expect(content).toBeVisible();
|
542
|
+
});
|
543
|
+
expect(document.activeElement).toBe(trigger);
|
544
|
+
},
|
545
|
+
};
|
546
|
+
export const testChangeSize = {
|
547
|
+
name: "test: size updates",
|
548
|
+
render: () => {
|
549
|
+
const [position, setPosition] = React.useState("bottom-start");
|
550
|
+
const [updatedHeight, setUpdatedHeight] = React.useState(false);
|
551
|
+
return (<>
|
552
|
+
<View direction="row" gap={4} align="center">
|
553
|
+
<Select name="position" options={[
|
554
|
+
"bottom-start",
|
555
|
+
"bottom",
|
556
|
+
"bottom-end",
|
557
|
+
"top-start",
|
558
|
+
"top",
|
559
|
+
"top-end",
|
560
|
+
"start-top",
|
561
|
+
"start",
|
562
|
+
"start-bottom",
|
563
|
+
"end-top",
|
564
|
+
"end",
|
565
|
+
"end-bottom",
|
566
|
+
].map((p) => ({ label: p, value: p }))} onChange={(args) => setPosition(args.value)} value={position}/>
|
567
|
+
<Switch name="height" onChange={(args) => setUpdatedHeight(args.checked)}>
|
568
|
+
Change height
|
569
|
+
</Switch>
|
570
|
+
</View>
|
571
|
+
<div style={{
|
572
|
+
position: "fixed",
|
573
|
+
top: "50%",
|
574
|
+
left: "50%",
|
575
|
+
transform: "translate(-50%, -50%)",
|
576
|
+
}}>
|
577
|
+
<Demo position={position} disableCloseOnOutsideClick active contentHeight={false}>
|
578
|
+
<View backgroundColor="neutral-faded" borderRadius="small" height={updatedHeight ? 50 : 25} attributes={{ style: { transition: "0.2s ease-in-out" } }}/>
|
579
|
+
</Demo>
|
580
|
+
</div>
|
581
|
+
</>);
|
582
|
+
},
|
583
|
+
};
|
@@ -0,0 +1,116 @@
|
|
1
|
+
import React from "react";
|
2
|
+
import useRTL from "../../hooks/useRTL.js";
|
3
|
+
import flyout from "./utilities/flyout.js";
|
4
|
+
import { defaultStyles, resetStyles } from "./Flyout.constants.js";
|
5
|
+
const flyoutReducer = (state, action) => {
|
6
|
+
switch (action.type) {
|
7
|
+
case "render":
|
8
|
+
if (state.status !== "idle")
|
9
|
+
return state;
|
10
|
+
// Disable events before it's positioned to avoid mouseleave getting triggered
|
11
|
+
return { ...state, status: "rendered", styles: { pointerEvents: "none", ...resetStyles } };
|
12
|
+
case "position":
|
13
|
+
if (!action.payload.sync && state.status !== "rendered")
|
14
|
+
return state;
|
15
|
+
if (action.payload.sync && state.status !== "visible")
|
16
|
+
return state;
|
17
|
+
return {
|
18
|
+
...state,
|
19
|
+
status: action.payload.sync ? "visible" : "positioned",
|
20
|
+
position: action.payload.position,
|
21
|
+
styles: { ...defaultStyles, ...action.payload.styles },
|
22
|
+
};
|
23
|
+
case "show":
|
24
|
+
if (state.status !== "positioned")
|
25
|
+
return state;
|
26
|
+
return { ...state, status: "visible" };
|
27
|
+
case "hide":
|
28
|
+
if (state.status !== "visible")
|
29
|
+
return state;
|
30
|
+
return { ...state, status: "hidden" };
|
31
|
+
case "remove":
|
32
|
+
if (state.status !== "hidden" && state.status !== "visible")
|
33
|
+
return state;
|
34
|
+
return { ...state, status: "idle", styles: resetStyles };
|
35
|
+
default:
|
36
|
+
throw new Error("[Reshaped] Invalid flyout reducer type");
|
37
|
+
}
|
38
|
+
};
|
39
|
+
const useFlyout = (args) => {
|
40
|
+
const { triggerElRef, flyoutElRef, triggerBounds, contentGap, contentShift, ...options } = args;
|
41
|
+
const { position: defaultPosition = "bottom", fallbackPositions, width, container } = options;
|
42
|
+
const lastUsedFallbackRef = React.useRef(defaultPosition);
|
43
|
+
// Memo the array internally to avoid new arrays triggering useCallback
|
44
|
+
const cachedFallbackPositions = React.useMemo(() => fallbackPositions,
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
46
|
+
[fallbackPositions?.join(" ")]);
|
47
|
+
const [isRTL] = useRTL();
|
48
|
+
const [state, dispatch] = React.useReducer(flyoutReducer, {
|
49
|
+
position: defaultPosition,
|
50
|
+
styles: defaultStyles,
|
51
|
+
status: "idle",
|
52
|
+
});
|
53
|
+
const render = React.useCallback(() => {
|
54
|
+
dispatch({ type: "render" });
|
55
|
+
}, []);
|
56
|
+
const show = React.useCallback(() => {
|
57
|
+
dispatch({ type: "show" });
|
58
|
+
}, []);
|
59
|
+
const hide = React.useCallback(() => {
|
60
|
+
dispatch({ type: "hide" });
|
61
|
+
}, []);
|
62
|
+
const remove = React.useCallback(() => {
|
63
|
+
dispatch({ type: "remove" });
|
64
|
+
}, []);
|
65
|
+
const handleFallback = React.useCallback((position) => {
|
66
|
+
lastUsedFallbackRef.current = position;
|
67
|
+
}, []);
|
68
|
+
const updatePosition = React.useCallback((options) => {
|
69
|
+
if (!flyoutElRef.current)
|
70
|
+
return;
|
71
|
+
const nextFlyoutData = flyout({
|
72
|
+
triggerEl: triggerElRef.current,
|
73
|
+
flyoutEl: flyoutElRef.current,
|
74
|
+
triggerBounds,
|
75
|
+
width,
|
76
|
+
position: defaultPosition,
|
77
|
+
fallbackPositions: cachedFallbackPositions,
|
78
|
+
lastUsedFallback: lastUsedFallbackRef.current,
|
79
|
+
onFallback: handleFallback,
|
80
|
+
rtl: isRTL,
|
81
|
+
container,
|
82
|
+
contentGap,
|
83
|
+
contentShift,
|
84
|
+
});
|
85
|
+
if (nextFlyoutData) {
|
86
|
+
dispatch({ type: "position", payload: { ...nextFlyoutData, sync: options?.sync } });
|
87
|
+
}
|
88
|
+
}, [
|
89
|
+
container,
|
90
|
+
defaultPosition,
|
91
|
+
cachedFallbackPositions,
|
92
|
+
isRTL,
|
93
|
+
flyoutElRef,
|
94
|
+
triggerElRef,
|
95
|
+
triggerBounds,
|
96
|
+
width,
|
97
|
+
contentGap,
|
98
|
+
contentShift,
|
99
|
+
handleFallback,
|
100
|
+
]);
|
101
|
+
React.useEffect(() => {
|
102
|
+
if (state.status === "rendered")
|
103
|
+
updatePosition();
|
104
|
+
}, [state.status, updatePosition]);
|
105
|
+
return React.useMemo(() => ({
|
106
|
+
position: state.position,
|
107
|
+
styles: state.styles,
|
108
|
+
status: state.status,
|
109
|
+
updatePosition,
|
110
|
+
render,
|
111
|
+
hide,
|
112
|
+
remove,
|
113
|
+
show,
|
114
|
+
}), [render, updatePosition, hide, remove, show, state.position, state.styles, state.status]);
|
115
|
+
};
|
116
|
+
export default useFlyout;
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import type * as T from "../Flyout.types";
|
2
|
+
/**
|
3
|
+
* Calculate styles for the current position
|
4
|
+
*/
|
5
|
+
declare const calculatePosition: (args: {
|
6
|
+
triggerBounds: DOMRect;
|
7
|
+
flyoutBounds: {
|
8
|
+
width: number;
|
9
|
+
height: number;
|
10
|
+
};
|
11
|
+
passedContainer?: HTMLElement | null;
|
12
|
+
containerBounds: DOMRect;
|
13
|
+
} & Pick<T.Options, "position" | "rtl" | "width" | "contentGap" | "contentShift">) => {
|
14
|
+
position: T.Position;
|
15
|
+
styles: {
|
16
|
+
width: number | undefined;
|
17
|
+
left: number | undefined;
|
18
|
+
right: number | undefined;
|
19
|
+
top: number | undefined;
|
20
|
+
bottom: number | undefined;
|
21
|
+
transform: string;
|
22
|
+
};
|
23
|
+
boundaries: {
|
24
|
+
left: number;
|
25
|
+
top: number;
|
26
|
+
height: number;
|
27
|
+
width: number;
|
28
|
+
};
|
29
|
+
};
|
30
|
+
export default calculatePosition;
|
@@ -0,0 +1,129 @@
|
|
1
|
+
const SCREEN_OFFSET = 16;
|
2
|
+
const getRTLPosition = (position) => {
|
3
|
+
if (position.includes("start"))
|
4
|
+
return position.replace("start", "end");
|
5
|
+
if (position.includes("end"))
|
6
|
+
return position.replace("end", "start");
|
7
|
+
return position;
|
8
|
+
};
|
9
|
+
/**
|
10
|
+
* Get a position value which centers 2 elements vertically or horizontally
|
11
|
+
*/
|
12
|
+
const centerBySize = (originSize, targetSize) => {
|
13
|
+
return Math.floor(originSize / 2 - targetSize / 2);
|
14
|
+
};
|
15
|
+
/**
|
16
|
+
* Calculate styles for the current position
|
17
|
+
*/
|
18
|
+
const calculatePosition = (args) => {
|
19
|
+
const { triggerBounds, flyoutBounds, containerBounds, position: passedPosition, rtl, width, contentGap = 0, contentShift = 0, passedContainer, } = args;
|
20
|
+
const isFullWidth = width === "full" || width === "100%";
|
21
|
+
let left = 0;
|
22
|
+
let top = 0;
|
23
|
+
let bottom = null;
|
24
|
+
let right = null;
|
25
|
+
let position = passedPosition;
|
26
|
+
if (rtl)
|
27
|
+
position = getRTLPosition(position);
|
28
|
+
if (isFullWidth || width === "trigger") {
|
29
|
+
position = position.includes("top") ? "top" : "bottom";
|
30
|
+
}
|
31
|
+
const isHorizontalPosition = !!position.match(/^(start|end)/);
|
32
|
+
const isVerticalPosition = !!position.match(/^(top|bottom)/);
|
33
|
+
// contentGap adds padding to the flyout to make sure it doesn't disapper while moving the mouse to the content
|
34
|
+
// So its width/height is bigger than the visible part of the content
|
35
|
+
const flyoutWidth = flyoutBounds.width + (isHorizontalPosition ? contentGap : 0);
|
36
|
+
const flyoutHeight = flyoutBounds.height + (isVerticalPosition ? contentGap : 0);
|
37
|
+
const triggerHeight = triggerBounds.height;
|
38
|
+
const triggerWidth = triggerBounds.width;
|
39
|
+
const containerY = passedContainer?.scrollTop || 0;
|
40
|
+
const containerX = passedContainer?.scrollLeft || 0;
|
41
|
+
const relativeLeft = triggerBounds.left - containerBounds.left + containerX;
|
42
|
+
const relativeTop = triggerBounds.top - containerBounds.top + containerY;
|
43
|
+
const relativeRight = containerBounds.right - triggerBounds.right - containerX;
|
44
|
+
const relativeBottom = containerBounds.bottom - triggerBounds.bottom - containerY;
|
45
|
+
switch (position) {
|
46
|
+
case "start":
|
47
|
+
case "start-top":
|
48
|
+
case "start-bottom":
|
49
|
+
right = relativeRight + triggerWidth;
|
50
|
+
left = relativeLeft - flyoutWidth;
|
51
|
+
break;
|
52
|
+
case "end":
|
53
|
+
case "end-top":
|
54
|
+
case "end-bottom":
|
55
|
+
left = relativeLeft + triggerWidth;
|
56
|
+
break;
|
57
|
+
case "bottom":
|
58
|
+
case "top":
|
59
|
+
left = relativeLeft + centerBySize(triggerWidth, flyoutWidth) + contentShift;
|
60
|
+
break;
|
61
|
+
case "top-start":
|
62
|
+
case "bottom-start":
|
63
|
+
left = relativeLeft + contentShift;
|
64
|
+
break;
|
65
|
+
case "top-end":
|
66
|
+
case "bottom-end":
|
67
|
+
right = relativeRight - contentShift;
|
68
|
+
left = relativeLeft + triggerWidth - flyoutWidth + contentShift;
|
69
|
+
break;
|
70
|
+
default:
|
71
|
+
break;
|
72
|
+
}
|
73
|
+
switch (position) {
|
74
|
+
case "top":
|
75
|
+
case "top-start":
|
76
|
+
case "top-end":
|
77
|
+
bottom = relativeBottom + triggerHeight;
|
78
|
+
top = relativeTop - flyoutHeight;
|
79
|
+
break;
|
80
|
+
case "bottom":
|
81
|
+
case "bottom-start":
|
82
|
+
case "bottom-end":
|
83
|
+
top = relativeTop + triggerHeight;
|
84
|
+
break;
|
85
|
+
case "start":
|
86
|
+
case "end":
|
87
|
+
top = relativeTop + centerBySize(triggerHeight, flyoutHeight) + contentShift;
|
88
|
+
break;
|
89
|
+
case "start-top":
|
90
|
+
case "end-top":
|
91
|
+
top = relativeTop + contentShift;
|
92
|
+
break;
|
93
|
+
case "start-bottom":
|
94
|
+
case "end-bottom":
|
95
|
+
bottom = relativeBottom - contentShift;
|
96
|
+
top = relativeTop + triggerHeight - flyoutHeight + contentShift;
|
97
|
+
break;
|
98
|
+
default:
|
99
|
+
break;
|
100
|
+
}
|
101
|
+
let widthStyle;
|
102
|
+
if (isFullWidth) {
|
103
|
+
left = SCREEN_OFFSET;
|
104
|
+
widthStyle = window.innerWidth - SCREEN_OFFSET * 2;
|
105
|
+
}
|
106
|
+
else if (width === "trigger") {
|
107
|
+
widthStyle = triggerBounds.width;
|
108
|
+
}
|
109
|
+
const translateX = right !== null ? -right : left;
|
110
|
+
const translateY = bottom !== null ? -bottom : top;
|
111
|
+
return {
|
112
|
+
position,
|
113
|
+
styles: {
|
114
|
+
width: widthStyle,
|
115
|
+
left: right === null ? 0 : undefined,
|
116
|
+
right: right === null ? undefined : 0,
|
117
|
+
top: bottom === null ? 0 : undefined,
|
118
|
+
bottom: bottom === null ? undefined : 0,
|
119
|
+
transform: `translate(${translateX}px, ${translateY}px)`,
|
120
|
+
},
|
121
|
+
boundaries: {
|
122
|
+
left,
|
123
|
+
top,
|
124
|
+
height: Math.ceil(flyoutHeight),
|
125
|
+
width: widthStyle ?? Math.ceil(flyoutWidth),
|
126
|
+
},
|
127
|
+
};
|
128
|
+
};
|
129
|
+
export default calculatePosition;
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import type * as G from "../../../types/global";
|
2
|
+
import type * as T from "../Flyout.types";
|
3
|
+
/**
|
4
|
+
* Set position of the target element to fit on the screen
|
5
|
+
*/
|
6
|
+
declare const flyout: (args: T.Options & {
|
7
|
+
flyoutEl: HTMLElement;
|
8
|
+
triggerEl: HTMLElement | null;
|
9
|
+
triggerBounds?: DOMRect | G.Coordinates | null;
|
10
|
+
}) => T.FlyoutData | undefined;
|
11
|
+
export default flyout;
|