@react-aria/menu 3.16.0 → 3.18.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.
Files changed (39) hide show
  1. package/dist/types.d.ts +8 -0
  2. package/dist/types.d.ts.map +1 -1
  3. package/dist/useMenu.main.js +6 -5
  4. package/dist/useMenu.main.js.map +1 -1
  5. package/dist/useMenu.mjs +7 -5
  6. package/dist/useMenu.module.js +7 -5
  7. package/dist/useMenu.module.js.map +1 -1
  8. package/dist/useMenuItem.main.js +30 -11
  9. package/dist/useMenuItem.main.js.map +1 -1
  10. package/dist/useMenuItem.mjs +30 -11
  11. package/dist/useMenuItem.module.js +30 -11
  12. package/dist/useMenuItem.module.js.map +1 -1
  13. package/dist/useMenuTrigger.main.js +9 -4
  14. package/dist/useMenuTrigger.main.js.map +1 -1
  15. package/dist/useMenuTrigger.mjs +10 -5
  16. package/dist/useMenuTrigger.module.js +10 -5
  17. package/dist/useMenuTrigger.module.js.map +1 -1
  18. package/dist/useSafelyMouseToSubmenu.main.js +9 -1
  19. package/dist/useSafelyMouseToSubmenu.main.js.map +1 -1
  20. package/dist/useSafelyMouseToSubmenu.mjs +9 -1
  21. package/dist/useSafelyMouseToSubmenu.module.js +9 -1
  22. package/dist/useSafelyMouseToSubmenu.module.js.map +1 -1
  23. package/dist/useSubmenuTrigger.main.js +20 -14
  24. package/dist/useSubmenuTrigger.main.js.map +1 -1
  25. package/dist/useSubmenuTrigger.mjs +20 -14
  26. package/dist/useSubmenuTrigger.module.js +20 -14
  27. package/dist/useSubmenuTrigger.module.js.map +1 -1
  28. package/dist/utils.main.js +20 -0
  29. package/dist/utils.main.js.map +1 -0
  30. package/dist/utils.mjs +15 -0
  31. package/dist/utils.module.js +15 -0
  32. package/dist/utils.module.js.map +1 -0
  33. package/package.json +15 -15
  34. package/src/useMenu.ts +10 -12
  35. package/src/useMenuItem.ts +44 -13
  36. package/src/useMenuTrigger.ts +8 -4
  37. package/src/useSafelyMouseToSubmenu.ts +19 -2
  38. package/src/useSubmenuTrigger.ts +32 -14
  39. package/src/utils.ts +22 -0
@@ -1,8 +1,8 @@
1
1
 
2
2
  import {RefObject} from '@react-types/shared';
3
3
  import {useEffect, useRef, useState} from 'react';
4
+ import {useEffectEvent, useResizeObserver} from '@react-aria/utils';
4
5
  import {useInteractionModality} from '@react-aria/interactions';
5
- import {useResizeObserver} from '@react-aria/utils';
6
6
 
7
7
  interface SafelyMouseToSubmenuOptions {
8
8
  /** Ref for the parent menu. */
@@ -51,6 +51,14 @@ export function useSafelyMouseToSubmenu(options: SafelyMouseToSubmenuOptions) {
51
51
 
52
52
  let modality = useInteractionModality();
53
53
 
54
+ // Prevent mouse down over safe triangle. Clicking while pointer-events: none is applied
55
+ // will cause focus to move unexpectedly since it will go to an element behind the menu.
56
+ let onPointerDown = useEffectEvent((e: PointerEvent) => {
57
+ if (preventPointerEvents) {
58
+ e.preventDefault();
59
+ }
60
+ });
61
+
54
62
  useEffect(() => {
55
63
  if (preventPointerEvents && menuRef.current) {
56
64
  (menuRef.current as HTMLElement).style.pointerEvents = 'none';
@@ -150,12 +158,21 @@ export function useSafelyMouseToSubmenu(options: SafelyMouseToSubmenuOptions) {
150
158
 
151
159
  window.addEventListener('pointermove', onPointerMove);
152
160
 
161
+ // Prevent pointer down over the safe triangle. See above comment.
162
+ // Do not enable in tests, because JSDom doesn't do hit testing.
163
+ if (process.env.NODE_ENV !== 'test') {
164
+ window.addEventListener('pointerdown', onPointerDown, true);
165
+ }
166
+
153
167
  return () => {
154
168
  window.removeEventListener('pointermove', onPointerMove);
169
+ if (process.env.NODE_ENV !== 'test') {
170
+ window.removeEventListener('pointerdown', onPointerDown, true);
171
+ }
155
172
  clearTimeout(timeout.current);
156
173
  clearTimeout(autoCloseTimeout.current);
157
174
  movementsTowardsSubmenuCount.current = ALLOWED_INVALID_MOVEMENTS;
158
175
  };
159
176
 
160
- }, [isDisabled, isOpen, menuRef, modality, setPreventPointerEvents, submenuRef]);
177
+ }, [isDisabled, isOpen, menuRef, modality, setPreventPointerEvents, onPointerDown, submenuRef]);
161
178
  }
@@ -14,9 +14,9 @@ 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, useEffectEvent, useId, useLayoutEffect} from '@react-aria/utils';
17
18
  import type {SubmenuTriggerState} from '@react-stately/menu';
18
19
  import {useCallback, useRef} from 'react';
19
- import {useEffectEvent, useId, useLayoutEffect} from '@react-aria/utils';
20
20
  import {useLocale} from '@react-aria/i18n';
21
21
  import {useSafelyMouseToSubmenu} from './useSafelyMouseToSubmenu';
22
22
 
