@react-aria/menu
Advanced tools
Comparing version
@@ -65,2 +65,4 @@ import { AriaMenuProps, MenuTriggerType } from "@react-types/menu"; | ||
isFocused: boolean; | ||
/** Whether the item is keyboard focused. */ | ||
isFocusVisible: boolean; | ||
/** Whether the item is currently selected. */ | ||
@@ -160,2 +162,4 @@ isSelected: boolean; | ||
delay?: number; | ||
/** Whether the submenu trigger uses virtual focus. */ | ||
shouldUseVirtualFocus?: boolean; | ||
} | ||
@@ -162,0 +166,0 @@ interface SubmenuTriggerProps extends Omit<AriaMenuItemProps, 'key'> { |
@@ -26,3 +26,3 @@ var $815e346b11b84016$exports = require("./utils.main.js"); | ||
let { shouldFocusWrap: shouldFocusWrap = true, onKeyDown: onKeyDown, onKeyUp: onKeyUp, ...otherProps } = props; | ||
if (!props['aria-label'] && !props['aria-labelledby']) console.warn('An aria-label or aria-labelledby prop is required for accessibility.'); | ||
if (!props['aria-label'] && !props['aria-labelledby'] && process.env.NODE_ENV !== 'production') console.warn('An aria-label or aria-labelledby prop is required for accessibility.'); | ||
let domProps = (0, $6CumN$reactariautils.filterDOMProps)(props, { | ||
@@ -29,0 +29,0 @@ labelable: true |
@@ -20,3 +20,3 @@ import {menuData as $fc79756100351201$export$6f49b4016bfc8d56} from "./utils.module.js"; | ||
let { shouldFocusWrap: shouldFocusWrap = true, onKeyDown: onKeyDown, onKeyUp: onKeyUp, ...otherProps } = props; | ||
if (!props['aria-label'] && !props['aria-labelledby']) console.warn('An aria-label or aria-labelledby prop is required for accessibility.'); | ||
if (!props['aria-label'] && !props['aria-labelledby'] && process.env.NODE_ENV !== 'production') console.warn('An aria-label or aria-labelledby prop is required for accessibility.'); | ||
let domProps = (0, $ieN2F$filterDOMProps)(props, { | ||
@@ -23,0 +23,0 @@ labelable: true |
@@ -5,2 +5,3 @@ var $815e346b11b84016$exports = require("./utils.main.js"); | ||
var $byVdR$reactariainteractions = require("@react-aria/interactions"); | ||
var $byVdR$react = require("react"); | ||
var $byVdR$reactariaselection = require("@react-aria/selection"); | ||
@@ -29,4 +30,5 @@ | ||
function $38191ed02615ec07$export$9d32628fc2aea7da(props, state, ref) { | ||
let { id: id, key: key, closeOnSelect: closeOnSelect, isVirtualized: isVirtualized, 'aria-haspopup': hasPopup, onPressStart: pressStartProp, onPressUp: pressUpProp, onPress: pressProp, onPressChange: onPressChange, onPressEnd: onPressEnd, onHoverStart: hoverStartProp, onHoverChange: onHoverChange, onHoverEnd: onHoverEnd, onKeyDown: onKeyDown, onKeyUp: onKeyUp, onFocus: onFocus, onFocusChange: onFocusChange, onBlur: onBlur, selectionManager: selectionManager = state.selectionManager } = props; | ||
let { id: id, key: key, closeOnSelect: closeOnSelect, isVirtualized: isVirtualized, 'aria-haspopup': hasPopup, onPressStart: onPressStart, onPressUp: pressUpProp, onPress: onPress, onPressChange: pressChangeProp, onPressEnd: onPressEnd, onClick: onClickProp, onHoverStart: hoverStartProp, onHoverChange: onHoverChange, onHoverEnd: onHoverEnd, onKeyDown: onKeyDown, onKeyUp: onKeyUp, onFocus: onFocus, onFocusChange: onFocusChange, onBlur: onBlur, selectionManager: selectionManager = state.selectionManager } = props; | ||
let isTrigger = !!hasPopup; | ||
@@ -42,3 +44,3 @@ let isTriggerExpanded = isTrigger && props['aria-expanded'] === 'true'; | ||
let router = (0, $byVdR$reactariautils.useRouter)(); | ||
let performAction = (e)=>{ | ||
let performAction = ()=>{ | ||
var _item_props; | ||
@@ -53,3 +55,2 @@ if (isTrigger) return; | ||
} | ||
if (e.target instanceof HTMLAnchorElement && item) router.open(e.target, e, item.props.href, item.props.routerOptions); | ||
}; | ||
@@ -83,26 +84,28 @@ let role = 'menuitem'; | ||
} | ||
let onPressStart = (e)=>{ | ||
if (e.pointerType === 'keyboard') performAction(e); | ||
pressStartProp === null || pressStartProp === void 0 ? void 0 : pressStartProp(e); | ||
let isPressedRef = (0, $byVdR$react.useRef)(false); | ||
let onPressChange = (isPressed)=>{ | ||
pressChangeProp === null || pressChangeProp === void 0 ? void 0 : pressChangeProp(isPressed); | ||
isPressedRef.current = isPressed; | ||
}; | ||
let maybeClose = ()=>{ | ||
// Pressing a menu item should close by default in single selection mode but not multiple | ||
// selection mode, except if overridden by the closeOnSelect prop. | ||
if (!isTrigger && onClose && (closeOnSelect !== null && closeOnSelect !== void 0 ? closeOnSelect : selectionManager.selectionMode !== 'multiple' || selectionManager.isLink(key))) onClose(); | ||
}; | ||
let interaction = (0, $byVdR$react.useRef)(null); | ||
let onPressUp = (e)=>{ | ||
if (e.pointerType !== 'keyboard') interaction.current = { | ||
pointerType: e.pointerType | ||
}; | ||
// If interacting with mouse, allow the user to mouse down on the trigger button, | ||
// drag, and release over an item (matching native behavior). | ||
if (e.pointerType === 'mouse') { | ||
performAction(e); | ||
maybeClose(); | ||
if (!isPressedRef.current) e.target.click(); | ||
} | ||
pressUpProp === null || pressUpProp === void 0 ? void 0 : pressUpProp(e); | ||
}; | ||
let onPress = (e)=>{ | ||
if (e.pointerType !== 'keyboard' && e.pointerType !== 'mouse') { | ||
performAction(e); | ||
maybeClose(); | ||
} | ||
pressProp === null || pressProp === void 0 ? void 0 : pressProp(e); | ||
let onClick = (e)=>{ | ||
var _interaction_current, _interaction_current1; | ||
onClickProp === null || onClickProp === void 0 ? void 0 : onClickProp(e); | ||
performAction(); | ||
(0, $byVdR$reactariautils.handleLinkClick)(e, router, item.props.href, item === null || item === void 0 ? void 0 : item.props.routerOptions); | ||
let shouldClose = ((_interaction_current = interaction.current) === null || _interaction_current === void 0 ? void 0 : _interaction_current.pointerType) === 'keyboard' ? ((_interaction_current1 = interaction.current) === null || _interaction_current1 === void 0 ? void 0 : _interaction_current1.key) === 'Enter' || selectionManager.selectionMode === 'none' || selectionManager.isLink(key) : selectionManager.selectionMode !== 'multiple' || selectionManager.isLink(key); | ||
shouldClose = closeOnSelect !== null && closeOnSelect !== void 0 ? closeOnSelect : shouldClose; | ||
if (onClose && !isTrigger && shouldClose) onClose(); | ||
interaction.current = null; | ||
}; | ||
@@ -135,3 +138,3 @@ let { itemProps: itemProps, isFocused: isFocused } = (0, $byVdR$reactariaselection.useSelectableItem)({ | ||
// Hovering over an already expanded sub dialog trigger should keep focus in the dialog. | ||
if (!(0, $byVdR$reactariainteractions.isFocusVisible)() && !(isTriggerExpanded && hasPopup === 'dialog')) { | ||
if (!(0, $byVdR$reactariainteractions.isFocusVisible)() && !(isTriggerExpanded && hasPopup)) { | ||
selectionManager.setFocused(true); | ||
@@ -155,7 +158,15 @@ selectionManager.setFocusedKey(key); | ||
case ' ': | ||
if (!isDisabled && selectionManager.selectionMode === 'none' && !isTrigger && closeOnSelect !== false && onClose) onClose(); | ||
interaction.current = { | ||
pointerType: 'keyboard', | ||
key: ' ' | ||
}; | ||
e.target.click(); | ||
break; | ||
case 'Enter': | ||
// The Enter key should always close on select, except if overridden. | ||
if (!isDisabled && closeOnSelect !== false && !isTrigger && onClose) onClose(); | ||
interaction.current = { | ||
pointerType: 'keyboard', | ||
key: 'Enter' | ||
}; | ||
// Trigger click unless this is a link. Links trigger click natively. | ||
if (e.target.tagName !== 'A') e.target.click(); | ||
break; | ||
@@ -183,6 +194,12 @@ default: | ||
onFocus: itemProps.onFocus, | ||
'data-collection': itemProps['data-collection'], | ||
'data-key': itemProps['data-key'] | ||
} : itemProps, pressProps, hoverProps, keyboardProps, focusProps), | ||
} : itemProps, pressProps, hoverProps, keyboardProps, focusProps, // Prevent DOM focus from moving on mouse down when using virtual focus or this is a submenu/subdialog trigger. | ||
data.shouldUseVirtualFocus || isTrigger ? { | ||
onMouseDown: (e)=>e.preventDefault() | ||
} : undefined, isDisabled ? undefined : { | ||
onClick: onClick | ||
}), | ||
// 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. | ||
tabIndex: itemProps.tabIndex != null && isTriggerExpanded ? -1 : itemProps.tabIndex | ||
tabIndex: itemProps.tabIndex != null && isTriggerExpanded && !data.shouldUseVirtualFocus ? -1 : itemProps.tabIndex | ||
}, | ||
@@ -199,2 +216,3 @@ labelProps: { | ||
isFocused: isFocused, | ||
isFocusVisible: isFocused && selectionManager.isFocused && (0, $byVdR$reactariainteractions.isFocusVisible)() && !isTriggerExpanded, | ||
isSelected: isSelected, | ||
@@ -201,0 +219,0 @@ isPressed: isPressed, |
import {menuData as $fc79756100351201$export$6f49b4016bfc8d56} from "./utils.module.js"; | ||
import {useRouter as $7Kjv5$useRouter, useSlotId as $7Kjv5$useSlotId, filterDOMProps as $7Kjv5$filterDOMProps, useLinkProps as $7Kjv5$useLinkProps, mergeProps as $7Kjv5$mergeProps} from "@react-aria/utils"; | ||
import {useRouter as $7Kjv5$useRouter, useSlotId as $7Kjv5$useSlotId, handleLinkClick as $7Kjv5$handleLinkClick, filterDOMProps as $7Kjv5$filterDOMProps, useLinkProps as $7Kjv5$useLinkProps, mergeProps as $7Kjv5$mergeProps} from "@react-aria/utils"; | ||
import {getItemCount as $7Kjv5$getItemCount} from "@react-stately/collections"; | ||
import {usePress as $7Kjv5$usePress, useHover as $7Kjv5$useHover, isFocusVisible as $7Kjv5$isFocusVisible, useKeyboard as $7Kjv5$useKeyboard, useFocus as $7Kjv5$useFocus} from "@react-aria/interactions"; | ||
import {useRef as $7Kjv5$useRef} from "react"; | ||
import {useSelectableItem as $7Kjv5$useSelectableItem} from "@react-aria/selection"; | ||
@@ -22,4 +23,5 @@ | ||
function $a2e5df62f93c7633$export$9d32628fc2aea7da(props, state, ref) { | ||
let { id: id, key: key, closeOnSelect: closeOnSelect, isVirtualized: isVirtualized, 'aria-haspopup': hasPopup, onPressStart: pressStartProp, onPressUp: pressUpProp, onPress: pressProp, onPressChange: onPressChange, onPressEnd: onPressEnd, onHoverStart: hoverStartProp, onHoverChange: onHoverChange, onHoverEnd: onHoverEnd, onKeyDown: onKeyDown, onKeyUp: onKeyUp, onFocus: onFocus, onFocusChange: onFocusChange, onBlur: onBlur, selectionManager: selectionManager = state.selectionManager } = props; | ||
let { id: id, key: key, closeOnSelect: closeOnSelect, isVirtualized: isVirtualized, 'aria-haspopup': hasPopup, onPressStart: onPressStart, onPressUp: pressUpProp, onPress: onPress, onPressChange: pressChangeProp, onPressEnd: onPressEnd, onClick: onClickProp, onHoverStart: hoverStartProp, onHoverChange: onHoverChange, onHoverEnd: onHoverEnd, onKeyDown: onKeyDown, onKeyUp: onKeyUp, onFocus: onFocus, onFocusChange: onFocusChange, onBlur: onBlur, selectionManager: selectionManager = state.selectionManager } = props; | ||
let isTrigger = !!hasPopup; | ||
@@ -35,3 +37,3 @@ let isTriggerExpanded = isTrigger && props['aria-expanded'] === 'true'; | ||
let router = (0, $7Kjv5$useRouter)(); | ||
let performAction = (e)=>{ | ||
let performAction = ()=>{ | ||
var _item_props; | ||
@@ -46,3 +48,2 @@ if (isTrigger) return; | ||
} | ||
if (e.target instanceof HTMLAnchorElement && item) router.open(e.target, e, item.props.href, item.props.routerOptions); | ||
}; | ||
@@ -76,26 +77,28 @@ let role = 'menuitem'; | ||
} | ||
let onPressStart = (e)=>{ | ||
if (e.pointerType === 'keyboard') performAction(e); | ||
pressStartProp === null || pressStartProp === void 0 ? void 0 : pressStartProp(e); | ||
let isPressedRef = (0, $7Kjv5$useRef)(false); | ||
let onPressChange = (isPressed)=>{ | ||
pressChangeProp === null || pressChangeProp === void 0 ? void 0 : pressChangeProp(isPressed); | ||
isPressedRef.current = isPressed; | ||
}; | ||
let maybeClose = ()=>{ | ||
// Pressing a menu item should close by default in single selection mode but not multiple | ||
// selection mode, except if overridden by the closeOnSelect prop. | ||
if (!isTrigger && onClose && (closeOnSelect !== null && closeOnSelect !== void 0 ? closeOnSelect : selectionManager.selectionMode !== 'multiple' || selectionManager.isLink(key))) onClose(); | ||
}; | ||
let interaction = (0, $7Kjv5$useRef)(null); | ||
let onPressUp = (e)=>{ | ||
if (e.pointerType !== 'keyboard') interaction.current = { | ||
pointerType: e.pointerType | ||
}; | ||
// If interacting with mouse, allow the user to mouse down on the trigger button, | ||
// drag, and release over an item (matching native behavior). | ||
if (e.pointerType === 'mouse') { | ||
performAction(e); | ||
maybeClose(); | ||
if (!isPressedRef.current) e.target.click(); | ||
} | ||
pressUpProp === null || pressUpProp === void 0 ? void 0 : pressUpProp(e); | ||
}; | ||
let onPress = (e)=>{ | ||
if (e.pointerType !== 'keyboard' && e.pointerType !== 'mouse') { | ||
performAction(e); | ||
maybeClose(); | ||
} | ||
pressProp === null || pressProp === void 0 ? void 0 : pressProp(e); | ||
let onClick = (e)=>{ | ||
var _interaction_current, _interaction_current1; | ||
onClickProp === null || onClickProp === void 0 ? void 0 : onClickProp(e); | ||
performAction(); | ||
(0, $7Kjv5$handleLinkClick)(e, router, item.props.href, item === null || item === void 0 ? void 0 : item.props.routerOptions); | ||
let shouldClose = ((_interaction_current = interaction.current) === null || _interaction_current === void 0 ? void 0 : _interaction_current.pointerType) === 'keyboard' ? ((_interaction_current1 = interaction.current) === null || _interaction_current1 === void 0 ? void 0 : _interaction_current1.key) === 'Enter' || selectionManager.selectionMode === 'none' || selectionManager.isLink(key) : selectionManager.selectionMode !== 'multiple' || selectionManager.isLink(key); | ||
shouldClose = closeOnSelect !== null && closeOnSelect !== void 0 ? closeOnSelect : shouldClose; | ||
if (onClose && !isTrigger && shouldClose) onClose(); | ||
interaction.current = null; | ||
}; | ||
@@ -128,3 +131,3 @@ let { itemProps: itemProps, isFocused: isFocused } = (0, $7Kjv5$useSelectableItem)({ | ||
// Hovering over an already expanded sub dialog trigger should keep focus in the dialog. | ||
if (!(0, $7Kjv5$isFocusVisible)() && !(isTriggerExpanded && hasPopup === 'dialog')) { | ||
if (!(0, $7Kjv5$isFocusVisible)() && !(isTriggerExpanded && hasPopup)) { | ||
selectionManager.setFocused(true); | ||
@@ -148,7 +151,15 @@ selectionManager.setFocusedKey(key); | ||
case ' ': | ||
if (!isDisabled && selectionManager.selectionMode === 'none' && !isTrigger && closeOnSelect !== false && onClose) onClose(); | ||
interaction.current = { | ||
pointerType: 'keyboard', | ||
key: ' ' | ||
}; | ||
e.target.click(); | ||
break; | ||
case 'Enter': | ||
// The Enter key should always close on select, except if overridden. | ||
if (!isDisabled && closeOnSelect !== false && !isTrigger && onClose) onClose(); | ||
interaction.current = { | ||
pointerType: 'keyboard', | ||
key: 'Enter' | ||
}; | ||
// Trigger click unless this is a link. Links trigger click natively. | ||
if (e.target.tagName !== 'A') e.target.click(); | ||
break; | ||
@@ -176,6 +187,12 @@ default: | ||
onFocus: itemProps.onFocus, | ||
'data-collection': itemProps['data-collection'], | ||
'data-key': itemProps['data-key'] | ||
} : itemProps, pressProps, hoverProps, keyboardProps, focusProps), | ||
} : itemProps, pressProps, hoverProps, keyboardProps, focusProps, // Prevent DOM focus from moving on mouse down when using virtual focus or this is a submenu/subdialog trigger. | ||
data.shouldUseVirtualFocus || isTrigger ? { | ||
onMouseDown: (e)=>e.preventDefault() | ||
} : undefined, isDisabled ? undefined : { | ||
onClick: onClick | ||
}), | ||
// 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. | ||
tabIndex: itemProps.tabIndex != null && isTriggerExpanded ? -1 : itemProps.tabIndex | ||
tabIndex: itemProps.tabIndex != null && isTriggerExpanded && !data.shouldUseVirtualFocus ? -1 : itemProps.tabIndex | ||
}, | ||
@@ -192,2 +209,3 @@ labelProps: { | ||
isFocused: isFocused, | ||
isFocusVisible: isFocused && selectionManager.isFocused && (0, $7Kjv5$isFocusVisible)() && !isTriggerExpanded, | ||
isSelected: isSelected, | ||
@@ -194,0 +212,0 @@ isPressed: isPressed, |
var $d1742ec2644a0949$exports = require("./intlStrings.main.js"); | ||
var $jo7gW$reactariautils = require("@react-aria/utils"); | ||
var $jo7gW$reactariainteractions = require("@react-aria/interactions"); | ||
var $jo7gW$reactariai18n = require("@react-aria/i18n"); | ||
var $jo7gW$reactariainteractions = require("@react-aria/interactions"); | ||
var $jo7gW$reactariaoverlays = require("@react-aria/overlays"); | ||
@@ -44,3 +44,7 @@ | ||
case ' ': | ||
if (trigger === 'longPress') return; | ||
// React puts listeners on the same root, so even if propagation was stopped, immediate propagation is still possible. | ||
// useTypeSelect will handle the spacebar first if it's running, so we don't want to open if it's handled it already. | ||
// We use isDefaultPrevented() instead of isPropagationStopped() because createEventHandler stops propagation by default. | ||
// And default prevented means that the event was handled by something else (typeahead), so we don't want to open the menu. | ||
if (trigger === 'longPress' || e.isDefaultPrevented()) return; | ||
// fallthrough | ||
@@ -75,10 +79,19 @@ case 'ArrowDown': | ||
let pressProps = { | ||
preventFocusOnPress: true, | ||
onPressStart (e) { | ||
// For consistency with native, open the menu on mouse/key down, but touch up. | ||
if (e.pointerType !== 'touch' && e.pointerType !== 'keyboard' && !isDisabled) // If opened with a screen reader, auto focus the first item. | ||
// Otherwise, the menu itself will be focused. | ||
state.open(e.pointerType === 'virtual' ? 'first' : null); | ||
if (e.pointerType !== 'touch' && e.pointerType !== 'keyboard' && !isDisabled) { | ||
// Ensure trigger has focus before opening the menu so it can be restored by FocusScope on close. | ||
(0, $jo7gW$reactariautils.focusWithoutScrolling)(e.target); | ||
// If opened with a screen reader, auto focus the first item. | ||
// Otherwise, the menu itself will be focused. | ||
state.open(e.pointerType === 'virtual' ? 'first' : null); | ||
} | ||
}, | ||
onPress (e) { | ||
if (e.pointerType === 'touch' && !isDisabled) state.toggle(); | ||
if (e.pointerType === 'touch' && !isDisabled) { | ||
// Ensure trigger has focus before opening the menu so it can be restored by FocusScope on close. | ||
(0, $jo7gW$reactariautils.focusWithoutScrolling)(e.target); | ||
state.toggle(); | ||
} | ||
} | ||
@@ -85,0 +98,0 @@ }; |
import $czs6v$intlStringsmodulejs from "./intlStrings.module.js"; | ||
import {useId as $czs6v$useId} from "@react-aria/utils"; | ||
import {useId as $czs6v$useId, focusWithoutScrolling as $czs6v$focusWithoutScrolling} from "@react-aria/utils"; | ||
import {useLongPress as $czs6v$useLongPress} from "@react-aria/interactions"; | ||
import {useLocalizedStringFormatter as $czs6v$useLocalizedStringFormatter} from "@react-aria/i18n"; | ||
import {useLongPress as $czs6v$useLongPress} from "@react-aria/interactions"; | ||
import {useOverlayTrigger as $czs6v$useOverlayTrigger} from "@react-aria/overlays"; | ||
@@ -38,3 +38,7 @@ | ||
case ' ': | ||
if (trigger === 'longPress') return; | ||
// React puts listeners on the same root, so even if propagation was stopped, immediate propagation is still possible. | ||
// useTypeSelect will handle the spacebar first if it's running, so we don't want to open if it's handled it already. | ||
// We use isDefaultPrevented() instead of isPropagationStopped() because createEventHandler stops propagation by default. | ||
// And default prevented means that the event was handled by something else (typeahead), so we don't want to open the menu. | ||
if (trigger === 'longPress' || e.isDefaultPrevented()) return; | ||
// fallthrough | ||
@@ -69,10 +73,19 @@ case 'ArrowDown': | ||
let pressProps = { | ||
preventFocusOnPress: true, | ||
onPressStart (e) { | ||
// For consistency with native, open the menu on mouse/key down, but touch up. | ||
if (e.pointerType !== 'touch' && e.pointerType !== 'keyboard' && !isDisabled) // If opened with a screen reader, auto focus the first item. | ||
// Otherwise, the menu itself will be focused. | ||
state.open(e.pointerType === 'virtual' ? 'first' : null); | ||
if (e.pointerType !== 'touch' && e.pointerType !== 'keyboard' && !isDisabled) { | ||
// Ensure trigger has focus before opening the menu so it can be restored by FocusScope on close. | ||
(0, $czs6v$focusWithoutScrolling)(e.target); | ||
// If opened with a screen reader, auto focus the first item. | ||
// Otherwise, the menu itself will be focused. | ||
state.open(e.pointerType === 'virtual' ? 'first' : null); | ||
} | ||
}, | ||
onPress (e) { | ||
if (e.pointerType === 'touch' && !isDisabled) state.toggle(); | ||
if (e.pointerType === 'touch' && !isDisabled) { | ||
// Ensure trigger has focus before opening the menu so it can be restored by FocusScope on close. | ||
(0, $czs6v$focusWithoutScrolling)(e.target); | ||
state.toggle(); | ||
} | ||
} | ||
@@ -79,0 +92,0 @@ }; |
var $g3RPq$react = require("react"); | ||
var $g3RPq$reactariautils = require("@react-aria/utils"); | ||
var $g3RPq$reactariainteractions = require("@react-aria/interactions"); | ||
var $g3RPq$reactariautils = require("@react-aria/utils"); | ||
@@ -44,2 +44,7 @@ | ||
let modality = (0, $g3RPq$reactariainteractions.useInteractionModality)(); | ||
// Prevent mouse down over safe triangle. Clicking while pointer-events: none is applied | ||
// will cause focus to move unexpectedly since it will go to an element behind the menu. | ||
let onPointerDown = (0, $g3RPq$reactariautils.useEffectEvent)((e)=>{ | ||
if (preventPointerEvents) e.preventDefault(); | ||
}); | ||
(0, $g3RPq$react.useEffect)(()=>{ | ||
@@ -118,4 +123,8 @@ if (preventPointerEvents && menuRef.current) menuRef.current.style.pointerEvents = 'none'; | ||
window.addEventListener('pointermove', onPointerMove); | ||
// Prevent pointer down over the safe triangle. See above comment. | ||
// Do not enable in tests, because JSDom doesn't do hit testing. | ||
if (process.env.NODE_ENV !== 'test') window.addEventListener('pointerdown', onPointerDown, true); | ||
return ()=>{ | ||
window.removeEventListener('pointermove', onPointerMove); | ||
if (process.env.NODE_ENV !== 'test') window.removeEventListener('pointerdown', onPointerDown, true); | ||
clearTimeout(timeout.current); | ||
@@ -131,2 +140,3 @@ clearTimeout(autoCloseTimeout.current); | ||
setPreventPointerEvents, | ||
onPointerDown, | ||
submenuRef | ||
@@ -133,0 +143,0 @@ ]); |
import {useRef as $fUfeP$useRef, useState as $fUfeP$useState, useEffect as $fUfeP$useEffect} from "react"; | ||
import {useResizeObserver as $fUfeP$useResizeObserver, useEffectEvent as $fUfeP$useEffectEvent} from "@react-aria/utils"; | ||
import {useInteractionModality as $fUfeP$useInteractionModality} from "@react-aria/interactions"; | ||
import {useResizeObserver as $fUfeP$useResizeObserver} from "@react-aria/utils"; | ||
@@ -38,2 +38,7 @@ | ||
let modality = (0, $fUfeP$useInteractionModality)(); | ||
// Prevent mouse down over safe triangle. Clicking while pointer-events: none is applied | ||
// will cause focus to move unexpectedly since it will go to an element behind the menu. | ||
let onPointerDown = (0, $fUfeP$useEffectEvent)((e)=>{ | ||
if (preventPointerEvents) e.preventDefault(); | ||
}); | ||
(0, $fUfeP$useEffect)(()=>{ | ||
@@ -112,4 +117,8 @@ if (preventPointerEvents && menuRef.current) menuRef.current.style.pointerEvents = 'none'; | ||
window.addEventListener('pointermove', onPointerMove); | ||
// Prevent pointer down over the safe triangle. See above comment. | ||
// Do not enable in tests, because JSDom doesn't do hit testing. | ||
if (process.env.NODE_ENV !== 'test') window.addEventListener('pointerdown', onPointerDown, true); | ||
return ()=>{ | ||
window.removeEventListener('pointermove', onPointerMove); | ||
if (process.env.NODE_ENV !== 'test') window.removeEventListener('pointerdown', onPointerDown, true); | ||
clearTimeout(timeout.current); | ||
@@ -125,2 +134,3 @@ clearTimeout(autoCloseTimeout.current); | ||
setPreventPointerEvents, | ||
onPointerDown, | ||
submenuRef | ||
@@ -127,0 +137,0 @@ ]); |
var $62347d8c4183e713$exports = require("./useSafelyMouseToSubmenu.main.js"); | ||
var $23MMN$reactariautils = require("@react-aria/utils"); | ||
var $23MMN$react = require("react"); | ||
var $23MMN$reactariautils = require("@react-aria/utils"); | ||
var $23MMN$reactariai18n = require("@react-aria/i18n"); | ||
@@ -27,3 +27,3 @@ | ||
function $5f4753043c9f6cdf$export$7138b0d059a6e743(props, state, ref) { | ||
let { parentMenuRef: parentMenuRef, submenuRef: submenuRef, type: type = 'menu', isDisabled: isDisabled, delay: delay = 200 } = props; | ||
let { parentMenuRef: parentMenuRef, submenuRef: submenuRef, type: type = 'menu', isDisabled: isDisabled, delay: delay = 200, shouldUseVirtualFocus: shouldUseVirtualFocus } = props; | ||
let submenuTriggerId = (0, $23MMN$reactariautils.useId)(); | ||
@@ -57,9 +57,12 @@ let overlayId = (0, $23MMN$reactariautils.useId)(); | ||
let submenuKeyDown = (e)=>{ | ||
// If focus is not within the menu, assume virtual focus is being used. | ||
// This means some other input element is also within the popover, so we shouldn't close the menu. | ||
if (!e.currentTarget.contains(document.activeElement)) return; | ||
switch(e.key){ | ||
case 'ArrowLeft': | ||
if (direction === 'ltr' && e.currentTarget.contains(e.target)) { | ||
var _ref_current; | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
onSubmenuClose(); | ||
(_ref_current = ref.current) === null || _ref_current === void 0 ? void 0 : _ref_current.focus(); | ||
if (!shouldUseVirtualFocus && ref.current) (0, $23MMN$reactariautils.focusWithoutScrolling)(ref.current); | ||
} | ||
@@ -69,11 +72,16 @@ break; | ||
if (direction === 'rtl' && e.currentTarget.contains(e.target)) { | ||
var _ref_current1; | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
onSubmenuClose(); | ||
(_ref_current1 = ref.current) === null || _ref_current1 === void 0 ? void 0 : _ref_current1.focus(); | ||
if (!shouldUseVirtualFocus && ref.current) (0, $23MMN$reactariautils.focusWithoutScrolling)(ref.current); | ||
} | ||
break; | ||
case 'Escape': | ||
e.stopPropagation(); | ||
state.closeAll(); | ||
var _submenuRef_current; | ||
// TODO: can remove this when we fix collection event leaks | ||
if ((_submenuRef_current = submenuRef.current) === null || _submenuRef_current === void 0 ? void 0 : _submenuRef_current.contains(e.target)) { | ||
e.stopPropagation(); | ||
onSubmenuClose(); | ||
if (!shouldUseVirtualFocus && ref.current) (0, $23MMN$reactariautils.focusWithoutScrolling)(ref.current); | ||
} | ||
break; | ||
@@ -98,4 +106,5 @@ } | ||
if (direction === 'ltr') { | ||
e.preventDefault(); | ||
if (!state.isOpen) onSubmenuOpen('first'); | ||
if (type === 'menu' && !!(submenuRef === null || submenuRef === void 0 ? void 0 : submenuRef.current) && document.activeElement === (ref === null || ref === void 0 ? void 0 : ref.current)) submenuRef.current.focus(); | ||
if (type === 'menu' && !!(submenuRef === null || submenuRef === void 0 ? void 0 : submenuRef.current) && document.activeElement === (ref === null || ref === void 0 ? void 0 : ref.current)) (0, $23MMN$reactariautils.focusWithoutScrolling)(submenuRef.current); | ||
} else if (state.isOpen) onSubmenuClose(); | ||
@@ -108,4 +117,5 @@ else e.continuePropagation(); | ||
if (direction === 'rtl') { | ||
e.preventDefault(); | ||
if (!state.isOpen) onSubmenuOpen('first'); | ||
if (type === 'menu' && !!(submenuRef === null || submenuRef === void 0 ? void 0 : submenuRef.current) && document.activeElement === (ref === null || ref === void 0 ? void 0 : ref.current)) submenuRef.current.focus(); | ||
if (type === 'menu' && !!(submenuRef === null || submenuRef === void 0 ? void 0 : submenuRef.current) && document.activeElement === (ref === null || ref === void 0 ? void 0 : ref.current)) (0, $23MMN$reactariautils.focusWithoutScrolling)(submenuRef.current); | ||
} else if (state.isOpen) onSubmenuClose(); | ||
@@ -115,5 +125,2 @@ else e.continuePropagation(); | ||
break; | ||
case 'Escape': | ||
state.closeAll(); | ||
break; | ||
default: | ||
@@ -142,6 +149,8 @@ e.continuePropagation(); | ||
}; | ||
let onBlur = (e)=>{ | ||
(0, $23MMN$reactariautils.useEvent)(parentMenuRef, 'focusin', (e)=>{ | ||
var _parentMenuRef_current; | ||
if (state.isOpen && ((_parentMenuRef_current = parentMenuRef.current) === null || _parentMenuRef_current === void 0 ? void 0 : _parentMenuRef_current.contains(e.relatedTarget))) onSubmenuClose(); | ||
}; | ||
// If we detect focus moved to a different item in the same menu that the currently open submenu trigger is in | ||
// then close the submenu. This is for a case where the user hovers a root menu item when multiple submenus are open | ||
if (state.isOpen && ((_parentMenuRef_current = parentMenuRef.current) === null || _parentMenuRef_current === void 0 ? void 0 : _parentMenuRef_current.contains(e.target)) && e.target !== ref.current) onSubmenuClose(); | ||
}); | ||
let shouldCloseOnInteractOutside = (target)=>{ | ||
@@ -167,3 +176,2 @@ if (target !== ref.current) return true; | ||
onKeyDown: submenuTriggerKeyDown, | ||
onBlur: onBlur, | ||
isOpen: state.isOpen | ||
@@ -174,3 +182,2 @@ }, | ||
isNonModal: true, | ||
disableFocusManagement: true, | ||
shouldCloseOnInteractOutside: shouldCloseOnInteractOutside | ||
@@ -177,0 +184,0 @@ } |
import {useSafelyMouseToSubmenu as $d275435c250248f8$export$85ec83e04c95f50a} from "./useSafelyMouseToSubmenu.module.js"; | ||
import {useId as $dXlYe$useId, useEffectEvent as $dXlYe$useEffectEvent, useLayoutEffect as $dXlYe$useLayoutEffect, focusWithoutScrolling as $dXlYe$focusWithoutScrolling, useEvent as $dXlYe$useEvent} from "@react-aria/utils"; | ||
import {useRef as $dXlYe$useRef, useCallback as $dXlYe$useCallback} from "react"; | ||
import {useId as $dXlYe$useId, useEffectEvent as $dXlYe$useEffectEvent, useLayoutEffect as $dXlYe$useLayoutEffect} from "@react-aria/utils"; | ||
import {useLocale as $dXlYe$useLocale} from "@react-aria/i18n"; | ||
@@ -21,3 +21,3 @@ | ||
function $0065b146e7192841$export$7138b0d059a6e743(props, state, ref) { | ||
let { parentMenuRef: parentMenuRef, submenuRef: submenuRef, type: type = 'menu', isDisabled: isDisabled, delay: delay = 200 } = props; | ||
let { parentMenuRef: parentMenuRef, submenuRef: submenuRef, type: type = 'menu', isDisabled: isDisabled, delay: delay = 200, shouldUseVirtualFocus: shouldUseVirtualFocus } = props; | ||
let submenuTriggerId = (0, $dXlYe$useId)(); | ||
@@ -51,9 +51,12 @@ let overlayId = (0, $dXlYe$useId)(); | ||
let submenuKeyDown = (e)=>{ | ||
// If focus is not within the menu, assume virtual focus is being used. | ||
// This means some other input element is also within the popover, so we shouldn't close the menu. | ||
if (!e.currentTarget.contains(document.activeElement)) return; | ||
switch(e.key){ | ||
case 'ArrowLeft': | ||
if (direction === 'ltr' && e.currentTarget.contains(e.target)) { | ||
var _ref_current; | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
onSubmenuClose(); | ||
(_ref_current = ref.current) === null || _ref_current === void 0 ? void 0 : _ref_current.focus(); | ||
if (!shouldUseVirtualFocus && ref.current) (0, $dXlYe$focusWithoutScrolling)(ref.current); | ||
} | ||
@@ -63,11 +66,16 @@ break; | ||
if (direction === 'rtl' && e.currentTarget.contains(e.target)) { | ||
var _ref_current1; | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
onSubmenuClose(); | ||
(_ref_current1 = ref.current) === null || _ref_current1 === void 0 ? void 0 : _ref_current1.focus(); | ||
if (!shouldUseVirtualFocus && ref.current) (0, $dXlYe$focusWithoutScrolling)(ref.current); | ||
} | ||
break; | ||
case 'Escape': | ||
e.stopPropagation(); | ||
state.closeAll(); | ||
var _submenuRef_current; | ||
// TODO: can remove this when we fix collection event leaks | ||
if ((_submenuRef_current = submenuRef.current) === null || _submenuRef_current === void 0 ? void 0 : _submenuRef_current.contains(e.target)) { | ||
e.stopPropagation(); | ||
onSubmenuClose(); | ||
if (!shouldUseVirtualFocus && ref.current) (0, $dXlYe$focusWithoutScrolling)(ref.current); | ||
} | ||
break; | ||
@@ -92,4 +100,5 @@ } | ||
if (direction === 'ltr') { | ||
e.preventDefault(); | ||
if (!state.isOpen) onSubmenuOpen('first'); | ||
if (type === 'menu' && !!(submenuRef === null || submenuRef === void 0 ? void 0 : submenuRef.current) && document.activeElement === (ref === null || ref === void 0 ? void 0 : ref.current)) submenuRef.current.focus(); | ||
if (type === 'menu' && !!(submenuRef === null || submenuRef === void 0 ? void 0 : submenuRef.current) && document.activeElement === (ref === null || ref === void 0 ? void 0 : ref.current)) (0, $dXlYe$focusWithoutScrolling)(submenuRef.current); | ||
} else if (state.isOpen) onSubmenuClose(); | ||
@@ -102,4 +111,5 @@ else e.continuePropagation(); | ||
if (direction === 'rtl') { | ||
e.preventDefault(); | ||
if (!state.isOpen) onSubmenuOpen('first'); | ||
if (type === 'menu' && !!(submenuRef === null || submenuRef === void 0 ? void 0 : submenuRef.current) && document.activeElement === (ref === null || ref === void 0 ? void 0 : ref.current)) submenuRef.current.focus(); | ||
if (type === 'menu' && !!(submenuRef === null || submenuRef === void 0 ? void 0 : submenuRef.current) && document.activeElement === (ref === null || ref === void 0 ? void 0 : ref.current)) (0, $dXlYe$focusWithoutScrolling)(submenuRef.current); | ||
} else if (state.isOpen) onSubmenuClose(); | ||
@@ -109,5 +119,2 @@ else e.continuePropagation(); | ||
break; | ||
case 'Escape': | ||
state.closeAll(); | ||
break; | ||
default: | ||
@@ -136,6 +143,8 @@ e.continuePropagation(); | ||
}; | ||
let onBlur = (e)=>{ | ||
(0, $dXlYe$useEvent)(parentMenuRef, 'focusin', (e)=>{ | ||
var _parentMenuRef_current; | ||
if (state.isOpen && ((_parentMenuRef_current = parentMenuRef.current) === null || _parentMenuRef_current === void 0 ? void 0 : _parentMenuRef_current.contains(e.relatedTarget))) onSubmenuClose(); | ||
}; | ||
// If we detect focus moved to a different item in the same menu that the currently open submenu trigger is in | ||
// then close the submenu. This is for a case where the user hovers a root menu item when multiple submenus are open | ||
if (state.isOpen && ((_parentMenuRef_current = parentMenuRef.current) === null || _parentMenuRef_current === void 0 ? void 0 : _parentMenuRef_current.contains(e.target)) && e.target !== ref.current) onSubmenuClose(); | ||
}); | ||
let shouldCloseOnInteractOutside = (target)=>{ | ||
@@ -161,3 +170,2 @@ if (target !== ref.current) return true; | ||
onKeyDown: submenuTriggerKeyDown, | ||
onBlur: onBlur, | ||
isOpen: state.isOpen | ||
@@ -168,3 +176,2 @@ }, | ||
isNonModal: true, | ||
disableFocusManagement: true, | ||
shouldCloseOnInteractOutside: shouldCloseOnInteractOutside | ||
@@ -171,0 +178,0 @@ } |
{ | ||
"name": "@react-aria/menu", | ||
"version": "3.0.0-nightly-d87cc4422-250109", | ||
"version": "3.0.0-nightly-da4ef7644-250826", | ||
"description": "Spectrum UI components in React", | ||
@@ -9,3 +9,7 @@ "license": "Apache-2.0", | ||
"exports": { | ||
"types": "./dist/types.d.ts", | ||
"source": "./src/index.ts", | ||
"types": [ | ||
"./dist/types.d.ts", | ||
"./src/index.ts" | ||
], | ||
"import": "./dist/import.mjs", | ||
@@ -26,15 +30,15 @@ "require": "./dist/main.js" | ||
"dependencies": { | ||
"@react-aria/focus": "3.0.0-nightly-d87cc4422-250109", | ||
"@react-aria/i18n": "3.0.0-nightly-d87cc4422-250109", | ||
"@react-aria/interactions": "3.0.0-nightly-d87cc4422-250109", | ||
"@react-aria/overlays": "3.0.0-nightly-d87cc4422-250109", | ||
"@react-aria/selection": "3.0.0-nightly-d87cc4422-250109", | ||
"@react-aria/utils": "3.0.0-nightly-d87cc4422-250109", | ||
"@react-stately/collections": "3.0.0-nightly-d87cc4422-250109", | ||
"@react-stately/menu": "3.0.0-nightly-d87cc4422-250109", | ||
"@react-stately/selection": "3.0.0-nightly-d87cc4422-250109", | ||
"@react-stately/tree": "3.0.0-nightly-d87cc4422-250109", | ||
"@react-types/button": "3.0.0-nightly-d87cc4422-250109", | ||
"@react-types/menu": "3.0.0-nightly-d87cc4422-250109", | ||
"@react-types/shared": "3.0.0-nightly-d87cc4422-250109", | ||
"@react-aria/focus": "3.0.0-nightly-da4ef7644-250826", | ||
"@react-aria/i18n": "3.0.0-nightly-da4ef7644-250826", | ||
"@react-aria/interactions": "3.0.0-nightly-da4ef7644-250826", | ||
"@react-aria/overlays": "3.0.0-nightly-da4ef7644-250826", | ||
"@react-aria/selection": "3.0.0-nightly-da4ef7644-250826", | ||
"@react-aria/utils": "3.0.0-nightly-da4ef7644-250826", | ||
"@react-stately/collections": "3.0.0-nightly-da4ef7644-250826", | ||
"@react-stately/menu": "3.0.0-nightly-da4ef7644-250826", | ||
"@react-stately/selection": "3.0.0-nightly-da4ef7644-250826", | ||
"@react-stately/tree": "3.0.0-nightly-da4ef7644-250826", | ||
"@react-types/button": "3.0.0-nightly-da4ef7644-250826", | ||
"@react-types/menu": "3.0.0-nightly-da4ef7644-250826", | ||
"@react-types/shared": "3.0.0-nightly-da4ef7644-250826", | ||
"@swc/helpers": "^0.5.0" | ||
@@ -41,0 +45,0 @@ }, |
@@ -53,3 +53,3 @@ /* | ||
if (!props['aria-label'] && !props['aria-labelledby']) { | ||
if (!props['aria-label'] && !props['aria-labelledby'] && process.env.NODE_ENV !== 'production') { | ||
console.warn('An aria-label or aria-labelledby prop is required for accessibility.'); | ||
@@ -56,0 +56,0 @@ } |
@@ -13,7 +13,8 @@ /* | ||
import {DOMAttributes, DOMProps, FocusableElement, FocusEvents, HoverEvents, Key, KeyboardEvents, PressEvent, PressEvents, RefObject, RouterOptions} from '@react-types/shared'; | ||
import {filterDOMProps, mergeProps, useLinkProps, useRouter, useSlotId} from '@react-aria/utils'; | ||
import {DOMAttributes, DOMProps, FocusableElement, FocusEvents, HoverEvents, Key, KeyboardEvents, PressEvent, PressEvents, RefObject} from '@react-types/shared'; | ||
import {filterDOMProps, handleLinkClick, mergeProps, useLinkProps, useRouter, useSlotId} from '@react-aria/utils'; | ||
import {getItemCount} from '@react-stately/collections'; | ||
import {isFocusVisible, useFocus, useHover, useKeyboard, usePress} from '@react-aria/interactions'; | ||
import {menuData} from './utils'; | ||
import {MouseEvent, useRef} from 'react'; | ||
import {SelectionManager} from '@react-stately/selection'; | ||
@@ -38,2 +39,4 @@ import {TreeState} from '@react-stately/tree'; | ||
isFocused: boolean, | ||
/** Whether the item is keyboard focused. */ | ||
isFocusVisible: boolean, | ||
/** Whether the item is currently selected. */ | ||
@@ -113,7 +116,8 @@ isSelected: boolean, | ||
'aria-haspopup': hasPopup, | ||
onPressStart: pressStartProp, | ||
onPressStart, | ||
onPressUp: pressUpProp, | ||
onPress: pressProp, | ||
onPressChange, | ||
onPress, | ||
onPressChange: pressChangeProp, | ||
onPressEnd, | ||
onClick: onClickProp, | ||
onHoverStart: hoverStartProp, | ||
@@ -138,3 +142,3 @@ onHoverChange, | ||
let router = useRouter(); | ||
let performAction = (e: PressEvent) => { | ||
let performAction = () => { | ||
if (isTrigger) { | ||
@@ -155,6 +159,2 @@ return; | ||
} | ||
if (e.target instanceof HTMLAnchorElement && item) { | ||
router.open(e.target, e, item.props.href, item.props.routerOptions as RouterOptions); | ||
} | ||
}; | ||
@@ -196,24 +196,20 @@ | ||
let onPressStart = (e: PressEvent) => { | ||
if (e.pointerType === 'keyboard') { | ||
performAction(e); | ||
} | ||
pressStartProp?.(e); | ||
let isPressedRef = useRef(false); | ||
let onPressChange = (isPressed: boolean) => { | ||
pressChangeProp?.(isPressed); | ||
isPressedRef.current = isPressed; | ||
}; | ||
let maybeClose = () => { | ||
// Pressing a menu item should close by default in single selection mode but not multiple | ||
// selection mode, except if overridden by the closeOnSelect prop. | ||
if (!isTrigger && onClose && (closeOnSelect ?? (selectionManager.selectionMode !== 'multiple' || selectionManager.isLink(key)))) { | ||
onClose(); | ||
let interaction = useRef<{pointerType: string, key?: string} | null>(null); | ||
let onPressUp = (e: PressEvent) => { | ||
if (e.pointerType !== 'keyboard') { | ||
interaction.current = {pointerType: e.pointerType}; | ||
} | ||
}; | ||
let onPressUp = (e: PressEvent) => { | ||
// If interacting with mouse, allow the user to mouse down on the trigger button, | ||
// drag, and release over an item (matching native behavior). | ||
if (e.pointerType === 'mouse') { | ||
performAction(e); | ||
maybeClose(); | ||
if (!isPressedRef.current) { | ||
(e.target as HTMLElement).click(); | ||
} | ||
} | ||
@@ -224,9 +220,19 @@ | ||
let onPress = (e: PressEvent) => { | ||
if (e.pointerType !== 'keyboard' && e.pointerType !== 'mouse') { | ||
performAction(e); | ||
maybeClose(); | ||
let onClick = (e: MouseEvent<FocusableElement>) => { | ||
onClickProp?.(e); | ||
performAction(); | ||
handleLinkClick(e, router, item!.props.href, item?.props.routerOptions); | ||
let shouldClose = interaction.current?.pointerType === 'keyboard' | ||
// Always close when pressing Enter key, or if item is not selectable. | ||
? interaction.current?.key === 'Enter' || selectionManager.selectionMode === 'none' || selectionManager.isLink(key) | ||
// Close except if multi-select is enabled. | ||
: selectionManager.selectionMode !== 'multiple' || selectionManager.isLink(key); | ||
shouldClose = closeOnSelect ?? shouldClose; | ||
if (onClose && !isTrigger && shouldClose) { | ||
onClose(); | ||
} | ||
pressProp?.(e); | ||
interaction.current = null; | ||
}; | ||
@@ -261,3 +267,3 @@ | ||
// Hovering over an already expanded sub dialog trigger should keep focus in the dialog. | ||
if (!isFocusVisible() && !(isTriggerExpanded && hasPopup === 'dialog')) { | ||
if (!isFocusVisible() && !(isTriggerExpanded && hasPopup)) { | ||
selectionManager.setFocused(true); | ||
@@ -283,10 +289,11 @@ selectionManager.setFocusedKey(key); | ||
case ' ': | ||
if (!isDisabled && selectionManager.selectionMode === 'none' && !isTrigger && closeOnSelect !== false && onClose) { | ||
onClose(); | ||
} | ||
interaction.current = {pointerType: 'keyboard', key: ' '}; | ||
(e.target as HTMLElement).click(); | ||
break; | ||
case 'Enter': | ||
// The Enter key should always close on select, except if overridden. | ||
if (!isDisabled && closeOnSelect !== false && !isTrigger && onClose) { | ||
onClose(); | ||
interaction.current = {pointerType: 'keyboard', key: 'Enter'}; | ||
// Trigger click unless this is a link. Links trigger click natively. | ||
if ((e.target as HTMLElement).tagName !== 'A') { | ||
(e.target as HTMLElement).click(); | ||
} | ||
@@ -314,5 +321,18 @@ break; | ||
...ariaProps, | ||
...mergeProps(domProps, linkProps, isTrigger ? {onFocus: itemProps.onFocus, 'data-key': itemProps['data-key']} : itemProps, pressProps, hoverProps, keyboardProps, focusProps), | ||
...mergeProps( | ||
domProps, | ||
linkProps, | ||
isTrigger | ||
? {onFocus: itemProps.onFocus, 'data-collection': itemProps['data-collection'], 'data-key': itemProps['data-key']} | ||
: itemProps, | ||
pressProps, | ||
hoverProps, | ||
keyboardProps, | ||
focusProps, | ||
// Prevent DOM focus from moving on mouse down when using virtual focus or this is a submenu/subdialog trigger. | ||
data.shouldUseVirtualFocus || isTrigger ? {onMouseDown: e => e.preventDefault()} : undefined, | ||
isDisabled ? undefined : {onClick} | ||
), | ||
// 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. | ||
tabIndex: itemProps.tabIndex != null && isTriggerExpanded ? -1 : itemProps.tabIndex | ||
tabIndex: itemProps.tabIndex != null && isTriggerExpanded && !data.shouldUseVirtualFocus ? -1 : itemProps.tabIndex | ||
}, | ||
@@ -329,2 +349,3 @@ labelProps: { | ||
isFocused, | ||
isFocusVisible: isFocused && selectionManager.isFocused && isFocusVisible() && !isTriggerExpanded, | ||
isSelected, | ||
@@ -331,0 +352,0 @@ isPressed, |
@@ -15,2 +15,4 @@ /* | ||
import {AriaMenuOptions} from './useMenu'; | ||
import {FocusableElement, RefObject} from '@react-types/shared'; | ||
import {focusWithoutScrolling, useId} from '@react-aria/utils'; | ||
// @ts-ignore | ||
@@ -20,6 +22,4 @@ import intlMessages from '../intl/*.json'; | ||
import {MenuTriggerType} from '@react-types/menu'; | ||
import {RefObject} from '@react-types/shared'; | ||
import {useId} from '@react-aria/utils'; | ||
import {PressProps, useLongPress} from '@react-aria/interactions'; | ||
import {useLocalizedStringFormatter} from '@react-aria/i18n'; | ||
import {useLongPress} from '@react-aria/interactions'; | ||
import {useOverlayTrigger} from '@react-aria/overlays'; | ||
@@ -73,3 +73,7 @@ | ||
case ' ': | ||
if (trigger === 'longPress') { | ||
// React puts listeners on the same root, so even if propagation was stopped, immediate propagation is still possible. | ||
// useTypeSelect will handle the spacebar first if it's running, so we don't want to open if it's handled it already. | ||
// We use isDefaultPrevented() instead of isPropagationStopped() because createEventHandler stops propagation by default. | ||
// And default prevented means that the event was handled by something else (typeahead), so we don't want to open the menu. | ||
if (trigger === 'longPress' || e.isDefaultPrevented()) { | ||
return; | ||
@@ -114,6 +118,10 @@ } | ||
let pressProps = { | ||
let pressProps: PressProps = { | ||
preventFocusOnPress: true, | ||
onPressStart(e) { | ||
// For consistency with native, open the menu on mouse/key down, but touch up. | ||
if (e.pointerType !== 'touch' && e.pointerType !== 'keyboard' && !isDisabled) { | ||
// Ensure trigger has focus before opening the menu so it can be restored by FocusScope on close. | ||
focusWithoutScrolling(e.target as FocusableElement); | ||
// If opened with a screen reader, auto focus the first item. | ||
@@ -126,2 +134,5 @@ // Otherwise, the menu itself will be focused. | ||
if (e.pointerType === 'touch' && !isDisabled) { | ||
// Ensure trigger has focus before opening the menu so it can be restored by FocusScope on close. | ||
focusWithoutScrolling(e.target as FocusableElement); | ||
state.toggle(); | ||
@@ -128,0 +139,0 @@ } |
import {RefObject} from '@react-types/shared'; | ||
import {useEffect, useRef, useState} from 'react'; | ||
import {useEffectEvent, useResizeObserver} from '@react-aria/utils'; | ||
import {useInteractionModality} from '@react-aria/interactions'; | ||
import {useResizeObserver} from '@react-aria/utils'; | ||
@@ -27,3 +27,3 @@ interface SafelyMouseToSubmenuOptions { | ||
*/ | ||
export function useSafelyMouseToSubmenu(options: SafelyMouseToSubmenuOptions) { | ||
export function useSafelyMouseToSubmenu(options: SafelyMouseToSubmenuOptions): void { | ||
let {menuRef, submenuRef, isOpen, isDisabled} = options; | ||
@@ -55,2 +55,10 @@ let prevPointerPos = useRef<{x: number, y: number} | undefined>(undefined); | ||
// Prevent mouse down over safe triangle. Clicking while pointer-events: none is applied | ||
// will cause focus to move unexpectedly since it will go to an element behind the menu. | ||
let onPointerDown = useEffectEvent((e: PointerEvent) => { | ||
if (preventPointerEvents) { | ||
e.preventDefault(); | ||
} | ||
}); | ||
useEffect(() => { | ||
@@ -155,4 +163,13 @@ if (preventPointerEvents && menuRef.current) { | ||
// Prevent pointer down over the safe triangle. See above comment. | ||
// Do not enable in tests, because JSDom doesn't do hit testing. | ||
if (process.env.NODE_ENV !== 'test') { | ||
window.addEventListener('pointerdown', onPointerDown, true); | ||
} | ||
return () => { | ||
window.removeEventListener('pointermove', onPointerMove); | ||
if (process.env.NODE_ENV !== 'test') { | ||
window.removeEventListener('pointerdown', onPointerDown, true); | ||
} | ||
clearTimeout(timeout.current); | ||
@@ -163,3 +180,3 @@ clearTimeout(autoCloseTimeout.current); | ||
}, [isDisabled, isOpen, menuRef, modality, setPreventPointerEvents, submenuRef]); | ||
}, [isDisabled, isOpen, menuRef, modality, setPreventPointerEvents, onPointerDown, submenuRef]); | ||
} |
@@ -17,5 +17,5 @@ /* | ||
import {FocusableElement, FocusStrategy, KeyboardEvent, Node, PressEvent, RefObject} from '@react-types/shared'; | ||
import {focusWithoutScrolling, useEffectEvent, useEvent, useId, useLayoutEffect} from '@react-aria/utils'; | ||
import type {SubmenuTriggerState} from '@react-stately/menu'; | ||
import {useCallback, useRef} from 'react'; | ||
import {useEffectEvent, useId, useLayoutEffect} from '@react-aria/utils'; | ||
import {useLocale} from '@react-aria/i18n'; | ||
@@ -42,3 +42,5 @@ import {useSafelyMouseToSubmenu} from './useSafelyMouseToSubmenu'; | ||
*/ | ||
delay?: number | ||
delay?: number, | ||
/** Whether the submenu trigger uses virtual focus. */ | ||
shouldUseVirtualFocus?: boolean | ||
} | ||
@@ -72,3 +74,3 @@ | ||
export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: SubmenuTriggerState, ref: RefObject<FocusableElement | null>): SubmenuTriggerAria<T> { | ||
let {parentMenuRef, submenuRef, type = 'menu', isDisabled, delay = 200} = props; | ||
let {parentMenuRef, submenuRef, type = 'menu', isDisabled, delay = 200, shouldUseVirtualFocus} = props; | ||
let submenuTriggerId = useId(); | ||
@@ -102,8 +104,17 @@ let overlayId = useId(); | ||
let submenuKeyDown = (e: KeyboardEvent) => { | ||
// If focus is not within the menu, assume virtual focus is being used. | ||
// This means some other input element is also within the popover, so we shouldn't close the menu. | ||
if (!e.currentTarget.contains(document.activeElement)) { | ||
return; | ||
} | ||
switch (e.key) { | ||
case 'ArrowLeft': | ||
if (direction === 'ltr' && e.currentTarget.contains(e.target as Element)) { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
onSubmenuClose(); | ||
ref.current?.focus(); | ||
if (!shouldUseVirtualFocus && ref.current) { | ||
focusWithoutScrolling(ref.current); | ||
} | ||
} | ||
@@ -113,10 +124,19 @@ break; | ||
if (direction === 'rtl' && e.currentTarget.contains(e.target as Element)) { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
onSubmenuClose(); | ||
ref.current?.focus(); | ||
if (!shouldUseVirtualFocus && ref.current) { | ||
focusWithoutScrolling(ref.current); | ||
} | ||
} | ||
break; | ||
case 'Escape': | ||
e.stopPropagation(); | ||
state.closeAll(); | ||
// TODO: can remove this when we fix collection event leaks | ||
if (submenuRef.current?.contains(e.target as Element)) { | ||
e.stopPropagation(); | ||
onSubmenuClose(); | ||
if (!shouldUseVirtualFocus && ref.current) { | ||
focusWithoutScrolling(ref.current); | ||
} | ||
} | ||
break; | ||
@@ -142,2 +162,3 @@ } | ||
if (direction === 'ltr') { | ||
e.preventDefault(); | ||
if (!state.isOpen) { | ||
@@ -148,3 +169,3 @@ onSubmenuOpen('first'); | ||
if (type === 'menu' && !!submenuRef?.current && document.activeElement === ref?.current) { | ||
submenuRef.current.focus(); | ||
focusWithoutScrolling(submenuRef.current); | ||
} | ||
@@ -162,2 +183,3 @@ } else if (state.isOpen) { | ||
if (direction === 'rtl') { | ||
e.preventDefault(); | ||
if (!state.isOpen) { | ||
@@ -168,3 +190,3 @@ onSubmenuOpen('first'); | ||
if (type === 'menu' && !!submenuRef?.current && document.activeElement === ref?.current) { | ||
submenuRef.current.focus(); | ||
focusWithoutScrolling(submenuRef.current); | ||
} | ||
@@ -178,5 +200,2 @@ } else if (state.isOpen) { | ||
break; | ||
case 'Escape': | ||
state.closeAll(); | ||
break; | ||
default: | ||
@@ -217,7 +236,9 @@ e.continuePropagation(); | ||
let onBlur = (e) => { | ||
if (state.isOpen && parentMenuRef.current?.contains(e.relatedTarget)) { | ||
useEvent(parentMenuRef, 'focusin', (e) => { | ||
// If we detect focus moved to a different item in the same menu that the currently open submenu trigger is in | ||
// then close the submenu. This is for a case where the user hovers a root menu item when multiple submenus are open | ||
if (state.isOpen && (parentMenuRef.current?.contains(e.target as HTMLElement) && e.target !== ref.current)) { | ||
onSubmenuClose(); | ||
} | ||
}; | ||
}); | ||
@@ -244,3 +265,2 @@ let shouldCloseOnInteractOutside = (target) => { | ||
onKeyDown: submenuTriggerKeyDown, | ||
onBlur, | ||
isOpen: state.isOpen | ||
@@ -251,3 +271,2 @@ }, | ||
isNonModal: true, | ||
disableFocusManagement: true, | ||
shouldCloseOnInteractOutside | ||
@@ -254,0 +273,0 @@ } |
@@ -22,2 +22,2 @@ /* | ||
export const menuData = new WeakMap<TreeState<unknown>, MenuData>(); | ||
export const menuData: WeakMap<TreeState<unknown>, MenuData> = new WeakMap<TreeState<unknown>, MenuData>(); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 1 instance in 1 package
351054
8.58%4184
5.28%9
Infinity%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated