reshaped 3.9.0 → 3.9.1-canary.3

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 (53) hide show
  1. package/dist/bundle.css +1 -1
  2. package/dist/bundle.js +2 -2
  3. package/dist/components/Accordion/AccordionControlled.js +0 -1
  4. package/dist/components/Actionable/Actionable.d.ts +8 -3
  5. package/dist/components/Actionable/Actionable.js +17 -70
  6. package/dist/components/Actionable/Actionable.types.d.ts +2 -36
  7. package/dist/components/Actionable/index.d.ts +2 -1
  8. package/dist/components/Badge/Badge.js +5 -4
  9. package/dist/components/Badge/Badge.module.css +1 -1
  10. package/dist/components/Calendar/Calendar.utils.js +6 -7
  11. package/dist/components/Card/Card.d.ts +1 -1
  12. package/dist/components/Carousel/Carousel.js +0 -1
  13. package/dist/components/Flyout/Flyout.module.css +1 -1
  14. package/dist/components/Flyout/Flyout.types.d.ts +7 -7
  15. package/dist/components/Flyout/FlyoutContent.js +3 -58
  16. package/dist/components/Flyout/FlyoutControlled.js +84 -83
  17. package/dist/components/Flyout/FlyoutTrigger.js +3 -3
  18. package/dist/components/Flyout/useFlyout.d.ts +3 -4
  19. package/dist/components/Flyout/useFlyout.js +70 -88
  20. package/dist/components/Flyout/utilities/safeArea.d.ts +10 -0
  21. package/dist/components/Flyout/utilities/safeArea.js +100 -0
  22. package/dist/components/Select/Select.js +1 -1
  23. package/dist/components/Select/SelectCustomControlled.js +0 -1
  24. package/dist/components/Tabs/TabsControlled.js +0 -1
  25. package/dist/components/Toast/ToastContainer.js +0 -1
  26. package/dist/components/_private/Expandable/Expandable.js +1 -3
  27. package/dist/components/_private/Portal/Portal.js +0 -3
  28. package/dist/core/Actionable/Actionable.d.ts +4 -0
  29. package/dist/core/Actionable/Actionable.js +73 -0
  30. package/dist/core/Actionable/Actionable.types.d.ts +34 -0
  31. package/dist/core/Actionable/Actionable.types.js +1 -0
  32. package/dist/core/Actionable/index.d.ts +2 -0
  33. package/dist/core/Actionable/index.js +1 -0
  34. package/dist/hooks/_private/usePrevious.js +0 -1
  35. package/dist/hooks/useOnClickOutside.js +8 -0
  36. package/dist/utilities/a11y/TrapFocus.js +9 -3
  37. package/dist/utilities/dom/index.d.ts +0 -1
  38. package/dist/utilities/dom/index.js +0 -1
  39. package/package.json +4 -2
  40. package/dist/components/Flyout/utilities/calculatePosition.d.ts +0 -31
  41. package/dist/components/Flyout/utilities/calculatePosition.js +0 -185
  42. package/dist/components/Flyout/utilities/constants.d.ts +0 -1
  43. package/dist/components/Flyout/utilities/constants.js +0 -1
  44. package/dist/components/Flyout/utilities/flyout.d.ts +0 -11
  45. package/dist/components/Flyout/utilities/flyout.js +0 -115
  46. package/dist/components/Flyout/utilities/getPositionFallbacks.d.ts +0 -3
  47. package/dist/components/Flyout/utilities/getPositionFallbacks.js +0 -39
  48. package/dist/components/Flyout/utilities/helpers.d.ts +0 -7
  49. package/dist/components/Flyout/utilities/helpers.js +0 -14
  50. package/dist/components/Flyout/utilities/isFullyVisible.d.ts +0 -13
  51. package/dist/components/Flyout/utilities/isFullyVisible.js +0 -23
  52. package/dist/utilities/dom/flyout.d.ts +0 -2
  53. package/dist/utilities/dom/flyout.js +0 -14
@@ -2,30 +2,31 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import useIsDismissible from "../../hooks/_private/useIsDismissible.js";
5
+ import usePrevious from "../../hooks/_private/usePrevious.js";
5
6
  import useElementId from "../../hooks/useElementId.js";
