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.
- package/CHANGELOG.md +9 -0
- package/bin/cli.js +0 -1
- package/dist/bundle.css +1 -1
- package/dist/bundle.d.ts +3 -1
- package/dist/bundle.js +10 -10
- package/dist/cjs/themes/_generator/utilities/color.d.ts +16 -0
- package/dist/cjs/themes/_generator/utilities/color.js +57 -7
- package/dist/cjs/themes/_generator/utilities/generateBackgroundColors.js +4 -0
- package/dist/cjs/themes/_generator/utilities/tests/color.test.js +73 -42
- package/dist/cjs/themes/index.d.ts +17 -0
- package/dist/cjs/themes/index.js +3 -0
- package/dist/cjs/types/config.d.ts +1 -0
- package/dist/components/Button/Button.module.css +1 -1
- package/dist/components/Button/tests/Button.stories.js +3 -1
- package/dist/components/Card/Card.module.css +1 -1
- package/dist/components/Checkbox/Checkbox.module.css +1 -1
- package/dist/components/Dismissible/Dismissible.module.css +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.d.ts +1 -0
- package/dist/components/DropdownMenu/DropdownMenu.js +1 -0
- package/dist/components/DropdownMenu/DropdownMenu.module.css +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.types.d.ts +1 -1
- package/dist/components/DropdownMenu/tests/DropdownMenu.stories.d.ts +1 -0
- package/dist/components/Modal/Modal.js +4 -3
- package/dist/components/Modal/tests/Modal.stories.js +1 -1
- package/dist/components/Overlay/Overlay.js +7 -7
- package/dist/components/Overlay/tests/Overlay.stories.js +3 -1
- package/dist/components/Popover/Popover.d.ts +2 -0
- package/dist/components/Popover/Popover.js +9 -3
- package/dist/components/Popover/Popover.types.d.ts +1 -1
- package/dist/components/Popover/tests/Popover.stories.d.ts +2 -0
- package/dist/components/Popover/tests/Popover.stories.js +16 -0
- package/dist/components/Radio/Radio.module.css +1 -1
- package/dist/components/Resizable/Resizable.d.ts +8 -0
- package/dist/components/Resizable/Resizable.js +149 -0
- package/dist/components/Resizable/Resizable.module.css +1 -0
- package/dist/components/Resizable/Resizable.types.d.ts +29 -0
- package/dist/components/Resizable/Resizable.types.js +1 -0
- package/dist/components/Resizable/index.d.ts +2 -0
- package/dist/components/Resizable/index.js +1 -0
- package/dist/components/Resizable/tests/Resizable.stories.d.ts +15 -0
- package/dist/components/Resizable/tests/Resizable.stories.js +58 -0
- package/dist/components/ScrollArea/ScrollArea.js +4 -4
- package/dist/components/Slider/Slider.module.css +1 -1
- package/dist/components/Slider/Slider.types.d.ts +2 -2
- package/dist/components/Slider/Slider.utilities.js +4 -4
- package/dist/components/Slider/SliderControlled.js +11 -9
- package/dist/components/Slider/SliderThumb.js +1 -1
- package/dist/components/Slider/tests/Slider.stories.js +4 -0
- package/dist/components/Switch/Switch.module.css +1 -1
- package/dist/components/Toast/Toast.types.d.ts +7 -6
- package/dist/components/Toast/index.d.ts +1 -1
- package/dist/components/Toast/useToast.d.ts +1 -1
- package/dist/components/Tooltip/tests/Tooltip.stories.js +31 -0
- package/dist/components/_private/Flyout/Flyout.context.d.ts +3 -1
- package/dist/components/_private/Flyout/Flyout.context.js +4 -1
- package/dist/components/_private/Flyout/Flyout.types.d.ts +1 -0
- package/dist/components/_private/Flyout/FlyoutContent.js +5 -7
- package/dist/components/_private/Flyout/FlyoutControlled.js +20 -14
- package/dist/components/_private/Flyout/FlyoutTrigger.js +3 -2
- package/dist/components/_private/Flyout/tests/Flyout.stories.d.ts +2 -7
- package/dist/components/_private/Flyout/tests/Flyout.stories.js +125 -49
- package/dist/components/_private/Portal/Portal.module.css +1 -1
- package/dist/hooks/_private/useOnClickOutside.js +5 -3
- package/dist/hooks/tests/useDrag.stories.d.ts +6 -0
- package/dist/hooks/tests/useDrag.stories.js +29 -0
- package/dist/hooks/useDrag.d.ts +17 -0
- package/dist/hooks/useDrag.js +116 -0
- package/dist/hooks/useHandlerRef.d.ts +8 -0
- package/dist/hooks/useHandlerRef.js +16 -0
- package/dist/hooks/useScrollLock.js +4 -3
- package/dist/hooks/useToggle.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/themes/_generator/tests/themes.stories.js +23 -0
- package/dist/themes/_generator/utilities/color.d.ts +16 -0
- package/dist/themes/_generator/utilities/color.js +54 -6
- package/dist/themes/_generator/utilities/generateBackgroundColors.js +4 -0
- package/dist/themes/index.d.ts +17 -0
- package/dist/themes/index.js +3 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/global.d.ts +1 -1
- 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
|
-
|
11
|
+
const ContentProvider = FlyoutContentContext.Provider;
|
12
|
+
export { Provider, TriggerProvider, ContentProvider, useFlyoutContext, useFlyoutTriggerContext, useFlyoutContentContext, };
|
10
13
|
export default FlyoutContext;
|
@@ -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
|
-
|
53
|
-
|
54
|
-
...
|
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 ||
|
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
|
-
|
62
|
-
|
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
|
-
|
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
|
184
|
+
includeTrigger: triggerType === "hover" && trapFocusMode !== "dialog" && !isSubmenu,
|
181
185
|
onNavigateOutside: () => {
|
182
186
|
handleClose();
|
183
187
|
},
|
184
188
|
});
|
185
|
-
}, [status, triggerType,
|
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
|
-
|
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
|
9
|
-
export declare const
|
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
|
52
|
-
<
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
<
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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 (<
|
87
|
-
<
|
88
|
-
<
|
89
|
-
|
90
|
-
|
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:
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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={() =>
|
165
|
-
|
166
|
-
|
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:
|
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 (!
|
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
|
-
|
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
|
-
}, [
|
29
|
+
}, [handlerRef, ...refs]);
|
28
30
|
};
|
29
31
|
export default useOnClickOutside;
|
@@ -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;
|