reshaped 3.9.0-canary.9 → 3.9.1-canary.2

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 (87) hide show
  1. package/dist/bundle.css +1 -1
  2. package/dist/bundle.d.ts +2 -0
  3. package/dist/bundle.js +2 -2
  4. package/dist/components/Accordion/AccordionControlled.js +0 -1
  5. package/dist/components/Actionable/Actionable.d.ts +8 -3
  6. package/dist/components/Actionable/Actionable.js +17 -70
  7. package/dist/components/Actionable/Actionable.module.css +1 -1
  8. package/dist/components/Actionable/Actionable.types.d.ts +2 -36
  9. package/dist/components/Actionable/index.d.ts +2 -1
  10. package/dist/components/Badge/Badge.js +2 -2
  11. package/dist/components/Badge/Badge.module.css +1 -1
  12. package/dist/components/Badge/Badge.types.d.ts +1 -1
  13. package/dist/components/Button/Button.module.css +1 -1
  14. package/dist/components/Calendar/Calendar.module.css +1 -1
  15. package/dist/components/Calendar/Calendar.utils.js +6 -7
  16. package/dist/components/Card/Card.d.ts +2 -2
  17. package/dist/components/Card/Card.types.d.ts +5 -5
  18. package/dist/components/Carousel/Carousel.js +0 -1
  19. package/dist/components/Flyout/Flyout.constants.d.ts +1 -0
  20. package/dist/components/Flyout/Flyout.constants.js +1 -0
  21. package/dist/components/Flyout/Flyout.module.css +1 -1
  22. package/dist/components/Flyout/Flyout.types.d.ts +10 -8
  23. package/dist/components/Flyout/FlyoutContent.js +4 -49
  24. package/dist/components/Flyout/FlyoutControlled.js +94 -76
  25. package/dist/components/Flyout/FlyoutTrigger.js +3 -3
  26. package/dist/components/Flyout/useFlyout.d.ts +3 -4
  27. package/dist/components/Flyout/useFlyout.js +70 -88
  28. package/dist/components/Flyout/utilities/safeArea.d.ts +10 -0
  29. package/dist/components/Flyout/utilities/safeArea.js +100 -0
  30. package/dist/components/Grid/Grid.types.d.ts +4 -4
  31. package/dist/components/HiddenInput/HiddenInput.js +2 -3
  32. package/dist/components/Image/Image.js +1 -1
  33. package/dist/components/Modal/Modal.js +0 -3
  34. package/dist/components/Popover/Popover.module.css +1 -1
  35. package/dist/components/Reshaped/Reshaped.css +1 -1
  36. package/dist/components/ScrollArea/ScrollArea.js +6 -6
  37. package/dist/components/Select/Select.js +1 -1
  38. package/dist/components/Select/SelectCustomControlled.js +0 -1
  39. package/dist/components/Slider/SliderControlled.js +5 -4
  40. package/dist/components/Tabs/Tabs.module.css +1 -1
  41. package/dist/components/Tabs/Tabs.types.d.ts +3 -1
  42. package/dist/components/Tabs/TabsContext.d.ts +1 -0
  43. package/dist/components/Tabs/TabsControlled.js +2 -2
  44. package/dist/components/Tabs/TabsItem.js +2 -2
  45. package/dist/components/Tabs/TabsList.js +9 -5
  46. package/dist/components/Tabs/TabsPanel.js +1 -1
  47. package/dist/components/Text/Text.d.ts +1 -1
  48. package/dist/components/Text/Text.types.d.ts +3 -3
  49. package/dist/components/Toast/ToastContainer.js +0 -1
  50. package/dist/components/Tooltip/Tooltip.js +2 -2
  51. package/dist/components/Tooltip/Tooltip.module.css +1 -1
  52. package/dist/components/Tooltip/Tooltip.types.d.ts +1 -1
  53. package/dist/components/View/View.types.d.ts +4 -4
  54. package/dist/components/_private/Expandable/Expandable.js +1 -3
  55. package/dist/components/_private/Portal/Portal.js +0 -3
  56. package/dist/core/Actionable/Actionable.d.ts +4 -0
  57. package/dist/core/Actionable/Actionable.js +73 -0
  58. package/dist/core/Actionable/Actionable.types.d.ts +34 -0
  59. package/dist/core/Actionable/Actionable.types.js +1 -0
  60. package/dist/core/Actionable/index.d.ts +2 -0
  61. package/dist/core/Actionable/index.js +1 -0
  62. package/dist/hooks/_private/useDrag.js +0 -3
  63. package/dist/hooks/_private/usePrevious.js +0 -1
  64. package/dist/hooks/useOnClickOutside.js +8 -0
  65. package/dist/index.d.ts +2 -0
  66. package/dist/index.js +1 -0
  67. package/dist/types/global.d.ts +1 -1
  68. package/dist/utilities/a11y/TrapFocus.js +9 -3
  69. package/dist/utilities/dom/index.d.ts +0 -2
  70. package/dist/utilities/dom/index.js +0 -2
  71. package/dist/utilities/scroll/disable.js +4 -2
  72. package/package.json +7 -99
  73. package/README.md +0 -24
  74. package/dist/components/Flyout/utilities/calculatePosition.d.ts +0 -31
  75. package/dist/components/Flyout/utilities/calculatePosition.js +0 -178
  76. package/dist/components/Flyout/utilities/flyout.d.ts +0 -11
  77. package/dist/components/Flyout/utilities/flyout.js +0 -87
  78. package/dist/components/Flyout/utilities/getPositionFallbacks.d.ts +0 -3
  79. package/dist/components/Flyout/utilities/getPositionFallbacks.js +0 -39
  80. package/dist/components/Flyout/utilities/helpers.d.ts +0 -7
  81. package/dist/components/Flyout/utilities/helpers.js +0 -14
  82. package/dist/components/Flyout/utilities/isFullyVisible.d.ts +0 -12
  83. package/dist/components/Flyout/utilities/isFullyVisible.js +0 -22
  84. package/dist/utilities/dom/flyout.d.ts +0 -2
  85. package/dist/utilities/dom/flyout.js +0 -14
  86. package/dist/utilities/dom/userSelect.d.ts +0 -2
  87. package/dist/utilities/dom/userSelect.js +0 -6
