@react-aria/menu 3.15.5 → 3.17.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 +9 -2
  2. package/dist/types.d.ts.map +1 -1
  3. package/dist/useMenu.main.js +7 -5
  4. package/dist/useMenu.main.js.map +1 -1
  5. package/dist/useMenu.mjs +8 -5
  6. package/dist/useMenu.module.js +8 -5
  7. package/dist/useMenu.module.js.map +1 -1
  8. package/dist/useMenuItem.main.js +39 -22
  9. package/dist/useMenuItem.main.js.map +1 -1
  10. package/dist/useMenuItem.mjs +39 -22
  11. package/dist/useMenuItem.module.js +39 -22
  12. package/dist/useMenuItem.module.js.map +1 -1
  13. package/dist/useMenuTrigger.main.js +1 -0
  14. package/dist/useMenuTrigger.main.js.map +1 -1
  15. package/dist/useMenuTrigger.mjs +1 -0
  16. package/dist/useMenuTrigger.module.js +1 -0
  17. package/dist/useMenuTrigger.module.js.map +1 -1
  18. package/dist/useSafelyMouseToSubmenu.main.js +1 -1
  19. package/dist/useSafelyMouseToSubmenu.main.js.map +1 -1
  20. package/dist/useSafelyMouseToSubmenu.mjs +1 -1
  21. package/dist/useSafelyMouseToSubmenu.module.js +1 -1
  22. package/dist/useSafelyMouseToSubmenu.module.js.map +1 -1
  23. package/dist/useSubmenuTrigger.main.js +8 -4
  24. package/dist/useSubmenuTrigger.main.js.map +1 -1
  25. package/dist/useSubmenuTrigger.mjs +8 -4
  26. package/dist/useSubmenuTrigger.module.js +8 -4
  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 +17 -16
  34. package/src/useMenu.ts +11 -13
  35. package/src/useMenuItem.ts +52 -28
  36. package/src/useMenuTrigger.ts +2 -1
  37. package/src/useSafelyMouseToSubmenu.ts +1 -1
  38. package/src/useSubmenuTrigger.ts +5 -5
  39. package/src/utils.ts +22 -0
@@ -50,16 +50,18 @@ function $0065b146e7192841$export$7138b0d059a6e743(props, state, ref) {
50
50
  switch(e.key){
51
51
  case 'ArrowLeft':
52
52
  if (direction === 'ltr' && e.currentTarget.contains(e.target)) {
53
+ var _ref_current;
53
54
  e.stopPropagation();
54
55
  onSubmenuClose();
55
- ref.current.focus();
56
+ (_ref_current = ref.current) === null || _ref_current === void 0 ? void 0 : _ref_current.focus();
56
57
  }
57
58
  break;
58
59
  case 'ArrowRight':
59
60
  if (direction === 'rtl' && e.currentTarget.contains(e.target)) {
61
+ var _ref_current1;
60
62
  e.stopPropagation();
61
63
  onSubmenuClose();
62
- ref.current.focus();
64
+ (_ref_current1 = ref.current) === null || _ref_current1 === void 0 ? void 0 : _ref_current1.focus();
63
65
  }
64
66
  break;
65
67
  case 'Escape':
@@ -68,13 +70,14 @@ function $0065b146e7192841$export$7138b0d059a6e743(props, state, ref) {
68
70
  break;
69
71
  }
70
72
  };
73
+ var _state_focusStrategy;
71
74
  let submenuProps = {
72
75
  id: overlayId,
73
76
  'aria-labelledby': submenuTriggerId,
74
77
  submenuLevel: state.submenuLevel,
75
78
  ...type === 'menu' && {
76
79
  onClose: state.closeAll,
77
- autoFocus: state.focusStrategy,
80
+ autoFocus: (_state_focusStrategy = state.focusStrategy) !== null && _state_focusStrategy !== void 0 ? _state_focusStrategy : undefined,
78
81
  onKeyDown: submenuKeyDown
79
82
  }
80
83
  };
@@ -125,7 +128,8 @@ function $0065b146e7192841$export$7138b0d059a6e743(props, state, ref) {
125
128
  }
126
129
  };