6
7
  import useHandlerRef from "../../hooks/useHandlerRef.js";
7
8
  import useHotkeys from "../../hooks/useHotkeys.js";
8
9
  import useIsomorphicLayoutEffect from "../../hooks/useIsomorphicLayoutEffect.js";
9
10
  import useOnClickOutside from "../../hooks/useOnClickOutside.js";
10
- import useRTL from "../../hooks/useRTL.js";
11
11
  import { TrapFocus, checkKeyboardMode } from "../../utilities/a11y/index.js";
12
- import { checkTransitions, onNextFrame } from "../../utilities/animation.js";
12
+ import { checkTransitions } from "../../utilities/animation.js";
13
13
  import * as timeouts from "./Flyout.constants.js";
14
14
  import { Provider, useFlyoutTriggerContext, useFlyoutContext, useFlyoutContentContext, } from "./Flyout.context.js";
15
15
  import useFlyout from "./useFlyout.js";
16
16
  import cooldown from "./utilities/cooldown.js";
17
+ import { createSafeArea } from "./utilities/safeArea.js";
17
18
  const FlyoutControlled = (props) => {
18
- const { triggerType = "click", groupTimeouts, onOpen, onClose, children, disabled, forcePosition, fallbackAdjustLayout, fallbackMinWidth, fallbackMinHeight, trapFocusMode = "dialog", width, disableHideAnimation, disableContentHover, disableCloseOnOutsideClick, autoFocus = true, originCoordinates, contentGap = 2, contentShift, contentMaxHeight, contentMaxWidth, contentClassName, contentAttributes, position: passedPosition, active: passedActive, id: passedId, instanceRef, containerRef, initialFocusRef, positionRef, } = props;
19
+ const { triggerType = "click", groupTimeouts, onOpen, onClose, children, disabled, forcePosition, fallbackAdjustLayout, fallbackMinHeight, trapFocusMode = "dialog", width, disableHideAnimation, disableContentHover, disableCloseOnOutsideClick, autoFocus = true, originCoordinates, contentGap = 2, contentShift, contentMaxHeight, contentMaxWidth, contentClassName, contentAttributes, position: passedPosition, active: passedActive, id: passedId, instanceRef, containerRef, initialFocusRef, positionRef, } = props;
19
20
  const fallbackPositions = props.fallbackPositions === false || forcePosition ? [] : props.fallbackPositions;
20
21
  const onOpenRef = useHandlerRef(onOpen);
21
22
  const onCloseRef = useHandlerRef(onClose);
22
- const resolvedActive = disabled === true ? false : passedActive;
23
+ const active = disabled === true ? false : passedActive;
24
+ const prevActive = usePrevious(active);
23
25
  const parentFlyoutContext = useFlyoutContext();
24
26
  const { elRef: parentTriggerRef } = useFlyoutTriggerContext() || {};
25
27
  const { elRef: parentContentRef } = useFlyoutContentContext() || {};
26
28
  const isSubmenu = parentFlyoutContext.trapFocusMode === "action-menu" ||
27
29
  parentFlyoutContext.trapFocusMode === "content-menu";
28
- const [isRTL] = useRTL();
29
30
  const internalTriggerElRef = React.useRef(null);
30
31
  /**
31
32
  * Reuse the parent trigger ref in case we render nested triggers
@@ -34,20 +35,14 @@ const FlyoutControlled = (props) => {
34
35
  * Resolving the same inside another Flyout.Content should reset the inheritance
35
36
  * For example, if you have a tooltip -> popover inside another popover.content, tooltip shouldn't use its parent context anymore
36
37
  */
37
- const isParentTriggerInsideFlyout =
38
- // eslint-disable-next-line react-hooks/refs
39
- !!parentTriggerRef?.current && parentContentRef?.current?.contains(parentTriggerRef.current);
38
+ const isParentTriggerInsideFlyout = !!parentTriggerRef?.current && parentContentRef?.current?.contains(parentTriggerRef.current);
40
39
  const tryParentTrigger = !parentContentRef || isParentTriggerInsideFlyout;
41
40
  const triggerElRef = (tryParentTrigger && parentTriggerRef) || internalTriggerElRef;
42
- const triggerBoundsRef = React.useRef(null);
43
41
  const flyoutElRef = React.useRef(null);
44
42
  const id = useElementId(passedId);
45
43
  const timerRef = React.useRef(null);
46
44
  const trapFocusRef = React.useRef(null);
47
45
  const lockedRef = React.useRef(false);
48
- // Check if transition had enough time to start when opening a flyout
49
- // In some cases there is not enough time to start, like when you're holding tab key
50
- const transitionStartedRef = React.useRef(false);
51
46
  // Lock blur event while pressing anywhere inside the flyout content
52
47
  const lockedBlurEffects = React.useRef(false);
53
48
  // Focus shouldn't return back to the trigger when user intentionally clicks outside the flyout
@@ -55,24 +50,24 @@ const FlyoutControlled = (props) => {
55
50
  // Touch devices trigger onMouseEnter but we don't need to apply regular hover timeouts
56
51
  // So we're saving a flag on touch start and then change the mouse enter behavior
57
52
  const hoverTriggeredWithTouchEventRef = React.useRef(false);
53
+ // Cleanup function for safe area tracking
54
+ const safeAreaRef = React.useRef(null);
58
55
  const originCoordinatesRef = React.useRef(originCoordinates ?? null);
59
- // eslint-disable-next-line react-hooks/refs
60
56
  originCoordinatesRef.current = originCoordinates ?? null;
61
57
  const flyout = useFlyout({
62
58
  triggerElRef: positionRef ?? triggerElRef,
63
59
  flyoutElRef,
64
- triggerBoundsRef: originCoordinates ? originCoordinatesRef : triggerBoundsRef,
60
+ triggerCoordinatesRef: originCoordinatesRef,
65
61
  width,
66
62
  position: passedPosition,
67
- defaultActive: resolvedActive,
68
- // eslint-disable-next-line react-hooks/refs
63
+ defaultActive: active,
69
64
  container: containerRef?.current,
70
65
  fallbackPositions,
71
66
  fallbackAdjustLayout,
72
- fallbackMinWidth,
73
67
  fallbackMinHeight,
74
68
  contentGap,
75
69
  contentShift,
70
+ onClose: onCloseRef.current,
76
71
  });
77
72
  const { status, updatePosition, render, hide, remove, show } = flyout;
78
73
  const isRendered = status !== "idle";
@@ -86,6 +81,25 @@ const FlyoutControlled = (props) => {
86
81
  if (timerRef.current)
87
82
  clearTimeout(timerRef.current);
88
83
  }, []);
84
+ /**
85
+ * Disable all triggers while mouse is moving over the safe area
86
+ */
87
+ const disableTriggers = React.useCallback(() => {
88
+ if (triggerType !== "hover")
89
+ return;
90
+ document.querySelectorAll("[data-rs-flyout-active]").forEach((el) => {
91
+ if (el === triggerElRef.current)
92
+ return;
93
+ el.style.pointerEvents = "none";
94
+ });
95
+ }, [triggerElRef, triggerType]);
96
+ const enableTriggers = React.useCallback(() => {
97
+ if (triggerType !== "hover")
98
+ return;
99
+ document.querySelectorAll("[data-rs-flyout-active]").forEach((el) => {
100
+ el.style.removeProperty("pointer-events");
101
+ });
102
+ }, [triggerType]);
89
103
  /**
90
104
  * Component open/close handlers
91
105
  * Called from the internal actions
@@ -96,17 +110,27 @@ const FlyoutControlled = (props) => {
96
110
  if (isRendered && triggerType !== "hover")
97
111
  return;
98
112
  onOpenRef.current?.();
99
- }, [onOpenRef, isRendered, triggerType]);
113
+ disableTriggers();
114
+ }, [onOpenRef, isRendered, triggerType, disableTriggers]);
100
115
  const handleClose = React.useCallback((options) => {
101
116
  const isLocked = triggerType === "click" && !isDismissible();
102
117
  const canClose = !isLocked && (isRendered || disabled);
103
118
  if (!canClose)
104
119
  return;
105
120
  onCloseRef.current?.({ reason: options.reason });
121
+ enableTriggers();
106
122
  if (options?.closeParents) {
107
123
  parentFlyoutContext?.handleClose?.({ closeParents: true, reason: options.reason });
108
124
  }
109
- }, [isRendered, isDismissible, triggerType, onCloseRef, disabled, parentFlyoutContext]);
125
+ }, [
126
+ isRendered,
127
+ isDismissible,
128
+ triggerType,
129
+ onCloseRef,
130
+ disabled,
131
+ parentFlyoutContext,
132
+ enableTriggers,
133
+ ]);
110
134
  /**
111
135
  * Trigger event handlers
112
136
  */
@@ -133,7 +157,7 @@ const FlyoutControlled = (props) => {
133
157
  return;
134
158
  hoverTriggeredWithTouchEventRef.current = true;
135
159
  }, [triggerType]);