@@ -3,15 +3,14 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import Portal from "../_private/Portal/index.js";
5
5
  import useIsomorphicLayoutEffect from "../../hooks/useIsomorphicLayoutEffect.js";
6
- import { findClosestPositionContainer, findClosestScrollableContainer } from "../../utilities/dom/index.js";
7
- import { rafThrottle } from "../../utilities/helpers.js";
6
+ import { findClosestPositionContainer } from "../../utilities/dom/index.js";
8
7
  import { classNames } from "../../utilities/props.js";
9
8
  import { useFlyoutContext, ContentProvider } from "./Flyout.context.js";
10
9
  import s from "./Flyout.module.css";
11
10
  import cooldown from "./utilities/cooldown.js";
12
11
  const FlyoutContent = (props) => {
13
12
  const { children, className, attributes } = props;
14
- const { flyout, id, flyoutElRef, triggerElRef, handleClose, handleTransitionEnd, handleTransitionStart, triggerType, handleMouseEnter, handleMouseLeave, handleContentMouseDown, handleContentMouseUp, contentClassName, contentAttributes, contentGap, contentMaxHeight, trapFocusMode, disableContentHover, autoFocus, width, containerRef: passedContainerRef, isSubmenu, } = useFlyoutContext();
13
+ const { flyout, id, flyoutElRef, triggerElRef, handleTransitionEnd, triggerType, handleContentMouseEnter, handleMouseLeave, handleContentMouseDown, handleContentMouseUp, contentClassName, contentAttributes, contentMaxHeight, contentMaxWidth, trapFocusMode, disableContentHover, autoFocus, width, containerRef: passedContainerRef, isSubmenu, } = useFlyoutContext();
15
14
  const { status, position } = flyout;
16
15
  const [mounted, setMounted] = React.useState(false);
17
16
  const closestFixedContainer = React.useMemo(() => {
@@ -19,56 +18,12 @@ const FlyoutContent = (props) => {
19
18
  return null;
20
19
  if (!triggerElRef)
21
20
  return null;
22
- // eslint-disable-next-line react-hooks/refs
23
21
  return findClosestPositionContainer({ el: triggerElRef.current });
24
22
  }, [mounted, triggerElRef]);
25
- const closestScrollableContainer = React.useMemo(() => {
26
- if (!mounted)
27
- return;
28
- // eslint-disable-next-line react-hooks/refs
29
- if (!triggerElRef?.current)
30
- return;
31
- // eslint-disable-next-line react-hooks/refs
32
- return findClosestScrollableContainer({ el: triggerElRef.current });
33
- }, [mounted, triggerElRef]);
34
23
  const containerRef = passedContainerRef || { current: closestFixedContainer };
35
24
  useIsomorphicLayoutEffect(() => {
36
25
  setMounted(true);
37
26
  }, []);
38
- /**
39
- * transitionStart doesn't exist as a jsx event handler and needs to be handled with vanilla js
40
- */
41
- React.useEffect(() => {
42
- const el = flyoutElRef.current;
43
- if (!el)
44
- return;
45
- el.addEventListener("transitionstart", handleTransitionStart);
46
- return () => el.removeEventListener("transitionstart", handleTransitionStart);
47
- }, [handleTransitionStart, flyoutElRef, status]);
48
- React.useEffect(() => {
49
- if (status !== "visible")
50
- return;
51
- if (!closestScrollableContainer)
52
- return;
53
- const triggerEl = triggerElRef?.current;
54
- const containerEl = closestScrollableContainer;
55
- const handleScroll = rafThrottle(() => {
56
- const triggerBounds = triggerEl?.getBoundingClientRect();
57
- const containerBounds = containerEl.getBoundingClientRect();
58
- if (triggerBounds &&
59
- (triggerBounds.top < containerBounds.top ||
60
- triggerBounds.left < containerBounds.left ||
61
- triggerBounds.right > containerBounds.right ||
62
- triggerBounds.bottom > containerBounds.bottom)) {
63
- handleClose({});
64
- }
65
- else {
66
- flyout.updatePosition({ sync: true, fallback: false });
67
- }
68
- });
69
- closestScrollableContainer.addEventListener("scroll", handleScroll, { passive: true });
70
- return () => closestScrollableContainer.removeEventListener("scroll", handleScroll);
71
- }, [closestScrollableContainer, flyout, status, handleClose, triggerElRef]);
72
27
  if (status === "idle" || !mounted)
73
28
  return null;
74
29
  const rootClassNames = classNames(s.content, triggerType === "hover" && s["--hover"], status === "visible" && s["--visible"],
@@ -95,9 +50,9 @@ const FlyoutContent = (props) => {
95
50
  role = "menubar";
96
51
  }
97
52
  const content = (_jsx(ContentProvider, { value: { elRef: flyoutElRef }, children: _jsx("div", { className: rootClassNames, style: {
98
- "--rs-flyout-gap": contentGap,
99
53
  "--rs-flyout-max-h": contentMaxHeight,
100
- }, 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, tabIndex: !autoFocus ? -1 : undefined, "aria-modal": role === "dialog" ? true : undefined, style: { ...attributes?.style, ...contentAttributes?.style }, className: innerClassNames, children: children }) }) }));
54
+ "--rs-flyout-max-w": contentMaxWidth,
55
+ }, ref: flyoutElRef, onTransitionEnd: handleTransitionEnd, onMouseEnter: triggerType === "hover" ? handleContentMouseEnter : undefined, onMouseLeave: triggerType === "hover" ? handleMouseLeave : undefined, onMouseDown: handleContentMouseDown, onTouchStart: handleContentMouseDown, onMouseUp: handleContentMouseUp, onTouchEnd: handleContentMouseUp, children: _jsx("div", { role: role, ...attributes, id: id, tabIndex: !autoFocus ? -1 : undefined, "aria-modal": role === "dialog" ? true : undefined, style: { ...attributes?.style, ...contentAttributes?.style }, className: innerClassNames, children: children }) }) }));
101
56
  return _jsx(Portal, { targetRef: containerRef, children: content });