127
130
  let onBlur = (e)=>{
128
- if (state.isOpen && parentMenuRef.current.contains(e.relatedTarget)) onSubmenuClose();
131
+ var _parentMenuRef_current;
132
+ if (state.isOpen && ((_parentMenuRef_current = parentMenuRef.current) === null || _parentMenuRef_current === void 0 ? void 0 : _parentMenuRef_current.contains(e.relatedTarget))) onSubmenuClose();
129
133
  };
130
134
  let shouldCloseOnInteractOutside = (target)=>{
131
135
  if (target !== ref.current) return true;
@@ -1 +1 @@
1
- {"mappings":"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","sources":["packages/@react-aria/menu/src/useSubmenuTrigger.ts"],"sourcesContent":["/*\n * Copyright 2023 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {AriaMenuItemProps} from './useMenuItem';\nimport {AriaMenuOptions} from './useMenu';\nimport type {AriaPopoverProps, OverlayProps} from '@react-aria/overlays';\nimport {FocusableElement, FocusStrategy, KeyboardEvent, Node, PressEvent, RefObject} from '@react-types/shared';\nimport type {SubmenuTriggerState} from '@react-stately/menu';\nimport {useCallback, useRef} from 'react';\nimport {useEffectEvent, useId, useLayoutEffect} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\nimport {useSafelyMouseToSubmenu} from './useSafelyMouseToSubmenu';\n\nexport interface AriaSubmenuTriggerProps {\n /**\n * An object representing the submenu trigger menu item. Contains all the relevant information that makes up the menu item.\n * @deprecated\n */\n node?: Node<unknown>,\n /** Whether the submenu trigger is disabled. */\n isDisabled?: boolean,\n /** The type of the contents that the submenu trigger opens. */\n type?: 'dialog' | 'menu',\n /** Ref of the menu that contains the submenu trigger. */\n parentMenuRef: RefObject<HTMLElement | null>,\n /** Ref of the submenu opened by the submenu trigger. */\n submenuRef: RefObject<HTMLElement | null>,\n /**\n * The delay time in milliseconds for the submenu to appear after hovering over the trigger.\n * @default 200\n */\n delay?: number\n}\n\ninterface SubmenuTriggerProps extends AriaMenuItemProps {\n /** Whether the submenu trigger is in an expanded state. */\n isOpen: boolean\n}\n\ninterface SubmenuProps<T> extends AriaMenuOptions<T> {\n /** The level of the submenu. */\n submenuLevel: number\n}\n\nexport interface SubmenuTriggerAria<T> {\n /** Props for the submenu trigger menu item. */\n submenuTriggerProps: SubmenuTriggerProps,\n /** Props for the submenu controlled by the submenu trigger menu item. */\n submenuProps: SubmenuProps<T>,\n /** Props for the submenu's popover container. */\n popoverProps: Pick<AriaPopoverProps, 'isNonModal' | 'shouldCloseOnInteractOutside'> & Pick<OverlayProps, 'disableFocusManagement'>\n}\n\n/**\n * Provides the behavior and accessibility implementation for a submenu trigger and its associated submenu.\n * @param props - Props for the submenu trigger and refs attach to its submenu and parent menu.\n * @param state - State for the submenu trigger.\n * @param ref - Ref to the submenu trigger element.\n */\nexport function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: SubmenuTriggerState, ref: RefObject<FocusableElement | null>): SubmenuTriggerAria<T> {\n let {parentMenuRef, submenuRef, type = 'menu', isDisabled, delay = 200} = props;\n let submenuTriggerId = useId();\n let overlayId = useId();\n let {direction} = useLocale();\n let openTimeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n let cancelOpenTimeout = useCallback(() => {\n if (openTimeout.current) {\n clearTimeout(openTimeout.current);\n openTimeout.current = undefined;\n }\n }, [openTimeout]);\n\n let onSubmenuOpen = useEffectEvent((focusStrategy?: FocusStrategy) => {\n cancelOpenTimeout();\n state.open(focusStrategy);\n });\n\n let onSubmenuClose = useEffectEvent(() => {\n cancelOpenTimeout();\n state.close();\n });\n\n useLayoutEffect(() => {\n return () => {\n cancelOpenTimeout();\n };\n }, [cancelOpenTimeout]);\n\n let submenuKeyDown = (e: KeyboardEvent) => {\n switch (e.key) {\n case 'ArrowLeft':\n if (direction === 'ltr' && e.currentTarget.contains(e.target as Element)) {\n e.stopPropagation();\n onSubmenuClose();\n ref.current.focus();\n }\n break;\n case 'ArrowRight':\n if (direction === 'rtl' && e.currentTarget.contains(e.target as Element)) {\n e.stopPropagation();\n onSubmenuClose();\n ref.current.focus();\n }\n break;\n case 'Escape':\n e.stopPropagation();\n state.closeAll();\n break;\n }\n };\n\n let submenuProps = {\n id: overlayId,\n 'aria-labelledby': submenuTriggerId,\n submenuLevel: state.submenuLevel,\n ...(type === 'menu' && {\n onClose: state.closeAll,\n autoFocus: state.focusStrategy,\n onKeyDown: submenuKeyDown\n })\n };\n\n let submenuTriggerKeyDown = (e: KeyboardEvent) => {\n switch (e.key) {\n case 'ArrowRight':\n if (!isDisabled) {\n if (direction === 'ltr') {\n if (!state.isOpen) {\n onSubmenuOpen('first');\n }\n\n if (type === 'menu' && !!submenuRef?.current && document.activeElement === ref?.current) {\n submenuRef.current.focus();\n }\n } else if (state.isOpen) {\n onSubmenuClose();\n } else {\n e.continuePropagation();\n }\n }\n\n break;\n case 'ArrowLeft':\n if (!isDisabled) {\n if (direction === 'rtl') {\n if (!state.isOpen) {\n onSubmenuOpen('first');\n }\n\n if (type === 'menu' && !!submenuRef?.current && document.activeElement === ref?.current) {\n submenuRef.current.focus();\n }\n } else if (state.isOpen) {\n onSubmenuClose();\n } else {\n e.continuePropagation();\n }\n }\n break;\n case 'Escape':\n state.closeAll();\n break;\n default:\n e.continuePropagation();\n break;\n }\n };\n\n let onPressStart = (e: PressEvent) => {\n if (!isDisabled && (e.pointerType === 'virtual' || e.pointerType === 'keyboard')) {\n // If opened with a screen reader or keyboard, auto focus the first submenu item.\n onSubmenuOpen('first');\n }\n };\n\n let onPress = (e: PressEvent) => {\n if (!isDisabled && (e.pointerType === 'touch' || e.pointerType === 'mouse')) {\n // For touch or on a desktop device with a small screen open on press up to possible problems with\n // press up happening on the newly opened tray items\n onSubmenuOpen();\n }\n };\n\n let onHoverChange = (isHovered) => {\n if (!isDisabled) {\n if (isHovered && !state.isOpen) {\n if (!openTimeout.current) {\n openTimeout.current = setTimeout(() => {\n onSubmenuOpen();\n }, delay);\n }\n } else if (!isHovered) {\n cancelOpenTimeout();\n }\n }\n };\n\n let onBlur = (e) => {\n if (state.isOpen && parentMenuRef.current.contains(e.relatedTarget)) {\n onSubmenuClose();\n }\n };\n\n let shouldCloseOnInteractOutside = (target) => {\n if (target !== ref.current) {\n return true;\n }\n\n return false;\n };\n\n useSafelyMouseToSubmenu({menuRef: parentMenuRef, submenuRef, isOpen: state.isOpen, isDisabled: isDisabled});\n\n return {\n submenuTriggerProps: {\n id: submenuTriggerId,\n 'aria-controls': state.isOpen ? overlayId : undefined,\n 'aria-haspopup': !isDisabled ? type : undefined,\n 'aria-expanded': state.isOpen ? 'true' : 'false',\n onPressStart,\n onPress,\n onHoverChange,\n onKeyDown: submenuTriggerKeyDown,\n onBlur,\n isOpen: state.isOpen\n },\n submenuProps,\n popoverProps: {\n isNonModal: true,\n disableFocusManagement: true,\n shouldCloseOnInteractOutside\n }\n };\n}\n"],"names":[],"version":3,"file":"useSubmenuTrigger.module.js.map"}
1
+ {"mappings":"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","sources":["packages/@react-aria/menu/src/useSubmenuTrigger.ts"],"sourcesContent":["/*\n * Copyright 2023 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {AriaMenuItemProps} from './useMenuItem';\nimport {AriaMenuOptions} from './useMenu';\nimport type {AriaPopoverProps, OverlayProps} from '@react-aria/overlays';\nimport {FocusableElement, FocusStrategy, KeyboardEvent, Node, PressEvent, RefObject} from '@react-types/shared';\nimport type {SubmenuTriggerState} from '@react-stately/menu';\nimport {useCallback, useRef} from 'react';\nimport {useEffectEvent, useId, useLayoutEffect} from '@react-aria/utils';\nimport {useLocale} from '@react-aria/i18n';\nimport {useSafelyMouseToSubmenu} from './useSafelyMouseToSubmenu';\n\nexport interface AriaSubmenuTriggerProps {\n /**\n * An object representing the submenu trigger menu item. Contains all the relevant information that makes up the menu item.\n * @deprecated\n */\n node?: Node<unknown>,\n /** Whether the submenu trigger is disabled. */\n isDisabled?: boolean,\n /** The type of the contents that the submenu trigger opens. */\n type?: 'dialog' | 'menu',\n /** Ref of the menu that contains the submenu trigger. */\n parentMenuRef: RefObject<HTMLElement | null>,\n /** Ref of the submenu opened by the submenu trigger. */\n submenuRef: RefObject<HTMLElement | null>,\n /**\n * The delay time in milliseconds for the submenu to appear after hovering over the trigger.\n * @default 200\n */\n delay?: number\n}\n\ninterface SubmenuTriggerProps extends Omit<AriaMenuItemProps, 'key'> {\n /** Whether the submenu trigger is in an expanded state. */\n isOpen: boolean\n}\n\ninterface SubmenuProps<T> extends AriaMenuOptions<T> {\n /** The level of the submenu. */\n submenuLevel: number\n}\n\nexport interface SubmenuTriggerAria<T> {\n /** Props for the submenu trigger menu item. */\n submenuTriggerProps: SubmenuTriggerProps,\n /** Props for the submenu controlled by the submenu trigger menu item. */\n submenuProps: SubmenuProps<T>,\n /** Props for the submenu's popover container. */\n popoverProps: Pick<AriaPopoverProps, 'isNonModal' | 'shouldCloseOnInteractOutside'> & Pick<OverlayProps, 'disableFocusManagement'>\n}\n\n/**\n * Provides the behavior and accessibility implementation for a submenu trigger and its associated submenu.\n * @param props - Props for the submenu trigger and refs attach to its submenu and parent menu.\n * @param state - State for the submenu trigger.\n * @param ref - Ref to the submenu trigger element.\n */\nexport function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: SubmenuTriggerState, ref: RefObject<FocusableElement | null>): SubmenuTriggerAria<T> {\n let {parentMenuRef, submenuRef, type = 'menu', isDisabled, delay = 200} = props;\n let submenuTriggerId = useId();\n let overlayId = useId();\n let {direction} = useLocale();\n let openTimeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n let cancelOpenTimeout = useCallback(() => {\n if (openTimeout.current) {\n clearTimeout(openTimeout.current);\n openTimeout.current = undefined;\n }\n }, [openTimeout]);\n\n let onSubmenuOpen = useEffectEvent((focusStrategy?: FocusStrategy) => {\n cancelOpenTimeout();\n state.open(focusStrategy);\n });\n\n let onSubmenuClose = useEffectEvent(() => {\n cancelOpenTimeout();\n state.close();\n });\n\n useLayoutEffect(() => {\n return () => {\n cancelOpenTimeout();\n };\n }, [cancelOpenTimeout]);\n\n let submenuKeyDown = (e: KeyboardEvent) => {\n switch (e.key) {\n case 'ArrowLeft':\n if (direction === 'ltr' && e.currentTarget.contains(e.target as Element)) {\n e.stopPropagation();\n onSubmenuClose();\n ref.current?.focus();\n }\n break;\n case 'ArrowRight':\n if (direction === 'rtl' && e.currentTarget.contains(e.target as Element)) {\n e.stopPropagation();\n onSubmenuClose();\n ref.current?.focus();\n }\n break;\n case 'Escape':\n e.stopPropagation();\n state.closeAll();\n break;\n }\n };\n\n let submenuProps = {\n id: overlayId,\n 'aria-labelledby': submenuTriggerId,\n submenuLevel: state.submenuLevel,\n ...(type === 'menu' && {\n onClose: state.closeAll,\n autoFocus: state.focusStrategy ?? undefined,\n onKeyDown: submenuKeyDown\n })\n };\n\n let submenuTriggerKeyDown = (e: KeyboardEvent) => {\n switch (e.key) {\n case 'ArrowRight':\n if (!isDisabled) {\n if (direction === 'ltr') {\n if (!state.isOpen) {\n onSubmenuOpen('first');\n }\n\n if (type === 'menu' && !!submenuRef?.current && document.activeElement === ref?.current) {\n submenuRef.current.focus();\n }\n } else if (state.isOpen) {\n onSubmenuClose();\n } else {\n e.continuePropagation();\n }\n }\n\n break;\n case 'ArrowLeft':\n if (!isDisabled) {\n if (direction === 'rtl') {\n if (!state.isOpen) {\n onSubmenuOpen('first');\n }\n\n if (type === 'menu' && !!submenuRef?.current && document.activeElement === ref?.current) {\n submenuRef.current.focus();\n }\n } else if (state.isOpen) {\n onSubmenuClose();\n } else {\n e.continuePropagation();\n }\n }\n break;\n case 'Escape':\n state.closeAll();\n break;\n default:\n e.continuePropagation();\n break;\n }\n };\n\n let onPressStart = (e: PressEvent) => {\n if (!isDisabled && (e.pointerType === 'virtual' || e.pointerType === 'keyboard')) {\n // If opened with a screen reader or keyboard, auto focus the first submenu item.\n onSubmenuOpen('first');\n }\n };\n\n let onPress = (e: PressEvent) => {\n if (!isDisabled && (e.pointerType === 'touch' || e.pointerType === 'mouse')) {\n // For touch or on a desktop device with a small screen open on press up to possible problems with\n // press up happening on the newly opened tray items\n onSubmenuOpen();\n }\n };\n\n let onHoverChange = (isHovered) => {\n if (!isDisabled) {\n if (isHovered && !state.isOpen) {\n if (!openTimeout.current) {\n openTimeout.current = setTimeout(() => {\n onSubmenuOpen();\n }, delay);\n }\n } else if (!isHovered) {\n cancelOpenTimeout();\n }\n }\n };\n\n let onBlur = (e) => {\n if (state.isOpen && parentMenuRef.current?.contains(e.relatedTarget)) {\n onSubmenuClose();\n }\n };\n\n let shouldCloseOnInteractOutside = (target) => {\n if (target !== ref.current) {\n return true;\n }\n\n return false;\n };\n\n useSafelyMouseToSubmenu({menuRef: parentMenuRef, submenuRef, isOpen: state.isOpen, isDisabled: isDisabled});\n\n return {\n submenuTriggerProps: {\n id: submenuTriggerId,\n 'aria-controls': state.isOpen ? overlayId : undefined,\n 'aria-haspopup': !isDisabled ? type : undefined,\n 'aria-expanded': state.isOpen ? 'true' : 'false',\n onPressStart,\n onPress,\n onHoverChange,\n onKeyDown: submenuTriggerKeyDown,\n onBlur,\n isOpen: state.isOpen\n },\n submenuProps,\n popoverProps: {\n isNonModal: true,\n disableFocusManagement: true,\n shouldCloseOnInteractOutside\n }\n };\n}\n"],"names":[],"version":3,"file":"useSubmenuTrigger.module.js.map"}
@@ -0,0 +1,20 @@
1
+
2
+ function $parcel$export(e, n, v, s) {
3
+ Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});
4
+ }
5
+
6
+ $parcel$export(module.exports, "menuData", () => $815e346b11b84016$export$6f49b4016bfc8d56);
7
+ /*
8
+ * Copyright 2024 Adobe. All rights reserved.
9
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
10
+ * you may not use this file except in compliance with the License. You may obtain a copy
11
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software distributed under
14
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
15
+ * OF ANY KIND, either express or implied. See the License for the specific language
16
+ * governing permissions and limitations under the License.
17
+ */ const $815e346b11b84016$export$6f49b4016bfc8d56 = new WeakMap();
18
+
19
+
20
+ //# sourceMappingURL=utils.main.js.map
@@ -0,0 +1 @@
1
+ {"mappings":";;;;;;AAAA;;;;;;;;;;CAUC,GAWM,MAAM,4CAAW,IAAI","sources":["packages/@react-aria/menu/src/utils.ts"],"sourcesContent":["/*\n * Copyright 2024 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {Key} from '@react-types/shared';\nimport {TreeState} from '@react-stately/tree';\n\ninterface MenuData {\n onClose?: () => void,\n onAction?: (key: Key) => void,\n shouldUseVirtualFocus?: boolean\n}\n\nexport const menuData = new WeakMap<TreeState<unknown>, MenuData>();\n"],"names":[],"version":3,"file":"utils.main.js.map"}
package/dist/utils.mjs ADDED
@@ -0,0 +1,15 @@
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
+ */ const $fc79756100351201$export$6f49b4016bfc8d56 = new WeakMap();
12
+
13
+
14
+ export {$fc79756100351201$export$6f49b4016bfc8d56 as menuData};
15
+ //# sourceMappingURL=utils.module.js.map
@@ -0,0 +1,15 @@
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
+ */ const $fc79756100351201$export$6f49b4016bfc8d56 = new WeakMap();
12
+
13
+
14
+ export {$fc79756100351201$export$6f49b4016bfc8d56 as menuData};
15
+ //# sourceMappingURL=utils.module.js.map
@@ -0,0 +1 @@
1
+ {"mappings":"AAAA;;;;;;;;;;CAUC,GAWM,MAAM,4CAAW,IAAI","sources":["packages/@react-aria/menu/src/utils.ts"],"sourcesContent":["/*\n * Copyright 2024 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {Key} from '@react-types/shared';\nimport {TreeState} from '@react-stately/tree';\n\ninterface MenuData {\n onClose?: () => void,\n onAction?: (key: Key) => void,\n shouldUseVirtualFocus?: boolean\n}\n\nexport const menuData = new WeakMap<TreeState<unknown>, MenuData>();\n"],"names":[],"version":3,"file":"utils.module.js.map"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-aria/menu",
3
- "version": "3.15.5",
3
+ "version": "3.17.0",
4
4
  "description": "Spectrum UI components in React",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/main.js",
@@ -22,26 +22,27 @@
22
22
  "url": "https://github.com/adobe/react-spectrum"
23
23
  },