136
- const handleMouseEnter = React.useCallback(() => {
160
+ const handleContentMouseEnter = React.useCallback(() => {
137
161
  clearTimer();
138
162
  if (hoverTriggeredWithTouchEventRef.current) {
139
163
  handleOpen();
@@ -144,13 +168,15 @@ const FlyoutControlled = (props) => {
144
168
  cooldown.warm();
145
169
  timerRef.current = setTimeout(() => {
146
170
  handleOpen();
147
- }, groupTimeouts && cooldown.status === "warming"
148
- ? timeouts.mouseEnter
149
- : isSubmenu
150
- ? timeouts.mouseEnter
151
- : 0);
171
+ }, groupTimeouts && cooldown.status === "warming" ? timeouts.mouseEnter : 0);
152
172
  }
153
- }, [clearTimer, handleOpen, groupTimeouts, isSubmenu]);
173
+ }, [clearTimer, handleOpen, groupTimeouts]);
174
+ const handleTriggerMouseEnter = React.useCallback((e) => {
175
+ if (e.currentTarget === triggerElRef.current) {
176
+ safeAreaRef.current?.cleanup();
177
+ }
178
+ handleContentMouseEnter();
179
+ }, [triggerElRef, handleContentMouseEnter]);
154
180
  const handleMouseLeave = React.useCallback((e) => {
155
181
  if (e.relatedTarget === flyoutElRef.current ||
156
182
  (e.relatedTarget instanceof Node && flyoutElRef.current?.contains(e.relatedTarget)))
@@ -160,15 +186,25 @@ const FlyoutControlled = (props) => {
160
186
  return;
161
187
  cooldown.cool();
162
188
  clearTimer();
163
- if (isSubmenu) {
164
- timerRef.current = setTimeout(() => {
165
- handleClose({});
166
- }, timeouts.mouseLeave);
189
+ safeAreaRef.current?.cleanup();
190
+ if (triggerType === "hover" && isRendered) {
191
+ // Safe area coordinates are defined based on the trigger mouse out, even when returning mouse from content to trigger
192
+ const origin = e.currentTarget === flyoutElRef.current && safeAreaRef.current?.origin
193
+ ? safeAreaRef.current.origin
194
+ : { x: e.clientX, y: e.clientY };
195
+ const cleanup = createSafeArea({
196
+ contentRef: flyoutElRef,
197
+ triggerRef: triggerElRef,
198
+ position: flyout.position,
199
+ onClose: () => handleClose({}),
200
+ origin,
201
+ });
202
+ safeAreaRef.current = { origin, cleanup };
167
203
  }
168
204
  else {
169
205
  handleClose({});
170
206
  }
171
- }, [clearTimer, handleClose, triggerElRef, flyoutElRef, isSubmenu]);
207
+ }, [clearTimer, handleClose, triggerElRef, flyoutElRef, triggerType, isRendered, flyout.position]);
172
208
  const handleTriggerClick = React.useCallback(() => {
173
209
  if (!isRendered) {
174
210
  handleOpen();
@@ -177,13 +213,6 @@ const FlyoutControlled = (props) => {
177
213
  handleClose({});
178
214
  }
179
215
  }, [isRendered, handleOpen, handleClose]);
180
- const handleTriggerMouseDown = React.useCallback(() => {
181
- const triggerEl = positionRef?.current ?? triggerElRef.current;
182
- const rect = triggerEl?.getBoundingClientRect();
183
- if (!rect)
184
- return;
185
- triggerBoundsRef.current = rect;
186
- }, [triggerElRef, positionRef]);
187
216
  const handleContentMouseDown = () => {
188
217
  lockedBlurEffects.current = true;
189
218
  hoverTriggeredWithTouchEventRef.current = true;
@@ -191,44 +220,27 @@ const FlyoutControlled = (props) => {
191
220
  const handleContentMouseUp = () => {
192
221
  lockedBlurEffects.current = false;
193
222
  };
194
- const handleTransitionStart = React.useCallback((e) => {
195
- if (!resolvedActive)
196
- return;
197
- if (flyoutElRef.current !== e.currentTarget || e.propertyName !== "transform")
198
- return;
199
- transitionStartedRef.current = true;
200
- /**
201
- * After animation has started, we're sure about the correct bounds
202
- * so drop the cache to make flyout work when trigger moves around
203
- */
204
- triggerBoundsRef.current = null;
205
- }, [resolvedActive]);
206
223
  const handleTransitionEnd = React.useCallback((e) => {
207
224
  if (flyoutElRef.current !== e.currentTarget || e.propertyName !== "transform")
208
225
  return;
209
- if (status === "hidden") {
210
- transitionStartedRef.current = false;
226
+ if (status === "hidden")
211
227
  remove();
212
- }
213
228
  }, [remove, status]);
214
229
  /**
215
230
  * Control the display based on the props
216
231
  */
217
232
  useIsomorphicLayoutEffect(() => {
218
- if (resolvedActive) {
233
+ if (active) {
219
234
  render();
220
235
  return;
221
236
  }
222
237
  if (disabled)
223
238
  cooldown.cool();
224
- /**
225
- * Check that transitions are enabled and it has been triggered on tooltip open
226
- * - keyboard focus navigation could move too fast and ignore the transitions completely
227
- * - warmed up tooltips get removed instantly
228
- */
239
+ // Prevent calling hide on component mount
240
+ if (prevActive === active)
241
+ return;
229
242
  if (checkTransitions() &&
230
243
  !disableHideAnimation &&
231
- transitionStartedRef.current &&
232
244
  (cooldown.status === "cooling" || !groupTimeouts)) {
233
245
  hide();
234
246
  }
@@ -236,11 +248,10 @@ const FlyoutControlled = (props) => {
236
248
  // In case transitions are disabled globally - remove from the DOM immediately
237
249
  remove();
238
250
  }
239
- }, [resolvedActive, render, hide, remove, disableHideAnimation, disabled, groupTimeouts]);
240
- React.useEffect(() => {
241
- // Wait after positioning before show is triggered to animate flyout from the right side
242
- if (status === "positioned")
243
- onNextFrame(() => show());
251
+ }, [active, prevActive, render, hide, remove, disableHideAnimation, disabled, groupTimeouts]);
252
+ useIsomorphicLayoutEffect(() => {
253
+ if (status === "rendered")
254
+ show();
244
255
  }, [status, show]);
245
256
  /**
246
257
  * Handle focus trap
@@ -278,7 +289,7 @@ const FlyoutControlled = (props) => {
278
289
  return;
279
290
  if (trapFocusRef.current?.trapped) {
280
291
  /* Locking the popover to not open it again on trigger focus */
281
- if (triggerType === "hover") {
292
+ if (triggerType === "hover" && checkKeyboardMode()) {
282
293
  lockedRef.current = true;
283
294
  setTimeout(() => {
284
295
  lockedRef.current = false;
@@ -295,29 +306,20 @@ const FlyoutControlled = (props) => {
295
306
  return () => trapFocusRef.current?.release();
296
307
  }, []);
297
308
  /**
298
- * Update position on resize or RTL
309
+ * Clean up safe polygon tracking on unmount or when flyout closes
299
310
  */
300
311
  React.useEffect(() => {
301
312
  if (!isRendered)
302
- return;
303
- const resizeObserver = new ResizeObserver(() => updatePosition({ sync: true }));
304
- resizeObserver.observe(document.body);
305
- if (triggerElRef.current)
306
- resizeObserver.observe(triggerElRef.current);
307
- if (flyoutElRef.current)
308
- resizeObserver.observe(flyoutElRef.current);
309
- return () => resizeObserver.disconnect();
310
- }, [updatePosition, triggerElRef, isRendered, flyoutElRef]);
311
- React.useEffect(() => {
312
- updatePosition({ sync: true });
313
- }, [isRTL, updatePosition]);
313
+ safeAreaRef.current?.cleanup();
314
+ return () => safeAreaRef.current?.cleanup();
315
+ }, [isRendered]);
314
316
  /**
315
317
  * Imperative methods for controlling Flyout
316
318
  */
317
319
  React.useImperativeHandle(instanceRef, () => ({
318
320
  open: handleOpen,
319
321
  close: () => handleClose({}),
320
- updatePosition: () => updatePosition({ sync: true }),
322
+ updatePosition: () => updatePosition(),
321
323
  }), [handleOpen, handleClose, updatePosition]);
322
324
  useHotkeys({ Escape: () => handleClose({ reason: "escape-key" }) }, [handleClose]);
323
325
  useOnClickOutside([flyoutElRef, triggerElRef], () => {
@@ -337,12 +339,11 @@ const FlyoutControlled = (props) => {
337
339
  handleOpen,
338
340
  handleFocus,
339
341
  handleBlur,
340
- handleMouseEnter,
342
+ handleTriggerMouseEnter,
343
+ handleContentMouseEnter,
341
344
  handleMouseLeave,
342
345
  handleTouchStart,
343
- handleTransitionStart,
344
346
  handleTransitionEnd,
345
- handleMouseDown: handleTriggerMouseDown,
346
347
  handleClick: handleTriggerClick,
347
348
  handleContentMouseDown,
348
349
  handleContentMouseUp,
@@ -3,17 +3,17 @@ 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, handleMouseDown, handleTouchStart, handleClick, trapFocusMode, isSubmenu, } = useFlyoutContext();
6
+ const { id, triggerElRef, triggerType, flyout, handleFocus, handleBlur, handleTriggerMouseEnter, handleMouseLeave, handleTouchStart, handleClick, trapFocusMode, isSubmenu, } = useFlyoutContext();
7
7
  const active = flyout.status !== "idle";
8
8
  const childrenAttributes = {
9
9
  ref: triggerElRef,
10
+ "data-rs-flyout-active": active,
10
11
  };
11
12
  if (triggerType === "click" || trapFocusMode === "action-menu") {
12
13
  childrenAttributes.onClick = handleClick;
13
- childrenAttributes.onMouseDown = handleMouseDown;
14
14
  }
15
15
  if (triggerType === "hover") {
16
- childrenAttributes.onMouseEnter = handleMouseEnter;
16
+ childrenAttributes.onMouseEnter = handleTriggerMouseEnter;
17
17
  childrenAttributes.onMouseLeave = handleMouseLeave;
18
18
  childrenAttributes.onTouchStart = handleTouchStart;
19
19
  }
@@ -1,15 +1,14 @@
1
- import React from "react";
2
1
  import type * as T from "./Flyout.types";
3
2
  import type * as G from "../../types/global";
4
- type UseFlyout = (args: Pick<T.Props, "width" | "position" | "defaultActive" | "fallbackAdjustLayout" | "fallbackMinWidth" | "fallbackMinHeight" | "contentGap" | "contentShift"> & {
3
+ type UseFlyout = (args: Pick<T.Props, "width" | "position" | "defaultActive" | "fallbackAdjustLayout" | "fallbackMinHeight" | "contentGap" | "contentShift" | "onClose"> & {
5
4
  fallbackPositions?: T.Position[];
6
5
  container?: HTMLElement | null;
7
6
  triggerElRef: React.RefObject<HTMLElement | null>;
8
7
  flyoutElRef: React.RefObject<HTMLElement | null>;
9
- triggerBoundsRef: React.RefObject<DOMRect | G.Coordinates | null>;
8
+ triggerCoordinatesRef: React.RefObject<DOMRect | G.Coordinates | null>;
10
9
  }) => Pick<T.State, "position" | "status"> & {
11
10
  updatePosition: (options?: {
12
- sync?: boolean;
11
+ fallback?: boolean;
13
12
  }) => void;
14
13
  render: () => void;
15
14
  hide: () => void;
@@ -1,113 +1,95 @@
1
- import React from "react";
2
- import useRTL from "../../hooks/useRTL.js";
3
- import flyout from "./utilities/flyout.js";
4
- const flyoutReducer = (state, action) => {
5
- switch (action.type) {
6
- case "render":
7
- // Disable events before it's positioned to avoid mouseleave getting triggered
8
- return { ...state, status: "rendered" };
9
- case "position":
10
- return {
11
- ...state,
12
- status: action.payload.sync ? state.status : "positioned",
13
- position: action.payload.position,
14
- };
15
- case "show":
16
- // Checking because we're positioning inside nextAnimationFrame
17
- if (state.status !== "positioned")
18
- return state;
19
- return { ...state, status: "visible" };
20
- case "hide":
21
- return { ...state, status: "hidden" };
22
- case "remove":
23
- return { ...state, status: "idle" };
24
- default:
25
- throw new Error("[Reshaped] Invalid flyout reducer type");
26
- }
27
- };
1
+ import { Flyout } from "@reshaped/utilities";
2
+ import { useCallback, useMemo, useRef, useState } from "react";
3
+ import useIsomorphicLayoutEffect from "../../hooks/useIsomorphicLayoutEffect.js";
28
4
  const useFlyout = (args) => {
29
- const { triggerElRef, flyoutElRef, triggerBoundsRef, contentGap, contentShift, ...options } = args;
30
- const { position: defaultPosition = "bottom", fallbackPositions, fallbackAdjustLayout, fallbackMinWidth, fallbackMinHeight, width, container, } = options;
31
- const lastUsedPositionRef = React.useRef(defaultPosition);
5
+ const { triggerElRef, flyoutElRef, triggerCoordinatesRef, contentGap, contentShift, onClose, ...options } = args;
6
+ const { position: defaultPosition = "bottom", fallbackPositions, fallbackAdjustLayout, fallbackMinHeight, width, container, } = options;
7
+ const [status, setStatus] = useState("idle");
8
+ const [position, setPosition] = useState(defaultPosition);
9
+ const flyoutRef = useRef(null);
32
10
  // Memo the array internally to avoid new arrays triggering useCallback
33
- const cachedFallbackPositions = React.useMemo(() => fallbackPositions,
11
+ const cachedFallbackPositions = useMemo(() => fallbackPositions,
34
12
  // eslint-disable-next-line react-hooks/exhaustive-deps
35
13
  [fallbackPositions?.join(" ")]);
36
- const [isRTL] = useRTL();
37
- const [state, dispatch] = React.useReducer(flyoutReducer, {
38
- position: defaultPosition,
39
- status: "idle",
40
- });
41
- const render = React.useCallback(() => {
42
- dispatch({ type: "render" });
43
- }, []);
44
- const show = React.useCallback(() => {
45
- dispatch({ type: "show" });
46
- }, []);
47
- const hide = React.useCallback(() => {
48
- dispatch({ type: "hide" });
14
+ const render = useCallback(() => {
15
+ setStatus("rendered");
49
16
  }, []);
50
- const remove = React.useCallback(() => {
51
- dispatch({ type: "remove" });
17
+ const hide = useCallback(() => {
18
+ setStatus("hidden");
52
19
  }, []);
53
- const handlePosition = React.useCallback((position) => {
54
- lastUsedPositionRef.current = position;
55
- }, []);
56
- const updatePosition = React.useCallback((options) => {
20
+ const getFlyoutOptions = useCallback(() => {
57
21
  if (!flyoutElRef.current)
58
22
  return;
59
- const changePositon = options?.fallback !== false;
60
- const nextFlyoutData = flyout({
61
- triggerEl: triggerElRef.current,
62
- flyoutEl: flyoutElRef.current,
63
- triggerBounds: triggerBoundsRef.current,
23
+ const baseUnit = getComputedStyle(flyoutElRef.current).getPropertyValue("--rs-unit-x1");
24
+ const unitModifier = baseUnit ? parseInt(baseUnit) : 4;
25
+ const handleClose = () => {
26
+ onClose?.({});
27
+ hide();
28
+ };
29
+ return {
30
+ trigger: triggerElRef.current,
31
+ content: flyoutElRef.current,
32
+ container,
33
+ triggerCoordinates: triggerCoordinatesRef.current,
64
34
  width,
65
- position: changePositon ? defaultPosition : lastUsedPositionRef.current,
66
- fallbackPositions: changePositon ? cachedFallbackPositions : [],
35
+ position: defaultPosition,
36
+ fallbackPositions: cachedFallbackPositions,
67
37
  fallbackAdjustLayout,
68
- fallbackMinWidth,
69
38
  fallbackMinHeight,
70
- lastUsedPosition: lastUsedPositionRef.current,
71
- onPositionChoose: handlePosition,
72
- rtl: isRTL,
73
- container,
74
- contentGap,
75
- contentShift,
76
- });
77
- if (nextFlyoutData) {
78
- dispatch({
79
- type: "position",
80
- payload: { ...nextFlyoutData, sync: options?.sync },
81
- });
82
- }
39
+ contentGap: (contentGap ?? 0) * unitModifier,
40
+ contentShift: (contentShift ?? 0) * unitModifier,
41
+ onClose: handleClose,
42
+ };
83
43
  }, [
44
+ cachedFallbackPositions,
84
45
  container,
46
+ contentGap,
47
+ contentShift,
85
48
  defaultPosition,
86
- cachedFallbackPositions,
87
49
  fallbackAdjustLayout,
88
- isRTL,
89
- flyoutElRef,
50
+ fallbackMinHeight,
51
+ triggerCoordinatesRef,
90
52
  triggerElRef,
91
- triggerBoundsRef,
92
53
  width,
93
- contentGap,
94
- contentShift,
95
- handlePosition,
96
- fallbackMinWidth,
97
- fallbackMinHeight,
54
+ hide,
55
+ onClose,
56
+ flyoutElRef,
98
57
  ]);
99
- React.useEffect(() => {
100
- if (state.status === "rendered")
101
- updatePosition();
102
- }, [state.status, updatePosition]);
103
- return React.useMemo(() => ({
104
- position: state.position,
105
- status: state.status,
58
+ const show = useCallback(() => {
59
+ const flyoutOptions = getFlyoutOptions();
60
+ if (!flyoutOptions)
61
+ return;
62
+ flyoutRef.current = new Flyout(flyoutOptions);
63
+ const result = flyoutRef.current.open();
64
+ setPosition(result.position);
65
+ setStatus("visible");
66
+ }, [getFlyoutOptions]);
67
+ const remove = useCallback(() => {
68
+ if (!flyoutRef.current)
69
+ return;
70
+ flyoutRef.current.close();
71
+ setStatus("idle");
72
+ }, []);
73
+ const updatePosition = useCallback(() => {
74
+ const flyoutOptions = getFlyoutOptions();
75
+ if (!flyoutRef.current)
76
+ return;
77
+ if (!flyoutOptions)
78
+ return;
79
+ const result = flyoutRef.current.update(flyoutOptions);
80
+ setPosition(result.position);
81
+ }, [getFlyoutOptions]);
82
+ useIsomorphicLayoutEffect(() => {
83
+ updatePosition();
84
+ }, [defaultPosition]);
85
+ return useMemo(() => ({
86
+ position,
87
+ status,
106
88
  updatePosition,
107
89
  render,
108
90
  hide,
109
91
  remove,
110
92
  show,
111
- }), [render, updatePosition, hide, remove, show, state.position, state.status]);
93
+ }), [render, updatePosition, hide, remove, show, position, status]);
112
94
  };
113
95
  export default useFlyout;
@@ -0,0 +1,10 @@
1
+ import type * as G from "../../../types/global";
2
+ type SafePolygonOptions = {
3
+ contentRef: React.RefObject<HTMLElement | null>;
4
+ triggerRef: React.RefObject<HTMLElement | null>;
5
+ position: string | null | undefined;
6
+ onClose: () => void;
7
+ origin: G.Coordinates;
8
+ };
9
+ export declare function createSafeArea(options: SafePolygonOptions): () => void;
10
+ export {};