Repository: mui/base-ui (2025-08-14T11:25:48Z)

Issue:
[menu] Focus is lost when `closeParentOnEsc: false`
# Bug report

## Current behavior

Currently, when `closeParentOnEsc: false` is set on Menu.Submenu, pressing the ESC key causes the Submenu to disappear and the focus to be lost as well.

https://github.com/user-attachments/assets/49c7d86f-e4b5-439a-baf8-f7f10c5bfa90

## Expected behavior

According to the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/menu_role#escape), when the focus is on a submenu, pressing the ESC key should close the submenu and move focus to the submenu trigger.

## Reproducible example

https://codesandbox.io/p/sandbox/silent-sound-x4fghr?file=%2Fsrc%2FApp.tsx%3A23%2C37

## Base UI version

v1.0.0-beta.2

## Which browser are you using?

Chrome

## Which OS are you using?

Mac OS

## Which assistive tech are you using (if applicable)?

X

## Additional context



Discussion:
Apologies for the confusion. I've resolved the issue by using the finalFocus prop on the Popup component. 🥲

Please feel free to close this issue. 
> I've resolved the issue by using the finalFocus prop on the Popup component

The [doc](https://master--base-ui.netlify.app/react/components/menu#MenuPopup-finalFocus) suggests that focus returns to the trigger by default, but the logic here looks like it doesn't for submenus: https://github.com/mui/base-ui/blob/master/packages/react/src/menu/popup/MenuPopup.tsx#L100-L103

Not sure if it's intended or not 🤔 
This fix will be available in the next npm release of Base UI.

In the meantime, you can try it out on our Canary release channel:

```sh
npm i https://pkg.pr.new/@base-ui-components/react@2493
```



Patch:
diff --git a/packages/react/src/menu/popup/MenuPopup.tsx b/packages/react/src/menu/popup/MenuPopup.tsx
index cf3d523432..2ad39f0179 100644
--- a/packages/react/src/menu/popup/MenuPopup.tsx
+++ b/packages/react/src/menu/popup/MenuPopup.tsx
@@ -39,6 +39,7 @@ export const MenuPopup = React.forwardRef(function MenuPopup(
     popupProps,
     mounted,
     instantType,
+    triggerElement,
     onOpenChangeComplete,
     parent,
     lastOpenChangeReason,
@@ -98,7 +99,7 @@ export const MenuPopup = React.forwardRef(function MenuPopup(
   });
 
   let returnFocus = parent.type === undefined || parent.type === 'context-menu';
-  if (parent.type === 'menubar' && lastOpenChangeReason !== 'outside-press') {
+  if (triggerElement || (parent.type === 'menubar' && lastOpenChangeReason !== 'outside-press')) {
     returnFocus = true;
   }
 
diff --git a/packages/react/src/menu/submenu-root/MenuSubmenuRoot.tsx b/packages/react/src/menu/submenu-root/MenuSubmenuRoot.tsx
index 2568b14795..7818cbdcb8 100644
--- a/packages/react/src/menu/submenu-root/MenuSubmenuRoot.tsx
+++ b/packages/react/src/menu/submenu-root/MenuSubmenuRoot.tsx
@@ -12,9 +12,11 @@ export { useMenuSubmenuRootContext } from './MenuSubmenuRootContext';
  * Documentation: [Base UI Menu](https://base-ui.com/react/components/menu)
  */
 export function MenuSubmenuRoot(props: MenuSubmenuRoot.Props) {
+  const { closeParentOnEsc = false } = props;
+
   return (
     <MenuSubmenuRootContext.Provider value>
-      <MenuRoot {...props} />
+      <MenuRoot closeParentOnEsc={closeParentOnEsc} {...props} />
     </MenuSubmenuRootContext.Provider>
   );
 }


Test patch:
diff --git a/packages/react/src/menu/root/MenuRoot.test.tsx b/packages/react/src/menu/root/MenuRoot.test.tsx
index d6b697fb04..5f2f5bf73f 100644
--- a/packages/react/src/menu/root/MenuRoot.test.tsx
+++ b/packages/react/src/menu/root/MenuRoot.test.tsx
@@ -733,19 +733,19 @@ describe('<Menu.Root />', () => {
   });
 
   describe('prop: closeParentOnEsc', () => {
-    it('closes the parent menu when the Escape key is pressed by default', async () => {
-      const { getByRole, queryByRole, user } = await render(
+    it('does not close the parent menu when the Escape key is pressed by default', async () => {
+      const { getByRole, queryAllByRole, user } = await render(
         <Menu.Root>
           <Menu.Trigger>Open</Menu.Trigger>
           <Menu.Portal>
             <Menu.Positioner>
-              <Menu.Popup>
+              <Menu.Popup id="parent-menu">
                 <Menu.Item>1</Menu.Item>
                 <Menu.SubmenuRoot>
                   <Menu.SubmenuTrigger>2</Menu.SubmenuTrigger>
                   <Menu.Portal>
                     <Menu.Positioner>
-                      <Menu.Popup>
+                      <Menu.Popup id="submenu">
                         <Menu.Item>2.1</Menu.Item>
                         <Menu.Item>2.2</Menu.Item>
                       </Menu.Popup>
@@ -779,24 +779,28 @@ describe('<Menu.Root />', () => {
       });
 
       await user.keyboard('[Escape]');
-      await flushMicrotasks();
 
-      expect(queryByRole('menu', { hidden: false })).to.equal(null);
+      const menus = queryAllByRole('menu', { hidden: false });
+      await waitFor(() => {
+        expect(menus.length).to.equal(1);
+      });
+
+      expect(menus[0].id).to.equal('parent-menu');
     });
 
-    it('does not close the parent menu when the Escape key is pressed if `closeParentOnEsc=false`', async () => {
-      const { getByRole, queryAllByRole, user } = await render(
+    it('closes the parent menu when the Escape key is pressed  if `closeParentOnEsc=true`', async () => {
+      const { getByRole, queryByRole, user } = await render(
         <Menu.Root>
           <Menu.Trigger>Open</Menu.Trigger>
           <Menu.Portal>
             <Menu.Positioner>
-              <Menu.Popup id="parent-menu">
+              <Menu.Popup>
                 <Menu.Item>1</Menu.Item>
-                <Menu.SubmenuRoot closeParentOnEsc={false}>
+                <Menu.SubmenuRoot closeParentOnEsc>
                   <Menu.SubmenuTrigger>2</Menu.SubmenuTrigger>
                   <Menu.Portal>
                     <Menu.Positioner>
-                      <Menu.Popup id="submenu">
+                      <Menu.Popup>
                         <Menu.Item>2.1</Menu.Item>
                         <Menu.Item>2.2</Menu.Item>
                       </Menu.Popup>
@@ -830,13 +834,9 @@ describe('<Menu.Root />', () => {
       });
 
       await user.keyboard('[Escape]');
+      await flushMicrotasks();
 
-      const menus = queryAllByRole('menu', { hidden: false });
-      await waitFor(() => {
-        expect(menus.length).to.equal(1);
-      });
-
-      expect(menus[0].id).to.equal('parent-menu');
+      expect(queryByRole('menu', { hidden: false })).to.equal(null);
     });
   });