102
57
  };
103
58
  FlyoutContent.displayName = "Flyout.Content";
@@ -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, 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,23 +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);
58
- // eslint-disable-next-line react-hooks/refs
53
+ // Cleanup function for safe area tracking
54
+ const safeAreaRef = React.useRef(null);
55
+ const originCoordinatesRef = React.useRef(originCoordinates ?? null);
56
+ originCoordinatesRef.current = originCoordinates ?? null;
59
57
  const flyout = useFlyout({
60
58
  triggerElRef: positionRef ?? triggerElRef,
61
59
  flyoutElRef,
62
- // eslint-disable-next-line react-hooks/refs
63
- triggerBounds: originCoordinates ?? triggerBoundsRef.current,
60
+ triggerCoordinatesRef: originCoordinatesRef,
64
61
  width,
65
62
  position: passedPosition,
66
- defaultActive: resolvedActive,
67
- // eslint-disable-next-line react-hooks/refs
63
+ defaultActive: active,
68
64
  container: containerRef?.current,
69
65
  fallbackPositions,
70
66
  fallbackAdjustLayout,
71
- fallbackMinWidth,
72
67
  fallbackMinHeight,
73
68
  contentGap,
74
69
  contentShift,
70
+ onClose: onCloseRef.current,
75
71
  });
