@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
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/floating-ui-react/hooks/useListNavigation.ts (v1.6.0),
|
|
2
|
+
// octane-adapted: reads the FloatingRootStore (`store.useState`/`select`/`setOpen`/`context`);
|
|
3
|
+
// native events (no `.nativeEvent`); `useIsoLayoutEffect` → octane's `useLayoutEffect` (a plain-`.ts`
|
|
4
|
+
// hook is not compiled, so an `undefined` dep array runs the effect every render — the same
|
|
5
|
+
// semantics as upstream's dep-less `useIsoLayoutEffect`); every hook threads an explicit slot.
|
|
6
|
+
// Returns `{ reference, floating, item, trigger }` prop bags.
|
|
7
|
+
//
|
|
8
|
+
// Adds arrow-key navigation of a list of items using either real DOM focus or virtual focus
|
|
9
|
+
// (`aria-activedescendant`). The two dev-only `console.warn` guards upstream emits for
|
|
10
|
+
// `allowEscape`/grid misconfiguration are dropped: this package carries no `process.env.NODE_ENV`
|
|
11
|
+
// branches (same call as `useStableCallback`'s dropped called-during-render guard).
|
|
12
|
+
import { useMemo, useRef, useLayoutEffect } from 'octane';
|
|
13
|
+
|
|
14
|
+
import { subSlot } from '../../internal';
|
|
15
|
+
import { isHTMLElement } from '../dom';
|
|
16
|
+
import { ownerDocument } from '../owner';
|
|
17
|
+
import { useAnimationFrame } from '../useAnimationFrame';
|
|
18
|
+
import { useStableCallback } from '../useStableCallback';
|
|
19
|
+
import { useValueAsRef } from '../useValueAsRef';
|
|
20
|
+
import { createChangeEventDetails, REASONS } from '../createChangeEventDetails';
|
|
21
|
+
import { ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, ARROW_UP } from '../composite/keys';
|
|
22
|
+
import {
|
|
23
|
+
findNonDisabledListIndex,
|
|
24
|
+
getMaxListIndex,
|
|
25
|
+
getMinListIndex,
|
|
26
|
+
isIndexOutOfListBounds,
|
|
27
|
+
stopEvent,
|
|
28
|
+
type DisabledIndices,
|
|
29
|
+
} from '../composite/list-utils';
|
|
30
|
+
import { useFloatingParentNodeId, useFloatingTree } from './FloatingTree';
|
|
31
|
+
import type { FloatingTreeStore } from './FloatingTreeStore';
|
|
32
|
+
import type { ElementProps, FloatingContext, FloatingRootContext } from './types';
|
|
33
|
+
import {
|
|
34
|
+
activeElement,
|
|
35
|
+
contains,
|
|
36
|
+
getFloatingFocusElement,
|
|
37
|
+
getTarget,
|
|
38
|
+
isTypeableCombobox,
|
|
39
|
+
} from './element';
|
|
40
|
+
import { enqueueFocus } from './enqueueFocus';
|
|
41
|
+
import { isVirtualClick, isVirtualPointerEvent } from './event';
|
|
42
|
+
|
|
43
|
+
export const ESCAPE = 'Escape';
|
|
44
|
+
|
|
45
|
+
export type ListOrientation = 'vertical' | 'horizontal' | 'both';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The two-dimensional navigator injected through the `grid` prop. Upstream types this as
|
|
49
|
+
* `typeof gridNavigation` (`.base-ui/…/floating-ui-react/hooks/gridNavigation.ts`) and notes that
|
|
50
|
+
* the positional arguments are deliberate — property names of an options object don't minify, and
|
|
51
|
+
* the signature is locked to the caller. The grid navigator itself lands with the grid-capable
|
|
52
|
+
* consumers (Combobox); non-grid consumers (Menu, Select) pass nothing so the grid helpers stay out
|
|
53
|
+
* of their bundles, which is the whole point of injecting it. A `gridNavigation` port satisfies this
|
|
54
|
+
* type structurally.
|
|
55
|
+
*/
|
|
56
|
+
export type GridNavigator = (
|
|
57
|
+
event: KeyboardEvent,
|
|
58
|
+
prevIndex: number,
|
|
59
|
+
listRef: { current: Array<HTMLElement | null> },
|
|
60
|
+
orientation: ListOrientation,
|
|
61
|
+
loopFocus: boolean,
|
|
62
|
+
rtl: boolean,
|
|
63
|
+
disabledIndices: DisabledIndices | undefined,
|
|
64
|
+
minIndex: number,
|
|
65
|
+
maxIndex: number,
|
|
66
|
+
cols?: number,
|
|
67
|
+
) => number | undefined;
|
|
68
|
+
|
|
69
|
+
function doSwitch(
|
|
70
|
+
orientation: ListOrientation | undefined,
|
|
71
|
+
vertical: boolean,
|
|
72
|
+
horizontal: boolean,
|
|
73
|
+
) {
|
|
74
|
+
switch (orientation) {
|
|
75
|
+
case 'vertical':
|
|
76
|
+
return vertical;
|
|
77
|
+
case 'horizontal':
|
|
78
|
+
return horizontal;
|
|
79
|
+
default:
|
|
80
|
+
return vertical || horizontal;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isMainOrientationKey(key: string, orientation: ListOrientation | undefined) {
|
|
85
|
+
const vertical = key === ARROW_UP || key === ARROW_DOWN;
|
|
86
|
+
const horizontal = key === ARROW_LEFT || key === ARROW_RIGHT;
|
|
87
|
+
return doSwitch(orientation, vertical, horizontal);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isMainOrientationToEndKey(
|
|
91
|
+
key: string,
|
|
92
|
+
orientation: ListOrientation | undefined,
|
|
93
|
+
rtl: boolean,
|
|
94
|
+
) {
|
|
95
|
+
const vertical = key === ARROW_DOWN;
|
|
96
|
+
const horizontal = rtl ? key === ARROW_LEFT : key === ARROW_RIGHT;
|
|
97
|
+
return (
|
|
98
|
+
doSwitch(orientation, vertical, horizontal) || key === 'Enter' || key === ' ' || key === ''
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isCrossOrientationOpenKey(
|
|
103
|
+
key: string,
|
|
104
|
+
orientation: ListOrientation | undefined,
|
|
105
|
+
rtl: boolean,
|
|
106
|
+
) {
|
|
107
|
+
const vertical = rtl ? key === ARROW_LEFT : key === ARROW_RIGHT;
|
|
108
|
+
const horizontal = key === ARROW_DOWN;
|
|
109
|
+
return doSwitch(orientation, vertical, horizontal);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isCrossOrientationCloseKey(
|
|
113
|
+
key: string,
|
|
114
|
+
orientation: ListOrientation | undefined,
|
|
115
|
+
rtl: boolean,
|
|
116
|
+
grid: boolean,
|
|
117
|
+
) {
|
|
118
|
+
const vertical = rtl ? key === ARROW_RIGHT : key === ARROW_LEFT;
|
|
119
|
+
const horizontal = key === ARROW_UP;
|
|
120
|
+
if (orientation === 'both' || (orientation === 'horizontal' && grid)) {
|
|
121
|
+
return key === ESCAPE;
|
|
122
|
+
}
|
|
123
|
+
return doSwitch(orientation, vertical, horizontal);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface UseListNavigationProps {
|
|
127
|
+
/**
|
|
128
|
+
* A ref that holds an array of list items.
|
|
129
|
+
* @default empty list
|
|
130
|
+
*/
|
|
131
|
+
listRef: { current: Array<HTMLElement | null> };
|
|
132
|
+
/**
|
|
133
|
+
* The index of the currently active (focused or highlighted) item, which may
|
|
134
|
+
* or may not be selected.
|
|
135
|
+
* @default null
|
|
136
|
+
*/
|
|
137
|
+
activeIndex: number | null;
|
|
138
|
+
/**
|
|
139
|
+
* A callback that is called when the user navigates to a new active item,
|
|
140
|
+
* passed in a new `activeIndex`.
|
|
141
|
+
*/
|
|
142
|
+
onNavigate?: ((activeIndex: number | null, event: Event | undefined) => void) | undefined;
|
|
143
|
+
/**
|
|
144
|
+
* Whether the Hook is enabled, including all internal Effects and event
|
|
145
|
+
* handlers.
|
|
146
|
+
* @default true
|
|
147
|
+
*/
|
|
148
|
+
enabled?: boolean | undefined;
|
|
149
|
+
/**
|
|
150
|
+
* The currently selected item index, which may or may not be active.
|
|
151
|
+
* @default null
|
|
152
|
+
*/
|
|
153
|
+
selectedIndex?: number | null | undefined;
|
|
154
|
+
/**
|
|
155
|
+
* Whether to focus the item upon opening the floating element. 'auto' infers
|
|
156
|
+
* what to do based on the input type (keyboard vs. pointer), while a boolean
|
|
157
|
+
* value will force the value.
|
|
158
|
+
* @default 'auto'
|
|
159
|
+
*/
|
|
160
|
+
focusItemOnOpen?: boolean | 'auto' | undefined;
|
|
161
|
+
/**
|
|
162
|
+
* Whether hovering an item synchronizes the focus.
|
|
163
|
+
* @default true
|
|
164
|
+
*/
|
|
165
|
+
focusItemOnHover?: boolean | undefined;
|
|
166
|
+
/**
|
|
167
|
+
* Whether pressing an arrow key on the navigation's main axis opens the
|
|
168
|
+
* floating element.
|
|
169
|
+
* @default true
|
|
170
|
+
*/
|
|
171
|
+
openOnArrowKeyDown?: boolean | undefined;
|
|
172
|
+
/**
|
|
173
|
+
* By default elements with either a `disabled` or `aria-disabled` attribute
|
|
174
|
+
* are skipped in the list navigation — however, this requires the items to
|
|
175
|
+
* be rendered.
|
|
176
|
+
* This prop allows you to manually specify indices which should be disabled,
|
|
177
|
+
* overriding the default logic.
|
|
178
|
+
* For Windows-style select popups, where the menu does not open when
|
|
179
|
+
* navigating via arrow keys, specify an empty array.
|
|
180
|
+
* @default undefined
|
|
181
|
+
*/
|
|
182
|
+
disabledIndices?: DisabledIndices | undefined;
|
|
183
|
+
/**
|
|
184
|
+
* Determines whether focus can escape the list, such that nothing is selected
|
|
185
|
+
* after navigating beyond the boundary of the list. In some
|
|
186
|
+
* autocomplete/combobox components, this may be desired, as screen
|
|
187
|
+
* readers will return to the input.
|
|
188
|
+
* `loopFocus` must be `true`.
|
|
189
|
+
* @default false
|
|
190
|
+
*/
|
|
191
|
+
allowEscape?: boolean | undefined;
|
|
192
|
+
/**
|
|
193
|
+
* Determines whether focus should loop around when navigating past the first
|
|
194
|
+
* or last item.
|
|
195
|
+
* @default false
|
|
196
|
+
*/
|
|
197
|
+
loopFocus?: boolean | undefined;
|
|
198
|
+
/**
|
|
199
|
+
* If the list is nested within another one (e.g. a nested submenu), the
|
|
200
|
+
* navigation semantics change.
|
|
201
|
+
* @default false
|
|
202
|
+
*/
|
|
203
|
+
nested?: boolean | undefined;
|
|
204
|
+
/**
|
|
205
|
+
* Allows to specify the orientation of the parent list, which is used to
|
|
206
|
+
* determine the direction of the navigation.
|
|
207
|
+
* This is useful when list navigation is used within a Composite,
|
|
208
|
+
* as the hook can't determine the orientation of the parent list automatically.
|
|
209
|
+
*/
|
|
210
|
+
parentOrientation?: ListOrientation | undefined;
|
|
211
|
+
/**
|
|
212
|
+
* Whether the direction of the floating element's navigation is in RTL
|
|
213
|
+
* layout.
|
|
214
|
+
* @default false
|
|
215
|
+
*/
|
|
216
|
+
rtl?: boolean | undefined;
|
|
217
|
+
/**
|
|
218
|
+
* Whether the focus is virtual (using `aria-activedescendant`).
|
|
219
|
+
* Use this if you need focus to remain on the reference element
|
|
220
|
+
* (such as an input), but allow arrow keys to navigate list items.
|
|
221
|
+
* This is common in autocomplete listbox components.
|
|
222
|
+
* Your virtually-focused list items must have a unique `id` set on them.
|
|
223
|
+
* @default false
|
|
224
|
+
*/
|
|
225
|
+
virtual?: boolean | undefined;
|
|
226
|
+
/**
|
|
227
|
+
* The orientation in which navigation occurs.
|
|
228
|
+
* @default 'vertical'
|
|
229
|
+
*/
|
|
230
|
+
orientation?: ListOrientation | undefined;
|
|
231
|
+
/**
|
|
232
|
+
* The id of the root component.
|
|
233
|
+
*/
|
|
234
|
+
id?: string | undefined;
|
|
235
|
+
/**
|
|
236
|
+
* Whether to clear the active index when the pointer leaves an item.
|
|
237
|
+
* @default true
|
|
238
|
+
*/
|
|
239
|
+
resetOnPointerLeave?: boolean | undefined;
|
|
240
|
+
/**
|
|
241
|
+
* External FloatingTree to use when the one provided by context can't be used.
|
|
242
|
+
*/
|
|
243
|
+
externalTree?: FloatingTreeStore | undefined;
|
|
244
|
+
/**
|
|
245
|
+
* Computes two-dimensional list navigation for grid-capable consumers.
|
|
246
|
+
*/
|
|
247
|
+
grid?: GridNavigator | null | undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Adds arrow key-based navigation of a list of items, either using real DOM
|
|
252
|
+
* focus or virtual focus.
|
|
253
|
+
* @see https://floating-ui.com/docs/useListNavigation
|
|
254
|
+
*/
|
|
255
|
+
export function useListNavigation(
|
|
256
|
+
context: FloatingRootContext | FloatingContext,
|
|
257
|
+
props: UseListNavigationProps,
|
|
258
|
+
slot: symbol | undefined,
|
|
259
|
+
): ElementProps {
|
|
260
|
+
const {
|
|
261
|
+
listRef,
|
|
262
|
+
activeIndex,
|
|
263
|
+
onNavigate: onNavigateProp,
|
|
264
|
+
enabled = true,
|
|
265
|
+
selectedIndex = null,
|
|
266
|
+
allowEscape = false,
|
|
267
|
+
loopFocus = false,
|
|
268
|
+
nested = false,
|
|
269
|
+
rtl = false,
|
|
270
|
+
virtual = false,
|
|
271
|
+
focusItemOnOpen = 'auto',
|
|
272
|
+
focusItemOnHover = true,
|
|
273
|
+
openOnArrowKeyDown = true,
|
|
274
|
+
disabledIndices = undefined,
|
|
275
|
+
orientation = 'vertical',
|
|
276
|
+
parentOrientation,
|
|
277
|
+
id,
|
|
278
|
+
resetOnPointerLeave = true,
|
|
279
|
+
externalTree,
|
|
280
|
+
grid: navigateGrid,
|
|
281
|
+
} = props;
|
|
282
|
+
const isGrid = navigateGrid != null;
|
|
283
|
+
|
|
284
|
+
const store = (context && 'rootStore' in context ? context.rootStore : context) as any;
|
|
285
|
+
|
|
286
|
+
const open = store.useState('open', subSlot(slot, 'open'));
|
|
287
|
+
const floatingElement = store.useState('floatingElement', subSlot(slot, 'fel'));
|
|
288
|
+
const domReferenceElement = store.useState('domReferenceElement', subSlot(slot, 'drel'));
|
|
289
|
+
|
|
290
|
+
const dataRef = store.context.dataRef;
|
|
291
|
+
|
|
292
|
+
const floatingFocusElement = getFloatingFocusElement(floatingElement);
|
|
293
|
+
const typeableComboboxReference = isTypeableCombobox(domReferenceElement);
|
|
294
|
+
const floatingFocusElementRef = useValueAsRef<HTMLElement | null>(
|
|
295
|
+
floatingFocusElement,
|
|
296
|
+
subSlot(slot, 'ffe'),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const parentId = useFloatingParentNodeId();
|
|
300
|
+
const tree = useFloatingTree(externalTree);
|
|
301
|
+
|
|
302
|
+
const focusItemOnOpenRef = useRef(focusItemOnOpen, subSlot(slot, 'fioo'));
|
|
303
|
+
const indexRef = useRef(selectedIndex ?? -1, subSlot(slot, 'index'));
|
|
304
|
+
const keyRef = useRef<null | string>(null, subSlot(slot, 'key'));
|
|
305
|
+
const isPointerModalityRef = useRef(true, subSlot(slot, 'ipm'));
|
|
306
|
+
|
|
307
|
+
const onNavigate = useStableCallback(
|
|
308
|
+
(event?: Event) => {
|
|
309
|
+
onNavigateProp?.(indexRef.current === -1 ? null : indexRef.current, event);
|
|
310
|
+
},
|
|
311
|
+
subSlot(slot, 'on'),
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const previousMountedRef = useRef(!!floatingElement, subSlot(slot, 'pm'));
|
|
315
|
+
const previousOpenRef = useRef(open, subSlot(slot, 'po'));
|
|
316
|
+
const forceSyncFocusRef = useRef(false, subSlot(slot, 'fsf'));
|
|
317
|
+
const forceScrollIntoViewRef = useRef(false, subSlot(slot, 'fsiv'));
|
|
318
|
+
const cancelQueuedFocusRef = useRef<(() => void) | null>(null, subSlot(slot, 'cqf'));
|
|
319
|
+
|
|
320
|
+
const disabledIndicesRef = useValueAsRef<DisabledIndices | undefined>(
|
|
321
|
+
disabledIndices,
|
|
322
|
+
subSlot(slot, 'dir'),
|
|
323
|
+
);
|
|
324
|
+
const latestOpenRef = useValueAsRef<boolean>(open, subSlot(slot, 'lo'));
|
|
325
|
+
const selectedIndexRef = useValueAsRef<number | null>(selectedIndex, subSlot(slot, 'si'));
|
|
326
|
+
const resetOnPointerLeaveRef = useValueAsRef<boolean>(resetOnPointerLeave, subSlot(slot, 'ropl'));
|
|
327
|
+
|
|
328
|
+
const focusFrame = useAnimationFrame(subSlot(slot, 'ff'));
|
|
329
|
+
const waitForListPopulatedFrame = useAnimationFrame(subSlot(slot, 'wflp'));
|
|
330
|
+
|
|
331
|
+
const focusItem = useStableCallback(
|
|
332
|
+
() => {
|
|
333
|
+
function runFocus(itemElement: HTMLElement) {
|
|
334
|
+
if (virtual) {
|
|
335
|
+
tree?.events.emit('virtualfocus', itemElement);
|
|
336
|
+
} else {
|
|
337
|
+
cancelQueuedFocusRef.current = enqueueFocus(itemElement, {
|
|
338
|
+
sync: forceSyncFocusRef.current,
|
|
339
|
+
preventScroll: true,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const initialItem = listRef.current[indexRef.current];
|
|
345
|
+
const forceScrollIntoView = forceScrollIntoViewRef.current;
|
|
346
|
+
|
|
347
|
+
if (initialItem) {
|
|
348
|
+
runFocus(initialItem);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const scheduler = forceSyncFocusRef.current
|
|
352
|
+
? (callback: () => void) => callback()
|
|
353
|
+
: (callback: () => void) => focusFrame.request(callback);
|
|
354
|
+
|
|
355
|
+
scheduler(() => {
|
|
356
|
+
const waitedItem = listRef.current[indexRef.current] || initialItem;
|
|
357
|
+
|
|
358
|
+
if (!waitedItem) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (!initialItem) {
|
|
363
|
+
runFocus(waitedItem);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// `item` is the prop bag declared further down; by the time this runs (from a layout
|
|
367
|
+
// effect or an event handler) it is always initialized, so this is upstream's
|
|
368
|
+
// truthiness guard verbatim.
|
|
369
|
+
const shouldScrollIntoView =
|
|
370
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
371
|
+
item && (forceScrollIntoView || !isPointerModalityRef.current);
|
|
372
|
+
|
|
373
|
+
if (shouldScrollIntoView) {
|
|
374
|
+
// JSDOM doesn't support `.scrollIntoView()` but it's widely supported
|
|
375
|
+
// by all browsers.
|
|
376
|
+
waitedItem.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
},
|
|
380
|
+
subSlot(slot, 'fi'),
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
useLayoutEffect(
|
|
384
|
+
() => {
|
|
385
|
+
dataRef.current.orientation = orientation;
|
|
386
|
+
},
|
|
387
|
+
[dataRef, orientation],
|
|
388
|
+
subSlot(slot, 'e:orientation'),
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// Sync `selectedIndex` to be the `activeIndex` upon opening the floating
|
|
392
|
+
// element. Also, reset `activeIndex` upon closing the floating element.
|
|
393
|
+
useLayoutEffect(
|
|
394
|
+
() => {
|
|
395
|
+
if (!enabled) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (open && floatingElement) {
|
|
400
|
+
indexRef.current = selectedIndex ?? -1;
|
|
401
|
+
if (focusItemOnOpenRef.current && selectedIndex != null) {
|
|
402
|
+
// Regardless of the pointer modality, we want to ensure the selected
|
|
403
|
+
// item comes into view when the floating element is opened.
|
|
404
|
+
forceScrollIntoViewRef.current = true;
|
|
405
|
+
onNavigate();
|
|
406
|
+
}
|
|
407
|
+
} else if (previousMountedRef.current) {
|
|
408
|
+
// Reset the active index when the list is no longer open and mounted (closing or
|
|
409
|
+
// unmounting). `onNavigate` is a stable callback that always forwards to the latest
|
|
410
|
+
// `onNavigate` prop.
|
|
411
|
+
indexRef.current = -1;
|
|
412
|
+
onNavigate();
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
[enabled, open, floatingElement, selectedIndex, onNavigate],
|
|
416
|
+
subSlot(slot, 'e:syncSelected'),
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
// Sync `activeIndex` to be the focused item while the floating element is
|
|
420
|
+
// open.
|
|
421
|
+
useLayoutEffect(
|
|
422
|
+
() => {
|
|
423
|
+
if (!enabled) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (!open) {
|
|
427
|
+
forceSyncFocusRef.current = false;
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (!floatingElement) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (activeIndex == null) {
|
|
435
|
+
forceSyncFocusRef.current = false;
|
|
436
|
+
|
|
437
|
+
if (selectedIndexRef.current != null) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Reset while the floating element was open (e.g. the list changed).
|
|
442
|
+
if (previousMountedRef.current) {
|
|
443
|
+
indexRef.current = -1;
|
|
444
|
+
focusItem();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Initial sync.
|
|
448
|
+
if (
|
|
449
|
+
(!previousOpenRef.current || !previousMountedRef.current) &&
|
|
450
|
+
focusItemOnOpenRef.current &&
|
|
451
|
+
(keyRef.current != null ||
|
|
452
|
+
(focusItemOnOpenRef.current === true && keyRef.current == null))
|
|
453
|
+
) {
|
|
454
|
+
let runs = 0;
|
|
455
|
+
const waitForListPopulated = () => {
|
|
456
|
+
if (listRef.current[0] == null) {
|
|
457
|
+
// Avoid letting the browser paint if possible on the first try,
|
|
458
|
+
// otherwise use rAF. Don't try more than twice, since something
|
|
459
|
+
// is wrong otherwise.
|
|
460
|
+
if (runs < 2) {
|
|
461
|
+
const scheduler = runs
|
|
462
|
+
? (callback: () => void) => waitForListPopulatedFrame.request(callback)
|
|
463
|
+
: queueMicrotask;
|
|
464
|
+
scheduler(waitForListPopulated);
|
|
465
|
+
}
|
|
466
|
+
runs += 1;
|
|
467
|
+
} else {
|
|
468
|
+
// Initially focus the first non-disabled item. `disabledIndices` is deliberately
|
|
469
|
+
// omitted here so attribute-disabled items (`disabled`/`aria-disabled`) are skipped
|
|
470
|
+
// on open even when the consumer passes an empty `disabledIndices` array. Passing it
|
|
471
|
+
// would regress that behavior (see mui/base-ui#2604).
|
|
472
|
+
indexRef.current =
|
|
473
|
+
keyRef.current == null ||
|
|
474
|
+
isMainOrientationToEndKey(keyRef.current, orientation, rtl) ||
|
|
475
|
+
nested
|
|
476
|
+
? getMinListIndex(listRef)
|
|
477
|
+
: getMaxListIndex(listRef);
|
|
478
|
+
keyRef.current = null;
|
|
479
|
+
onNavigate();
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
waitForListPopulated();
|
|
484
|
+
}
|
|
485
|
+
} else if (!isIndexOutOfListBounds(listRef.current, activeIndex)) {
|
|
486
|
+
indexRef.current = activeIndex;
|
|
487
|
+
focusItem();
|
|
488
|
+
forceScrollIntoViewRef.current = false;
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
[
|
|
492
|
+
enabled,
|
|
493
|
+
open,
|
|
494
|
+
floatingElement,
|
|
495
|
+
activeIndex,
|
|
496
|
+
selectedIndexRef,
|
|
497
|
+
nested,
|
|
498
|
+
listRef,
|
|
499
|
+
orientation,
|
|
500
|
+
rtl,
|
|
501
|
+
onNavigate,
|
|
502
|
+
focusItem,
|
|
503
|
+
waitForListPopulatedFrame,
|
|
504
|
+
],
|
|
505
|
+
subSlot(slot, 'e:syncActive'),
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
// Ensure the parent floating element has focus when a nested child closes
|
|
509
|
+
// to allow arrow key navigation to work after the pointer leaves the child.
|
|
510
|
+
useLayoutEffect(
|
|
511
|
+
() => {
|
|
512
|
+
if (!enabled || floatingElement || !tree || virtual || !previousMountedRef.current) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const nodes = tree.nodesRef.current;
|
|
517
|
+
const parent = nodes.find((node) => node.id === parentId)?.context?.elements.floating;
|
|
518
|
+
// `floatingElement` is null here (see the guard above), so resolve the owner document from an
|
|
519
|
+
// in-DOM element for realm-safety (shadow DOM/iframes): the reference element, falling back to
|
|
520
|
+
// the parent floating element when the reference is virtual (`domReferenceElement` is null).
|
|
521
|
+
const activeEl = activeElement(ownerDocument(domReferenceElement ?? parent ?? null));
|
|
522
|
+
const treeContainsActiveEl = nodes.some(
|
|
523
|
+
(node) => node.context && contains(node.context.elements.floating, activeEl),
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
if (parent && !treeContainsActiveEl && isPointerModalityRef.current) {
|
|
527
|
+
parent.focus({ preventScroll: true });
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
[enabled, floatingElement, domReferenceElement, tree, parentId, virtual],
|
|
531
|
+
subSlot(slot, 'e:parentFocus'),
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
useLayoutEffect(
|
|
535
|
+
() => {
|
|
536
|
+
previousOpenRef.current = open;
|
|
537
|
+
previousMountedRef.current = !!floatingElement;
|
|
538
|
+
},
|
|
539
|
+
undefined,
|
|
540
|
+
subSlot(slot, 'e:previous'),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
useLayoutEffect(
|
|
544
|
+
() => {
|
|
545
|
+
if (!open) {
|
|
546
|
+
keyRef.current = null;
|
|
547
|
+
focusItemOnOpenRef.current = focusItemOnOpen;
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
[open, focusItemOnOpen],
|
|
551
|
+
subSlot(slot, 'e:resetKey'),
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
const hasActiveIndex = activeIndex != null;
|
|
555
|
+
|
|
556
|
+
const syncCurrentTarget = useStableCallback(
|
|
557
|
+
(event: any) => {
|
|
558
|
+
if (!latestOpenRef.current) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const index = listRef.current.indexOf(event.currentTarget);
|
|
563
|
+
if (index !== -1 && (indexRef.current !== index || activeIndex !== index)) {
|
|
564
|
+
indexRef.current = index;
|
|
565
|
+
onNavigate(event);
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
subSlot(slot, 'sct'),
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
const getParentOrientation = useStableCallback(
|
|
572
|
+
(): ListOrientation | undefined => {
|
|
573
|
+
return (
|
|
574
|
+
parentOrientation ??
|
|
575
|
+
(tree?.nodesRef.current.find((node) => node.id === parentId)?.context?.dataRef?.current
|
|
576
|
+
.orientation as ListOrientation | undefined)
|
|
577
|
+
);
|
|
578
|
+
},
|
|
579
|
+
subSlot(slot, 'gpo'),
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
const getMinEnabledIndex = useStableCallback(
|
|
583
|
+
() => {
|
|
584
|
+
return getMinListIndex(listRef, disabledIndicesRef.current);
|
|
585
|
+
},
|
|
586
|
+
subSlot(slot, 'gmei'),
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
const commonOnKeyDown = useStableCallback(
|
|
590
|
+
(event: any) => {
|
|
591
|
+
isPointerModalityRef.current = false;
|
|
592
|
+
forceSyncFocusRef.current = true;
|
|
593
|
+
|
|
594
|
+
// When composing a character, Chrome fires ArrowDown twice. Firefox/Safari
|
|
595
|
+
// don't appear to suffer from this. `event.isComposing` is avoided due to
|
|
596
|
+
// Safari not supporting it properly (although it's not needed in the first
|
|
597
|
+
// place for Safari, just avoiding any possible issues).
|
|
598
|
+
if (event.which === 229) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// If the floating element is animating out, ignore navigation. Otherwise,
|
|
603
|
+
// the `activeIndex` gets set to 0 despite not being open so the next time
|
|
604
|
+
// the user ArrowDowns, the first item won't be focused.
|
|
605
|
+
if (!latestOpenRef.current && event.currentTarget === floatingFocusElementRef.current) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (nested && isCrossOrientationCloseKey(event.key, orientation, rtl, isGrid)) {
|
|
610
|
+
// If the nested list's close key is also the parent navigation key,
|
|
611
|
+
// let the parent navigate. Otherwise, stop propagating the event.
|
|
612
|
+
if (!isMainOrientationKey(event.key, getParentOrientation())) {
|
|
613
|
+
stopEvent(event);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
store.setOpen(false, createChangeEventDetails(REASONS.listNavigation, event));
|
|
617
|
+
|
|
618
|
+
if (isHTMLElement(domReferenceElement)) {
|
|
619
|
+
if (virtual) {
|
|
620
|
+
tree?.events.emit('virtualfocus', domReferenceElement);
|
|
621
|
+
} else {
|
|
622
|
+
domReferenceElement.focus();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const currentIndex = indexRef.current;
|
|
630
|
+
const minIndex = getMinListIndex(listRef, disabledIndices);
|
|
631
|
+
const maxIndex = getMaxListIndex(listRef, disabledIndices);
|
|
632
|
+
|
|
633
|
+
if (!typeableComboboxReference) {
|
|
634
|
+
if (event.key === 'Home') {
|
|
635
|
+
stopEvent(event);
|
|
636
|
+
indexRef.current = minIndex;
|
|
637
|
+
onNavigate(event);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (event.key === 'End') {
|
|
641
|
+
stopEvent(event);
|
|
642
|
+
indexRef.current = maxIndex;
|
|
643
|
+
onNavigate(event);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Grid navigation is injected by grid-capable consumers so non-grid
|
|
648
|
+
// consumers (menu, select) tree-shake the grid helpers out.
|
|
649
|
+
if (navigateGrid != null) {
|
|
650
|
+
const index = navigateGrid(
|
|
651
|
+
event,
|
|
652
|
+
indexRef.current,
|
|
653
|
+
listRef,
|
|
654
|
+
orientation,
|
|
655
|
+
loopFocus,
|
|
656
|
+
rtl,
|
|
657
|
+
disabledIndices,
|
|
658
|
+
minIndex,
|
|
659
|
+
maxIndex,
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
if (index != null) {
|
|
663
|
+
indexRef.current = index;
|
|
664
|
+
onNavigate(event);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (orientation === 'both') {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (isMainOrientationKey(event.key, orientation)) {
|
|
673
|
+
stopEvent(event);
|
|
674
|
+
|
|
675
|
+
// Reset the index if no item is focused.
|
|
676
|
+
if (
|
|
677
|
+
open &&
|
|
678
|
+
!virtual &&
|
|
679
|
+
activeElement(event.currentTarget.ownerDocument) === event.currentTarget
|
|
680
|
+
) {
|
|
681
|
+
indexRef.current = isMainOrientationToEndKey(event.key, orientation, rtl)
|
|
682
|
+
? minIndex
|
|
683
|
+
: maxIndex;
|
|
684
|
+
onNavigate(event);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (isMainOrientationToEndKey(event.key, orientation, rtl)) {
|
|
689
|
+
if (loopFocus) {
|
|
690
|
+
if (currentIndex >= maxIndex) {
|
|
691
|
+
if (allowEscape && currentIndex !== listRef.current.length) {
|
|
692
|
+
indexRef.current = -1;
|
|
693
|
+
} else {
|
|
694
|
+
// Give time for virtualizers to update the listRef.
|
|
695
|
+
forceSyncFocusRef.current = false;
|
|
696
|
+
indexRef.current = minIndex;
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
indexRef.current = findNonDisabledListIndex(listRef.current, {
|
|
700
|
+
startingIndex: currentIndex,
|
|
701
|
+
disabledIndices,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
} else {
|
|
705
|
+
indexRef.current = Math.min(
|
|
706
|
+
maxIndex,
|
|
707
|
+
findNonDisabledListIndex(listRef.current, {
|
|
708
|
+
startingIndex: currentIndex,
|
|
709
|
+
disabledIndices,
|
|
710
|
+
}),
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
} else if (loopFocus) {
|
|
714
|
+
if (currentIndex <= minIndex) {
|
|
715
|
+
if (allowEscape && currentIndex !== -1) {
|
|
716
|
+
indexRef.current = listRef.current.length;
|
|
717
|
+
} else {
|
|
718
|
+
// Give time for virtualizers to update the listRef.
|
|
719
|
+
forceSyncFocusRef.current = false;
|
|
720
|
+
indexRef.current = maxIndex;
|
|
721
|
+
}
|
|
722
|
+
} else {
|
|
723
|
+
indexRef.current = findNonDisabledListIndex(listRef.current, {
|
|
724
|
+
startingIndex: currentIndex,
|
|
725
|
+
decrement: true,
|
|
726
|
+
disabledIndices,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
} else {
|
|
730
|
+
indexRef.current = Math.max(
|
|
731
|
+
minIndex,
|
|
732
|
+
findNonDisabledListIndex(listRef.current, {
|
|
733
|
+
startingIndex: currentIndex,
|
|
734
|
+
decrement: true,
|
|
735
|
+
disabledIndices,
|
|
736
|
+
}),
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (isIndexOutOfListBounds(listRef.current, indexRef.current)) {
|
|
741
|
+
indexRef.current = -1;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
onNavigate(event);
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
subSlot(slot, 'cokd'),
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
const item = useMemo(
|
|
751
|
+
() => {
|
|
752
|
+
const itemProps: ElementProps['item'] = {
|
|
753
|
+
onFocus(event: any) {
|
|
754
|
+
forceSyncFocusRef.current = true;
|
|
755
|
+
syncCurrentTarget(event);
|
|
756
|
+
},
|
|
757
|
+
onClick: ({ currentTarget }: any) => currentTarget.focus({ preventScroll: true }), // Safari
|
|
758
|
+
onMouseMove(event: any) {
|
|
759
|
+
forceSyncFocusRef.current = true;
|
|
760
|
+
forceScrollIntoViewRef.current = false;
|
|
761
|
+
if (focusItemOnHover) {
|
|
762
|
+
syncCurrentTarget(event);
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
onPointerLeave(event: any) {
|
|
766
|
+
if (
|
|
767
|
+
!latestOpenRef.current ||
|
|
768
|
+
!isPointerModalityRef.current ||
|
|
769
|
+
event.pointerType === 'touch'
|
|
770
|
+
) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
forceSyncFocusRef.current = true;
|
|
775
|
+
|
|
776
|
+
const relatedTarget = event.relatedTarget as HTMLElement | null;
|
|
777
|
+
|
|
778
|
+
if (!focusItemOnHover || listRef.current.includes(relatedTarget)) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (!resetOnPointerLeaveRef.current) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
cancelQueuedFocusRef.current?.();
|
|
787
|
+
cancelQueuedFocusRef.current = null;
|
|
788
|
+
|
|
789
|
+
indexRef.current = -1;
|
|
790
|
+
onNavigate(event);
|
|
791
|
+
|
|
792
|
+
if (!virtual) {
|
|
793
|
+
const floatingFocusEl = floatingFocusElementRef.current;
|
|
794
|
+
const activeEl = activeElement(ownerDocument(floatingFocusEl));
|
|
795
|
+
if (floatingFocusEl && contains(floatingFocusEl, activeEl)) {
|
|
796
|
+
floatingFocusEl.focus({ preventScroll: true });
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
return itemProps;
|
|
803
|
+
},
|
|
804
|
+
[
|
|
805
|
+
syncCurrentTarget,
|
|
806
|
+
latestOpenRef,
|
|
807
|
+
floatingFocusElementRef,
|
|
808
|
+
focusItemOnHover,
|
|
809
|
+
listRef,
|
|
810
|
+
onNavigate,
|
|
811
|
+
resetOnPointerLeaveRef,
|
|
812
|
+
virtual,
|
|
813
|
+
],
|
|
814
|
+
subSlot(slot, 'item'),
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
const ariaActiveDescendantProp = useMemo(
|
|
818
|
+
() => {
|
|
819
|
+
return (
|
|
820
|
+
virtual &&
|
|
821
|
+
open &&
|
|
822
|
+
hasActiveIndex && {
|
|
823
|
+
'aria-activedescendant': `${id}-${activeIndex}`,
|
|
824
|
+
}
|
|
825
|
+
);
|
|
826
|
+
},
|
|
827
|
+
[virtual, open, hasActiveIndex, id, activeIndex],
|
|
828
|
+
subSlot(slot, 'aad'),
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
const floating: ElementProps['floating'] = useMemo(
|
|
832
|
+
() => {
|
|
833
|
+
return {
|
|
834
|
+
'aria-orientation': orientation === 'both' ? undefined : orientation,
|
|
835
|
+
...(!typeableComboboxReference ? ariaActiveDescendantProp : {}),
|
|
836
|
+
onKeyDown(event: any) {
|
|
837
|
+
// Close submenu on Shift+Tab
|
|
838
|
+
if (event.key === 'Tab' && event.shiftKey && open && !virtual) {
|
|
839
|
+
// If the event originated from within a nested element (e.g., a Dialog opened from
|
|
840
|
+
// within the menu), don't close the menu. The nested element has its own focus
|
|
841
|
+
// management and should handle the Tab key.
|
|
842
|
+
const target = getTarget(event) as Element | null;
|
|
843
|
+
if (target && !contains(floatingFocusElementRef.current, target)) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
stopEvent(event);
|
|
848
|
+
store.setOpen(false, createChangeEventDetails(REASONS.focusOut, event));
|
|
849
|
+
|
|
850
|
+
if (isHTMLElement(domReferenceElement)) {
|
|
851
|
+
domReferenceElement.focus();
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
commonOnKeyDown(event);
|
|
858
|
+
},
|
|
859
|
+
onPointerMove() {
|
|
860
|
+
isPointerModalityRef.current = true;
|
|
861
|
+
},
|
|
862
|
+
};
|
|
863
|
+
},
|
|
864
|
+
[
|
|
865
|
+
ariaActiveDescendantProp,
|
|
866
|
+
commonOnKeyDown,
|
|
867
|
+
floatingFocusElementRef,
|
|
868
|
+
orientation,
|
|
869
|
+
typeableComboboxReference,
|
|
870
|
+
store,
|
|
871
|
+
open,
|
|
872
|
+
virtual,
|
|
873
|
+
domReferenceElement,
|
|
874
|
+
],
|
|
875
|
+
subSlot(slot, 'floating'),
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
const trigger: ElementProps['trigger'] = useMemo(
|
|
879
|
+
() => {
|
|
880
|
+
function openOnNavigationKeyDown(event: any) {
|
|
881
|
+
store.setOpen(
|
|
882
|
+
true,
|
|
883
|
+
createChangeEventDetails(
|
|
884
|
+
REASONS.listNavigation,
|
|
885
|
+
event,
|
|
886
|
+
event.currentTarget as HTMLElement,
|
|
887
|
+
),
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function checkVirtualMouse(event: any) {
|
|
892
|
+
if (focusItemOnOpen === 'auto' && isVirtualClick(event)) {
|
|
893
|
+
focusItemOnOpenRef.current = !virtual;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function checkVirtualPointer(event: any) {
|
|
898
|
+
// `pointerdown` fires first, reset the state then perform the checks.
|
|
899
|
+
focusItemOnOpenRef.current = focusItemOnOpen;
|
|
900
|
+
if (focusItemOnOpen === 'auto' && isVirtualPointerEvent(event)) {
|
|
901
|
+
focusItemOnOpenRef.current = true;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
onKeyDown(event: any) {
|
|
907
|
+
// non-reactive open state (to prevent re-creation of the handler)
|
|
908
|
+
const currentOpen = store.select('open');
|
|
909
|
+
isPointerModalityRef.current = false;
|
|
910
|
+
|
|
911
|
+
const isArrowKey = event.key.startsWith('Arrow');
|
|
912
|
+
const isParentCrossOpenKey = isCrossOrientationOpenKey(
|
|
913
|
+
event.key,
|
|
914
|
+
getParentOrientation(),
|
|
915
|
+
rtl,
|
|
916
|
+
);
|
|
917
|
+
const isMainKey = isMainOrientationKey(event.key, orientation);
|
|
918
|
+
const isNavigationKey =
|
|
919
|
+
(nested ? isParentCrossOpenKey : isMainKey) ||
|
|
920
|
+
event.key === 'Enter' ||
|
|
921
|
+
event.key.trim() === '';
|
|
922
|
+
|
|
923
|
+
if (virtual && currentOpen) {
|
|
924
|
+
return commonOnKeyDown(event);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// If a floating element should not open on arrow key down, avoid
|
|
928
|
+
// setting `activeIndex` while it's closed.
|
|
929
|
+
if (!currentOpen && !openOnArrowKeyDown && isArrowKey) {
|
|
930
|
+
return undefined;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (isNavigationKey) {
|
|
934
|
+
const isParentMainKey = isMainOrientationKey(event.key, getParentOrientation());
|
|
935
|
+
keyRef.current = nested && isParentMainKey ? null : event.key;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (nested) {
|
|
939
|
+
if (isParentCrossOpenKey) {
|
|
940
|
+
stopEvent(event);
|
|
941
|
+
|
|
942
|
+
if (currentOpen) {
|
|
943
|
+
indexRef.current = getMinEnabledIndex();
|
|
944
|
+
onNavigate(event);
|
|
945
|
+
} else {
|
|
946
|
+
openOnNavigationKeyDown(event);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return undefined;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (isMainKey) {
|
|
954
|
+
if (selectedIndexRef.current != null) {
|
|
955
|
+
indexRef.current = selectedIndexRef.current;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
stopEvent(event);
|
|
959
|
+
|
|
960
|
+
if (!currentOpen && openOnArrowKeyDown) {
|
|
961
|
+
openOnNavigationKeyDown(event);
|
|
962
|
+
} else {
|
|
963
|
+
commonOnKeyDown(event);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (currentOpen) {
|
|
967
|
+
onNavigate(event);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return undefined;
|
|
972
|
+
},
|
|
973
|
+
onFocus(event: any) {
|
|
974
|
+
if (store.select('open') && !virtual) {
|
|
975
|
+
indexRef.current = -1;
|
|
976
|
+
onNavigate(event);
|
|
977
|
+
}
|
|
978
|
+
},
|
|
979
|
+
onPointerDown: checkVirtualPointer,
|
|
980
|
+
onPointerEnter: checkVirtualPointer,
|
|
981
|
+
onMouseDown: checkVirtualMouse,
|
|
982
|
+
onClick: checkVirtualMouse,
|
|
983
|
+
};
|
|
984
|
+
},
|
|
985
|
+
[
|
|
986
|
+
commonOnKeyDown,
|
|
987
|
+
focusItemOnOpen,
|
|
988
|
+
getMinEnabledIndex,
|
|
989
|
+
nested,
|
|
990
|
+
onNavigate,
|
|
991
|
+
store,
|
|
992
|
+
openOnArrowKeyDown,
|
|
993
|
+
orientation,
|
|
994
|
+
getParentOrientation,
|
|
995
|
+
rtl,
|
|
996
|
+
selectedIndexRef,
|
|
997
|
+
virtual,
|
|
998
|
+
],
|
|
999
|
+
subSlot(slot, 'trigger'),
|
|
1000
|
+
);
|
|
1001
|
+
|
|
1002
|
+
const reference: ElementProps['reference'] = useMemo(
|
|
1003
|
+
() => {
|
|
1004
|
+
return {
|
|
1005
|
+
...ariaActiveDescendantProp,
|
|
1006
|
+
...trigger,
|
|
1007
|
+
};
|
|
1008
|
+
},
|
|
1009
|
+
[ariaActiveDescendantProp, trigger],
|
|
1010
|
+
subSlot(slot, 'ref'),
|
|
1011
|
+
);
|
|
1012
|
+
|
|
1013
|
+
return useMemo(
|
|
1014
|
+
() => (enabled ? { reference, floating, item, trigger } : {}),
|
|
1015
|
+
[enabled, reference, floating, trigger, item],
|
|
1016
|
+
subSlot(slot, 'out'),
|
|
1017
|
+
);
|
|
1018
|
+
}
|