reshaped 3.0.8-rc.1 → 3.0.10

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 (82) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/bin/cli.js +0 -1
  3. package/dist/bundle.css +1 -1
  4. package/dist/bundle.d.ts +3 -1
  5. package/dist/bundle.js +10 -10
  6. package/dist/cjs/themes/_generator/utilities/color.d.ts +16 -0
  7. package/dist/cjs/themes/_generator/utilities/color.js +57 -7
  8. package/dist/cjs/themes/_generator/utilities/generateBackgroundColors.js +4 -0
  9. package/dist/cjs/themes/_generator/utilities/tests/color.test.js +73 -42
  10. package/dist/cjs/themes/index.d.ts +17 -0
  11. package/dist/cjs/themes/index.js +3 -0
  12. package/dist/cjs/types/config.d.ts +1 -0
  13. package/dist/components/Button/Button.module.css +1 -1
  14. package/dist/components/Button/tests/Button.stories.js +3 -1
  15. package/dist/components/Card/Card.module.css +1 -1
  16. package/dist/components/Checkbox/Checkbox.module.css +1 -1
  17. package/dist/components/Dismissible/Dismissible.module.css +1 -1
  18. package/dist/components/DropdownMenu/DropdownMenu.d.ts +1 -0
  19. package/dist/components/DropdownMenu/DropdownMenu.js +1 -0
  20. package/dist/components/DropdownMenu/DropdownMenu.module.css +1 -1
  21. package/dist/components/DropdownMenu/DropdownMenu.types.d.ts +1 -1
  22. package/dist/components/DropdownMenu/tests/DropdownMenu.stories.d.ts +1 -0
  23. package/dist/components/Modal/Modal.js +4 -3
  24. package/dist/components/Modal/tests/Modal.stories.js +1 -1
  25. package/dist/components/Overlay/Overlay.js +7 -7
  26. package/dist/components/Overlay/tests/Overlay.stories.js +3 -1
  27. package/dist/components/Popover/Popover.d.ts +2 -0
  28. package/dist/components/Popover/Popover.js +9 -3
  29. package/dist/components/Popover/Popover.types.d.ts +1 -1
  30. package/dist/components/Popover/tests/Popover.stories.d.ts +2 -0
  31. package/dist/components/Popover/tests/Popover.stories.js +16 -0
  32. package/dist/components/Radio/Radio.module.css +1 -1
  33. package/dist/components/Resizable/Resizable.d.ts +8 -0
  34. package/dist/components/Resizable/Resizable.js +149 -0
  35. package/dist/components/Resizable/Resizable.module.css +1 -0
  36. package/dist/components/Resizable/Resizable.types.d.ts +29 -0
  37. package/dist/components/Resizable/Resizable.types.js +1 -0
  38. package/dist/components/Resizable/index.d.ts +2 -0
  39. package/dist/components/Resizable/index.js +1 -0
  40. package/dist/components/Resizable/tests/Resizable.stories.d.ts +15 -0
  41. package/dist/components/Resizable/tests/Resizable.stories.js +58 -0
  42. package/dist/components/ScrollArea/ScrollArea.js +4 -4
  43. package/dist/components/Slider/Slider.module.css +1 -1
  44. package/dist/components/Slider/Slider.types.d.ts +2 -2
  45. package/dist/components/Slider/Slider.utilities.js +4 -4
  46. package/dist/components/Slider/SliderControlled.js +11 -9
  47. package/dist/components/Slider/SliderThumb.js +1 -1
  48. package/dist/components/Slider/tests/Slider.stories.js +4 -0
  49. package/dist/components/Switch/Switch.module.css +1 -1
  50. package/dist/components/Toast/Toast.types.d.ts +7 -6
  51. package/dist/components/Toast/index.d.ts +1 -1
  52. package/dist/components/Toast/useToast.d.ts +1 -1
  53. package/dist/components/Tooltip/tests/Tooltip.stories.js +31 -0
  54. package/dist/components/_private/Flyout/Flyout.context.d.ts +3 -1
  55. package/dist/components/_private/Flyout/Flyout.context.js +4 -1
  56. package/dist/components/_private/Flyout/Flyout.types.d.ts +1 -0
  57. package/dist/components/_private/Flyout/FlyoutContent.js +5 -7
  58. package/dist/components/_private/Flyout/FlyoutControlled.js +20 -14
  59. package/dist/components/_private/Flyout/FlyoutTrigger.js +3 -2
  60. package/dist/components/_private/Flyout/tests/Flyout.stories.d.ts +2 -7
  61. package/dist/components/_private/Flyout/tests/Flyout.stories.js +125 -49
  62. package/dist/components/_private/Portal/Portal.module.css +1 -1
  63. package/dist/hooks/_private/useOnClickOutside.js +5 -3
  64. package/dist/hooks/tests/useDrag.stories.d.ts +6 -0
  65. package/dist/hooks/tests/useDrag.stories.js +29 -0
  66. package/dist/hooks/useDrag.d.ts +17 -0
  67. package/dist/hooks/useDrag.js +116 -0
  68. package/dist/hooks/useHandlerRef.d.ts +8 -0
  69. package/dist/hooks/useHandlerRef.js +16 -0
  70. package/dist/hooks/useScrollLock.js +4 -3
  71. package/dist/hooks/useToggle.js +1 -1
  72. package/dist/index.d.ts +3 -1
  73. package/dist/index.js +1 -0
  74. package/dist/themes/_generator/tests/themes.stories.js +23 -0
  75. package/dist/themes/_generator/utilities/color.d.ts +16 -0
  76. package/dist/themes/_generator/utilities/color.js +54 -6
  77. package/dist/themes/_generator/utilities/generateBackgroundColors.js +4 -0
  78. package/dist/themes/index.d.ts +17 -0
  79. package/dist/themes/index.js +3 -0
  80. package/dist/types/config.d.ts +1 -0
  81. package/dist/types/global.d.ts +1 -1
  82. package/package.json +1 -1
