@octanejs/base-ui 0.1.11 → 0.1.15

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.
@@ -0,0 +1,248 @@
1
+ // Ported from .base-ui/packages/react/src/floating-ui-react/hooks/useFocus.ts (v1.6.0). Opens the
2
+ // popup while its trigger has focus, like CSS `:focus` — with the blocked-focus bookkeeping that
3
+ // stops a press/Escape dismissal from immediately re-opening when focus returns to the trigger.
4
+ //
5
+ // octane adaptations: handlers receive the NATIVE event, so `event.nativeEvent` collapses to
6
+ // `event`; every composed hook threads an explicit slot.
7
+ //
8
+ // SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
9
+ import { useEffect, useMemo, useRef } from 'octane';
10
+
11
+ import { S, splitSlot, subSlot } from '../../internal';
12
+ import { addEventListener } from '../addEventListener';
13
+ import { mergeCleanups } from '../mergeCleanups';
14
+ import { ownerDocument, ownerWindow } from '../owner';
15
+ import { platform } from '../platform';
16
+ import { useTimeout } from '../useTimeout';
17
+ import { isElement, isHTMLElement } from '../dom';
18
+ import { matchesFocusVisible } from '../matchesFocusVisible';
19
+ import { createChangeEventDetails, REASONS } from '../createChangeEventDetails';
20
+ import { createAttribute } from './createAttribute';
21
+ import {
22
+ activeElement,
23
+ contains,
24
+ getTarget,
25
+ isTargetInsideEnabledTrigger,
26
+ isTypeableElement,
27
+ } from './element';
28
+
29
+ const isMacSafari = platform.os.mac && platform.engine.webkit;
30
+
31
+ export interface UseFocusProps {
32
+ /**
33
+ * Whether the hook is enabled, including all internal effects and event handlers.
34
+ * @default true
35
+ */
36
+ enabled?: boolean | undefined;
37
+ /**
38
+ * Waits for the specified time before opening.
39
+ * @default undefined
40
+ */
41
+ delay?: number | (() => number | undefined) | undefined;
42
+ }
43
+
44
+ export function useFocus(...args: any[]): Record<string, any> {
45
+ const [user, slotArg] = splitSlot(args);
46
+ const slot = slotArg ?? S('useFocus');
47
+ const context = user[0] as any;
48
+ const props = (user[1] as UseFocusProps) ?? {};
49
+
50
+ const { enabled = true, delay } = props;
51
+
52
+ const store = context != null && 'rootStore' in context ? context.rootStore : context;
53
+
54
+ const { events, dataRef } = store.context;
55
+
56
+ const blockFocusRef = useRef(false, subSlot(slot, 'block'));
57
+ // Track which reference should be blocked from re-opening after Escape/press dismissal.
58
+ const blockedReferenceRef = useRef<Element | null>(null, subSlot(slot, 'blockedRef'));
59
+ const keyboardModalityRef = useRef(true, subSlot(slot, 'kbd'));
60
+
61
+ const timeout = useTimeout(subSlot(slot, 'to'));
62
+
63
+ useEffect(
64
+ () => {
65
+ const domReference = store.select('domReferenceElement');
66
+
67
+ if (!enabled) {
68
+ return undefined;
69
+ }
70
+
71
+ const win = ownerWindow(domReference);
72
+
73
+ // If the reference was focused and the user left the tab/window while the popup was
74
+ // closed, block the focus-open when they return.
75
+ function onBlur() {
76
+ const currentDomReference = store.select('domReferenceElement');
77
+ if (
78
+ !store.select('open') &&
79
+ isHTMLElement(currentDomReference) &&
80
+ currentDomReference === activeElement(ownerDocument(currentDomReference))
81
+ ) {
82
+ blockFocusRef.current = true;
83
+ }
84
+ }
85
+
86
+ function onKeyDown() {
87
+ keyboardModalityRef.current = true;
88
+ }
89
+
90
+ function onPointerDown() {
91
+ keyboardModalityRef.current = false;
92
+ }
93
+
94
+ return mergeCleanups(
95
+ addEventListener(win, 'blur', onBlur),
96
+ isMacSafari && addEventListener(win, 'keydown', onKeyDown, true),
97
+ isMacSafari && addEventListener(win, 'pointerdown', onPointerDown, true),
98
+ );
99
+ },
100
+ [store, enabled],
101
+ subSlot(slot, 'e:win'),
102
+ );
103
+
104
+ useEffect(
105
+ () => {
106
+ if (!enabled) {
107
+ return undefined;
108
+ }
109
+
110
+ function onOpenChangeLocal(details: any) {
111
+ if (details.reason === REASONS.triggerPress || details.reason === REASONS.escapeKey) {
112
+ const referenceElement = store.select('domReferenceElement');
113
+ if (isElement(referenceElement)) {
114
+ blockedReferenceRef.current = referenceElement;
115
+ blockFocusRef.current = true;
116
+ }
117
+ }
118
+ }
119
+
120
+ events.on('openchange', onOpenChangeLocal);
121
+ return () => {
122
+ events.off('openchange', onOpenChangeLocal);
123
+ };
124
+ },
125
+ [events, enabled, store],
126
+ subSlot(slot, 'e:openchange'),
127
+ );
128
+
129
+ const reference = useMemo(
130
+ () => {
131
+ function resetBlockedFocus() {
132
+ blockFocusRef.current = false;
133
+ blockedReferenceRef.current = null;
134
+ }
135
+
136
+ return {
137
+ onMouseLeave() {
138
+ resetBlockedFocus();
139
+ },
140
+ onFocus(event: FocusEvent) {
141
+ const focusTarget = event.currentTarget as Element;
142
+
143
+ if (blockFocusRef.current) {
144
+ if (blockedReferenceRef.current === focusTarget) {
145
+ return;
146
+ }
147
+ resetBlockedFocus();
148
+ }
149
+
150
+ const target = getTarget(event);
151
+
152
+ if (isElement(target)) {
153
+ // Safari fails to match `:focus-visible` if focus was initially outside the
154
+ // document.
155
+ if (isMacSafari && !event.relatedTarget) {
156
+ if (!keyboardModalityRef.current && !isTypeableElement(target)) {
157
+ return;
158
+ }
159
+ } else if (!matchesFocusVisible(target as Element)) {
160
+ return;
161
+ }
162
+ }
163
+
164
+ const movedFromOtherEnabledTrigger = isTargetInsideEnabledTrigger(
165
+ event.relatedTarget,
166
+ store.context.triggerElements,
167
+ );
168
+
169
+ const currentTarget = event.currentTarget;
170
+ const delayValue = typeof delay === 'function' ? delay() : delay;
171
+
172
+ if (
173
+ (store.select('open') && movedFromOtherEnabledTrigger) ||
174
+ delayValue === 0 ||
175
+ delayValue === undefined
176
+ ) {
177
+ store.setOpen(
178
+ true,
179
+ createChangeEventDetails(REASONS.triggerFocus, event, currentTarget as HTMLElement),
180
+ );
181
+ return;
182
+ }
183
+
184
+ timeout.start(delayValue, () => {
185
+ if (blockFocusRef.current) {
186
+ return;
187
+ }
188
+ store.setOpen(
189
+ true,
190
+ createChangeEventDetails(REASONS.triggerFocus, event, currentTarget as HTMLElement),
191
+ );
192
+ });
193
+ },
194
+ onBlur(event: FocusEvent) {
195
+ resetBlockedFocus();
196
+
197
+ const relatedTarget = event.relatedTarget;
198
+
199
+ // Hit the non-modal focus management portal guard. Focus will be moved into the
200
+ // floating element immediately after.
201
+ const movedToFocusGuard =
202
+ isElement(relatedTarget) &&
203
+ (relatedTarget as Element).hasAttribute(createAttribute('focus-guard')) &&
204
+ (relatedTarget as Element).getAttribute('data-type') === 'outside';
205
+
206
+ // Wait for the window blur listener to fire.
207
+ timeout.start(0, () => {
208
+ const domReference = store.select('domReferenceElement');
209
+ const activeEl = activeElement(ownerDocument(domReference));
210
+
211
+ // Focus left the page — keep it open.
212
+ if (!relatedTarget && activeEl === domReference) {
213
+ return;
214
+ }
215
+
216
+ // When focusing the reference element (e.g. a regular click), then clicking into
217
+ // the floating element, prevent it from hiding. `relatedTarget` only points to
218
+ // the shadow host, so the active element is the reliable check.
219
+ if (
220
+ contains(dataRef.current.floatingContext?.refs.floating.current, activeEl) ||
221
+ contains(domReference, activeEl) ||
222
+ movedToFocusGuard
223
+ ) {
224
+ return;
225
+ }
226
+
227
+ // If the next focused element is one of the triggers, do not close: that
228
+ // trigger's own focus handler owns the open state.
229
+ const nextFocusedElement = relatedTarget ?? activeEl;
230
+ if (isTargetInsideEnabledTrigger(nextFocusedElement, store.context.triggerElements)) {
231
+ return;
232
+ }
233
+
234
+ store.setOpen(false, createChangeEventDetails(REASONS.triggerFocus, event));
235
+ });
236
+ },
237
+ };
238
+ },
239
+ [dataRef, delay, store, timeout],
240
+ subSlot(slot, 'ref'),
241
+ );
242
+
243
+ return useMemo(
244
+ () => (enabled ? { reference, trigger: reference } : {}),
245
+ [enabled, reference],
246
+ subSlot(slot, 'out'),
247
+ );
248
+ }
@@ -1,12 +1,302 @@
1
- // STUB — the popup side of the open-on-hover interaction (Base UI's useHoverFloatingInteraction:
2
- // keep-open-while-hovering-popup + closeDelay + safePolygon, part of the ~1400-line hover system).
3
- // It is only active when a trigger sets `openOnHover` (off by default), so returning nothing here
4
- // leaves click-to-open Popover/Tooltip fully functional. TODO: port the real popup-hover keep-open
5
- // behavior alongside `useHoverReferenceInteraction` when hover-open tests land.
6
- export function useHoverFloatingInteraction(
7
- _context: any,
8
- _props: any,
9
- _slot?: symbol | undefined,
10
- ): void {
11
- // no-op while hover-open is out of scope
1
+ // Ported from .base-ui/packages/react/src/floating-ui-react/hooks/useHoverFloatingInteraction.ts
2
+ // (v1.6.0). The popup side of open-on-hover: keeps the popup open while the cursor is inside it,
3
+ // closes it (after `closeDelay`) on leave, defers to the safe-polygon handler when one is active,
4
+ // and applies the pointer-events mutation that shields siblings while the cursor travels.
5
+ //
6
+ // octane adaptations: native events; `useIsoLayoutEffect` → `useLayoutEffect`; every composed hook
7
+ // threads an explicit slot.
8
+ //
9
+ // SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
10
+ import { useEffect, useLayoutEffect } from 'octane';
11
+
12
+ import { S, splitSlot, subSlot } from '../../internal';
13
+ import { addEventListener } from '../addEventListener';
14
+ import { mergeCleanups } from '../mergeCleanups';
15
+ import { ownerDocument } from '../owner';
16
+ import { useStableCallback } from '../useStableCallback';
17
+ import { useTimeout } from '../useTimeout';
18
+ import { isElement } from '../dom';
19
+ import { createChangeEventDetails, REASONS } from '../createChangeEventDetails';
20
+ import { useFloatingParentNodeId, useFloatingTree } from './FloatingTree';
21
+ import { contains, getTarget } from './element';
22
+ import { getNodeChildren } from './nodes';
23
+ import {
24
+ applySafePolygonPointerEventsMutation,
25
+ clearSafePolygonPointerEventsMutation,
26
+ isInteractiveElement,
27
+ useHoverInteractionSharedState,
28
+ } from './useHoverInteractionSharedState';
29
+ import {
30
+ getDelay,
31
+ isClickLikeOpenEvent as isClickLikeOpenEventShared,
32
+ isHoverOpenEvent,
33
+ isInsideEnabledTrigger,
34
+ } from './useHoverShared';
35
+
36
+ export interface UseHoverFloatingInteractionProps {
37
+ /**
38
+ * Whether the hook is enabled, including all internal effects and event handlers.
39
+ * @default true
40
+ */
41
+ enabled?: boolean | undefined;
42
+ /**
43
+ * Waits for the specified time when the event listener runs before changing the `open` state.
44
+ * @default 0
45
+ */
46
+ closeDelay?: number | (() => number) | undefined;
47
+ /**
48
+ * Tree node id override for floating elements that participate in the tree without a
49
+ * `FloatingContext`, such as inline nested navigation menus.
50
+ */
51
+ nodeId?: string | undefined;
52
+ }
53
+
54
+ export function useHoverFloatingInteraction(...args: any[]): void {
55
+ const [user, slotArg] = splitSlot(args);
56
+ const slot = slotArg ?? S('useHoverFloatingInteraction');
57
+ const context = user[0] as any;
58
+ const parameters = (user[1] as UseHoverFloatingInteractionProps) ?? {};
59
+
60
+ const { enabled = true, closeDelay: closeDelayProp = 0, nodeId: nodeIdProp } = parameters;
61
+
62
+ const store = context != null && 'rootStore' in context ? context.rootStore : context;
63
+
64
+ const open = store.useState('open', subSlot(slot, 's:open'));
65
+ const floatingElement = store.useState('floatingElement', subSlot(slot, 's:floating'));
66
+ const domReferenceElement = store.useState('domReferenceElement', subSlot(slot, 's:ref'));
67
+ const dataRef = store.context.dataRef;
68
+
69
+ const tree = useFloatingTree();
70
+ const parentId = useFloatingParentNodeId();
71
+ const instance = useHoverInteractionSharedState(store, subSlot(slot, 'shared'));
72
+
73
+ const childClosedTimeout = useTimeout(subSlot(slot, 'cct'));
74
+
75
+ const isClickLikeOpenEvent = useStableCallback(
76
+ () => {
77
+ return isClickLikeOpenEventShared(dataRef.current.openEvent?.type, instance.interactedInside);
78
+ },
79
+ subSlot(slot, 'iclo'),
80
+ );
81
+
82
+ const isHoverOpen = useStableCallback(
83
+ () => {
84
+ return isHoverOpenEvent(dataRef.current.openEvent?.type);
85
+ },
86
+ subSlot(slot, 'iho'),
87
+ );
88
+
89
+ const clearPointerEvents = useStableCallback(
90
+ () => {
91
+ clearSafePolygonPointerEventsMutation(instance);
92
+ },
93
+ subSlot(slot, 'cpe'),
94
+ );
95
+
96
+ useLayoutEffect(
97
+ () => {
98
+ if (!open) {
99
+ instance.pointerType = undefined;
100
+ instance.restTimeoutPending = false;
101
+ instance.interactedInside = false;
102
+ clearPointerEvents();
103
+ }
104
+ },
105
+ [open, instance, clearPointerEvents],
106
+ subSlot(slot, 'l:reset'),
107
+ );
108
+
109
+ useEffect(
110
+ () => {
111
+ return clearPointerEvents;
112
+ },
113
+ [clearPointerEvents],
114
+ subSlot(slot, 'e:cpe'),
115
+ );
116
+
117
+ useLayoutEffect(
118
+ () => {
119
+ if (!enabled) {
120
+ return undefined;
121
+ }
122
+
123
+ if (
124
+ open &&
125
+ instance.handleCloseOptions?.blockPointerEvents &&
126
+ isHoverOpen() &&
127
+ isElement(domReferenceElement) &&
128
+ floatingElement
129
+ ) {
130
+ const ref = domReferenceElement as HTMLElement | SVGSVGElement;
131
+ const floatingEl = floatingElement as HTMLElement;
132
+ const doc = ownerDocument(floatingElement as Element);
133
+
134
+ const parentFloating = tree?.nodesRef.current.find((node: any) => node.id === parentId)
135
+ ?.context?.elements.floating as HTMLElement | null;
136
+
137
+ if (parentFloating) {
138
+ parentFloating.style.pointerEvents = '';
139
+ }
140
+
141
+ // A keep-mounted submenu can appear in the tree before it opens, so a cached scope or
142
+ // parent lookup may resolve to the submenu itself. That would not shield sibling items
143
+ // in the parent menu.
144
+ const cachedScopeElement =
145
+ instance.pointerEventsScopeElement !== floatingEl
146
+ ? instance.pointerEventsScopeElement
147
+ : null;
148
+ const parentScopeElement = parentFloating !== floatingEl ? parentFloating : null;
149
+ const scopeElement =
150
+ instance.handleCloseOptions?.getScope?.() ??
151
+ cachedScopeElement ??
152
+ parentScopeElement ??
153
+ (ref.closest('[data-rootownerid]') as HTMLElement | SVGSVGElement | null) ??
154
+ doc.body;
155
+
156
+ applySafePolygonPointerEventsMutation(instance, {
157
+ scopeElement,
158
+ referenceElement: ref,
159
+ floatingElement: floatingEl,
160
+ });
161
+
162
+ return () => {
163
+ clearPointerEvents();
164
+ };
165
+ }
166
+
167
+ return undefined;
168
+ },
169
+ [
170
+ enabled,
171
+ open,
172
+ domReferenceElement,
173
+ floatingElement,
174
+ instance,
175
+ isHoverOpen,
176
+ tree,
177
+ parentId,
178
+ clearPointerEvents,
179
+ ],
180
+ subSlot(slot, 'l:blockPointer'),
181
+ );
182
+
183
+ useEffect(
184
+ () => {
185
+ if (!enabled) {
186
+ return undefined;
187
+ }
188
+
189
+ function hasParentChildren() {
190
+ return !!(tree && parentId && getNodeChildren(tree.nodesRef.current, parentId).length > 0);
191
+ }
192
+
193
+ function closeWithDelay(event: MouseEvent) {
194
+ const closeDelay = getDelay(closeDelayProp, 'close', instance.pointerType);
195
+ const close = () => {
196
+ store.setOpen(false, createChangeEventDetails(REASONS.triggerHover, event));
197
+ tree?.events.emit('floating.closed', event);
198
+ };
199
+
200
+ if (closeDelay) {
201
+ instance.openChangeTimeout.start(closeDelay, close);
202
+ } else {
203
+ instance.openChangeTimeout.clear();
204
+ close();
205
+ }
206
+ }
207
+
208
+ function handleInteractInside(event: PointerEvent) {
209
+ const target = getTarget(event) as Element | null;
210
+ if (!isInteractiveElement(target)) {
211
+ instance.interactedInside = false;
212
+ return;
213
+ }
214
+ instance.interactedInside = target?.closest('[aria-haspopup]') != null;
215
+ }
216
+
217
+ function onFloatingMouseEnter() {
218
+ instance.openChangeTimeout.clear();
219
+ childClosedTimeout.clear();
220
+ tree?.events.off('floating.closed', onNodeClosed);
221
+ clearPointerEvents();
222
+ }
223
+
224
+ function onFloatingMouseLeave(event: MouseEvent) {
225
+ if (hasParentChildren() && tree) {
226
+ tree.events.on('floating.closed', onNodeClosed);
227
+ return;
228
+ }
229
+
230
+ // Leaving toward another trigger of the same popup just moves it; don't close.
231
+ if (isInsideEnabledTrigger(event.relatedTarget, store.context.triggerElements)) {
232
+ return;
233
+ }
234
+
235
+ const currentNodeId = dataRef.current.floatingContext?.nodeId ?? nodeIdProp;
236
+ const relatedTarget = event.relatedTarget;
237
+ const isMovingIntoDescendantFloating =
238
+ tree &&
239
+ currentNodeId &&
240
+ isElement(relatedTarget) &&
241
+ getNodeChildren(tree.nodesRef.current, currentNodeId, false).some((node: any) =>
242
+ contains(node.context?.elements.floating, relatedTarget as Element),
243
+ );
244
+
245
+ if (isMovingIntoDescendantFloating) {
246
+ return;
247
+ }
248
+
249
+ // If the safePolygon handler is active, let it handle the close logic.
250
+ if (instance.handler) {
251
+ instance.handler(event);
252
+ return;
253
+ }
254
+
255
+ clearPointerEvents();
256
+ if (isHoverOpen() && !isClickLikeOpenEvent()) {
257
+ closeWithDelay(event);
258
+ }
259
+ }
260
+
261
+ function onNodeClosed(event: MouseEvent) {
262
+ if (!tree || !parentId || hasParentChildren()) {
263
+ return;
264
+ }
265
+ // Allow the mouseenter event to fire in case the child closed because the mouse moved
266
+ // into the parent.
267
+ childClosedTimeout.start(0, () => {
268
+ tree.events.off('floating.closed', onNodeClosed);
269
+ store.setOpen(false, createChangeEventDetails(REASONS.triggerHover, event));
270
+ tree.events.emit('floating.closed', event);
271
+ });
272
+ }
273
+
274
+ const floating = floatingElement as HTMLElement | null;
275
+ return mergeCleanups(
276
+ floating && addEventListener(floating, 'mouseenter', onFloatingMouseEnter as EventListener),
277
+ floating && addEventListener(floating, 'mouseleave', onFloatingMouseLeave as EventListener),
278
+ floating &&
279
+ addEventListener(floating, 'pointerdown', handleInteractInside as EventListener, true),
280
+ () => {
281
+ tree?.events.off('floating.closed', onNodeClosed);
282
+ },
283
+ );
284
+ },
285
+ [
286
+ enabled,
287
+ floatingElement,
288
+ store,
289
+ dataRef,
290
+ closeDelayProp,
291
+ nodeIdProp,
292
+ isHoverOpen,
293
+ isClickLikeOpenEvent,
294
+ clearPointerEvents,
295
+ instance,
296
+ tree,
297
+ parentId,
298
+ childClosedTimeout,
299
+ ],
300
+ subSlot(slot, 'e:listeners'),
301
+ );
12
302
  }
@@ -0,0 +1,139 @@
1
+ // Ported from .base-ui/packages/react/src/floating-ui-react/hooks/useHoverInteractionSharedState.ts
2
+ // (v1.6.0). The reference-side and floating-side hover hooks act on ONE popup, so they share a
3
+ // single mutable interaction record parked on the floating root store's `dataRef` — pointer type,
4
+ // the live safe-polygon mousemove handler, the open/rest timers, and the pointer-events mutation
5
+ // that keeps the cursor's path to the popup clickable.
6
+ //
7
+ // SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
8
+ import { useEffect } from 'octane';
9
+
10
+ import { S, splitSlot, subSlot } from '../../internal';
11
+ import { Timeout } from '../useTimeout';
12
+ import { useRefWithInit } from '../useRefWithInit';
13
+ import type { HandleCloseOptions } from './useHoverShared';
14
+
15
+ export { isInteractiveElement } from './element';
16
+
17
+ export class HoverInteraction {
18
+ pointerType: string | undefined = undefined;
19
+
20
+ interactedInside = false;
21
+
22
+ handler: ((event: MouseEvent) => void) | undefined = undefined;
23
+
24
+ blockMouseMove = true;
25
+
26
+ performedPointerEventsMutation = false;
27
+
28
+ pointerEventsScopeElement: HTMLElement | SVGSVGElement | null = null;
29
+
30
+ pointerEventsReferenceElement: HTMLElement | SVGSVGElement | null = null;
31
+
32
+ pointerEventsFloatingElement: HTMLElement | null = null;
33
+
34
+ restTimeoutPending = false;
35
+
36
+ openChangeTimeout: Timeout = new Timeout();
37
+
38
+ restTimeout: Timeout = new Timeout();
39
+
40
+ handleCloseOptions: HandleCloseOptions | undefined = undefined;
41
+
42
+ static create(): HoverInteraction {
43
+ return new HoverInteraction();
44
+ }
45
+
46
+ dispose = (): void => {
47
+ this.openChangeTimeout.clear();
48
+ this.restTimeout.clear();
49
+ };
50
+
51
+ disposeEffect = (): (() => void) => {
52
+ return this.dispose;
53
+ };
54
+ }
55
+
56
+ type PointerEventsMutationState = Pick<
57
+ HoverInteraction,
58
+ | 'performedPointerEventsMutation'
59
+ | 'pointerEventsScopeElement'
60
+ | 'pointerEventsReferenceElement'
61
+ | 'pointerEventsFloatingElement'
62
+ >;
63
+
64
+ // Only one interaction may own the pointer-events mutation for a given scope element, otherwise a
65
+ // sibling popup's cleanup would strip the styles the active one still needs.
66
+ const pointerEventsMutationOwnerByScopeElement = new WeakMap<
67
+ HTMLElement | SVGSVGElement,
68
+ PointerEventsMutationState
69
+ >();
70
+
71
+ export function clearSafePolygonPointerEventsMutation(instance: PointerEventsMutationState): void {
72
+ if (!instance.performedPointerEventsMutation) {
73
+ return;
74
+ }
75
+
76
+ const scopeElement = instance.pointerEventsScopeElement;
77
+
78
+ if (scopeElement && pointerEventsMutationOwnerByScopeElement.get(scopeElement) === instance) {
79
+ instance.pointerEventsScopeElement?.style.removeProperty('pointer-events');
80
+ instance.pointerEventsReferenceElement?.style.removeProperty('pointer-events');
81
+ instance.pointerEventsFloatingElement?.style.removeProperty('pointer-events');
82
+ pointerEventsMutationOwnerByScopeElement.delete(scopeElement);
83
+ }
84
+
85
+ instance.performedPointerEventsMutation = false;
86
+ instance.pointerEventsScopeElement = null;
87
+ instance.pointerEventsReferenceElement = null;
88
+ instance.pointerEventsFloatingElement = null;
89
+ }
90
+
91
+ export function applySafePolygonPointerEventsMutation(
92
+ instance: PointerEventsMutationState,
93
+ options: {
94
+ scopeElement: HTMLElement | SVGSVGElement;
95
+ referenceElement: HTMLElement | SVGSVGElement;
96
+ floatingElement: HTMLElement;
97
+ },
98
+ ): void {
99
+ const { scopeElement, referenceElement, floatingElement } = options;
100
+
101
+ const existingOwner = pointerEventsMutationOwnerByScopeElement.get(scopeElement);
102
+ if (existingOwner && existingOwner !== instance) {
103
+ clearSafePolygonPointerEventsMutation(existingOwner);
104
+ }
105
+
106
+ clearSafePolygonPointerEventsMutation(instance);
107
+ instance.performedPointerEventsMutation = true;
108
+ instance.pointerEventsScopeElement = scopeElement;
109
+ instance.pointerEventsReferenceElement = referenceElement;
110
+ instance.pointerEventsFloatingElement = floatingElement;
111
+ pointerEventsMutationOwnerByScopeElement.set(scopeElement, instance);
112
+
113
+ scopeElement.style.pointerEvents = 'none';
114
+ referenceElement.style.pointerEvents = 'auto';
115
+ floatingElement.style.pointerEvents = 'auto';
116
+ }
117
+
118
+ export function useHoverInteractionSharedState(...args: any[]): HoverInteraction {
119
+ const [user, slotArg] = splitSlot(args);
120
+ const slot = slotArg ?? S('useHoverInteractionSharedState');
121
+ const store = user[0] as any;
122
+
123
+ const data = store.context.dataRef.current as {
124
+ hoverInteractionState?: HoverInteraction | undefined;
125
+ };
126
+ const instance = useRefWithInit<HoverInteraction>(
127
+ () => data.hoverInteractionState ?? HoverInteraction.create(),
128
+ undefined,
129
+ subSlot(slot, 'inst'),
130
+ ).current;
131
+
132
+ if (!data.hoverInteractionState) {
133
+ data.hoverInteractionState = instance;
134
+ }
135
+
136
+ useEffect(data.hoverInteractionState.disposeEffect, [], subSlot(slot, 'e:mount'));
137
+
138
+ return data.hoverInteractionState;
139
+ }