@react-aria/menu 3.15.5 → 3.16.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.
@@ -15,6 +15,7 @@ import {filterDOMProps, mergeProps, useLinkProps, useRouter, useSlotId} from '@r
15
15
  import {getItemCount} from '@react-stately/collections';
16
16
  import {isFocusVisible, useFocus, useHover, useKeyboard, usePress} from '@react-aria/interactions';
17
17
  import {menuData} from './useMenu';
18
+ import {SelectionManager} from '@react-stately/selection';
18
19
  import {TreeState} from '@react-stately/tree';
19
20
  import {useSelectableItem} from '@react-aria/selection';
20
21
 
@@ -58,7 +59,7 @@ export interface AriaMenuItemProps extends DOMProps, PressEvents, HoverEvents, K
58
59
  'aria-label'?: string,
59
60
 
60
61
  /** The unique key for the menu item. */
61
- key?: Key,
62
+ key: Key,
62
63
 
63
64
  /**
64
65
  * Handler that is called when the menu should close after selecting an item.
@@ -88,7 +89,10 @@ export interface AriaMenuItemProps extends DOMProps, PressEvents, HoverEvents, K
88
89
  'aria-expanded'?: boolean | 'true' | 'false',
89
90
 
90
91
  /** Identifies the menu item's popup element whose contents or presence is controlled by the menu item. */
91
- 'aria-controls'?: string
92
+ 'aria-controls'?: string,
93
+
94
+ /** Override of the selection manager. By default, `state.selectionManager` is used. */
95
+ selectionManager?: SelectionManager
92
96
  }
93
97
 
94
98
  /**
@@ -116,13 +120,15 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
116
120
  onKeyUp,
117
121
  onFocus,
118
122
  onFocusChange,
119
- onBlur
123
+ onBlur,
124
+ selectionManager = state.selectionManager
120
125
  } = props;
121
126
 
122
127
  let isTrigger = !!hasPopup;
123
- let isDisabled = props.isDisabled ?? state.selectionManager.isDisabled(key);
124
- let isSelected = props.isSelected ?? state.selectionManager.isSelected(key);
125
- let data = menuData.get(state);
128
+ let isTriggerExpanded = isTrigger && props['aria-expanded'] === 'true';
129
+ let isDisabled = props.isDisabled ?? selectionManager.isDisabled(key);
130
+ let isSelected = props.isSelected ?? selectionManager.isSelected(key);
131
+ let data = menuData.get(state)!;
126
132
  let item = state.collection.getItem(key);
127
133
  let onClose = props.onClose || data.onClose;
128
134
  let router = useRouter();
@@ -143,16 +149,16 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
143
149
  onAction(key);
144
150
  }
145
151
 
146
- if (e.target instanceof HTMLAnchorElement) {
152
+ if (e.target instanceof HTMLAnchorElement && item) {
147
153
  router.open(e.target, e, item.props.href, item.props.routerOptions as RouterOptions);
148
154
  }
149
155
  };
150
156
 
151
157
  let role = 'menuitem';
152
158
  if (!isTrigger) {
153
- if (state.selectionManager.selectionMode === 'single') {
159
+ if (selectionManager.selectionMode === 'single') {
154
160
  role = 'menuitemradio';
155
- } else if (state.selectionManager.selectionMode === 'multiple') {
161
+ } else if (selectionManager.selectionMode === 'multiple') {
156
162
  role = 'menuitemcheckbox';
157
163
  }
158
164
  }
@@ -173,7 +179,7 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
173
179
  'aria-expanded': props['aria-expanded']
174
180
  };
175
181
 
176
- if (state.selectionManager.selectionMode !== 'none' && !isTrigger) {
182
+ if (selectionManager.selectionMode !== 'none' && !isTrigger) {
177
183
  ariaProps['aria-checked'] = isSelected;
178
184
  }
179
185
 
@@ -196,7 +202,7 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
196
202
 
197
203
  // Pressing a menu item should close by default in single selection mode but not multiple
198
204
  // selection mode, except if overridden by the closeOnSelect prop.
199
- if (!isTrigger && onClose && (closeOnSelect ?? (state.selectionManager.selectionMode !== 'multiple' || state.selectionManager.isLink(key)))) {
205
+ if (!isTrigger && onClose && (closeOnSelect ?? (selectionManager.selectionMode !== 'multiple' || selectionManager.isLink(key)))) {
200
206
  onClose();
201
207
  }
202
208
  }
@@ -205,7 +211,7 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
205
211
  };
206
212
 
207
213
  let {itemProps, isFocused} = useSelectableItem({
208
- selectionManager: state.selectionManager,
214
+ selectionManager: selectionManager,
209
215
  key,
210
216
  ref,
211
217
  shouldSelectOnPressUp: true,
@@ -228,9 +234,10 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
228
234
  let {hoverProps} = useHover({
229
235
  isDisabled,
230
236
  onHoverStart(e) {
231
- if (!isFocusVisible()) {
232
- state.selectionManager.setFocused(true);
233
- state.selectionManager.setFocusedKey(key);
237
+ // Hovering over an already expanded sub dialog trigger should keep focus in the dialog.
238
+ if (!isFocusVisible() && !(isTriggerExpanded && hasPopup === 'dialog')) {
239
+ selectionManager.setFocused(true);
240
+ selectionManager.setFocusedKey(key);
234
241
  }
235
242
  hoverStartProp?.(e);
236
243
  },
@@ -249,7 +256,7 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
249
256
 
250
257
  switch (e.key) {
251
258
  case ' ':
252
- if (!isDisabled && state.selectionManager.selectionMode === 'none' && !isTrigger && closeOnSelect !== false && onClose) {
259
+ if (!isDisabled && selectionManager.selectionMode === 'none' && !isTrigger && closeOnSelect !== false && onClose) {
253
260
  onClose();
254
261
  }
255
262
  break;
@@ -272,15 +279,16 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
272
279
  });
273
280
 
274
281
  let {focusProps} = useFocus({onBlur, onFocus, onFocusChange});
275
- let domProps = filterDOMProps(item.props);
282
+ let domProps = filterDOMProps(item?.props);
276
283
  delete domProps.id;
277
- let linkProps = useLinkProps(item.props);
284
+ let linkProps = useLinkProps(item?.props);
278
285
 
279
286
  return {
280
287
  menuItemProps: {
281
288
  ...ariaProps,
282
289
  ...mergeProps(domProps, linkProps, isTrigger ? {onFocus: itemProps.onFocus, 'data-key': itemProps['data-key']} : itemProps, pressProps, hoverProps, keyboardProps, focusProps),
283
- tabIndex: itemProps.tabIndex != null ? -1 : undefined
290
+ // If a submenu is expanded, set the tabIndex to -1 so that shift tabbing goes out of the menu instead of the parent menu item.
291
+ tabIndex: itemProps.tabIndex != null && isTriggerExpanded ? -1 : itemProps.tabIndex
284
292
  },
285
293
  labelProps: {
286
294
  id: labelId
@@ -47,7 +47,7 @@ export interface MenuTriggerAria<T> {
47
47
  */