@@ -2,9 +2,12 @@
2
2
  import React from "react";
3
3
  const FlyoutContext = React.createContext({});
4
4
  const FlyoutTriggerContext = React.createContext({});
5
+ const FlyoutContentContext = React.createContext(false);
5
6
  const useFlyoutContext = () => React.useContext(FlyoutContext);
6
7
  const useFlyoutTriggerContext = () => React.useContext(FlyoutTriggerContext);
8
+ const useFlyoutContentContext = () => React.useContext(FlyoutContentContext);
7
9
  const Provider = FlyoutContext.Provider;
8
10
  const TriggerProvider = FlyoutTriggerContext.Provider;
9
- export { Provider, TriggerProvider, useFlyoutContext, useFlyoutTriggerContext };
11
+ const ContentProvider = FlyoutContentContext.Provider;
12
+ export { Provider, TriggerProvider, ContentProvider, useFlyoutContext, useFlyoutTriggerContext, useFlyoutContentContext, };
10
13
  export default FlyoutContext;
@@ -68,6 +68,7 @@ type BaseProps = {
68
68
  disabled?: boolean;
69
69
  disableHideAnimation?: boolean;
70
70
  disableContentHover?: boolean;
71
+ disableCloseOnOutsideClick?: boolean;
71
72
  children?: React.ReactNode;
72
73
  onOpen?: () => void;
73
74
  onClose?: () => void;
@@ -6,7 +6,7 @@ import useIsomorphicLayoutEffect from "../../../hooks/useIsomorphicLayoutEffect.
6
6
  import Portal from "../Portal/index.js";
7
7
  import { getClosestFlyoutTarget } from "../../../utilities/dom.js";
8
8
  import cooldown from "./utilities/cooldown.js";
9
- import { useFlyoutContext } from "./Flyout.context.js";
9
+ import { useFlyoutContext, ContentProvider } from "./Flyout.context.js";
10
10
  import s from "./Flyout.module.css";
11
11
  const FlyoutContent = (props) => {
12
12
  const { children, className, attributes } = props;
@@ -48,12 +48,10 @@ const FlyoutContent = (props) => {
48
48
  else if (trapFocusMode === "action-menu") {
49
49
  role = "menu";
50
50
  }
51
- const content = (
52
- // eslint-disable-next-line jsx-a11y/no-static-element-interactions
53
- _jsx("div", { className: contentClassNames, style: {
54
- ...styles,
55
- "--rs-flyout-gap": contentGap,
56
- }, ref: flyoutElRef, onTransitionEnd: handleTransitionEnd, onMouseEnter: triggerType === "hover" ? handleMouseEnter : undefined, onMouseLeave: triggerType === "hover" ? handleMouseLeave : undefined, onMouseDown: handleContentMouseDown, onTouchStart: handleContentMouseDown, onMouseUp: handleContentMouseUp, onTouchEnd: handleContentMouseUp, children: _jsx("div", { role: role, ...attributes, id: id, "aria-modal": triggerType === "click", style: contentAttributes?.style, className: innerClassNames, children: children }) }));
51
+ const content = (_jsx(ContentProvider, { value: true, children: _jsx("div", { className: contentClassNames, style: {
52
+ ...styles,
53
+ "--rs-flyout-gap": contentGap,
54
+ }, ref: flyoutElRef, onTransitionEnd: handleTransitionEnd, onMouseEnter: triggerType === "hover" ? handleMouseEnter : undefined, onMouseLeave: triggerType === "hover" ? handleMouseLeave : undefined, onMouseDown: handleContentMouseDown, onTouchStart: handleContentMouseDown, onMouseUp: handleContentMouseUp, onTouchEnd: handleContentMouseUp, children: _jsx("div", { role: role, ...attributes, id: id, "aria-modal": triggerType === "click", style: contentAttributes?.style, className: innerClassNames, children: children }) }) }));
57
55
  const closestScrollable = getClosestFlyoutTarget(triggerElRef.current);
58
56
  const scrollableRef = closestScrollable === document.body ? undefined : { current: closestScrollable };
59
57
  return _jsx(Portal, { targetRef: containerRef || scrollableRef, children: content });
@@ -13,12 +13,16 @@ import { checkKeyboardMode } from "../../../utilities/a11y/keyboardMode.js";
13
13
  import useFlyout from "./useFlyout.js";
14
14
  import * as timeouts from "./Flyout.constants.js";
15
15
  import cooldown from "./utilities/cooldown.js";
16
- import { Provider, useFlyoutTriggerContext, useFlyoutContext } from "./Flyout.context.js";
16
+ import { Provider, useFlyoutTriggerContext, useFlyoutContext, useFlyoutContentContext, } from "./Flyout.context.js";
17
+ import useHandlerRef from "../../../hooks/useHandlerRef.js";
17
18
  const FlyoutRoot = (props) => {
18
- const { triggerType = "click", onOpen, onClose, children, disabled, forcePosition, trapFocusMode, width, disableHideAnimation, disableContentHover, contentGap, contentClassName, contentAttributes, position: passedPosition, active: passedActive, id: passedId, instanceRef, containerRef, } = props;
19
+ const { triggerType = "click", onOpen, onClose, children, disabled, forcePosition, trapFocusMode, width, disableHideAnimation, disableContentHover, disableCloseOnOutsideClick, contentGap, contentClassName, contentAttributes, position: passedPosition, active: passedActive, id: passedId, instanceRef, containerRef, } = props;
20
+ const onOpenRef = useHandlerRef(onOpen);
21
+ const onCloseRef = useHandlerRef(onClose);
19
22
  const resolvedActive = disabled === true ? false : passedActive;
20
23
  const parentFlyoutContext = useFlyoutContext();
21
24
  const parentFlyoutTriggerContext = useFlyoutTriggerContext();
25
+ const parentFlyoutContentContext = useFlyoutContentContext();
22
26
  const isSubmenu = parentFlyoutContext.trapFocusMode === "action-menu" ||
23
27
  parentFlyoutContext.trapFocusMode === "content-menu";
24
28
  const [isRTL] = useRTL();
@@ -27,7 +31,8 @@ const FlyoutRoot = (props) => {
27
31
  * Reuse the parent trigger ref in case we render nested triggers
28
32
  * For example, when we apply tooltip and popover to the same button
29
33
  */
30
- const triggerElRef = parentFlyoutTriggerContext?.triggerElRef || internalTriggerElRef;
34
+ const triggerElRef = (!parentFlyoutContentContext && parentFlyoutTriggerContext?.triggerElRef) ||
35
+ internalTriggerElRef;
31
36
  const flyoutElRef = React.useRef(null);
32
37
  const id = useElementId(passedId);
33
38
  const timerRef = React.useRef();
@@ -58,20 +63,17 @@ const FlyoutRoot = (props) => {
58
63
  const canOpen = !lockedRef.current && status === "idle";
59
64
  if (!canOpen)
60
65
  return;
61
- onOpen?.();
62
- // eslint-disable-next-line react-hooks/exhaustive-deps
63
- }, [status]);
66
+ onOpenRef.current?.();
67
+ }, [status, onOpenRef]);
64
68
  const handleClose = React.useCallback((options) => {
65
69
  const isLocked = triggerType === "click" && !isDismissible();
66
70
  const canClose = !isLocked && (status !== "idle" || disabled);
67
71
  if (!canClose)
68
72
  return;
69
- onClose?.();
73
+ onCloseRef.current?.();
70
74
  if (options?.closeParents)
71
75
  parentFlyoutContext?.handleClose?.();
72
- },
73
- // eslint-disable-next-line react-hooks/exhaustive-deps
74
- [status, isDismissible, triggerType]);
76
+ }, [status, isDismissible, triggerType, onCloseRef, disabled, parentFlyoutContext]);
75
77
  /**
76
78
  * Trigger event handlers
77
79
  */
