Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions packages/@react-spectrum/menu/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ function Menu<T extends object>(props: SpectrumMenuProps<T>, ref: DOMRef<HTMLDiv
hasOpenSubmenu={hasOpenSubmenu}
isSubmenu={isSubmenu}
parentMenuTreeState={parentMenuTreeState}
rootMenuTriggerState={rootMenuTriggerState}>
rootMenuTriggerState={rootMenuTriggerState}
menuRef={domRef}>
<div
{...menuProps}
style={mergeProps(styleProps.style, menuProps.style)}
Expand Down Expand Up @@ -125,7 +126,7 @@ function Menu<T extends object>(props: SpectrumMenuProps<T>, ref: DOMRef<HTMLDiv
}

export function TrayHeaderWrapper(props) {
let {children, isSubmenu, hasOpenSubmenu, parentMenuTreeState, rootMenuTriggerState, onBackButtonPress, wrapperKeyDown} = props;
let {children, isSubmenu, hasOpenSubmenu, parentMenuTreeState, rootMenuTriggerState, onBackButtonPress, wrapperKeyDown, menuRef} = props;
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/menu');
let backButtonText = parentMenuTreeState?.collection.getItem(rootMenuTriggerState?.UNSTABLE_expandedKeysStack.slice(-1)[0])?.textValue;
let backButtonLabel = stringFormatter.format('backButton', {
Expand Down Expand Up @@ -158,6 +159,23 @@ export function TrayHeaderWrapper(props) {
};
}, []);

// When opening submenu in tray, focus the first item in the submenu after animation completes
// This fixes an issue with iOS VO where the closed submenu was getting focus
let focusTimeoutRef = useRef(null);
useEffect(() => {
if (isMobile && isSubmenu && !hasOpenSubmenu && traySubmenuAnimation === 'spectrum-TraySubmenu-enter') {
focusTimeoutRef.current = setTimeout(() => {
let firstItem = menuRef.current.querySelector('[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]') as HTMLElement;
firstItem?.focus();
}, 220);
}
return () => {
if (focusTimeoutRef.current) {
clearTimeout(focusTimeoutRef.current);
}
};
}, [hasOpenSubmenu, isMobile, isSubmenu, menuRef, traySubmenuAnimation]);

return (
<>
<div
Expand Down
10 changes: 6 additions & 4 deletions packages/@react-spectrum/menu/test/SubMenuTrigger.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,8 @@ describe('Submenu', function () {
expect(menuWrappers[1]).toContainElement(menus[1]);

let submenu1 = menus[0];
expect(document.activeElement).toBe(submenu1);
let submenu1Items = within(submenu1).getAllByRole('menuitem');
expect(document.activeElement).toBe(submenu1Items[0]);
expect(submenu1).toHaveAttribute('aria-label', submenuTrigger1.textContent);
let trayDialog = within(tray).getByRole('dialog');
expect(trayDialog).toBeTruthy();
Expand All @@ -835,7 +836,6 @@ describe('Submenu', function () {
let menuHeader = within(trayDialog).getAllByText(submenuTrigger1.textContent)[0];
expect(menuHeader).toBeVisible();
expect(menuHeader.tagName).toBe('H1');
let submenu1Items = within(submenu1).getAllByRole('menuitem');
let submenuTrigger2 = submenu1Items[2];
triggerTouch(submenuTrigger2);
act(() => {jest.runAllTimers();});
Expand All @@ -850,7 +850,8 @@ describe('Submenu', function () {
expect(menuWrappers[2]).toContainElement(menus[2]);

let submenu2 = menus[0];
expect(document.activeElement).toBe(submenu2);
let submenu2Items = within(submenu2).getAllByRole('menuitem');
expect(document.activeElement).toBe(submenu2Items[0]);
expect(submenu2).toHaveAttribute('aria-label', submenuTrigger2.textContent);
trayDialog = within(tray).getByRole('dialog');
backButton = within(trayDialog).getByRole('button');
Expand Down Expand Up @@ -896,7 +897,8 @@ describe('Submenu', function () {
expect(menus).toHaveLength(2);
menuItems = within(menus[0]).getAllByRole('menuitem');
expect(menuItems[0]).toHaveTextContent('Lvl 2');
expect(document.activeElement).toBe(submenuTrigger2);
act(() => {jest.runAllTimers();});
expect(document.activeElement).toBe(menuItems[0]);
buttons = within(tray).getAllByRole('button');
expect(buttons).toHaveLength(3);
expect(buttons[1]).toHaveAttribute('aria-label', `Return to ${submenuTrigger1.textContent}`);
Expand Down