76
72
  const { status, updatePosition, render, hide, remove, show } = flyout;
77
73
  const isRendered = status !== "idle";
@@ -85,6 +81,25 @@ const FlyoutControlled = (props) => {
85
81
  if (timerRef.current)
86
82
  clearTimeout(timerRef.current);
87
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]);
88
103
  /**
89
104
  * Component open/close handlers
90
105
  * Called from the internal actions
@@ -95,17 +110,27 @@ const FlyoutControlled = (props) => {
95
110
  if (isRendered && triggerType !== "hover")
96
111
  return;
97
112
  onOpenRef.current?.();
98
- }, [onOpenRef, isRendered, triggerType]);
113
+ disableTriggers();
114
+ }, [onOpenRef, isRendered, triggerType, disableTriggers]);
99
115
  const handleClose = React.useCallback((options) => {
100
116
  const isLocked = triggerType === "click" && !isDismissible();
101
117
  const canClose = !isLocked && (isRendered || disabled);
102
118
  if (!canClose)
103
119
  return;
104
120
  onCloseRef.current?.({ reason: options.reason });
121
+ enableTriggers();
105
122
  if (options?.closeParents) {
106
- parentFlyoutContext?.handleClose?.({});
123
+ parentFlyoutContext?.handleClose?.({ closeParents: true, reason: options.reason });
107
124
  }
108
- }, [isRendered, isDismissible, triggerType, onCloseRef, disabled, parentFlyoutContext]);
125
+ }, [
126
+ isRendered,
127
+ isDismissible,
128
+ triggerType,
129
+ onCloseRef,
130
+ disabled,
131
+ parentFlyoutContext,
132
+ enableTriggers,
133
+ ]);
109
134
  /**
110
135
  * Trigger event handlers
111
136
  */
@@ -132,7 +157,7 @@ const FlyoutControlled = (props) => {
132
157
  return;
133
158
  hoverTriggeredWithTouchEventRef.current = true;
134
159
  }, [triggerType]);