24
24
  "dependencies": {
25
- "@react-aria/focus": "^3.18.4",
26
- "@react-aria/i18n": "^3.12.3",
27
- "@react-aria/interactions": "^3.22.4",
28
- "@react-aria/overlays": "^3.23.4",
29
- "@react-aria/selection": "^3.20.1",
30
- "@react-aria/utils": "^3.25.3",
31
- "@react-stately/collections": "^3.11.0",
32
- "@react-stately/menu": "^3.8.3",
33
- "@react-stately/tree": "^3.8.5",
34
- "@react-types/button": "^3.10.0",
35
- "@react-types/menu": "^3.9.12",
36
- "@react-types/shared": "^3.25.0",
25
+ "@react-aria/focus": "^3.19.1",
26
+ "@react-aria/i18n": "^3.12.5",
27
+ "@react-aria/interactions": "^3.23.0",
28
+ "@react-aria/overlays": "^3.25.0",
29
+ "@react-aria/selection": "^3.22.0",
30
+ "@react-aria/utils": "^3.27.0",
31
+ "@react-stately/collections": "^3.12.1",
32
+ "@react-stately/menu": "^3.9.1",
33
+ "@react-stately/selection": "^3.19.0",
34
+ "@react-stately/tree": "^3.8.7",
35
+ "@react-types/button": "^3.10.2",
36
+ "@react-types/menu": "^3.9.14",
37
+ "@react-types/shared": "^3.27.0",
37
38
  "@swc/helpers": "^0.5.0"
38
39
  },
