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.
Files changed (78) hide show
  1. package/CHANGELOG.md +11 -21
  2. package/dist/bundle.css +1 -1
  3. package/dist/bundle.d.ts +36 -31
  4. package/dist/bundle.js +10 -10
  5. package/dist/components/Autocomplete/tests/Autocomplete.stories.js +0 -1
  6. package/dist/components/Button/Button.types.d.ts +1 -1
  7. package/dist/components/Card/Card.d.ts +1 -1
  8. package/dist/components/Checkbox/Checkbox.module.css +1 -1
  9. package/dist/components/DropdownMenu/DropdownMenu.js +1 -1
  10. package/dist/components/DropdownMenu/DropdownMenu.types.d.ts +1 -1
  11. package/dist/components/DropdownMenu/tests/DropdownMenu.stories.d.ts +1 -1
  12. package/dist/components/DropdownMenu/tests/DropdownMenu.test.stories.d.ts +1 -1
  13. package/dist/components/Flyout/Flyout.constants.d.ts +6 -0
  14. package/dist/components/Flyout/Flyout.constants.js +19 -0
  15. package/dist/components/{_private/Flyout → Flyout}/Flyout.types.d.ts +3 -3
  16. package/dist/components/{_private/Flyout → Flyout}/FlyoutContent.js +25 -20
  17. package/dist/components/{_private/Flyout → Flyout}/FlyoutControlled.js +9 -9
  18. package/dist/components/{_private/Flyout → Flyout}/tests/Flyout.stories.d.ts +6 -4
  19. package/dist/components/{_private/Flyout → Flyout}/tests/Flyout.stories.js +128 -118
  20. package/dist/components/{_private/Flyout → Flyout}/useFlyout.d.ts +1 -1
  21. package/dist/components/Flyout/useFlyout.js +116 -0
  22. package/dist/components/Flyout/utilities/calculatePosition.d.ts +30 -0
  23. package/dist/components/Flyout/utilities/calculatePosition.js +129 -0
  24. package/dist/components/Flyout/utilities/flyout.d.ts +11 -0
  25. package/dist/components/Flyout/utilities/flyout.js +79 -0
  26. package/dist/components/Flyout/utilities/isFullyVisible.d.ts +10 -0
  27. package/dist/components/Flyout/utilities/isFullyVisible.js +24 -0
  28. package/dist/components/Link/Link.d.ts +1 -1
  29. package/dist/components/Popover/Popover.d.ts +1 -1
  30. package/dist/components/Popover/Popover.js +1 -1
  31. package/dist/components/Popover/Popover.types.d.ts +2 -1
  32. package/dist/components/Popover/tests/Popover.stories.d.ts +2 -2
  33. package/dist/components/Popover/tests/Popover.test.stories.d.ts +2 -2
  34. package/dist/components/Radio/Radio.module.css +1 -1
  35. package/dist/components/ScrollArea/ScrollArea.js +6 -3
  36. package/dist/components/ScrollArea/tests/ScrollArea.stories.d.ts +4 -13
  37. package/dist/components/ScrollArea/tests/ScrollArea.stories.js +30 -129
  38. package/dist/components/ScrollArea/tests/ScrollArea.test.stories.d.ts +23 -0
  39. package/dist/components/ScrollArea/tests/ScrollArea.test.stories.js +66 -0
  40. package/dist/components/Tabs/TabsContext.d.ts +2 -2
  41. package/dist/components/Tooltip/Tooltip.js +1 -1
  42. package/dist/components/Tooltip/Tooltip.types.d.ts +1 -1
  43. package/dist/config/tailwind.d.ts +1 -1
  44. package/dist/hooks/useIsomorphicLayoutEffect.d.ts +1 -1
  45. package/dist/index.d.ts +36 -31
  46. package/dist/index.js +20 -16
  47. package/dist/utilities/dom/find.d.ts +6 -9
  48. package/dist/utilities/dom/find.js +17 -15
  49. package/dist/utilities/dom/index.d.ts +1 -1
  50. package/dist/utilities/dom/index.js +1 -1
  51. package/dist/utilities/scroll/lock.js +4 -3
  52. package/package.json +10 -9
  53. package/CHANGELOG-old.md +0 -14
  54. package/dist/components/_private/Flyout/Flyout.constants.d.ts +0 -3
  55. package/dist/components/_private/Flyout/Flyout.constants.js +0 -3
  56. package/dist/components/_private/Flyout/useFlyout.js +0 -211
  57. package/dist/components/_private/Flyout/utilities/calculatePosition.d.ts +0 -19
  58. package/dist/components/_private/Flyout/utilities/calculatePosition.js +0 -102
  59. package/dist/components/_private/Flyout/utilities/isFullyVisible.d.ts +0 -8
  60. package/dist/components/_private/Flyout/utilities/isFullyVisible.js +0 -16
  61. /package/dist/components/{_private/Flyout → Flyout}/Flyout.context.d.ts +0 -0
  62. /package/dist/components/{_private/Flyout → Flyout}/Flyout.context.js +0 -0
  63. /package/dist/components/{_private/Flyout → Flyout}/Flyout.d.ts +0 -0
  64. /package/dist/components/{_private/Flyout → Flyout}/Flyout.js +0 -0
  65. /package/dist/components/{_private/Flyout → Flyout}/Flyout.module.css +0 -0
  66. /package/dist/components/{_private/Flyout → Flyout}/Flyout.types.js +0 -0
  67. /package/dist/components/{_private/Flyout → Flyout}/FlyoutContent.d.ts +0 -0
  68. /package/dist/components/{_private/Flyout → Flyout}/FlyoutControlled.d.ts +0 -0
  69. /package/dist/components/{_private/Flyout → Flyout}/FlyoutTrigger.d.ts +0 -0
  70. /package/dist/components/{_private/Flyout → Flyout}/FlyoutTrigger.js +0 -0
  71. /package/dist/components/{_private/Flyout → Flyout}/FlyoutUncontrolled.d.ts +0 -0
  72. /package/dist/components/{_private/Flyout → Flyout}/FlyoutUncontrolled.js +0 -0
  73. /package/dist/components/{_private/Flyout → Flyout}/index.d.ts +0 -0
  74. /package/dist/components/{_private/Flyout → Flyout}/index.js +0 -0
  75. /package/dist/components/{_private/Flyout → Flyout}/utilities/cooldown.d.ts +0 -0
  76. /package/dist/components/{_private/Flyout → Flyout}/utilities/cooldown.js +0 -0
  77. /package/dist/components/{_private/Flyout → Flyout}/utilities/getPositionFallbacks.d.ts +0 -0
  78. /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 "../../../../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";
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 "../../../TextField/index.js";
11
- import MenuItem from "../../../MenuItem/index.js";
12
- import { sleep } from "../../../../utilities/helpers.js";
13
- export default { title: "Internal/Flyout" };
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 ?? 150,
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" defaultActive/>
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
- "triggerType: click, trapFocusMode: dialog",
159
- "tab navigation, completely traps the focus inside",
160
- ]}>
161
- <Demo position="bottom-start" trapFocusMode="dialog">
162
- <View direction="row" gap={2}>
163
- <Button onClick={() => { }}>Action 1</Button>
164
- <Button onClick={() => { }}>Action 2</Button>
165
- <Button onClick={() => { }}>Action 3</Button>
166
- </View>
167
- </Demo>
168
- </Example.Item>
169
-
170
- <Example.Item title={[
171
- "triggerType: click, trapFocusMode: action-menu",
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
- "triggerType: focus, trapFocusMode: selection-menu",
235
- "keeps real focus on trigger and simulates arrow key item selection focus on the content",
236
- ]}>
237
- <Demo position="bottom-start" trapFocusMode="selection-menu" triggerType="focus">
238
- <View gap={1}>
239
- <MenuItem onClick={() => { }} roundedCorners>
240
- Action 1
241
- </MenuItem>
242
- <MenuItem onClick={() => { }} roundedCorners>
243
- Action 2
244
- </MenuItem>
245
- <MenuItem onClick={() => { }} roundedCorners>
246
- Action 3
247
- </MenuItem>
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, no fallbacks passed">
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
- return (<View backgroundColor="neutral-faded" borderRadius="small" height={80} attributes={{ ref: portalRef, "data-testid": "container" }} justify="end" align="start" padding={4}>
354
- <Demo containerRef={portalRef} defaultActive position="bottom-start"/>
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.getByText("Content");
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}>`Trigger</Button>}
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" insetTop={2} insetStart={2} insetEnd={2} backgroundColor="elevation-overlay" borderColor="neutral-faded" borderRadius="small" padding={4} zIndex={10} attributes={{ "data-testid": "container" }}>
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">Reshaped button</Button>
555
- <Theme name="slate">
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
- Slate button
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">Slate button</Button>
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
+ };
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import type * as G from "../../../types/global";
2
+ import type * as G from "../../types/global";
3
3
  import type * as T from "./Flyout.types";
4
4
  type UseFlyout = (args: {
5
5
  width?: T.Width;
@@ -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;