48
48
  export function useMenuTrigger<T>(props: AriaMenuTriggerProps, state: MenuTriggerState, ref: RefObject<Element | null>): MenuTriggerAria<T> {
49
49
  let {
50
- type = 'menu' as AriaMenuTriggerProps['type'],
50
+ type = 'menu',
51
51
  isDisabled,
52
52
  trigger = 'press'
53
53
  } = props;
@@ -128,6 +128,7 @@ export function useMenuTrigger<T>(props: AriaMenuTriggerProps, state: MenuTrigge
128
128
  delete triggerProps.onPress;
129
129
 
130
130
  return {
131
+ // @ts-ignore - TODO we pass out both DOMAttributes AND AriaButtonProps, but useButton will discard the longPress event handlers, it's only through PressResponder magic that this works for RSP and RAC. it does not work in aria examples
131
132
  menuTriggerProps: {
132
133
  ...triggerProps,
133
134
  ...(trigger === 'press' ? pressProps : longPressProps),
@@ -63,7 +63,7 @@ export function useSafelyMouseToSubmenu(options: SafelyMouseToSubmenuOptions) {
63
63
  let submenu = submenuRef.current;
64
64
  let menu = menuRef.current;
65
65
 
66
- if (isDisabled || !submenu || !isOpen || modality !== 'pointer') {
66
+ if (isDisabled || !submenu || !isOpen || modality !== 'pointer' || !menu) {
67
67
  reset();
68
68
  return;
69
69
  }
@@ -41,7 +41,7 @@ export interface AriaSubmenuTriggerProps {
41
41
  delay?: number
42
42
  }
43
43
 
44
- interface SubmenuTriggerProps extends AriaMenuItemProps {
44
+ interface SubmenuTriggerProps extends Omit<AriaMenuItemProps, 'key'> {
45
45
  /** Whether the submenu trigger is in an expanded state. */
46
46
  isOpen: boolean
47
47
  }
@@ -101,14 +101,14 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
101
101
  if (direction === 'ltr' && e.currentTarget.contains(e.target as Element)) {
102
102
  e.stopPropagation();
103
103
  onSubmenuClose();
104
- ref.current.focus();
104
+ ref.current?.focus();
105
105
  }
106
106
  break;
107
107
  case 'ArrowRight':
108
108
  if (direction === 'rtl' && e.currentTarget.contains(e.target as Element)) {
109
109
  e.stopPropagation();
110
110
  onSubmenuClose();
111
- ref.current.focus();
111
+ ref.current?.focus();
112
112
  }
113
113
  break;
114
114
  case 'Escape':
@@ -124,7 +124,7 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
124
124
  submenuLevel: state.submenuLevel,
125
125
  ...(type === 'menu' && {
126
126
  onClose: state.closeAll,
127
- autoFocus: state.focusStrategy,
127
+ autoFocus: state.focusStrategy ?? undefined,
128
128
  onKeyDown: submenuKeyDown
129
129
  })
130
130
  };
@@ -205,7 +205,7 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
205
205
  };
206
206
 
207
207
  let onBlur = (e) => {
208
- if (state.isOpen && parentMenuRef.current.contains(e.relatedTarget)) {
208
+ if (state.isOpen && parentMenuRef.current?.contains(e.relatedTarget)) {
209
209
  onSubmenuClose();
210
210
  }
211
211
  };