@@ -91,10 +93,10 @@ const FlyoutRoot = (props) => {
91
93
  handleClose();
92
94
  }, [handleClose, triggerType, trapFocusMode]);
93
95
  const handleFocus = React.useCallback(() => {
94
- if (!checkKeyboardMode())
96
+ if (triggerType === "hover" && !checkKeyboardMode())
95
97
  return;
96
98
  handleOpen();
97
- }, [handleOpen]);
99
+ }, [handleOpen, triggerType]);
98
100
  const handleMouseEnter = React.useCallback(() => {
99
101
  clearTimer();
100
102
  timerRef.current = setTimeout(handleOpen, cooldown.timer || isSubmenu ? timeouts.mouseEnterShort : timeouts.mouseEnter);
@@ -174,15 +176,17 @@ const FlyoutRoot = (props) => {
174
176
  useIsomorphicLayoutEffect(() => {
175
177
  if (status !== "visible" || !flyoutElRef.current)
176
178
  return;
179
+ if (trapFocusRef.current?.trapped)
180
+ return;
177
181
  trapFocusRef.current = new TrapFocus(flyoutElRef.current);
178
182
  trapFocusRef.current.trap({
179
183
  mode: trapFocusMode,
180
- includeTrigger: triggerType === "hover" && trapFocusMode === "content-menu",
184
+ includeTrigger: triggerType === "hover" && trapFocusMode !== "dialog" && !isSubmenu,
181
185
  onNavigateOutside: () => {
182
186
  handleClose();
183
187
  },
184
188
  });
185
- }, [status, triggerType, handleClose, trapFocusMode]);
189
+ }, [status, triggerType, trapFocusMode]);
186
190
  React.useEffect(() => {
187
191
  if (!disableHideAnimation && status !== "hidden")
188
192
  return;
@@ -229,6 +233,8 @@ const FlyoutRoot = (props) => {
229
233
  }), [handleOpen, handleClose, updatePosition]);
230
234
  useHotkeys({ Escape: () => handleClose() }, [handleClose]);
231
235
  useOnClickOutside([flyoutElRef, triggerElRef], () => {
236
+ if (disableCloseOnOutsideClick)
237
+ return;
232
238
  // Clicking outside changes focused element so we don't need to set it back ourselves
233
239
  shouldReturnFocusRef.current = false;
234
240
  handleClose();
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useFlyoutContext, TriggerProvider } from "./Flyout.context.js";
4
4
  const FlyoutTrigger = (props) => {
5
5
  const { children } = props;
6
- const { id, triggerElRef, triggerType, flyout, handleFocus, handleBlur, handleMouseEnter, handleMouseLeave, handleClick, trapFocusMode, } = useFlyoutContext();
6
+ const { id, triggerElRef, triggerType, flyout, handleFocus, handleBlur, handleMouseEnter, handleMouseLeave, handleClick, trapFocusMode, isSubmenu, } = useFlyoutContext();
7
7
  let childrenAttributes = {
8
8
  onBlur: handleBlur,
9
9
  ref: triggerElRef,
@@ -15,7 +15,8 @@ const FlyoutTrigger = (props) => {
15
15
  childrenAttributes.onMouseEnter = handleMouseEnter;
16
16
  childrenAttributes.onMouseLeave = handleMouseLeave;
17
17
  }
18
- if ((triggerType === "hover" && trapFocusMode !== "action-menu") || triggerType === "focus") {
18
+ // Submenus open on keypress instead of hover
19
+ if ((triggerType === "hover" && !isSubmenu) || triggerType === "focus") {
19
20
  childrenAttributes.onFocus = handleFocus;
20
21
  childrenAttributes["aria-describedby"] = id;
21
22
  }
@@ -5,13 +5,8 @@ declare const _default: {
5
5
  export default _default;
6
6
  export declare const positions: () => React.JSX.Element;
7
7
  export declare const dynamicPosition: () => React.JSX.Element;
8
- export declare const modeDialogClick: () => React.JSX.Element;
9
- export declare const modeActionMenuClick: () => React.JSX.Element;
10
- export declare const modeContentMenuClick: () => React.JSX.Element;
11
- export declare const modeDialogHover: () => React.JSX.Element;
12
- export declare const modeActionMenuHover: () => React.JSX.Element;
13
- export declare const modeContentMenuHover: () => React.JSX.Element;
14
- export declare const disableContentHover: () => React.JSX.Element;
8
+ export declare const modes: () => React.JSX.Element;
9
+ export declare const disableFlags: () => React.JSX.Element;
15
10
  export declare const customPortalTarget: () => React.JSX.Element;
16
11
  export declare const testWidthOverflowOnMobile: () => React.JSX.Element;
17
12
  export declare const testInsideScrollArea: () => React.JSX.Element;
@@ -1,5 +1,7 @@
1
1
  import React from "react";
2
+ import { createRoot } from "react-dom/client";
2
3
  import { Example } from "../../../../utilities/storybook/index.js";
4
+ import Reshaped from "../../../Reshaped/index.js";
3
5
  import View from "../../../View/index.js";
4
6
  import Theme from "../../../Theme/index.js";
5
7
  import Button from "../../../Button/index.js";
@@ -48,60 +50,118 @@ export const positions = () => (<div style={{ paddingTop: 200 }}>
48
50
  export const dynamicPosition = () => (<div style={{ position: "absolute", top: 0, left: "50%" }}>
49
51
  <Demo position="top"/>
50
52
  </div>);
51
- export const modeDialogClick = () => (<Demo position="bottom-start" trapFocusMode="dialog">
52
- <button type="button">Item 1</button>
53
- <button type="button">Item 2</button>
54
- <button type="button">Close</button>
55
- </Demo>);
56
- export const modeActionMenuClick = () => (<Demo position="bottom-start" trapFocusMode="action-menu">
57
- <button type="button">Item 1</button>
58
- <button type="button">Item 2</button>
59
- <button type="button">Close</button>
60
- </Demo>);
61
- export const modeContentMenuClick = () => (<Demo position="bottom-start" trapFocusMode="content-menu">
62
- <button type="button">Item 1</button>
63
- <button type="button">Item 2</button>
64
- <button type="button">Close</button>
65
- </Demo>);
66
- export const modeDialogHover = () => (<Demo position="bottom-start" trapFocusMode="dialog" triggerType="hover">
67
- <button type="button">Item 1</button>
68
- <button type="button">Item 2</button>
69
- <button type="button">Close</button>
70
- </Demo>);
71
- export const modeActionMenuHover = () => (<Demo position="bottom-start" trapFocusMode="action-menu" triggerType="hover">
72
- <button type="button">Item 1</button>
73
- <button type="button">Item 2</button>
74
- <button type="button">Close</button>
75
- </Demo>);
76
- export const modeContentMenuHover = () => (<Demo position="bottom-start" trapFocusMode="content-menu" triggerType="hover">
77
- <button type="button">Item 1</button>
78
- <button type="button">Item 2</button>
79
- <button type="button">Close</button>
80
- </Demo>);
81
- export const disableContentHover = () => (<Demo triggerType="hover" disableContentHover>
82
- Content
83
- </Demo>);
53
+ export const modes = () => (<Example>
54
+ <Example.Item title="dialog click">
55
+ <Demo position="bottom-start" trapFocusMode="dialog">
56
+ <button type="button">Item 1</button>
57
+ <button type="button">Item 2</button>
58
+ <button type="button">Close</button>
59
+ </Demo>
60
+ </Example.Item>
61
+
62
+ <Example.Item title="action-menu click">
63
+ <Demo position="bottom-start" trapFocusMode="action-menu">
64
+ <button type="button">Item 1</button>
65
+ <button type="button">Item 2</button>
66
+ <button type="button">Close</button>
67
+ </Demo>
68
+ </Example.Item>
69
+
70
+ <Example.Item title="content-menu click">
71
+ <Demo position="bottom-start" trapFocusMode="content-menu">
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="dialog hover">
79
+ <Demo position="bottom-start" trapFocusMode="dialog" triggerType="hover">
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>
85
+
86
+ <Example.Item title="action-menu hover">
87
+ <Demo position="bottom-start" trapFocusMode="action-menu" triggerType="hover">
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>
93
+
94
+ <Example.Item title="content-menu hover">
95
+ <Demo position="bottom-start" trapFocusMode="content-menu" 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>
101
+ </Example>);
102
+ export const disableFlags = () => (<Example>
103
+ <Example.Item title="disableContentHover">
104
+ <Demo triggerType="hover" disableContentHover>
105
+ Content
106
+ </Demo>
107
+ </Example.Item>
108
+
109
+ <Example.Item title="disableCloseOnOutsideClick">
110
+ <Demo disableCloseOnOutsideClick>Content</Demo>
111
+ </Example.Item>
112
+
113
+ <Example.Item title="disableHideAnimation">
114
+ <Demo disableHideAnimation>Content</Demo>
115
+ </Example.Item>
116
+ </Example>);
117
+ class CustomElement extends window.HTMLElement {
118
+ constructor() {
119
+ super();
120
+ this.attachShadow({ mode: "open" });
121
+ if (!this.shadowRoot)
122
+ return;
123
+ const node = (<Reshaped>
124
+ <Flyout active>
125
+ <Flyout.Trigger>{(attributes) => <button {...attributes}>Open</button>}</Flyout.Trigger>
126
+ <Flyout.Content>Content</Flyout.Content>
127
+ </Flyout>
128
+ </Reshaped>);
129
+ const root = createRoot(this.shadowRoot);
130
+ root.render(node);
131
+ }
132
+ }
133
+ if (!window.customElements.get("custom-element")) {
134
+ window.customElements.define("custom-element", CustomElement);
135
+ }
84
136
  export const customPortalTarget = () => {
85
137
  const portalRef = React.useRef(null);
86
- return (<div style={{ position: "relative", padding: 16, height: 200, overflow: "auto" }} ref={portalRef}>
87
- <Flyout position="bottom-start" containerRef={portalRef} active>
88
- <Flyout.Trigger>{(attributes) => <button {...attributes}>Open</button>}</Flyout.Trigger>
89
- <Flyout.Content>
90
- <div style={{
138
+ return (<Example>
139
+ <Example.Item title="Custom containerRef">
140
+ <View padding={4} paddingInline={40} height={50} overflow="auto" backgroundColor="neutral-faded" borderRadius="small" attributes={{ ref: portalRef }}>
141
+ <Flyout position="bottom-end" containerRef={portalRef} active>
142
+ <Flyout.Trigger>{(attributes) => <button {...attributes}>Open</button>}</Flyout.Trigger>
143
+ <Flyout.Content>
144
+ <div style={{
91
145
  background: "var(--rs-color-background-elevation-overlay)",
92
146
  padding: "var(--rs-unit-x4)",
93
- height: 100,
147
+ height: 200,
94
148
  width: 160,
95
149
  borderRadius: "var(--rs-radius-medium)",
96
150
  border: "1px solid var(--rs-color-border-neutral-faded)",
97
151
  boxSizing: "border-box",
98
152
  }}>
99
- {"Content"}
100
- </div>
101
- </Flyout.Content>
102
- </Flyout>
103
- <div style={{ height: 1000 }}/>
104
- </div>);
153
+ {"Content"}
154
+ </div>
155
+ </Flyout.Content>
156
+ </Flyout>
157
+ <div style={{ height: 1000 }}/>
158
+ </View>
159
+ </Example.Item>
160
+ <Example.Item title="Shadow dom">
161
+ {/* @ts-ignore */}
162
+ <custom-element />
163
+ </Example.Item>
164
+ </Example>);
105
165
  };
106
166
  export const testWidthOverflowOnMobile = () => (<Demo position="bottom-start" width={600}>
107
167
  Should work on mobile
@@ -159,16 +219,32 @@ export const testInsideFixed = () => (<Example>
159
219
  export const testDynamicBounds = () => {
160
220
  const [left, setLeft] = React.useState("50%");
161
221
  const [size, setSize] = React.useState("medium");
222
+ const flyoutRef = React.useRef();
223
+ React.useEffect(() => {
224
+ flyoutRef.current?.updatePosition();
225
+ }, [left]);
162
226
  return (<View gap={4}>
163
227
  <View direction="row" gap={2}>
164
- <Button onClick={() => setLeft("0%")}>Left</Button>
165
- <Button onClick={() => setLeft("50%")}>Center</Button>
166
- <Button onClick={() => setLeft("100%")}>Right</Button>
228
+ <Button onClick={() => {
229
+ setLeft("20%");
230
+ }}>
231
+ Left
232
+ </Button>
233
+ <Button onClick={() => {
234
+ setLeft("50%");
235
+ }}>
236
+ Center
237
+ </Button>
238
+ <Button onClick={() => {
239
+ setLeft("70%");
240
+ }}>
241
+ Right
242
+ </Button>
167
243
  <Button onClick={() => setSize("large")}>Large button</Button>
168
244
  <Button onClick={() => setSize("medium")}>Small button</Button>
169
245
  </View>
170
246
  <View height={100}>
171
- <Flyout position="bottom" active>
247
+ <Flyout position="bottom" active instanceRef={flyoutRef}>
172
248
  <Flyout.Trigger>
173
249
  {(attributes) => (<div style={{ position: "absolute", left, top: "50%" }}>
174
250
  <Button color="primary" attributes={attributes} size={size}>
@@ -1 +1 @@
1
- .root{display:contents}
1
+ .root{display:none}
@@ -1,7 +1,9 @@
1
+ import useHandlerRef from "../useHandlerRef.js";
1
2
  import React from "react";
2
3
  const useOnClickOutside = (refs, handler) => {
4
+ const handlerRef = useHandlerRef(handler);
3
5
  React.useEffect(() => {
4
- if (!handler)
6
+ if (!handlerRef.current)
5
7
  return;
6
8
  const handleClick = (event) => {
7
9
  let isInside = false;
@@ -14,7 +16,7 @@ const useOnClickOutside = (refs, handler) => {
14
16
  });
15
17
  if (isInside)
16
18
  return;
17
- handler(event);
19
+ handlerRef.current?.(event);
18
20
  };
19
21
  // Using events that happen before click to handle cases when element is hidden on click
20
22
  document.addEventListener("mousedown", handleClick);
@@ -24,6 +26,6 @@ const useOnClickOutside = (refs, handler) => {
24
26
  document.removeEventListener("touchstart", handleClick);
25
27
  };
26
28
  // eslint-disable-next-line react-hooks/exhaustive-deps
27
- }, [handler, ...refs]);
29
+ }, [handlerRef, ...refs]);
28
30
  };
29
31
  export default useOnClickOutside;
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ declare const _default: {
3
+ title: string;
4
+ };
5
+ export default _default;
6
+ export declare const state: () => React.JSX.Element;
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import View from "../../components/View/index.js";
3
+ import useDrag from "../useDrag.js";
4
+ import useToggle from "../useToggle.js";
5
+ import Button from "../../components/Button/index.js";
6
+ export default { title: "Hooks/useDrag" };
7
+ function Example() {
8
+ const [state, setState] = React.useState({ x: 0, y: 0 });
9
+ const disabledToggle = useToggle();
10
+ const { ref, containerRef, active } = useDrag((args) => {
11
+ setState(args);
12
+ }, {
13
+ disabled: disabledToggle.active,
14
+ });
15
+ return (<View direction="row" gap={4}>
16
+ <View backgroundColor="neutral-faded" borderRadius="medium" width="200px" height="200px" attributes={{ ref: containerRef }}>
17
+ <View height={8} width={8} borderRadius="small" animated backgroundColor={active ? "primary" : "neutral"} attributes={{
18
+ role: "button",
19
+ tabIndex: 0,
20
+ ref,
21
+ style: { translate: `${state.x}px ${state.y}px`, cursor: active ? "grabbing" : "grab" },
22
+ }}/>
23
+ </View>
24
+ <Button onClick={disabledToggle.toggle}>
25
+ {disabledToggle.active ? "Enable" : "Disable"}
26
+ </Button>
27
+ </View>);
28
+ }
29
+ export const state = () => <Example />;
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+ export type UseDragCallbackArgs = {
3
+ x: number;
4
+ y: number;
5
+ triggerX: number;
6
+ triggerY: number;
7
+ };
8
+ declare const useDrag: <TriggerElement extends HTMLElement = HTMLButtonElement, ContainerElement extends HTMLElement = HTMLDivElement>(cb: (args: UseDragCallbackArgs) => void, options?: {
9
+ disabled?: boolean;
10
+ containerRef?: React.RefObject<ContainerElement>;
11
+ orientation?: "horizontal" | "vertical" | "all";
12
+ }) => {
13
+ ref: React.MutableRefObject<TriggerElement | null>;
14
+ containerRef: React.RefObject<ContainerElement>;
15
+ active: boolean;
16
+ };
17
+ export default useDrag;
@@ -0,0 +1,116 @@
1
+ "use client";
2
+ import React from "react";
3
+ import { disableUserSelect, enableUserSelect, disableScroll, enableScroll } from "../utilities/dom.js";
4
+ import useToggle from "./useToggle.js";
5
+ import useHotkeys from "./useHotkeys.js";
6
+ import * as keys from "../constants/keys.js";
7
+ import useHandlerRef from "./useHandlerRef.js";
8
+ const useDrag = (cb, options) => {
9
+ const { disabled, containerRef: passedContainerRef, orientation = "all" } = options || {};
10
+ const cbRef = useHandlerRef(cb);
11
+ const toggle = useToggle();
12
+ const triggerRef = React.useRef(null);
13
+ const internalContainerRef = React.useRef(null);
14
+ const containerRef = passedContainerRef || internalContainerRef;
15
+ const triggerCompensationRef = React.useRef({ x: 0, y: 0 });
16
+ const isVertical = orientation === "vertical" || orientation === "all";
17
+ const isHorizontal = orientation === "horizontal" || orientation === "all";
18
+ const handleKeyboard = (x, y) => {
19
+ const triggerEl = triggerRef.current;
20
+ if (!triggerEl)
21
+ return;
22
+ const container = containerRef.current ?? document.body;
23
+ const containerRect = container.getBoundingClientRect();
24
+ const triggerRect = triggerEl?.getBoundingClientRect();
25
+ const nextArgs = { x: 0, y: 0, triggerX: 0, triggerY: 0 };
26
+ if (isVertical) {
27
+ const relativeY = Math.round(triggerRect.y) - containerRect.y + y;
28
+ nextArgs.y = Math.max(0, Math.min(relativeY, containerRect.height - triggerRect.height));
29
+ nextArgs.triggerY = triggerRect.y - containerRect.y;
30
+ }
31
+ if (isHorizontal) {
32
+ const relativeX = Math.round(triggerRect.x) - containerRect.x + x;
33
+ nextArgs.x = Math.max(0, Math.min(relativeX, containerRect.width - triggerRect.width));
34
+ nextArgs.triggerX = triggerRect.x - containerRect.x;
35
+ }
36
+ cb(nextArgs);
37
+ };
38
+ useHotkeys({
39
+ [keys.LEFT]: () => isHorizontal && handleKeyboard(-20, 0),
40
+ [keys.RIGHT]: () => isHorizontal && handleKeyboard(20, 0),
41
+ [keys.UP]: () => isVertical && handleKeyboard(0, -20),
42
+ [keys.DOWN]: () => isVertical && handleKeyboard(0, 20),
43
+ }, [], {
44
+ ref: triggerRef,
45
+ disabled,
46
+ });
47
+ React.useEffect(() => {
48
+ const triggerEl = triggerRef.current;
49
+ if (!triggerEl)
50
+ return;
51
+ if (!toggle.active)
52
+ return;
53
+ const handleDrag = (event) => {
54
+ const resolvedEvent = event instanceof MouseEvent ? event : event.changedTouches[0];
55
+ const container = containerRef.current ?? document.body;
56
+ const containerRect = container.getBoundingClientRect();
57
+ const triggerRect = triggerEl.getBoundingClientRect();
58
+ const triggerX = resolvedEvent.clientX - containerRect.x;
59
+ const triggerY = resolvedEvent.clientY - containerRect.y;
60
+ // Calculate position relative to the container
61
+ const relativeX = triggerX - triggerCompensationRef.current.x;
62
+ const relativeY = triggerY - triggerCompensationRef.current.y;
63
+ cbRef.current?.({
64
+ x: isHorizontal
65
+ ? Math.max(0, Math.min(relativeX, containerRect.width - triggerRect.width))
66
+ : 0,
67
+ y: isVertical
68
+ ? Math.max(0, Math.min(relativeY, containerRect.height - triggerRect.height))
69
+ : 0,
70
+ triggerX: triggerRect.x - containerRect.x,
71
+ triggerY: triggerRect.y - containerRect.y,
72
+ });
73
+ };
74
+ const handleDragEnd = () => {
75
+ triggerCompensationRef.current = { x: 0, y: 0 };
76
+ toggle.deactivate();
77
+ enableUserSelect();
78
+ enableScroll();
79
+ };
80
+ document.addEventListener("touchmove", handleDrag, { passive: true });
81
+ document.addEventListener("touchend", handleDragEnd, { passive: true });
82
+ document.addEventListener("mousemove", handleDrag, { passive: true });
83
+ document.addEventListener("mouseup", handleDragEnd, { passive: true });
84
+ return () => {
85
+ document.removeEventListener("touchmove", handleDrag);
86
+ document.removeEventListener("touchend", handleDragEnd);
87
+ document.removeEventListener("mousemove", handleDrag);
88
+ document.removeEventListener("mouseup", handleDragEnd);
89
+ };
90
+ }, [toggle, isHorizontal, isVertical, containerRef, cbRef]);
91
+ React.useEffect(() => {
92
+ const triggerEl = triggerRef.current;
93
+ if (!triggerEl || disabled)
94
+ return;
95
+ const handleStart = (event) => {
96
+ const resolvedEvent = event instanceof MouseEvent ? event : event.changedTouches[0];
97
+ // Find the coordinate of the event inside the trigger
98
+ const triggerRect = triggerEl.getBoundingClientRect();
99
+ triggerCompensationRef.current = {
100
+ x: resolvedEvent.clientX - triggerRect.x,
101
+ y: resolvedEvent.clientY - triggerRect.y,
102
+ };
103
+ toggle.activate();
104
+ disableUserSelect();
105
+ disableScroll();
106
+ };
107
+ triggerEl.addEventListener("touchstart", handleStart, { passive: true });
108
+ triggerEl.addEventListener("mousedown", handleStart, { passive: true });
109
+ return () => {
110
+ triggerEl.removeEventListener("touchstart", handleStart);
111
+ triggerEl.removeEventListener("mousedown", handleStart);
112
+ };
113
+ }, [toggle, disabled]);
114
+ return { ref: triggerRef, containerRef, active: toggle.active };
115
+ };
116
+ export default useDrag;
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+ /**
3
+ * Hook for wrapping event handlers passed as props with a ref
4
+ * This way we can keep the instance of the ref the same and pass this ref to the effects dependency array
5
+ * While also making sure that function implementation stays up-to-date
6
+ */
7
+ declare const useHandlerRef: <T>(cb: T) => React.RefObject<T>;
8
+ export default useHandlerRef;
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect.js";
3
+ /**
4
+ * Hook for wrapping event handlers passed as props with a ref
5
+ * This way we can keep the instance of the ref the same and pass this ref to the effects dependency array
6
+ * While also making sure that function implementation stays up-to-date
7
+ */
8
+ const useHandlerRef = (cb) => {
9
+ const ref = React.useRef(cb);
10
+ // Update the callback on every render, keeping the ref instance the same
11
+ useIsomorphicLayoutEffect(() => {
12
+ ref.current = cb;
13
+ });
14
+ return ref;
15
+ };
16
+ export default useHandlerRef;