135
- const handleMouseEnter = React.useCallback(() => {
160
+ const handleContentMouseEnter = React.useCallback(() => {
136
161
  clearTimer();
137
162
  if (hoverTriggeredWithTouchEventRef.current) {
138
163
  handleOpen();
@@ -146,15 +171,40 @@ const FlyoutControlled = (props) => {
146
171
  }, groupTimeouts && cooldown.status === "warming" ? timeouts.mouseEnter : 0);
147
172
  }
148
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]);
149
180
  const handleMouseLeave = React.useCallback((e) => {
150
- if (e.relatedTarget === flyoutElRef.current)
181
+ if (e.relatedTarget === flyoutElRef.current ||
182
+ (e.relatedTarget instanceof Node && flyoutElRef.current?.contains(e.relatedTarget)))
151
183
  return;
152
- if (e.relatedTarget === triggerElRef.current)
184
+ if (e.relatedTarget === triggerElRef.current ||
185
+ (e.relatedTarget instanceof Node && triggerElRef.current?.contains(e.relatedTarget)))
153
186
  return;
154
187
  cooldown.cool();
155
188
  clearTimer();
156
- handleClose({});
157
- }, [clearTimer, handleClose, triggerElRef, flyoutElRef]);
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 };
203
+ }
204
+ else {
205
+ handleClose({});
206
+ }
207
+ }, [clearTimer, handleClose, triggerElRef, flyoutElRef, triggerType, isRendered, flyout.position]);
158
208
  const handleTriggerClick = React.useCallback(() => {
159
209
  if (!isRendered) {
160
210
  handleOpen();
@@ -163,13 +213,6 @@ const FlyoutControlled = (props) => {
163
213
  handleClose({});
164
214
  }
165
215
  }, [isRendered, handleOpen, handleClose]);
166
- const handleTriggerMouseDown = React.useCallback(() => {
167
- const triggerEl = positionRef?.current ?? triggerElRef.current;
168
- const rect = triggerEl?.getBoundingClientRect();
169
- if (!rect)
170
- return;
171
- triggerBoundsRef.current = rect;
172
- }, [triggerElRef, positionRef]);
173
216
  const handleContentMouseDown = () => {
174
217
  lockedBlurEffects.current = true;
175
218
  hoverTriggeredWithTouchEventRef.current = true;
@@ -177,44 +220,27 @@ const FlyoutControlled = (props) => {
177
220
  const handleContentMouseUp = () => {
178
221
  lockedBlurEffects.current = false;
179
222
  };
180
- const handleTransitionStart = React.useCallback((e) => {
181
- if (!resolvedActive)
182
- return;
183
- if (flyoutElRef.current !== e.currentTarget || e.propertyName !== "transform")
184
- return;
185
- transitionStartedRef.current = true;
186
- /**
187
- * After animation has started, we're sure about the correct bounds
188
- * so drop the cache to make flyout work when trigger moves around
189
- */
190
- triggerBoundsRef.current = null;
191
- }, [resolvedActive]);
192
223
  const handleTransitionEnd = React.useCallback((e) => {
193
224
  if (flyoutElRef.current !== e.currentTarget || e.propertyName !== "transform")
194
225
  return;
195
- if (status === "hidden") {
196
- transitionStartedRef.current = false;
226
+ if (status === "hidden")
197
227
  remove();
198
- }
199
228
  }, [remove, status]);
200
229
  /**
201
230
  * Control the display based on the props
202
231
  */
203
232
  useIsomorphicLayoutEffect(() => {
204
- if (resolvedActive) {
233
+ if (active) {
205
234
  render();
206
235
  return;
207
236
  }
208
237
  if (disabled)
209
238
  cooldown.cool();
210
- /**
211
- * Check that transitions are enabled and it has been triggered on tooltip open
212
- * - keyboard focus navigation could move too fast and ignore the transitions completely
213
- * - warmed up tooltips get removed instantly
214
- */
239
+ // Prevent calling hide on component mount
240
+ if (prevActive === active)
241
+ return;
215
242
  if (checkTransitions() &&
216
243
  !disableHideAnimation &&
217
- transitionStartedRef.current &&
218
244
  (cooldown.status === "cooling" || !groupTimeouts)) {
219
245
  hide();
220
246
  }
@@ -222,11 +248,10 @@ const FlyoutControlled = (props) => {
222
248
  // In case transitions are disabled globally - remove from the DOM immediately
223
249
  remove();
224
250
  }
225
- }, [resolvedActive, render, hide, remove, disableHideAnimation, disabled, groupTimeouts]);
226
- React.useEffect(() => {
227
- // Wait after positioning before show is triggered to animate flyout from the right side
228
- if (status === "positioned")
229
- onNextFrame(() => show());
251
+ }, [active, prevActive, render, hide, remove, disableHideAnimation, disabled, groupTimeouts]);
252
+ useIsomorphicLayoutEffect(() => {
253
+ if (status === "rendered")
254
+ show();
230
255
  }, [status, show]);
231
256
  /**
232
257
  * Handle focus trap
@@ -264,7 +289,7 @@ const FlyoutControlled = (props) => {
264
289
  return;
265
290
  if (trapFocusRef.current?.trapped) {
266
291
  /* Locking the popover to not open it again on trigger focus */
267
- if (triggerType === "hover") {
292
+ if (triggerType === "hover" && checkKeyboardMode()) {
268
293
  lockedRef.current = true;
269
294
  setTimeout(() => {
270
295
  lockedRef.current = false;
@@ -281,27 +306,20 @@ const FlyoutControlled = (props) => {
281
306
  return () => trapFocusRef.current?.release();
282
307
  }, []);
283
308
  /**
284
- * Update position on resize or RTL
309
+ * Clean up safe polygon tracking on unmount or when flyout closes
285
310
  */
286
311
  React.useEffect(() => {
287
312
  if (!isRendered)
288
- return;
289
- const resizeObserver = new ResizeObserver(() => updatePosition({ sync: true }));
290
- resizeObserver.observe(document.body);
291
- if (triggerElRef.current)
292
- resizeObserver.observe(triggerElRef.current);
293
- return () => resizeObserver.disconnect();
294
- }, [updatePosition, triggerElRef, isRendered]);
295
- React.useEffect(() => {
296
- updatePosition({ sync: true });
297
- }, [isRTL, updatePosition]);
313
+ safeAreaRef.current?.cleanup();
314
+ return () => safeAreaRef.current?.cleanup();
315
+ }, [isRendered]);
298
316
  /**
299
317
  * Imperative methods for controlling Flyout
300
318
  */
301
319
  React.useImperativeHandle(instanceRef, () => ({
302
320
  open: handleOpen,
303
321
  close: () => handleClose({}),
304
- updatePosition: () => updatePosition({ sync: true }),
322
+ updatePosition: () => updatePosition(),
305
323
  }), [handleOpen, handleClose, updatePosition]);
306
324
  useHotkeys({ Escape: () => handleClose({ reason: "escape-key" }) }, [handleClose]);
307
325
  useOnClickOutside([flyoutElRef, triggerElRef], () => {
@@ -321,12 +339,11 @@ const FlyoutControlled = (props) => {
321
339
  handleOpen,
322
340
  handleFocus,
323
341
  handleBlur,
324
- handleMouseEnter,
342
+ handleTriggerMouseEnter,
343
+ handleContentMouseEnter,
325
344
  handleMouseLeave,
326
345
  handleTouchStart,
327
- handleTransitionStart,
328
346
  handleTransitionEnd,
329
- handleMouseDown: handleTriggerMouseDown,
330
347
  handleClick: handleTriggerClick,
331
348
  handleContentMouseDown,
332
349
  handleContentMouseUp,
@@ -336,6 +353,7 @@ const FlyoutControlled = (props) => {
336
353
  contentAttributes,
337
354
  contentGap,
338
355
  contentMaxHeight,
356
+ contentMaxWidth,
339
357
  containerRef,
340
358
  disableContentHover,
341
359
  autoFocus,
@@ -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
- triggerBounds?: 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, triggerBounds, 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,
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
- triggerBounds,
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;