reshaped 3.1.4 → 3.1.6
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 +21 -0
- package/dist/bundle.css +1 -1
- package/dist/bundle.d.ts +2 -0
- package/dist/bundle.js +11 -12
- package/dist/components/Actionable/Actionable.d.ts +1 -1
- package/dist/components/Actionable/Actionable.js +2 -2
- package/dist/components/Actionable/Actionable.module.css +1 -1
- package/dist/components/Actionable/Actionable.types.d.ts +1 -0
- package/dist/components/Autocomplete/Autocomplete.js +10 -4
- package/dist/components/Button/Button.js +1 -1
- package/dist/components/Card/Card.d.ts +1 -1
- package/dist/components/Card/tests/Card.stories.d.ts +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.types.d.ts +1 -1
- package/dist/components/FormControl/FormControl.context.d.ts +2 -1
- package/dist/components/Grid/Grid.d.ts +6 -0
- package/dist/components/Grid/Grid.js +46 -0
- package/dist/components/Grid/Grid.module.css +1 -0
- package/dist/components/Grid/Grid.types.d.ts +31 -0
- package/dist/components/Grid/Grid.types.js +1 -0
- package/dist/components/Grid/index.d.ts +2 -0
- package/dist/components/Grid/index.js +1 -0
- package/dist/components/Grid/tests/Grid.stories.d.ts +18 -0
- package/dist/components/Grid/tests/Grid.stories.js +170 -0
- package/dist/components/Icon/Icon.module.css +1 -1
- package/dist/components/Link/Link.d.ts +1 -1
- package/dist/components/Loader/Loader.module.css +1 -1
- package/dist/components/Loader/Loader.types.d.ts +1 -1
- package/dist/components/Loader/tests/Loader.stories.js +5 -3
- package/dist/components/Overlay/tests/Overlay.stories.js +1 -1
- package/dist/components/Popover/Popover.js +2 -4
- package/dist/components/Popover/Popover.types.d.ts +1 -1
- package/dist/components/Select/Select.js +1 -1
- package/dist/components/Table/Table.js +6 -4
- package/dist/components/Table/Table.types.d.ts +6 -1
- package/dist/components/Tabs/Tabs.d.ts +1 -1
- package/dist/components/Tabs/Tabs.module.css +1 -1
- package/dist/components/Tabs/TabsItem.d.ts +1 -1
- package/dist/components/Tabs/TabsItem.js +2 -3
- package/dist/components/Tabs/tests/Tabs.stories.d.ts +15 -13
- package/dist/components/Tabs/tests/Tabs.stories.js +71 -8
- package/dist/components/Tooltip/Tooltip.js +1 -1
- package/dist/components/View/View.js +7 -3
- package/dist/components/View/View.module.css +1 -1
- package/dist/components/View/View.types.d.ts +2 -2
- package/dist/components/_private/Expandable/Expandable.js +9 -5
- package/dist/components/_private/Flyout/Flyout.module.css +1 -1
- package/dist/components/_private/Flyout/Flyout.types.d.ts +11 -2
- package/dist/components/_private/Flyout/FlyoutControlled.js +33 -18
- package/dist/components/_private/Flyout/tests/Flyout.stories.d.ts +1 -0
- package/dist/components/_private/Flyout/tests/Flyout.stories.js +28 -18
- package/dist/components/_private/Flyout/useFlyout.d.ts +2 -1
- package/dist/components/_private/Flyout/useFlyout.js +46 -57
- package/dist/components/_private/Flyout/utilities/calculatePosition.js +16 -11
- package/dist/components/_private/Flyout/utilities/cooldown.d.ts +1 -1
- package/dist/components/_private/Flyout/utilities/cooldown.js +17 -5
- package/dist/components/_private/Flyout/utilities/getPositionFallbacks.d.ts +3 -0
- package/dist/components/_private/Flyout/utilities/getPositionFallbacks.js +39 -0
- package/dist/config/tailwind.d.ts +1 -1
- package/dist/hooks/_private/useOnClickOutside.js +3 -2
- package/dist/hooks/_private/useSingletonHotkeys.js +16 -13
- package/dist/hooks/tests/useHotkeys.stories.js +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/styles/align/align.module.css +1 -0
- package/dist/styles/align/index.d.ts +3 -0
- package/dist/styles/align/index.js +10 -0
- package/dist/styles/justify/index.d.ts +3 -0
- package/dist/styles/justify/index.js +10 -0
- package/dist/styles/justify/justify.module.css +1 -0
- package/dist/styles/types.d.ts +2 -0
- package/dist/tests/ShadowDOM.stories.d.ts +6 -0
- package/dist/tests/ShadowDOM.stories.js +110 -0
- package/dist/themes/_generator/tests/themes.stories.js +1 -1
- package/dist/utilities/a11y/TrapFocus.d.ts +1 -1
- package/dist/utilities/a11y/TrapFocus.js +14 -5
- package/dist/utilities/a11y/focus.d.ts +1 -1
- package/dist/utilities/a11y/focus.js +10 -5
- package/dist/utilities/dom.d.ts +2 -1
- package/dist/utilities/dom.js +12 -2
- package/package.json +31 -29
@@ -16,7 +16,8 @@ import cooldown from "./utilities/cooldown.js";
|
|
16
16
|
import { Provider, useFlyoutTriggerContext, useFlyoutContext, useFlyoutContentContext, } from "./Flyout.context.js";
|
17
17
|
import useHandlerRef from "../../../hooks/useHandlerRef.js";
|
18
18
|
const FlyoutRoot = (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;
|
19
|
+
const { triggerType = "click", groupTimeouts, onOpen, onClose, children, disabled, forcePosition, trapFocusMode, width, disableHideAnimation, disableContentHover, disableCloseOnOutsideClick, contentGap = 2, contentClassName, contentAttributes, position: passedPosition, active: passedActive, id: passedId, instanceRef, containerRef, initialFocusRef, } = props;
|
20
|
+
const fallbackPositions = props.fallbackPositions === false || forcePosition ? [] : props.fallbackPositions;
|
20
21
|
const onOpenRef = useHandlerRef(onOpen);
|
21
22
|
const onCloseRef = useHandlerRef(onClose);
|
22
23
|
const resolvedActive = disabled === true ? false : passedActive;
|
@@ -33,7 +34,7 @@ const FlyoutRoot = (props) => {
|
|
33
34
|
*/
|
34
35
|
const triggerElRef = (!parentFlyoutContentContext && parentFlyoutTriggerContext?.triggerElRef) ||
|
35
36
|
internalTriggerElRef;
|
36
|
-
const triggerBoundsRef = React.useRef();
|
37
|
+
const triggerBoundsRef = React.useRef(null);
|
37
38
|
const flyoutElRef = React.useRef(null);
|
38
39
|
const id = useElementId(passedId);
|
39
40
|
const timerRef = React.useRef();
|
@@ -57,11 +58,13 @@ const FlyoutRoot = (props) => {
|
|
57
58
|
position: passedPosition,
|
58
59
|
defaultActive: resolvedActive,
|
59
60
|
container: containerRef?.current,
|
60
|
-
|
61
|
+
fallbackPositions,
|
62
|
+
contentGap,
|
61
63
|
});
|
62
64
|
const { status, updatePosition, render, hide, remove, show } = flyout;
|
65
|
+
const isRendered = status !== "idle";
|
63
66
|
// Don't create dismissible queue for hover flyout because they close all together on mouseout
|
64
|
-
const isDismissible = useIsDismissible(triggerType !== "hover" &&
|
67
|
+
const isDismissible = useIsDismissible(triggerType !== "hover" && isRendered, flyoutElRef, triggerElRef);
|
65
68
|
const clearTimer = React.useCallback(() => {
|
66
69
|
if (timerRef.current)
|
67
70
|
clearTimeout(timerRef.current);
|
@@ -71,20 +74,20 @@ const FlyoutRoot = (props) => {
|
|
71
74
|
* Called from the internal actions
|
72
75
|
*/
|
73
76
|
const handleOpen = React.useCallback(() => {
|
74
|
-
const canOpen = !lockedRef.current &&
|
77
|
+
const canOpen = !lockedRef.current && !isRendered;
|
75
78
|
if (!canOpen)
|
76
79
|
return;
|
77
80
|
onOpenRef.current?.();
|
78
|
-
}, [
|
81
|
+
}, [isRendered, onOpenRef]);
|
79
82
|
const handleClose = React.useCallback((options) => {
|
80
83
|
const isLocked = triggerType === "click" && !isDismissible();
|
81
|
-
const canClose = !isLocked && (
|
84
|
+
const canClose = !isLocked && (isRendered || disabled);
|
82
85
|
if (!canClose)
|
83
86
|
return;
|
84
87
|
onCloseRef.current?.();
|
85
88
|
if (options?.closeParents)
|
86
89
|
parentFlyoutContext?.handleClose?.();
|
87
|
-
}, [
|
90
|
+
}, [isRendered, isDismissible, triggerType, onCloseRef, disabled, parentFlyoutContext]);
|
88
91
|
/**
|
89
92
|
* Trigger event handlers
|
90
93
|
*/
|
@@ -118,24 +121,28 @@ const FlyoutRoot = (props) => {
|
|
118
121
|
hoverTriggeredWithTouchEventRef.current = false;
|
119
122
|
}
|
120
123
|
else {
|
121
|
-
|
122
|
-
if (!isSubmenu && triggerType === "hover")
|
124
|
+
if (groupTimeouts)
|
123
125
|
cooldown.warm();
|
126
|
+
timerRef.current = setTimeout(() => {
|
127
|
+
handleOpen();
|
128
|
+
}, groupTimeouts && cooldown.status === "warming"
|
129
|
+
? timeouts.mouseEnter
|
130
|
+
: timeouts.mouseEnterShort);
|
124
131
|
}
|
125
|
-
}, [clearTimer, timerRef, handleOpen,
|
132
|
+
}, [clearTimer, timerRef, handleOpen, groupTimeouts]);
|
126
133
|
const handleMouseLeave = React.useCallback(() => {
|
127
134
|
cooldown.cool();
|
128
135
|
clearTimer();
|
129
136
|
timerRef.current = setTimeout(() => handleClose(), timeouts.mouseLeave);
|
130
137
|
}, [clearTimer, timerRef, handleClose]);
|
131
138
|
const handleTriggerClick = React.useCallback(() => {
|
132
|
-
if (
|
139
|
+
if (!isRendered) {
|
133
140
|
handleOpen();
|
134
141
|
}
|
135
142
|
else {
|
136
143
|
handleClose();
|
137
144
|
}
|
138
|
-
}, [
|
145
|
+
}, [isRendered, handleOpen, handleClose]);
|
139
146
|
const handleTriggerMouseDown = React.useCallback(() => {
|
140
147
|
const rect = triggerElRef.current?.getBoundingClientRect();
|
141
148
|
triggerBoundsRef.current = rect;
|
@@ -153,6 +160,11 @@ const FlyoutRoot = (props) => {
|
|
153
160
|
if (flyoutElRef.current !== e.currentTarget || e.propertyName !== "transform")
|
154
161
|
return;
|
155
162
|
transitionStartedRef.current = true;
|
163
|
+
/**
|
164
|
+
* After animation has started, we're sure about the correct bounds
|
165
|
+
* so drop the cache to make flyout work when trigger moves around
|
166
|
+
*/
|
167
|
+
triggerBoundsRef.current = null;
|
156
168
|
}, [resolvedActive]);
|
157
169
|
const handleTransitionEnd = React.useCallback((e) => {
|
158
170
|
if (flyoutElRef.current !== e.currentTarget || e.propertyName !== "transform")
|
@@ -180,14 +192,14 @@ const FlyoutRoot = (props) => {
|
|
180
192
|
if (checkTransitions() &&
|
181
193
|
!disableHideAnimation &&
|
182
194
|
transitionStartedRef.current &&
|
183
|
-
(cooldown.status
|
195
|
+
(cooldown.status === "cooling" || !groupTimeouts)) {
|
184
196
|
hide();
|
185
197
|
}
|
186
198
|
else {
|
187
199
|
// In case transitions are disabled globally - remove from the DOM immediately
|
188
200
|
remove();
|
189
201
|
}
|
190
|
-
}, [resolvedActive, render, hide, remove, disableHideAnimation, disabled]);
|
202
|
+
}, [resolvedActive, render, hide, remove, disableHideAnimation, disabled, groupTimeouts]);
|
191
203
|
React.useEffect(() => {
|
192
204
|
// Wait after positioning before show is triggered to animate flyout from the right side
|
193
205
|
if (status === "positioned")
|
@@ -210,6 +222,7 @@ const FlyoutRoot = (props) => {
|
|
210
222
|
trapFocusRef.current = new TrapFocus(flyoutElRef.current);
|
211
223
|
trapFocusRef.current.trap({
|
212
224
|
mode: trapFocusMode,
|
225
|
+
initialFocusEl: initialFocusRef?.current,
|
213
226
|
includeTrigger: triggerType === "hover" && trapFocusMode !== "dialog" && !isSubmenu,
|
214
227
|
onNavigateOutside: () => {
|
215
228
|
handleClose();
|
@@ -219,7 +232,7 @@ const FlyoutRoot = (props) => {
|
|
219
232
|
React.useEffect(() => {
|
220
233
|
if (!disableHideAnimation && status !== "hidden")
|
221
234
|
return;
|
222
|
-
if (disableHideAnimation &&
|
235
|
+
if (disableHideAnimation && isRendered)
|
223
236
|
return;
|
224
237
|
if (trapFocusRef.current?.trapped) {
|
225
238
|
/* Locking the popover to not open it again on trigger focus */
|
@@ -232,7 +245,7 @@ const FlyoutRoot = (props) => {
|
|
232
245
|
trapFocusRef.current.release({ withoutFocusReturn: !shouldReturnFocusRef.current });
|
233
246
|
shouldReturnFocusRef.current = true;
|
234
247
|
}
|
235
|
-
}, [status, triggerType, disableHideAnimation]);
|
248
|
+
}, [status, isRendered, triggerType, disableHideAnimation]);
|
236
249
|
/**
|
237
250
|
* Release focus trapping on unmount
|
238
251
|
*/
|
@@ -243,12 +256,14 @@ const FlyoutRoot = (props) => {
|
|
243
256
|
* Update position on resize or RTL
|
244
257
|
*/
|
245
258
|
React.useEffect(() => {
|
259
|
+
if (!isRendered)
|
260
|
+
return;
|
246
261
|
const resizeObserver = new ResizeObserver(() => updatePosition({ sync: true }));
|
247
262
|
resizeObserver.observe(document.body);
|
248
263
|
if (triggerElRef.current)
|
249
264
|
resizeObserver.observe(triggerElRef.current);
|
250
265
|
return () => resizeObserver.disconnect();
|
251
|
-
}, [updatePosition, triggerElRef]);
|
266
|
+
}, [updatePosition, triggerElRef, isRendered]);
|
252
267
|
React.useEffect(() => {
|
253
268
|
updatePosition();
|
254
269
|
}, [isRTL, updatePosition]);
|
@@ -7,6 +7,7 @@ export declare const positions: () => React.JSX.Element;
|
|
7
7
|
export declare const dynamicPosition: () => React.JSX.Element;
|
8
8
|
export declare const modes: () => React.JSX.Element;
|
9
9
|
export declare const disableFlags: () => React.JSX.Element;
|
10
|
+
export declare const initialFocus: () => React.JSX.Element;
|
10
11
|
export declare const customPortalTarget: () => React.JSX.Element;
|
11
12
|
export declare const testWidthOverflowOnMobile: () => React.JSX.Element;
|
12
13
|
export declare const testInsideFixed: () => React.JSX.Element;
|
@@ -6,6 +6,7 @@ import View from "../../../View/index.js";
|
|
6
6
|
import Theme from "../../../Theme/index.js";
|
7
7
|
import Button from "../../../Button/index.js";
|
8
8
|
import Flyout from "../index.js";
|
9
|
+
import TextField from "../../../TextField/index.js";
|
9
10
|
export default { title: "Utilities/Internal/Flyout" };
|
10
11
|
const Demo = (props) => {
|
11
12
|
const { position = "bottom-start", children, ...rest } = props;
|
@@ -17,7 +18,7 @@ const Demo = (props) => {
|
|
17
18
|
<div style={{
|
18
19
|
background: "var(--rs-color-background-elevation-overlay)",
|
19
20
|
padding: "var(--rs-unit-x4)",
|
20
|
-
height:
|
21
|
+
height: 150,
|
21
22
|
width: 160,
|
22
23
|
borderRadius: "var(--rs-radius-medium)",
|
23
24
|
border: "1px solid var(--rs-color-border-neutral-faded)",
|
@@ -120,6 +121,19 @@ export const disableFlags = () => (<Example>
|
|
120
121
|
<Demo disableHideAnimation>Content</Demo>
|
121
122
|
</Example.Item>
|
122
123
|
</Example>);
|
124
|
+
export const initialFocus = () => {
|
125
|
+
const initialFocusRef = React.useRef(null);
|
126
|
+
return (<Example>
|
127
|
+
<Example.Item title="focuses input on open">
|
128
|
+
<Demo initialFocusRef={initialFocusRef}>
|
129
|
+
<View gap={4}>
|
130
|
+
<Button onClick={() => { }}>Click me</Button>
|
131
|
+
<TextField name="foo" inputAttributes={{ ref: initialFocusRef }}/>
|
132
|
+
</View>
|
133
|
+
</Demo>
|
134
|
+
</Example.Item>
|
135
|
+
</Example>);
|
136
|
+
};
|
123
137
|
class CustomElement extends window.HTMLElement {
|
124
138
|
constructor() {
|
125
139
|
super();
|
@@ -199,36 +213,32 @@ export const testInsideFixed = () => (<Example>
|
|
199
213
|
</Example.Item>
|
200
214
|
</Example>);
|
201
215
|
export const testDynamicBounds = () => {
|
202
|
-
const [left, setLeft] = React.useState(
|
216
|
+
const [left, setLeft] = React.useState(50);
|
217
|
+
const [top, setTop] = React.useState(50);
|
203
218
|
const [size, setSize] = React.useState("medium");
|
204
219
|
const flyoutRef = React.useRef();
|
205
220
|
React.useEffect(() => {
|
206
221
|
flyoutRef.current?.updatePosition();
|
207
|
-
}, [left]);
|
222
|
+
}, [left, top]);
|
208
223
|
return (<View gap={4}>
|
209
224
|
<View direction="row" gap={2}>
|
225
|
+
<Button onClick={() => setLeft((prev) => prev - 10)}>Left</Button>
|
226
|
+
<Button onClick={() => setLeft((prev) => prev + 10)}>Right</Button>
|
227
|
+
<Button onClick={() => setTop((prev) => prev - 10)}>Up</Button>
|
228
|
+
<Button onClick={() => setTop((prev) => prev + 10)}>Down</Button>
|
210
229
|
<Button onClick={() => {
|
211
|
-
setLeft(
|
212
|
-
|
213
|
-
Left
|
214
|
-
</Button>
|
215
|
-
<Button onClick={() => {
|
216
|
-
setLeft("50%");
|
230
|
+
setLeft(50);
|
231
|
+
setTop(50);
|
217
232
|
}}>
|
218
233
|
Center
|
219
234
|
</Button>
|
220
|
-
<Button onClick={() => {
|
221
|
-
setLeft("70%");
|
222
|
-
}}>
|
223
|
-
Right
|
224
|
-
</Button>
|
225
235
|
<Button onClick={() => setSize("large")}>Large button</Button>
|
226
236
|
<Button onClick={() => setSize("medium")}>Small button</Button>
|
227
237
|
</View>
|
228
238
|
<View height={100}>
|
229
|
-
<Flyout position="bottom"
|
239
|
+
<Flyout position="bottom" instanceRef={flyoutRef} disableCloseOnOutsideClick fallbackPositions={["top", "bottom"]}>
|
230
240
|
<Flyout.Trigger>
|
231
|
-
{(attributes) => (<div style={{ position: "absolute", left
|
241
|
+
{(attributes) => (<div style={{ position: "absolute", left: `${left}%`, top: `${top}%` }}>
|
232
242
|
<Button color="primary" attributes={attributes} size={size}>
|
233
243
|
Open
|
234
244
|
</Button>
|
@@ -239,12 +249,12 @@ export const testDynamicBounds = () => {
|
|
239
249
|
background: "var(--rs-color-background-elevation-overlay)",
|
240
250
|
padding: "var(--rs-unit-x4)",
|
241
251
|
height: 100,
|
242
|
-
|
252
|
+
minWidth: 160,
|
243
253
|
borderRadius: "var(--rs-radius-medium)",
|
244
254
|
border: "1px solid var(--rs-color-border-neutral-faded)",
|
245
255
|
boxSizing: "border-box",
|
246
256
|
}}>
|
247
|
-
|
257
|
+
Content
|
248
258
|
</div>
|
249
259
|
</Flyout.Content>
|
250
260
|
</Flyout>
|
@@ -8,13 +8,14 @@ type PassedFlyoutOptions = {
|
|
8
8
|
width?: T.Width;
|
9
9
|
position?: T.Position;
|
10
10
|
defaultActive?: boolean;
|
11
|
-
|
11
|
+
fallbackPositions?: T.Position[];
|
12
12
|
container?: HTMLElement | null;
|
13
13
|
};
|
14
14
|
type UseFlyout = (args: PassedFlyoutOptions & {
|
15
15
|
triggerElRef: ElementRef;
|
16
16
|
flyoutElRef: ElementRef;
|
17
17
|
triggerBoundsRef: React.RefObject<DOMRect | undefined>;
|
18
|
+
contentGap?: number;
|
18
19
|
}) => Pick<T.State, "styles" | "position" | "status"> & {
|
19
20
|
updatePosition: (options?: {
|
20
21
|
sync?: boolean;
|
@@ -1,25 +1,8 @@
|
|
1
1
|
import React from "react";
|
2
2
|
import useRTL from "../../../hooks/useRTL.js";
|
3
|
-
import { getClosestFlyoutTarget } from "../../../utilities/dom.js";
|
3
|
+
import { getClosestFlyoutTarget, getShadowRoot } from "../../../utilities/dom.js";
|
4
4
|
import calculatePosition from "./utilities/calculatePosition.js";
|
5
|
-
|
6
|
-
const bottomPos = ["bottom-start", "bottom", "bottom-end"];
|
7
|
-
const startPos = ["start", "start-bottom", "start-top"];
|
8
|
-
const endPos = ["end", "end-bottom", "end-top"];
|
9
|
-
const order = {
|
10
|
-
top: [...topPos, ...bottomPos, ...endPos, ...startPos],
|
11
|
-
bottom: [...bottomPos, ...topPos, ...endPos, ...startPos],
|
12
|
-
start: [...startPos, ...endPos, ...topPos, ...bottomPos],
|
13
|
-
end: [...endPos, ...startPos, ...topPos, ...bottomPos],
|
14
|
-
};
|
15
|
-
/**
|
16
|
-
* Get an order of positions to try to fit popover on the screen based on its starting position
|
17
|
-
*/
|
18
|
-
const getPositionOrder = (position) => {
|
19
|
-
const types = ["top", "bottom", "start", "end"];
|
20
|
-
const type = types.find((type) => position.startsWith(type)) || "bottom";
|
21
|
-
return order[type];
|
22
|
-
};
|
5
|
+
import getPositionFallbacks from "./utilities/getPositionFallbacks.js";
|
23
6
|
/**
|
24
7
|
* Check if element visually fits on the screen
|
25
8
|
*/
|
@@ -60,12 +43,13 @@ const resetStyles = {
|
|
60
43
|
* Set position of the target element to fit on the screen
|
61
44
|
*/
|
62
45
|
const flyout = (args) => {
|
63
|
-
const { triggerEl, flyoutEl, triggerBounds: passedTriggerBounds, ...options } = args;
|
64
|
-
const { position,
|
46
|
+
const { triggerEl, flyoutEl, triggerBounds: passedTriggerBounds, contentGap = 0, ...options } = args;
|
47
|
+
const { position, fallbackPositions, width, container, lastUsedFallback, onFallback } = options;
|
65
48
|
const targetClone = flyoutEl.cloneNode(true);
|
66
49
|
const triggerBounds = passedTriggerBounds || triggerEl.getBoundingClientRect();
|
50
|
+
const contentGapModifier = parseInt(getComputedStyle(flyoutEl).getPropertyValue("--rs-unit-x1"));
|
67
51
|
// Reset all styles applied on the previous hook execution
|
68
|
-
targetClone.style = "";
|
52
|
+
targetClone.style.cssText = "";
|
69
53
|
Object.keys(resetStyles).forEach((key) => {
|
70
54
|
const value = resetStyles[key];
|
71
55
|
targetClone.style[key] = value.toString();
|
@@ -78,8 +62,7 @@ const flyout = (args) => {
|
|
78
62
|
targetClone.style.width = width;
|
79
63
|
}
|
80
64
|
}
|
81
|
-
const
|
82
|
-
const shadowRoot = rootNode instanceof ShadowRoot ? rootNode : null;
|
65
|
+
const shadowRoot = getShadowRoot(triggerEl);
|
83
66
|
// Insert inside shadow root if possible to make sure styles are applied correctly
|
84
67
|
(shadowRoot || document.body).appendChild(targetClone);
|
85
68
|
const flyoutBounds = targetClone.getBoundingClientRect();
|
@@ -89,37 +72,30 @@ const flyout = (args) => {
|
|
89
72
|
top: containerBounds.top + document.documentElement.scrollTop - containerParent.scrollTop,
|
90
73
|
left: containerBounds.left + document.documentElement.scrollLeft - containerParent.scrollLeft,
|
91
74
|
};
|
92
|
-
let calculated =
|
93
|
-
|
94
|
-
|
95
|
-
const
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
});
|
110
|
-
if (fullyVisible(tested)) {
|
111
|
-
calculated = tested;
|
112
|
-
return true;
|
113
|
-
}
|
114
|
-
return false;
|
115
|
-
});
|
116
|
-
};
|
117
|
-
test(order);
|
118
|
-
if (!fullyVisible(calculated)) {
|
119
|
-
test(mobileOrder, { fullWidth: true });
|
75
|
+
let calculated = null;
|
76
|
+
const testOrder = getPositionFallbacks(position, fallbackPositions);
|
77
|
+
testOrder.some((currentPosition, index) => {
|
78
|
+
const tested = calculatePosition({
|
79
|
+
...options,
|
80
|
+
triggerBounds,
|
81
|
+
flyoutBounds,
|
82
|
+
scopeOffset,
|
83
|
+
position: currentPosition,
|
84
|
+
contentGap: contentGap * contentGapModifier,
|
85
|
+
});
|
86
|
+
const visible = fullyVisible(tested);
|
87
|
+
const validPosition = visible || fallbackPositions?.length === 0;
|
88
|
+
// Saving first try in case non of the options work
|
89
|
+
if (validPosition || lastUsedFallback === currentPosition) {
|
90
|
+
calculated = tested;
|
91
|
+
onFallback(currentPosition);
|
120
92
|
}
|
93
|
+
return validPosition;
|
94
|
+
});
|
95
|
+
if (!calculated) {
|
96
|
+
throw new Error(`Reshaped: Can't calculate styles for the ${position} position`);
|
121
97
|
}
|
122
|
-
targetClone.parentNode
|
98
|
+
targetClone.parentNode?.removeChild(targetClone);
|
123
99
|
return calculated;
|
124
100
|
};
|
125
101
|
const flyoutReducer = (state, action) => {
|
@@ -157,8 +133,13 @@ const flyoutReducer = (state, action) => {
|
|
157
133
|
}
|
158
134
|
};
|
159
135
|
const useFlyout = (args) => {
|
160
|
-
const { triggerElRef, flyoutElRef, triggerBoundsRef, ...options } = args;
|
161
|
-
const { position: defaultPosition = "bottom",
|
136
|
+
const { triggerElRef, flyoutElRef, triggerBoundsRef, contentGap, ...options } = args;
|
137
|
+
const { position: defaultPosition = "bottom", fallbackPositions, width, container } = options;
|
138
|
+
const lastUsedFallbackRef = React.useRef(defaultPosition);
|
139
|
+
// Memo the array internally to avoid new arrays triggering useCallback
|
140
|
+
const cachedFallbackPositions = React.useMemo(() => fallbackPositions,
|
141
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
142
|
+
[fallbackPositions?.join(" ")]);
|
162
143
|
const [isRTL] = useRTL();
|
163
144
|
const [state, dispatch] = React.useReducer(flyoutReducer, {
|
164
145
|
position: defaultPosition,
|
@@ -177,6 +158,9 @@ const useFlyout = (args) => {
|
|
177
158
|
const remove = React.useCallback(() => {
|
178
159
|
dispatch({ type: "remove" });
|
179
160
|
}, []);
|
161
|
+
const handleFallback = React.useCallback((position) => {
|
162
|
+
lastUsedFallbackRef.current = position;
|
163
|
+
}, []);
|
180
164
|
const updatePosition = React.useCallback((options) => {
|
181
165
|
if (!triggerElRef.current || !flyoutElRef.current)
|
182
166
|
return;
|
@@ -186,21 +170,26 @@ const useFlyout = (args) => {
|
|
186
170
|
triggerBounds: triggerBoundsRef.current,
|
187
171
|
width,
|
188
172
|
position: defaultPosition,
|
189
|
-
|
173
|
+
fallbackPositions: cachedFallbackPositions,
|
174
|
+
lastUsedFallback: lastUsedFallbackRef.current,
|
175
|
+
onFallback: handleFallback,
|
190
176
|
rtl: isRTL,
|
191
177
|
container,
|
178
|
+
contentGap,
|
192
179
|
});
|
193
180
|
if (nextFlyoutData)
|
194
181
|
dispatch({ type: "position", payload: { ...nextFlyoutData, sync: options?.sync } });
|
195
182
|
}, [
|
196
183
|
container,
|
197
184
|
defaultPosition,
|
198
|
-
|
185
|
+
cachedFallbackPositions,
|
199
186
|
isRTL,
|
200
187
|
flyoutElRef,
|
201
188
|
triggerElRef,
|
202
189
|
triggerBoundsRef,
|
203
190
|
width,
|
191
|
+
contentGap,
|
192
|
+
handleFallback,
|
204
193
|
]);
|
205
194
|
React.useEffect(() => {
|
206
195
|
if (state.status === "rendered")
|
@@ -16,24 +16,29 @@ const centerBySize = (originSize, targetSize) => {
|
|
16
16
|
* Calculate styles for the current position
|
17
17
|
*/
|
18
18
|
const calculatePosition = (args) => {
|
19
|
-
const { triggerBounds, flyoutBounds, scopeOffset, position: passedPosition, rtl, width } = args;
|
19
|
+
const { triggerBounds, flyoutBounds, scopeOffset, position: passedPosition, rtl, width, contentGap = 0, } = args;
|
20
|
+
const isFullWidth = width === "full" || width === "100%";
|
20
21
|
let left = 0;
|
21
22
|
let top = 0;
|
22
23
|
let position = passedPosition;
|
23
24
|
if (rtl)
|
24
25
|
position = getRTLPosition(position);
|
25
|
-
if (
|
26
|
+
if (isFullWidth || width === "trigger") {
|
26
27
|
position = position.includes("top") ? "top" : "bottom";
|
27
28
|
}
|
29
|
+
const isHorizontalPosition = position.match(/^(start|end)/);
|
30
|
+
const isVerticalPosition = position.match(/^(top|bottom)/);
|
31
|
+
const flyoutWidth = flyoutBounds.width + (isHorizontalPosition ? contentGap : 0);
|
32
|
+
const flyoutHeight = flyoutBounds.height + (isVerticalPosition ? contentGap : 0);
|
28
33
|
switch (position) {
|
29
34
|
case "bottom":
|
30
35
|
case "top":
|
31
|
-
left = centerBySize(triggerBounds.width,
|
36
|
+
left = centerBySize(triggerBounds.width, flyoutWidth) + triggerBounds.left;
|
32
37
|
break;
|
33
38
|
case "start":
|
34
39
|
case "start-top":
|
35
40
|
case "start-bottom":
|
36
|
-
left = triggerBounds.left -
|
41
|
+
left = triggerBounds.left - flyoutWidth;
|
37
42
|
break;
|
38
43
|
case "end":
|
39
44
|
case "end-top":
|
@@ -46,7 +51,7 @@ const calculatePosition = (args) => {
|
|
46
51
|
break;
|
47
52
|
case "top-end":
|
48
53
|
case "bottom-end":
|
49
|
-
left = triggerBounds.right -
|
54
|
+
left = triggerBounds.right - flyoutWidth;
|
50
55
|
break;
|
51
56
|
default:
|
52
57
|
break;
|
@@ -55,7 +60,7 @@ const calculatePosition = (args) => {
|
|
55
60
|
case "top":
|
56
61
|
case "top-start":
|
57
62
|
case "top-end":
|
58
|
-
top = triggerBounds.top -
|
63
|
+
top = triggerBounds.top - flyoutHeight;
|
59
64
|
break;
|
60
65
|
case "bottom":
|
61
66
|
case "bottom-start":
|
@@ -64,7 +69,7 @@ const calculatePosition = (args) => {
|
|
64
69
|
break;
|
65
70
|
case "start":
|
66
71
|
case "end":
|
67
|
-
top = centerBySize(triggerBounds.height,
|
72
|
+
top = centerBySize(triggerBounds.height, flyoutHeight) + triggerBounds.top;
|
68
73
|
break;
|
69
74
|
case "start-top":
|
70
75
|
case "end-top":
|
@@ -72,7 +77,7 @@ const calculatePosition = (args) => {
|
|
72
77
|
break;
|
73
78
|
case "start-bottom":
|
74
79
|
case "end-bottom":
|
75
|
-
top = triggerBounds.bottom -
|
80
|
+
top = triggerBounds.bottom - flyoutHeight;
|
76
81
|
break;
|
77
82
|
default:
|
78
83
|
break;
|
@@ -82,9 +87,9 @@ const calculatePosition = (args) => {
|
|
82
87
|
}
|
83
88
|
top = Math.round(top + (window.scrollY || 0) - scopeOffset.top);
|
84
89
|
left = Math.round(left + (window.scrollX || 0) - scopeOffset.left);
|
85
|
-
let widthStyle = Math.ceil(
|
86
|
-
const height = Math.ceil(
|
87
|
-
if (
|
90
|
+
let widthStyle = Math.ceil(flyoutWidth);
|
91
|
+
const height = Math.ceil(flyoutHeight);
|
92
|
+
if (isFullWidth) {
|
88
93
|
left = SCREEN_OFFSET;
|
89
94
|
widthStyle = window.innerWidth - SCREEN_OFFSET * 2;
|
90
95
|
}
|
@@ -1,18 +1,30 @@
|
|
1
|
+
import * as timeouts from "../Flyout.constants.js";
|
1
2
|
class Cooldown {
|
2
3
|
status = "cold";
|
3
4
|
timer;
|
4
5
|
warm = () => {
|
5
6
|
clearTimeout(this.timer);
|
6
|
-
this.status
|
7
|
+
if (this.status === "cooling") {
|
8
|
+
this.status = "warm";
|
9
|
+
return;
|
10
|
+
}
|
11
|
+
this.status = "warming";
|
12
|
+
this.timer = setTimeout(() => {
|
13
|
+
this.status = "warm";
|
14
|
+
this.timer = undefined;
|
15
|
+
}, timeouts.mouseEnterShort);
|
7
16
|
};
|
8
17
|
cool = () => {
|
18
|
+
clearTimeout(this.timer);
|
19
|
+
if (this.status === "warming") {
|
20
|
+
this.status = "cold";
|
21
|
+
return;
|
22
|
+
}
|
9
23
|
this.status = "cooling";
|
10
|
-
|
24
|
+
this.timer = setTimeout(() => {
|
11
25
|
this.status = "cold";
|
12
|
-
|
13
|
-
this.timer = undefined;
|
26
|
+
this.timer = undefined;
|
14
27
|
}, 500);
|
15
|
-
this.timer = currentTimer;
|
16
28
|
};
|
17
29
|
}
|
18
30
|
export default new Cooldown();
|
@@ -0,0 +1,39 @@
|
|
1
|
+
// All available positions for each side
|
2
|
+
const positions = {
|
3
|
+
top: ["top-start", "top-end", "top"],
|
4
|
+
bottom: ["bottom-start", "bottom-end", "bottom"],
|
5
|
+
start: ["start-top", "start-bottom", "start"],
|
6
|
+
end: ["end-top", "end-bottom", "end"],
|
7
|
+
};
|
8
|
+
// Order of sides to try depending on the starting side
|
9
|
+
const fallbackOrder = {
|
10
|
+
top: ["bottom", "start", "end"],
|
11
|
+
bottom: ["top", "end", "start"],
|
12
|
+
start: ["end", "top", "bottom"],
|
13
|
+
end: ["start", "bottom", "top"],
|
14
|
+
};
|
15
|
+
// Get an order of positions to try to fit flyout on the screen based on its starting position
|
16
|
+
const getPositionFallbacks = (position, availableFallbacks) => {
|
17
|
+
const result = [];
|
18
|
+
const chunks = position.split("-");
|
19
|
+
const [firstChunk] = chunks;
|
20
|
+
const passedPositionOrder = positions[firstChunk];
|
21
|
+
const startingFallbackIndex = passedPositionOrder.indexOf(position);
|
22
|
+
const fallbackIndexOrder = [startingFallbackIndex];
|
23
|
+
passedPositionOrder.forEach((_, index) => {
|
24
|
+
if (index === startingFallbackIndex)
|
25
|
+
return;
|
26
|
+
fallbackIndexOrder.push(index);
|
27
|
+
});
|
28
|
+
[firstChunk, ...fallbackOrder[firstChunk]].forEach((fallbackSide) => {
|
29
|
+
const fallbackOrder = positions[fallbackSide];
|
30
|
+
fallbackIndexOrder.forEach((index) => {
|
31
|
+
const position = fallbackOrder[index];
|
32
|
+
if (availableFallbacks?.indexOf(position) === -1)
|
33
|
+
return;
|
34
|
+
result.push(position);
|
35
|
+
});
|
36
|
+
});
|
37
|
+
return result;
|
38
|
+
};
|
39
|
+
export default getPositionFallbacks;
|
@@ -1,2 +1,2 @@
|
|
1
1
|
import type { ThemeDefinition } from "../themes/_generator/tokens/types";
|
2
|
-
export declare const getTheme: (theme?: ThemeDefinition) => Record<"borderRadius" | "backgroundColor" | "borderColor" | "
|
2
|
+
export declare const getTheme: (theme?: ThemeDefinition) => Record<"borderRadius" | "backgroundColor" | "borderColor" | "boxShadow" | "textColor" | "colors" | "spacing" | "screens", Record<string, string>>;
|
@@ -7,10 +7,11 @@ const useOnClickOutside = (refs, handler) => {
|
|
7
7
|
return;
|
8
8
|
const handleClick = (event) => {
|
9
9
|
let isInside = false;
|
10
|
+
const clickedEl = event.composedPath()[0];
|
10
11
|
refs.forEach((elRef) => {
|
11
12
|
if (!elRef.current ||
|
12
|
-
elRef.current ===
|
13
|
-
elRef.current.contains(
|
13
|
+
elRef.current === clickedEl ||
|
14
|
+
elRef.current.contains(clickedEl)) {
|
14
15
|
isInside = true;
|
15
16
|
}
|
16
17
|
});
|