39
40
  "peerDependencies": {
40
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0",
41
- "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
41
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
42
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
42
43
  },
43
44
  "publishConfig": {
44
45
  "access": "public"
45
46
  },
46
- "gitHead": "8e0a28d188cdbdbd2b32296fa034b1b02ddde229"
47
+ "gitHead": "09e7f44bebdc9d89122926b2b439a0a38a2814ea"
47
48
  }
package/src/useMenu.ts CHANGED
@@ -11,8 +11,9 @@
11
11
  */
12
12
 
13
13
  import {AriaMenuProps} from '@react-types/menu';
14
- import {DOMAttributes, Key, KeyboardDelegate, KeyboardEvents, RefObject} from '@react-types/shared';
14
+ import {DOMAttributes, KeyboardDelegate, KeyboardEvents, RefObject} from '@react-types/shared';
15
15
  import {filterDOMProps, mergeProps} from '@react-aria/utils';
16
+ import {menuData} from './utils';
16
17
  import {TreeState} from '@react-stately/tree';
17
18
  import {useSelectableList} from '@react-aria/selection';
18
19
 
@@ -24,21 +25,17 @@ export interface MenuAria {
24
25
  export interface AriaMenuOptions<T> extends Omit<AriaMenuProps<T>, 'children'>, KeyboardEvents {
25
26
  /** Whether the menu uses virtual scrolling. */
26
27
  isVirtualized?: boolean,
27
-
28
28
  /**
29
29
  * An optional keyboard delegate implementation for type to select,
30
30
  * to override the default.
31
31
  */
32
- keyboardDelegate?: KeyboardDelegate
33
- }
34
-
35
- interface MenuData {
36
- onClose?: () => void,
37
- onAction?: (key: Key) => void
32
+ keyboardDelegate?: KeyboardDelegate,
33
+ /**
34
+ * Whether the menu items should use virtual focus instead of being focused directly.
35
+ */
36
+ shouldUseVirtualFocus?: boolean
38
37
  }
39
38
 
40
- export const menuData = new WeakMap<TreeState<unknown>, MenuData>();
41
-
42
39
  /**
43
40
  * Provides the behavior and accessibility implementation for a menu component.
44
41
  * A menu displays a list of actions or options that a user can choose.
@@ -70,7 +67,8 @@ export function useMenu<T>(props: AriaMenuOptions<T>, state: TreeState<T>, ref:
70
67
 
71
68
  menuData.set(state, {
72
69
  onClose: props.onClose,
73
- onAction: props.onAction
70
+ onAction: props.onAction,
71
+ shouldUseVirtualFocus: props.shouldUseVirtualFocus
74
72
  });
75
73
 
76
74
  return {
@@ -79,8 +77,8 @@ export function useMenu<T>(props: AriaMenuOptions<T>, state: TreeState<T>, ref:
79
77
  ...listProps,
80
78
  onKeyDown: (e) => {
81
79
  // don't clear the menu selected keys if the user is presses escape since escape closes the menu
82
- if (e.key !== 'Escape') {
83
- listProps.onKeyDown(e);
80
+ if (e.key !== 'Escape' || props.shouldUseVirtualFocus) {
81
+ listProps.onKeyDown?.(e);
84
82
  }
85
83
  }
86
84
  })
@@ -14,7 +14,8 @@ import {DOMAttributes, DOMProps, FocusableElement, FocusEvents, HoverEvents, Key
14
14
  import {filterDOMProps, mergeProps, useLinkProps, useRouter, useSlotId} from '@react-aria/utils';
15
15
  import {getItemCount} from '@react-stately/collections';
16
16
  import {isFocusVisible, useFocus, useHover, useKeyboard, usePress} from '@react-aria/interactions';
17
- import {menuData} from './useMenu';
17
+ import {menuData} from './utils';
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
  /**
@@ -106,7 +110,7 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
106
110
  'aria-haspopup': hasPopup,
107
111
  onPressStart: pressStartProp,
108
112
  onPressUp: pressUpProp,
109
- onPress,
113
+ onPress: pressProp,
110
114
  onPressChange,
111
115
  onPressEnd,
112
116
  onHoverStart: hoverStartProp,
@@ -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
 
@@ -190,22 +196,37 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
190
196
  pressStartProp?.(e);
191
197
  };
192
198
 
199
+ let maybeClose = () => {
200
+ // Pressing a menu item should close by default in single selection mode but not multiple
201
+ // selection mode, except if overridden by the closeOnSelect prop.
202
+ if (!isTrigger && onClose && (closeOnSelect ?? (selectionManager.selectionMode !== 'multiple' || selectionManager.isLink(key)))) {
203
+ onClose();
204
+ }
205
+ };
206
+
193
207
  let onPressUp = (e: PressEvent) => {
194
- if (e.pointerType !== 'keyboard') {
208
+ // If interacting with mouse, allow the user to mouse down on the trigger button,
209
+ // drag, and release over an item (matching native behavior).
210
+ if (e.pointerType === 'mouse') {
195
211
  performAction(e);
196
-
197
- // Pressing a menu item should close by default in single selection mode but not multiple
198
- // selection mode, except if overridden by the closeOnSelect prop.
199
- if (!isTrigger && onClose && (closeOnSelect ?? (state.selectionManager.selectionMode !== 'multiple' || state.selectionManager.isLink(key)))) {
200
- onClose();
201
- }
212
+ maybeClose();
202
213
  }
203
214
 
204
215
  pressUpProp?.(e);
205
216
  };
206
217
 
218
+ let onPress = (e: PressEvent) => {
219
+ if (e.pointerType !== 'keyboard' && e.pointerType !== 'mouse') {
220
+ performAction(e);
221
+ maybeClose();
222
+ }
223
+
224
+ pressProp?.(e);
225
+ };
226
+
207
227
  let {itemProps, isFocused} = useSelectableItem({
208
- selectionManager: state.selectionManager,
228
+ id,
229
+ selectionManager: selectionManager,
209
230
  key,
210
231
  ref,
211
232
  shouldSelectOnPressUp: true,
@@ -214,7 +235,8 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
214
235
  // because we handle it ourselves. The behavior of menus
215
236
  // is slightly different from other collections because
216
237
  // actions are performed on key down rather than key up.
217
- linkBehavior: 'none'
238
+ linkBehavior: 'none',
239
+ shouldUseVirtualFocus: data.shouldUseVirtualFocus
218
240
  });
219
241
 
220
242
  let {pressProps, isPressed} = usePress({
@@ -228,9 +250,10 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
228
250
  let {hoverProps} = useHover({
229
251
  isDisabled,
230
252
  onHoverStart(e) {
231
- if (!isFocusVisible()) {
232
- state.selectionManager.setFocused(true);
233
- state.selectionManager.setFocusedKey(key);
253
+ // Hovering over an already expanded sub dialog trigger should keep focus in the dialog.
254
+ if (!isFocusVisible() && !(isTriggerExpanded && hasPopup === 'dialog')) {
255
+ selectionManager.setFocused(true);
256
+ selectionManager.setFocusedKey(key);
234
257
  }
235
258
  hoverStartProp?.(e);
236
259
  },
@@ -249,7 +272,7 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
249
272
 
250
273
  switch (e.key) {
251
274
  case ' ':
252
- if (!isDisabled && state.selectionManager.selectionMode === 'none' && !isTrigger && closeOnSelect !== false && onClose) {
275
+ if (!isDisabled && selectionManager.selectionMode === 'none' && !isTrigger && closeOnSelect !== false && onClose) {
253
276
  onClose();
254
277
  }
255
278
  break;
@@ -272,15 +295,16 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
272
295
  });
273
296
 
274
297
  let {focusProps} = useFocus({onBlur, onFocus, onFocusChange});
275
- let domProps = filterDOMProps(item.props);
298
+ let domProps = filterDOMProps(item?.props);
276
299
  delete domProps.id;
277
- let linkProps = useLinkProps(item.props);
300
+ let linkProps = useLinkProps(item?.props);
278
301
 
279
302
  return {
280
303
  menuItemProps: {
281
304
  ...ariaProps,
282
305
  ...mergeProps(domProps, linkProps, isTrigger ? {onFocus: itemProps.onFocus, 'data-key': itemProps['data-key']} : itemProps, pressProps, hoverProps, keyboardProps, focusProps),
283
- tabIndex: itemProps.tabIndex != null ? -1 : undefined
306
+ // 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.
307
+ tabIndex: itemProps.tabIndex != null && isTriggerExpanded ? -1 : itemProps.tabIndex
284
308
  },
285
309
  labelProps: {
286
310
  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
  };
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>();