@@ -38,7 +38,9 @@ export interface AriaSubmenuTriggerProps {
38
38
  * The delay time in milliseconds for the submenu to appear after hovering over the trigger.
39
39
  * @default 200
40
40
  */
41
- delay?: number
41
+ delay?: number,
42
+ /** Whether the submenu trigger uses virtual focus. */
43
+ shouldUseVirtualFocus?: boolean
42
44
  }
43
45
 
44
46
  interface SubmenuTriggerProps extends Omit<AriaMenuItemProps, 'key'> {
@@ -67,7 +69,7 @@ export interface SubmenuTriggerAria<T> {
67
69
  * @param ref - Ref to the submenu trigger element.
68
70
  */
69
71
  export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: SubmenuTriggerState, ref: RefObject<FocusableElement | null>): SubmenuTriggerAria<T> {
70
- let {parentMenuRef, submenuRef, type = 'menu', isDisabled, delay = 200} = props;
72
+ let {parentMenuRef, submenuRef, type = 'menu', isDisabled, delay = 200, shouldUseVirtualFocus} = props;
71
73
  let submenuTriggerId = useId();
72
74
  let overlayId = useId();
73
75
  let {direction} = useLocale();
@@ -96,24 +98,42 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
96
98
  }, [cancelOpenTimeout]);
97
99
 
98
100
  let submenuKeyDown = (e: KeyboardEvent) => {
101
+ // If focus is not within the menu, assume virtual focus is being used.
102
+ // This means some other input element is also within the popover, so we shouldn't close the menu.
103
+ if (!e.currentTarget.contains(document.activeElement)) {
104
+ return;
105
+ }
106
+
99
107
  switch (e.key) {
100
108
  case 'ArrowLeft':
101
109
  if (direction === 'ltr' && e.currentTarget.contains(e.target as Element)) {
110
+ e.preventDefault();
102
111
  e.stopPropagation();
103
112
  onSubmenuClose();
104
- ref.current?.focus();
113
+ if (!shouldUseVirtualFocus && ref.current) {
114
+ focusWithoutScrolling(ref.current);
115
+ }
105
116
  }
106
117
  break;
107
118
  case 'ArrowRight':
108
119
  if (direction === 'rtl' && e.currentTarget.contains(e.target as Element)) {
120
+ e.preventDefault();
109
121
  e.stopPropagation();
110
122
  onSubmenuClose();
111
- ref.current?.focus();
123
+ if (!shouldUseVirtualFocus && ref.current) {
124
+ focusWithoutScrolling(ref.current);
125
+ }
112
126
  }
113
127
  break;
114
128
  case 'Escape':
115
- e.stopPropagation();
116
- state.closeAll();
129
+ // TODO: can remove this when we fix collection event leaks
130
+ if (submenuRef.current?.contains(e.target as Element)) {
131
+ e.stopPropagation();
132
+ onSubmenuClose();
133
+ if (!shouldUseVirtualFocus && ref.current) {
134
+ focusWithoutScrolling(ref.current);
135
+ }
136
+ }
117
137
  break;
118
138
  }
119
139
  };
@@ -134,12 +154,13 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
134
154
  case 'ArrowRight':
135
155
  if (!isDisabled) {
136
156
  if (direction === 'ltr') {
157
+ e.preventDefault();
137
158
  if (!state.isOpen) {
138
159
  onSubmenuOpen('first');
139
160
  }
140
161
 
141
162
  if (type === 'menu' && !!submenuRef?.current && document.activeElement === ref?.current) {
142
- submenuRef.current.focus();
163
+ focusWithoutScrolling(submenuRef.current);
143
164
  }
144
165
  } else if (state.isOpen) {
145
166
  onSubmenuClose();
@@ -152,12 +173,13 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
152
173
  case 'ArrowLeft':
153
174
  if (!isDisabled) {
154
175
  if (direction === 'rtl') {
176
+ e.preventDefault();
155
177
  if (!state.isOpen) {
156
178
  onSubmenuOpen('first');
157
179
  }
158
180
 
159
181
  if (type === 'menu' && !!submenuRef?.current && document.activeElement === ref?.current) {
160
- submenuRef.current.focus();
182
+ focusWithoutScrolling(submenuRef.current);
161
183
  }
162
184
  } else if (state.isOpen) {
163
185
  onSubmenuClose();
@@ -166,9 +188,6 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
166
188
  }
167
189
  }
168
190
  break;
169
- case 'Escape':
170
- state.closeAll();
171
- break;
172
191
  default:
173
192
  e.continuePropagation();
174
193
  break;
@@ -205,7 +224,7 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
205
224
  };
206
225
 
207
226
  let onBlur = (e) => {
208
- if (state.isOpen && parentMenuRef.current?.contains(e.relatedTarget)) {
227
+ if (state.isOpen && (parentMenuRef.current?.contains(e.relatedTarget))) {
209
228
  onSubmenuClose();
210
229
  }
211
230
  };
@@ -236,7 +255,6 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
236
255
  submenuProps,
237
256
  popoverProps: {
238
257
  isNonModal: true,
239
- disableFocusManagement: true,
240
258
  shouldCloseOnInteractOutside
241
259
  }
242
260
  };
package/src/utils.ts ADDED
@@ -0,0 +1,22 @@
1
+ /*
2
+ * Copyright 2024 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {Key} from '@react-types/shared';
14
+ import {TreeState} from '@react-stately/tree';
15
+
16
+ interface MenuData {
17
+ onClose?: () => void,
18
+ onAction?: (key: Key) => void,
19
+ shouldUseVirtualFocus?: boolean
20
+ }
21
+
22
+ export const menuData = new WeakMap<TreeState<unknown>, MenuData>();