@react-aria/menu 3.19.4 → 3.21.0
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/dist/types.d.ts +6 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/useMenuItem.main.js +17 -8
- package/dist/useMenuItem.main.js.map +1 -1
- package/dist/useMenuItem.mjs +19 -10
- package/dist/useMenuItem.module.js +19 -10
- package/dist/useMenuItem.module.js.map +1 -1
- package/dist/useSafelyMouseToSubmenu.main.js +3 -3
- package/dist/useSafelyMouseToSubmenu.main.js.map +1 -1
- package/dist/useSafelyMouseToSubmenu.mjs +3 -3
- package/dist/useSafelyMouseToSubmenu.module.js +3 -3
- package/dist/useSafelyMouseToSubmenu.module.js.map +1 -1
- package/dist/useSubmenuTrigger.main.js +7 -9
- package/dist/useSubmenuTrigger.main.js.map +1 -1
- package/dist/useSubmenuTrigger.mjs +8 -10
- package/dist/useSubmenuTrigger.module.js +8 -10
- package/dist/useSubmenuTrigger.module.js.map +1 -1
- package/package.json +15 -15
- package/src/useMenuItem.ts +32 -14
- package/src/useSafelyMouseToSubmenu.ts +3 -3
- package/src/useSubmenuTrigger.ts +9 -9
package/src/useMenuItem.ts
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {DOMAttributes, DOMProps, FocusableElement, FocusEvents, HoverEvents, Key, KeyboardEvents, PressEvent, PressEvents, RefObject} from '@react-types/shared';
|
|
14
|
-
import {filterDOMProps, handleLinkClick, mergeProps, useLinkProps, useRouter, useSlotId} from '@react-aria/utils';
|
|
14
|
+
import {filterDOMProps, getEventTarget, handleLinkClick, mergeProps, useLinkProps, useRouter, useSlotId} from '@react-aria/utils';
|
|
15
15
|
import {getItemCount} from '@react-stately/collections';
|
|
16
|
-
import {isFocusVisible,
|
|
16
|
+
import {isFocusVisible, setInteractionModality, useFocusable, useHover, useKeyboard, usePress} from '@react-aria/interactions';
|
|
17
17
|
import {menuData} from './utils';
|
|
18
18
|
import {MouseEvent, useRef} from 'react';
|
|
19
19
|
import {SelectionManager} from '@react-stately/selection';
|
|
@@ -72,10 +72,13 @@ export interface AriaMenuItemProps extends DOMProps, PressEvents, HoverEvents, K
|
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* Whether the menu should close when the menu item is selected.
|
|
75
|
-
* @
|
|
75
|
+
* @deprecated - use shouldCloseOnSelect instead.
|
|
76
76
|
*/
|
|
77
77
|
closeOnSelect?: boolean,
|
|
78
78
|
|
|
79
|
+
/** Whether the menu should close when the menu item is selected. */
|
|
80
|
+
shouldCloseOnSelect?: boolean,
|
|
81
|
+
|
|
79
82
|
/** Whether the menu item is contained in a virtual scrolling menu. */
|
|
80
83
|
isVirtualized?: boolean,
|
|
81
84
|
|
|
@@ -94,6 +97,9 @@ export interface AriaMenuItemProps extends DOMProps, PressEvents, HoverEvents, K
|
|
|
94
97
|
/** Identifies the menu item's popup element whose contents or presence is controlled by the menu item. */
|
|
95
98
|
'aria-controls'?: string,
|
|
96
99
|
|
|
100
|
+
/** Identifies the element(s) that describe the menu item. */
|
|
101
|
+
'aria-describedby'?: string,
|
|
102
|
+
|
|
97
103
|
/** Override of the selection manager. By default, `state.selectionManager` is used. */
|
|
98
104
|
selectionManager?: SelectionManager
|
|
99
105
|
}
|
|
@@ -109,6 +115,7 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
|
|
|
109
115
|
id,
|
|
110
116
|
key,
|
|
111
117
|
closeOnSelect,
|
|
118
|
+
shouldCloseOnSelect,
|
|
112
119
|
isVirtualized,
|
|
113
120
|
'aria-haspopup': hasPopup,
|
|
114
121
|
onPressStart,
|
|
@@ -173,7 +180,7 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
|
|
|
173
180
|
role,
|
|
174
181
|
'aria-label': props['aria-label'],
|
|
175
182
|
'aria-labelledby': labelId,
|
|
176
|
-
'aria-describedby': [descriptionId, keyboardId].filter(Boolean).join(' ') || undefined,
|
|
183
|
+
'aria-describedby': [props['aria-describedby'], descriptionId, keyboardId].filter(Boolean).join(' ') || undefined,
|
|
177
184
|
'aria-controls': props['aria-controls'],
|
|
178
185
|
'aria-haspopup': hasPopup,
|
|
179
186
|
'aria-expanded': props['aria-expanded']
|
|
@@ -184,7 +191,8 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
|
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
if (isVirtualized) {
|
|
187
|
-
|
|
194
|
+
let index = Number(item?.index);
|
|
195
|
+
ariaProps['aria-posinset'] = Number.isNaN(index) ? undefined : index + 1;
|
|
188
196
|
ariaProps['aria-setsize'] = getItemCount(state.collection);
|
|
189
197
|
}
|
|
190
198
|
|
|
@@ -221,8 +229,10 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
|
|
|
221
229
|
? interaction.current?.key === 'Enter' || selectionManager.selectionMode === 'none' || selectionManager.isLink(key)
|
|
222
230
|
// Close except if multi-select is enabled.
|
|
223
231
|
: selectionManager.selectionMode !== 'multiple' || selectionManager.isLink(key);
|
|
224
|
-
|
|
225
|
-
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
shouldClose = shouldCloseOnSelect ?? closeOnSelect ?? shouldClose;
|
|
235
|
+
|
|
226
236
|
if (onClose && !isTrigger && shouldClose) {
|
|
227
237
|
onClose();
|
|
228
238
|
}
|
|
@@ -279,15 +289,23 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
|
|
|
279
289
|
switch (e.key) {
|
|
280
290
|
case ' ':
|
|
281
291
|
interaction.current = {pointerType: 'keyboard', key: ' '};
|
|
282
|
-
(e
|
|
292
|
+
(getEventTarget(e) as HTMLElement).click();
|
|
293
|
+
|
|
294
|
+
// click above sets modality to "virtual", need to set interaction modality back to 'keyboard' so focusSafely calls properly move focus
|
|
295
|
+
// to the newly opened submenu's first item.
|
|
296
|
+
setInteractionModality('keyboard');
|
|
283
297
|
break;
|
|
284
298
|
case 'Enter':
|
|
285
299
|
interaction.current = {pointerType: 'keyboard', key: 'Enter'};
|
|
286
300
|
|
|
287
301
|
// Trigger click unless this is a link. Links trigger click natively.
|
|
288
|
-
if ((e
|
|
289
|
-
(e
|
|
302
|
+
if ((getEventTarget(e) as HTMLElement).tagName !== 'A') {
|
|
303
|
+
(getEventTarget(e) as HTMLElement).click();
|
|
290
304
|
}
|
|
305
|
+
|
|
306
|
+
// click above sets modality to "virtual", need to set interaction modality back to 'keyboard' so focusSafely calls properly move focus
|
|
307
|
+
// to the newly opened submenu's first item.
|
|
308
|
+
setInteractionModality('keyboard');
|
|
291
309
|
break;
|
|
292
310
|
default:
|
|
293
311
|
if (!isTrigger) {
|
|
@@ -301,7 +319,7 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
|
|
|
301
319
|
onKeyUp
|
|
302
320
|
});
|
|
303
321
|
|
|
304
|
-
let {
|
|
322
|
+
let {focusableProps} = useFocusable({onBlur, onFocus, onFocusChange}, ref);
|
|
305
323
|
let domProps = filterDOMProps(item?.props);
|
|
306
324
|
delete domProps.id;
|
|
307
325
|
let linkProps = useLinkProps(item?.props);
|
|
@@ -312,13 +330,13 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
|
|
|
312
330
|
...mergeProps(
|
|
313
331
|
domProps,
|
|
314
332
|
linkProps,
|
|
315
|
-
isTrigger
|
|
316
|
-
? {onFocus: itemProps.onFocus, 'data-collection': itemProps['data-collection'], 'data-key': itemProps['data-key']}
|
|
333
|
+
isTrigger
|
|
334
|
+
? {onFocus: itemProps.onFocus, 'data-collection': itemProps['data-collection'], 'data-key': itemProps['data-key']}
|
|
317
335
|
: itemProps,
|
|
318
336
|
pressProps,
|
|
319
337
|
hoverProps,
|
|
320
338
|
keyboardProps,
|
|
321
|
-
|
|
339
|
+
focusableProps,
|
|
322
340
|
// Prevent DOM focus from moving on mouse down when using virtual focus or this is a submenu/subdialog trigger.
|
|
323
341
|
data.shouldUseVirtualFocus || isTrigger ? {onMouseDown: e => e.preventDefault()} : undefined,
|
|
324
342
|
isDisabled ? undefined : {onClick}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
|
+
import {nodeContains, useEffectEvent, useLayoutEffect, useResizeObserver} from '@react-aria/utils';
|
|
2
3
|
import {RefObject} from '@react-types/shared';
|
|
3
4
|
import {useEffect, useRef, useState} from 'react';
|
|
4
|
-
import {useEffectEvent, useLayoutEffect, useResizeObserver} from '@react-aria/utils';
|
|
5
5
|
import {useInteractionModality} from '@react-aria/interactions';
|
|
6
6
|
|
|
7
7
|
interface SafelyMouseToSubmenuOptions {
|
|
@@ -41,7 +41,7 @@ export function useSafelyMouseToSubmenu(options: SafelyMouseToSubmenuOptions): v
|
|
|
41
41
|
submenuSide.current = undefined;
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
|
-
useResizeObserver({ref: submenuRef, onResize: updateSubmenuRect});
|
|
44
|
+
useResizeObserver({ref: isOpen ? submenuRef : undefined, onResize: updateSubmenuRect});
|
|
45
45
|
|
|
46
46
|
let reset = () => {
|
|
47
47
|
setPreventPointerEvents(false);
|
|
@@ -148,7 +148,7 @@ export function useSafelyMouseToSubmenu(options: SafelyMouseToSubmenuOptions): v
|
|
|
148
148
|
// Fire a pointerover event to trigger the menu to close.
|
|
149
149
|
// Wait until pointer-events:none is no longer applied
|
|
150
150
|
let target = document.elementFromPoint(mouseX, mouseY);
|
|
151
|
-
if (target && menu
|
|
151
|
+
if (target && nodeContains(menu, target)) {
|
|
152
152
|
target.dispatchEvent(new PointerEvent('pointerover', {bubbles: true, cancelable: true}));
|
|
153
153
|
}
|
|
154
154
|
}, 100);
|
package/src/useSubmenuTrigger.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {AriaMenuItemProps} from './useMenuItem';
|
|
|
14
14
|
import {AriaMenuOptions} from './useMenu';
|
|
15
15
|
import type {AriaPopoverProps, OverlayProps} from '@react-aria/overlays';
|
|
16
16
|
import {FocusableElement, FocusStrategy, KeyboardEvent, Node, PressEvent, RefObject} from '@react-types/shared';
|
|
17
|
-
import {focusWithoutScrolling, useEvent, useId, useLayoutEffect} from '@react-aria/utils';
|
|
17
|
+
import {focusWithoutScrolling, getActiveElement, getEventTarget, isFocusWithin, nodeContains, useEvent, useId, useLayoutEffect} from '@react-aria/utils';
|
|
18
18
|
import type {SubmenuTriggerState} from '@react-stately/menu';
|
|
19
19
|
import {useCallback, useRef} from 'react';
|
|
20
20
|
import {useLocale} from '@react-aria/i18n';
|
|
@@ -43,7 +43,7 @@ export interface AriaSubmenuTriggerProps {
|
|
|
43
43
|
shouldUseVirtualFocus?: boolean
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
interface SubmenuTriggerProps extends Omit<AriaMenuItemProps, 'key'> {
|
|
46
|
+
interface SubmenuTriggerProps extends Omit<AriaMenuItemProps, 'key' | 'onAction'> {
|
|
47
47
|
/** Whether the submenu trigger is in an expanded state. */
|
|
48
48
|
isOpen: boolean
|
|
49
49
|
}
|
|
@@ -100,13 +100,13 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
|
|
|
100
100
|
let submenuKeyDown = (e: KeyboardEvent) => {
|
|
101
101
|
// If focus is not within the menu, assume virtual focus is being used.
|
|
102
102
|
// This means some other input element is also within the popover, so we shouldn't close the menu.
|
|
103
|
-
if (!e.currentTarget
|
|
103
|
+
if (!isFocusWithin(e.currentTarget)) {
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
switch (e.key) {
|
|
108
108
|
case 'ArrowLeft':
|
|
109
|
-
if (direction === 'ltr' && e.currentTarget
|
|
109
|
+
if (direction === 'ltr' && nodeContains(e.currentTarget, getEventTarget(e) as Element)) {
|
|
110
110
|
e.preventDefault();
|
|
111
111
|
e.stopPropagation();
|
|
112
112
|
onSubmenuClose();
|
|
@@ -116,7 +116,7 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
|
|
|
116
116
|
}
|
|
117
117
|
break;
|
|
118
118
|
case 'ArrowRight':
|
|
119
|
-
if (direction === 'rtl' && e.currentTarget
|
|
119
|
+
if (direction === 'rtl' && nodeContains(e.currentTarget, getEventTarget(e) as Element)) {
|
|
120
120
|
e.preventDefault();
|
|
121
121
|
e.stopPropagation();
|
|
122
122
|
onSubmenuClose();
|
|
@@ -127,7 +127,7 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
|
|
|
127
127
|
break;
|
|
128
128
|
case 'Escape':
|
|
129
129
|
// TODO: can remove this when we fix collection event leaks
|
|
130
|
-
if (submenuRef.current
|
|
130
|
+
if (nodeContains(submenuRef.current, getEventTarget(e) as Element)) {
|
|
131
131
|
e.stopPropagation();
|
|
132
132
|
onSubmenuClose();
|
|
133
133
|
if (!shouldUseVirtualFocus && ref.current) {
|
|
@@ -159,7 +159,7 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
|
|
|
159
159
|
onSubmenuOpen('first');
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
if (type === 'menu' && !!submenuRef?.current &&
|
|
162
|
+
if (type === 'menu' && !!submenuRef?.current && getActiveElement() === ref?.current) {
|
|
163
163
|
focusWithoutScrolling(submenuRef.current);
|
|
164
164
|
}
|
|
165
165
|
} else if (state.isOpen) {
|
|
@@ -178,7 +178,7 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
|
|
|
178
178
|
onSubmenuOpen('first');
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
if (type === 'menu' && !!submenuRef?.current &&
|
|
181
|
+
if (type === 'menu' && !!submenuRef?.current && getActiveElement() === ref?.current) {
|
|
182
182
|
focusWithoutScrolling(submenuRef.current);
|
|
183
183
|
}
|
|
184
184
|
} else if (state.isOpen) {
|
|
@@ -226,7 +226,7 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
|
|
|
226
226
|
useEvent(parentMenuRef, 'focusin', (e) => {
|
|
227
227
|
// If we detect focus moved to a different item in the same menu that the currently open submenu trigger is in
|
|
228
228
|
// then close the submenu. This is for a case where the user hovers a root menu item when multiple submenus are open
|
|
229
|
-
if (state.isOpen && (parentMenuRef.current
|
|
229
|
+
if (state.isOpen && (nodeContains(parentMenuRef.current, getEventTarget(e) as HTMLElement) && getEventTarget(e) !== ref.current)) {
|
|
230
230
|
onSubmenuClose();
|
|
231
231
|
}
|
|
232
232
|
});
|