@octanejs/base-ui 0.1.14 → 0.1.16
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/package.json +5 -5
- package/src/alert-dialog.ts +2 -0
- package/src/button.ts +63 -0
- package/src/context-menu.ts +317 -0
- package/src/csp-provider.ts +37 -0
- package/src/dialog.ts +89 -22
- package/src/direction-provider.ts +32 -0
- package/src/index.ts +10 -0
- package/src/menu.ts +3387 -0
- package/src/menubar.ts +160 -0
- package/src/popover.ts +99 -25
- package/src/preview-card.ts +812 -0
- package/src/tooltip.ts +1053 -0
- package/src/unstable-use-media-query.ts +101 -0
- package/src/utils/CSPContext.ts +21 -0
- package/src/utils/ContextMenuRootContext.ts +44 -0
- package/src/utils/FloatingPortalLite.ts +29 -0
- package/src/utils/MenubarContext.ts +38 -0
- package/src/utils/RequestQueue.ts +132 -0
- package/src/utils/TimeoutManager.ts +32 -0
- package/src/utils/composite/grid-utils.ts +437 -0
- package/src/utils/composite/gridNavigation.ts +127 -0
- package/src/utils/composite/list-utils.ts +56 -28
- package/src/utils/composite/useCompositeRoot.ts +11 -13
- package/src/utils/constants.ts +22 -0
- package/src/utils/createChangeEventDetails.ts +4 -0
- package/src/utils/findRootOwnerId.ts +19 -0
- package/src/utils/floating/FloatingDelayGroup.ts +280 -0
- package/src/utils/floating/element.ts +36 -2
- package/src/utils/floating/gridNavigation.ts +54 -0
- package/src/utils/floating/safePolygon.ts +448 -0
- package/src/utils/floating/useClientPoint.ts +276 -0
- package/src/utils/floating/useFocus.ts +248 -0
- package/src/utils/floating/useHoverFloatingInteraction.ts +301 -11
- package/src/utils/floating/useHoverInteractionSharedState.ts +139 -0
- package/src/utils/floating/useHoverReferenceInteraction.ts +507 -14
- package/src/utils/floating/useHoverShared.ts +78 -0
- package/src/utils/floating/useListNavigation.ts +1018 -0
- package/src/utils/floating/useTypeahead.ts +287 -0
- package/src/utils/getCssDimensions.ts +28 -0
- package/src/utils/getPseudoElementBounds.ts +54 -0
- package/src/utils/platform.ts +10 -0
- package/src/utils/popups/index.ts +6 -0
- package/src/utils/popups/inlineRect.ts +289 -0
- package/src/utils/useMixedToggleClickHandler.ts +77 -0
- package/src/utils/useOpenInteractionType.ts +37 -1
- package/src/utils/usePopupAutoResize.ts +250 -0
- package/src/utils/usePopupViewport.ts +375 -0
- package/src/utils/usePreviousValue.ts +25 -0
|
@@ -141,14 +141,9 @@ export function useCompositeRoot(...args: any[]): {
|
|
|
141
141
|
|
|
142
142
|
if (activeIndex !== -1) {
|
|
143
143
|
onHighlightedIndexChange(activeIndex);
|
|
144
|
-
} else if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const firstEnabledIndex = findNonDisabledListIndex(
|
|
148
|
-
{ current: sortedElements },
|
|
149
|
-
{ disabledIndices },
|
|
150
|
-
);
|
|
151
|
-
if (!isIndexOutOfListBounds({ current: sortedElements }, firstEnabledIndex)) {
|
|
144
|
+
} else if (isListIndexDisabled(sortedElements, highlightedIndex, disabledIndices)) {
|
|
145
|
+
const firstEnabledIndex = findNonDisabledListIndex(sortedElements, { disabledIndices });
|
|
146
|
+
if (!isIndexOutOfListBounds(sortedElements, firstEnabledIndex)) {
|
|
152
147
|
onHighlightedIndexChange(firstEnabledIndex);
|
|
153
148
|
}
|
|
154
149
|
}
|
|
@@ -168,9 +163,9 @@ export function useCompositeRoot(...args: any[]): {
|
|
|
168
163
|
return;
|
|
169
164
|
}
|
|
170
165
|
const elements = elementsRef.current;
|
|
171
|
-
if (isListIndexDisabled(
|
|
172
|
-
const firstEnabledIndex = findNonDisabledListIndex(
|
|
173
|
-
if (!isIndexOutOfListBounds(
|
|
166
|
+
if (isListIndexDisabled(elements, highlightedIndex, disabledIndices)) {
|
|
167
|
+
const firstEnabledIndex = findNonDisabledListIndex(elements, { disabledIndices });
|
|
168
|
+
if (!isIndexOutOfListBounds(elements, firstEnabledIndex)) {
|
|
174
169
|
onHighlightedIndexChange(firstEnabledIndex);
|
|
175
170
|
}
|
|
176
171
|
}
|
|
@@ -294,7 +289,7 @@ export function useCompositeRoot(...args: any[]): {
|
|
|
294
289
|
nextIndex = onLoop(event, highlightedIndex, nextIndex, elementsRef);
|
|
295
290
|
}
|
|
296
291
|
} else {
|
|
297
|
-
nextIndex = findNonDisabledListIndex(elementsRef, {
|
|
292
|
+
nextIndex = findNonDisabledListIndex(elementsRef.current, {
|
|
298
293
|
startingIndex: nextIndex,
|
|
299
294
|
decrement: backwardKeys.includes(event.key),
|
|
300
295
|
disabledIndices,
|
|
@@ -302,7 +297,10 @@ export function useCompositeRoot(...args: any[]): {
|
|
|
302
297
|
}
|
|
303
298
|
}
|
|
304
299
|
|
|
305
|
-
if (
|
|
300
|
+
if (
|
|
301
|
+
nextIndex !== highlightedIndex &&
|
|
302
|
+
!isIndexOutOfListBounds(elementsRef.current, nextIndex)
|
|
303
|
+
) {
|
|
306
304
|
if (stopEventPropagation) {
|
|
307
305
|
event.stopPropagation();
|
|
308
306
|
}
|
package/src/utils/constants.ts
CHANGED
|
@@ -2,8 +2,30 @@
|
|
|
2
2
|
// overlay/positioner layer needs. `POPUP_COLLISION_AVOIDANCE` is the default collision config
|
|
3
3
|
// popups pass to `useAnchorPositioning`; `DISABLED_TRANSITIONS_STYLE` disables transitions while
|
|
4
4
|
// a popup is in the `starting` transition phase (so the mount frame doesn't animate).
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Used by regular popups that usually aren't scrollable and are allowed to
|
|
8
|
+
* freely flip to any axis of placement.
|
|
9
|
+
*/
|
|
5
10
|
export const POPUP_COLLISION_AVOIDANCE = {
|
|
6
11
|
fallbackAxisSide: 'end',
|
|
7
12
|
} as const;
|
|
8
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Used for dropdowns that usually strictly prefer top/bottom placements and
|
|
16
|
+
* use `var(--available-height)` to limit their height.
|
|
17
|
+
*/
|
|
18
|
+
export const DROPDOWN_COLLISION_AVOIDANCE = {
|
|
19
|
+
fallbackAxisSide: 'none',
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
9
22
|
export const DISABLED_TRANSITIONS_STYLE = { style: { transition: 'none' } } as const;
|
|
23
|
+
|
|
24
|
+
/** How long a typeahead buffer survives without a keystroke before it resets. */
|
|
25
|
+
export const TYPEAHEAD_RESET_MS = 500;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* How long after a hover-open a click is still treated as "patient" (and therefore does not
|
|
29
|
+
* immediately close the popup it just opened).
|
|
30
|
+
*/
|
|
31
|
+
export const PATIENT_CLICK_THRESHOLD = 500;
|
|
@@ -22,13 +22,17 @@ export const REASONS = {
|
|
|
22
22
|
scrub: 'scrub',
|
|
23
23
|
trackPress: 'track-press',
|
|
24
24
|
drag: 'drag',
|
|
25
|
+
disabled: 'disabled',
|
|
25
26
|
triggerHover: 'trigger-hover',
|
|
26
27
|
triggerFocus: 'trigger-focus',
|
|
27
28
|
outsidePress: 'outside-press',
|
|
28
29
|
closePress: 'close-press',
|
|
29
30
|
focusOut: 'focus-out',
|
|
31
|
+
listNavigation: 'list-navigation',
|
|
30
32
|
escapeKey: 'escape-key',
|
|
31
33
|
imperativeAction: 'imperative-action',
|
|
34
|
+
cancelOpen: 'cancel-open',
|
|
35
|
+
siblingOpen: 'sibling-open',
|
|
32
36
|
} as const;
|
|
33
37
|
|
|
34
38
|
export type BaseUIEventReason = (typeof REASONS)[keyof typeof REASONS];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/menu/utils/findRootOwnerId.ts (v1.6.0). Pure —
|
|
2
|
+
// no octane adaptations needed.
|
|
3
|
+
//
|
|
4
|
+
// Walks up from a node to the nearest element carrying `data-rootownerid` (stamped on
|
|
5
|
+
// `Menu.Popup`), so a mouseup landing in ANOTHER popup of the same menu root can be told apart
|
|
6
|
+
// from a mouseup that landed outside the menu tree entirely.
|
|
7
|
+
import { getParentNode, isHTMLElement, isLastTraversableNode } from '@floating-ui/utils/dom';
|
|
8
|
+
|
|
9
|
+
export function findRootOwnerId(node: Node): string | undefined {
|
|
10
|
+
if (isHTMLElement(node) && node.hasAttribute('data-rootownerid')) {
|
|
11
|
+
return node.getAttribute('data-rootownerid') ?? undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (isLastTraversableNode(node)) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return findRootOwnerId(getParentNode(node));
|
|
19
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/floating-ui-react/components/FloatingDelayGroup.tsx
|
|
2
|
+
// (v1.6.0). A group of tooltips that share one hover delay: once any member has opened, the others
|
|
3
|
+
// open instantly until the group's `timeoutMs` window lapses. The delay lives in a ref so changing
|
|
4
|
+
// it does not re-render unrelated consumers.
|
|
5
|
+
//
|
|
6
|
+
// octane adaptations: `createContext`/`createElement` instead of JSX; `useIsoLayoutEffect` →
|
|
7
|
+
// `useLayoutEffect`; every composed hook threads an explicit slot.
|
|
8
|
+
import {
|
|
9
|
+
createContext,
|
|
10
|
+
createElement,
|
|
11
|
+
useContext,
|
|
12
|
+
useLayoutEffect,
|
|
13
|
+
useMemo,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
} from 'octane';
|
|
17
|
+
|
|
18
|
+
import { S, splitSlot, subSlot } from '../../internal';
|
|
19
|
+
import { Timeout, useTimeout } from '../useTimeout';
|
|
20
|
+
import { createChangeEventDetails, REASONS } from '../createChangeEventDetails';
|
|
21
|
+
import { getDelay } from './useHoverShared';
|
|
22
|
+
import type { HoverDelay } from './useHoverShared';
|
|
23
|
+
|
|
24
|
+
interface DelayGroupContextValue {
|
|
25
|
+
hasProvider: boolean;
|
|
26
|
+
timeoutMs: number;
|
|
27
|
+
delayRef: { current: HoverDelay };
|
|
28
|
+
initialDelayRef: { current: HoverDelay };
|
|
29
|
+
timeout: Timeout;
|
|
30
|
+
currentIdRef: { current: any };
|
|
31
|
+
currentContextRef: {
|
|
32
|
+
current: {
|
|
33
|
+
onOpenChange: (open: boolean, eventDetails: any) => void;
|
|
34
|
+
setIsInstantPhase: (value: boolean) => void;
|
|
35
|
+
} | null;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DEFAULT_CONTEXT: DelayGroupContextValue = {
|
|
40
|
+
hasProvider: false,
|
|
41
|
+
timeoutMs: 0,
|
|
42
|
+
delayRef: { current: 0 },
|
|
43
|
+
initialDelayRef: { current: 0 },
|
|
44
|
+
timeout: new Timeout(),
|
|
45
|
+
currentIdRef: { current: null },
|
|
46
|
+
currentContextRef: { current: null },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const FloatingDelayGroupContext = createContext<DelayGroupContextValue>(DEFAULT_CONTEXT);
|
|
50
|
+
|
|
51
|
+
function resetDelayRef(
|
|
52
|
+
delayRef: { current: HoverDelay },
|
|
53
|
+
initialDelayRef: { current: HoverDelay },
|
|
54
|
+
): void {
|
|
55
|
+
delayRef.current = initialDelayRef.current;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface FloatingDelayGroupProps {
|
|
59
|
+
children?: any;
|
|
60
|
+
/** The delay to use for the group when it's not in the instant phase. */
|
|
61
|
+
delay: HoverDelay;
|
|
62
|
+
/**
|
|
63
|
+
* An explicit timeout for the group, representing when grouping logic stops being active
|
|
64
|
+
* after the close delay completes. Useful when there is no close delay at all.
|
|
65
|
+
*/
|
|
66
|
+
timeoutMs?: number | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function FloatingDelayGroup(props: FloatingDelayGroupProps): any {
|
|
70
|
+
const slot = S('FloatingDelayGroup');
|
|
71
|
+
const { children, delay, timeoutMs = 0 } = props;
|
|
72
|
+
|
|
73
|
+
const delayRef = useRef<HoverDelay>(delay, subSlot(slot, 'delay'));
|
|
74
|
+
const initialDelayRef = useRef<HoverDelay>(delay, subSlot(slot, 'initial'));
|
|
75
|
+
const currentIdRef = useRef<string | null>(null, subSlot(slot, 'id'));
|
|
76
|
+
const currentContextRef = useRef<any>(null, subSlot(slot, 'ctxRef'));
|
|
77
|
+
const timeout = useTimeout(subSlot(slot, 'to'));
|
|
78
|
+
|
|
79
|
+
useLayoutEffect(
|
|
80
|
+
() => {
|
|
81
|
+
initialDelayRef.current = delay;
|
|
82
|
+
|
|
83
|
+
if (!currentIdRef.current) {
|
|
84
|
+
delayRef.current = delay;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// A member is already open: keep the (zeroed) open delay, adopt the new close delay.
|
|
89
|
+
delayRef.current = {
|
|
90
|
+
open: getDelay(delayRef.current, 'open'),
|
|
91
|
+
close: getDelay(delay, 'close'),
|
|
92
|
+
} as HoverDelay;
|
|
93
|
+
},
|
|
94
|
+
[delay, currentIdRef, delayRef, initialDelayRef],
|
|
95
|
+
subSlot(slot, 'l:delay'),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const contextValue = useMemo(
|
|
99
|
+
() => ({
|
|
100
|
+
hasProvider: true,
|
|
101
|
+
delayRef,
|
|
102
|
+
initialDelayRef,
|
|
103
|
+
currentIdRef,
|
|
104
|
+
timeoutMs,
|
|
105
|
+
currentContextRef,
|
|
106
|
+
timeout,
|
|
107
|
+
}),
|
|
108
|
+
[timeoutMs, timeout],
|
|
109
|
+
subSlot(slot, 'ctx'),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return createElement(FloatingDelayGroupContext.Provider, { value: contextValue, children });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface UseDelayGroupReturn {
|
|
116
|
+
delayRef: { current: HoverDelay };
|
|
117
|
+
isInstantPhase: boolean;
|
|
118
|
+
hasProvider: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function useDelayGroup(...args: any[]): UseDelayGroupReturn {
|
|
122
|
+
const [user, slotArg] = splitSlot(args);
|
|
123
|
+
const slot = slotArg ?? S('useDelayGroup');
|
|
124
|
+
const context = user[0] as any;
|
|
125
|
+
const options = (user[1] as { open: boolean } | undefined) ?? { open: false };
|
|
126
|
+
|
|
127
|
+
const { open } = options;
|
|
128
|
+
|
|
129
|
+
const store = context != null && 'rootStore' in context ? context.rootStore : context;
|
|
130
|
+
const floatingId = store.useState('floatingId', subSlot(slot, 's:id'));
|
|
131
|
+
|
|
132
|
+
const groupContext = useContext(FloatingDelayGroupContext);
|
|
133
|
+
const {
|
|
134
|
+
currentIdRef,
|
|
135
|
+
delayRef,
|
|
136
|
+
timeoutMs,
|
|
137
|
+
initialDelayRef,
|
|
138
|
+
currentContextRef,
|
|
139
|
+
hasProvider,
|
|
140
|
+
timeout,
|
|
141
|
+
} = groupContext;
|
|
142
|
+
|
|
143
|
+
const [isInstantPhase, setIsInstantPhase] = useState(false, subSlot(slot, 'instant'));
|
|
144
|
+
const openRef = useRef(open, subSlot(slot, 'open'));
|
|
145
|
+
const isUnmountedRef = useRef(false, subSlot(slot, 'unmounted'));
|
|
146
|
+
|
|
147
|
+
useLayoutEffect(
|
|
148
|
+
() => {
|
|
149
|
+
openRef.current = open;
|
|
150
|
+
},
|
|
151
|
+
[open],
|
|
152
|
+
subSlot(slot, 'l:openRef'),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
useLayoutEffect(
|
|
156
|
+
() => {
|
|
157
|
+
return () => {
|
|
158
|
+
isUnmountedRef.current = true;
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
[],
|
|
162
|
+
subSlot(slot, 'l:unmount'),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
useLayoutEffect(
|
|
166
|
+
() => {
|
|
167
|
+
function unset() {
|
|
168
|
+
if (!isUnmountedRef.current) {
|
|
169
|
+
setIsInstantPhase(false);
|
|
170
|
+
}
|
|
171
|
+
currentContextRef.current?.setIsInstantPhase(false);
|
|
172
|
+
currentIdRef.current = null;
|
|
173
|
+
currentContextRef.current = null;
|
|
174
|
+
delayRef.current = initialDelayRef.current;
|
|
175
|
+
timeout.clear();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!currentIdRef.current) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!open && currentIdRef.current === floatingId) {
|
|
183
|
+
setIsInstantPhase(false);
|
|
184
|
+
|
|
185
|
+
if (timeoutMs) {
|
|
186
|
+
const closingId = floatingId;
|
|
187
|
+
timeout.start(timeoutMs, () => {
|
|
188
|
+
// If another tooltip has taken over the group, skip resetting.
|
|
189
|
+
if (
|
|
190
|
+
store.select('open') ||
|
|
191
|
+
(currentIdRef.current && currentIdRef.current !== closingId)
|
|
192
|
+
) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
unset();
|
|
196
|
+
});
|
|
197
|
+
return () => {
|
|
198
|
+
if (openRef.current || currentIdRef.current !== closingId) {
|
|
199
|
+
timeout.clear();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
unset();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return undefined;
|
|
208
|
+
},
|
|
209
|
+
[
|
|
210
|
+
open,
|
|
211
|
+
floatingId,
|
|
212
|
+
currentIdRef,
|
|
213
|
+
delayRef,
|
|
214
|
+
timeoutMs,
|
|
215
|
+
initialDelayRef,
|
|
216
|
+
currentContextRef,
|
|
217
|
+
timeout,
|
|
218
|
+
store,
|
|
219
|
+
],
|
|
220
|
+
subSlot(slot, 'l:close'),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
useLayoutEffect(
|
|
224
|
+
() => {
|
|
225
|
+
if (!open) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const prevContext = currentContextRef.current;
|
|
230
|
+
const prevId = currentIdRef.current;
|
|
231
|
+
|
|
232
|
+
// A new tooltip is opening, so cancel any pending timeout that would reset the group's
|
|
233
|
+
// delay back to the initial value.
|
|
234
|
+
timeout.clear();
|
|
235
|
+
currentContextRef.current = { onOpenChange: store.setOpen, setIsInstantPhase };
|
|
236
|
+
currentIdRef.current = floatingId;
|
|
237
|
+
delayRef.current = {
|
|
238
|
+
open: 0,
|
|
239
|
+
close: getDelay(initialDelayRef.current, 'close'),
|
|
240
|
+
} as HoverDelay;
|
|
241
|
+
|
|
242
|
+
if (prevId !== null && prevId !== floatingId) {
|
|
243
|
+
setIsInstantPhase(true);
|
|
244
|
+
prevContext?.setIsInstantPhase(true);
|
|
245
|
+
prevContext?.onOpenChange(false, createChangeEventDetails(REASONS.none));
|
|
246
|
+
} else {
|
|
247
|
+
setIsInstantPhase(false);
|
|
248
|
+
prevContext?.setIsInstantPhase(false);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
[open, floatingId, store, currentIdRef, delayRef, initialDelayRef, currentContextRef, timeout],
|
|
252
|
+
subSlot(slot, 'l:open'),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
useLayoutEffect(
|
|
256
|
+
() => {
|
|
257
|
+
return () => {
|
|
258
|
+
if (currentIdRef.current === floatingId) {
|
|
259
|
+
currentContextRef.current = null;
|
|
260
|
+
|
|
261
|
+
if (!openRef.current) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
currentIdRef.current = null;
|
|
266
|
+
resetDelayRef(delayRef, initialDelayRef);
|
|
267
|
+
timeout.clear();
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
[currentContextRef, currentIdRef, delayRef, floatingId, initialDelayRef, timeout],
|
|
272
|
+
subSlot(slot, 'l:teardown'),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
return useMemo(
|
|
276
|
+
() => ({ hasProvider, delayRef, isInstantPhase }),
|
|
277
|
+
[hasProvider, delayRef, isInstantPhase],
|
|
278
|
+
subSlot(slot, 'out'),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
// Ported from .base-ui/packages/react/src/floating-ui-react/utils/element.ts (v1.6.0) — the subset
|
|
2
2
|
// used so far. `getTarget` is reused from the composite list utils.
|
|
3
|
-
import { isHTMLElement } from '../dom';
|
|
3
|
+
import { isElement, isHTMLElement } from '../dom';
|
|
4
|
+
import type { PopupTriggerMap } from '../popups/popupTriggerMap';
|
|
4
5
|
import { TYPEABLE_SELECTOR, FOCUSABLE_ATTRIBUTE } from './constants';
|
|
5
6
|
|
|
7
|
+
import { contains } from '../contains';
|
|
8
|
+
|
|
6
9
|
export { getTarget } from '../composite/list-utils';
|
|
7
|
-
export { contains }
|
|
10
|
+
export { contains };
|
|
11
|
+
|
|
12
|
+
// Whether the target sits on (or inside) one of this popup's triggers, and that trigger is not
|
|
13
|
+
// marked disabled. Hover uses it to keep a popup open while the cursor crosses between triggers.
|
|
14
|
+
export function isTargetInsideEnabledTrigger(
|
|
15
|
+
target: EventTarget | null,
|
|
16
|
+
triggerElements: PopupTriggerMap,
|
|
17
|
+
): boolean {
|
|
18
|
+
if (!isElement(target)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const targetElement = target as Element;
|
|
22
|
+
if (triggerElements.hasElement(targetElement)) {
|
|
23
|
+
return !targetElement.hasAttribute('data-trigger-disabled');
|
|
24
|
+
}
|
|
25
|
+
for (const [, trigger] of triggerElements.entries()) {
|
|
26
|
+
if (contains(trigger, targetElement)) {
|
|
27
|
+
return !trigger.hasAttribute('data-trigger-disabled');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Whether the element (or an ancestor) is something the user can interact with directly — used to
|
|
34
|
+
// decide whether a pointerdown inside a popup counts as a click-like open.
|
|
35
|
+
export function isInteractiveElement(element: Element | null): boolean {
|
|
36
|
+
return (
|
|
37
|
+
element?.closest(
|
|
38
|
+
`button,a[href],[role="button"],select,[tabindex]:not([tabindex="-1"]),${TYPEABLE_SELECTOR}`,
|
|
39
|
+
) != null
|
|
40
|
+
);
|
|
41
|
+
}
|
|
8
42
|
|
|
9
43
|
// Ported from .base-ui/…/internals/shadowDom.ts — the deepest active element across shadow roots.
|
|
10
44
|
export function activeElement(doc: Document): Element | null {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/floating-ui-react/hooks/gridNavigation.ts (v1.6.0). The
|
|
2
|
+
// grid navigator `useListNavigation` injects for its own `grid` option — distinct from
|
|
3
|
+
// `../composite/gridNavigation`, which builds the navigator `useCompositeRoot` takes.
|
|
4
|
+
//
|
|
5
|
+
// octane adaptations: the event is the NATIVE `KeyboardEvent`, and `React.RefObject<T>` is a plain
|
|
6
|
+
// `{ current: T }`. Not a hook, so no slot. Upstream's `utils/composite` module is split in this
|
|
7
|
+
// binding: the list algebra lives in `../composite/list-utils`, the grid algorithm in
|
|
8
|
+
// `../composite/grid-utils`.
|
|
9
|
+
import { getGridNavigatedIndex } from '../composite/grid-utils';
|
|
10
|
+
import { isIndexOutOfListBounds, type DisabledIndices } from '../composite/list-utils';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Positional arguments are deliberate: property names of an options object
|
|
14
|
+
* don't minify, and the signature is locked to the caller via `typeof` on the
|
|
15
|
+
* `grid` option of `useListNavigation`.
|
|
16
|
+
*
|
|
17
|
+
* The injected grid navigator only ever operates on a uniform 1x1 grid (sizes are
|
|
18
|
+
* always `1x1` and packing is never dense), so the cell-map machinery that supports
|
|
19
|
+
* multi-cell items collapses to an identity transform over the item list. Calling
|
|
20
|
+
* `getGridNavigatedIndex` directly keeps the cell-map helpers
|
|
21
|
+
* (`createGridCellMap`/`getGridCellIndexOfCorner`/`getGridCellIndices`) out of
|
|
22
|
+
* grid-combobox bundles.
|
|
23
|
+
*/
|
|
24
|
+
export function gridNavigation(
|
|
25
|
+
event: KeyboardEvent,
|
|
26
|
+
prevIndex: number,
|
|
27
|
+
listRef: { current: Array<HTMLElement | null> },
|
|
28
|
+
orientation: 'horizontal' | 'vertical' | 'both',
|
|
29
|
+
loopFocus: boolean,
|
|
30
|
+
rtl: boolean,
|
|
31
|
+
disabledIndices: DisabledIndices | undefined,
|
|
32
|
+
minIndex: number,
|
|
33
|
+
maxIndex: number,
|
|
34
|
+
cols = 2,
|
|
35
|
+
): number | undefined {
|
|
36
|
+
const nextIndex = getGridNavigatedIndex(listRef.current, {
|
|
37
|
+
event,
|
|
38
|
+
orientation,
|
|
39
|
+
loopFocus,
|
|
40
|
+
rtl,
|
|
41
|
+
cols,
|
|
42
|
+
disabledIndices,
|
|
43
|
+
minIndex,
|
|
44
|
+
maxIndex,
|
|
45
|
+
// An out-of-range previous index falls back to the first enabled item.
|
|
46
|
+
prevIndex: prevIndex > maxIndex ? minIndex : prevIndex,
|
|
47
|
+
stopEvent: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// `getGridNavigatedIndex` can return an out-of-bounds sentinel (e.g. `-1` when there is no
|
|
51
|
+
// previous item to move from); surface that as `undefined` so the caller treats it as
|
|
52
|
+
// "no navigation" rather than highlighting index `-1`.
|
|
53
|
+
return isIndexOutOfListBounds(listRef.current, nextIndex) ? undefined : nextIndex;
|
|
54
|
+
}
|