Skip to content

Commit 30e6762

Browse files
committed
prevent focus from being lost to the body when submenutrigger is virtually focused
typically submenus dont have focus restore turned on since it would move focus manually back to the trigger when keyboard closing the menu. However, we cant move focus to virtually focused triggers so enable focus restore on the submenu in these cases
1 parent 5e3fb0b commit 30e6762

File tree

3 files changed

+20
-14
lines changed

3 files changed

+20
-14
lines changed

packages/@react-aria/menu/src/useSubmenuTrigger.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ export interface AriaSubmenuTriggerProps {
3838
* The delay time in milliseconds for the submenu to appear after hovering over the trigger.
3939
* @default 200
4040
*/
41-
delay?: number
41+
delay?: number,
42+
/** Whether the submenu trigger uses virtual focus. */
43+
isVirtualFocus?: boolean
4244
}
4345

4446
interface SubmenuTriggerProps extends Omit<AriaMenuItemProps, 'key'> {
@@ -67,7 +69,7 @@ export interface SubmenuTriggerAria<T> {
6769
* @param ref - Ref to the submenu trigger element.
6870
*/
6971
export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: SubmenuTriggerState, ref: RefObject<FocusableElement | null>): SubmenuTriggerAria<T> {
70-
let {parentMenuRef, submenuRef, type = 'menu', isDisabled, delay = 200} = props;
72+
let {parentMenuRef, submenuRef, type = 'menu', isDisabled, delay = 200, isVirtualFocus} = props;
7173
let submenuTriggerId = useId();
7274
let overlayId = useId();
7375
let {direction} = useLocale();
@@ -101,14 +103,18 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
101103
if (direction === 'ltr' && e.currentTarget.contains(e.target as Element)) {
102104
e.stopPropagation();
103105
onSubmenuClose();
104-
ref.current?.focus();
106+
if (!isVirtualFocus) {
107+
ref.current?.focus();
108+
}
105109
}
106110
break;
107111
case 'ArrowRight':
108112
if (direction === 'rtl' && e.currentTarget.contains(e.target as Element)) {
109113
e.stopPropagation();
110114
onSubmenuClose();
111-
ref.current?.focus();
115+
if (!isVirtualFocus) {
116+
ref.current?.focus();
117+
}
112118
}
113119
break;
114120
case 'Escape':
@@ -121,9 +127,10 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
121127
let subDialogKeyDown = (e: KeyboardEvent) => {
122128
switch (e.key) {
123129
case 'Escape':
124-
e.stopPropagation();
125130
onSubmenuClose();
126-
ref.current?.focus();
131+
if (!isVirtualFocus) {
132+
ref.current?.focus();
133+
}
127134
break;
128135
}
129136
};
@@ -247,8 +254,7 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
247254
submenuProps,
248255
popoverProps: {
249256
isNonModal: true,
250-
// TODO: does this break anything in RSP implementation?
251-
disableFocusManagement: type === 'menu',
257+
disableFocusManagement: type === 'menu' && !isVirtualFocus,
252258
shouldCloseOnInteractOutside
253259
}
254260
};

packages/react-aria-components/src/Menu.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export interface SubmenuTriggerProps {
118118
delay?: number
119119
}
120120

121-
const SubmenuTriggerContext = createContext<{parentMenuRef: RefObject<HTMLElement | null>} | null>(null);
121+
const SubmenuTriggerContext = createContext<{parentMenuRef: RefObject<HTMLElement | null>, isVirtualFocus?: boolean} | null>(null);
122122

123123
/**
124124
* A submenu trigger is used to wrap a submenu's trigger item and the submenu itself.
@@ -132,11 +132,12 @@ export const SubmenuTrigger = /*#__PURE__*/ createBranchComponent('submenutrigg
132132
let submenuTriggerState = useSubmenuTriggerState({triggerKey: item.key}, rootMenuTriggerState);
133133
let submenuRef = useRef<HTMLDivElement>(null);
134134
let itemRef = useObjectRef(ref);
135-
let {parentMenuRef} = useContext(SubmenuTriggerContext)!;
135+
let {parentMenuRef, isVirtualFocus} = useContext(SubmenuTriggerContext)!;
136136
let {submenuTriggerProps, submenuProps, popoverProps} = useSubmenuTrigger({
137137
parentMenuRef,
138138
submenuRef,
139-
delay: props.delay
139+
delay: props.delay,
140+
isVirtualFocus
140141
}, submenuTriggerState, itemRef);
141142

142143
return (
@@ -187,7 +188,6 @@ export const SubdialogTrigger = /*#__PURE__*/ createBranchComponent('subdialogt
187188
let submenuTriggerState = useSubmenuTriggerState({triggerKey: item.key}, rootMenuTriggerState);
188189
let subdialogRef = useRef<HTMLDivElement>(null);
189190
let itemRef = useObjectRef(ref);
190-
// TODO: We will probably support nested subdialogs so test that use case
191191
let {parentMenuRef} = useContext(SubmenuTriggerContext)!;
192192
let {submenuTriggerProps, submenuProps, popoverProps} = useSubmenuTrigger({
193193
parentMenuRef,
@@ -302,8 +302,9 @@ function MenuInner<T extends object>({props, collection, menuRef: ref}: MenuInne
302302
[MenuStateContext, state],
303303
[SeparatorContext, {elementType: 'div'}],
304304
[SectionContext, {name: 'MenuSection', render: MenuSectionInner}],
305-
[SubmenuTriggerContext, {parentMenuRef: ref}],
305+
[SubmenuTriggerContext, {parentMenuRef: ref, isVirtualFocus: autocompleteMenuProps?.shouldUseVirtualFocus}],
306306
[MenuItemContext, null],
307+
[UNSTABLE_InternalAutocompleteContext, null],
307308
[SelectionManagerContext, state.selectionManager]
308309
]}>
309310
<CollectionRoot

packages/react-aria-components/stories/Autocomplete.stories.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,6 @@ export const AutocompleteInPopoverDialogTrigger = {
671671
}
672672
};
673673

674-
// TODO: hitting escape sometimes closes both the root menu and the leaf menu, seems to happen if you arrow key to an option in the submenu's options and then hits escape...
675674
export const AutocompleteMenuInPopoverDialogTrigger = {
676675
render: (args) => {
677676
let {onAction, onSelectionChange, selectionMode} = args;

0 commit comments

Comments
 (0)