Repository: mui/material-ui (TypeScript)
Date: 2019-07-02 11:16:12+00:00
Task type: Bug Fix

Issue:
Menu keyboard navigation broken in demos
<!--- Provide a general summary of the issue in the Title above -->
In #15901, the Menu demos were changed to use the `keepMounted` property. This breaks the auto-focus functionality in `MenuList` since it is then mounted before it is open and the `focus` call happens on mount. 

<!--
    Thank you very much for contributing to Material-UI by creating an issue! ❤️
    To avoid duplicate issues we ask you to check off the following list.
-->

<!-- Checked checkbox should look like this: [x] -->
- [x] This is not a v0.x issue. <!-- (v0.x is no longer maintained) -->
- [x] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

## Expected Behavior 🤔
<!---
    Describe what should happen.
-->
After opening the menu in the Simple Menu demo (and other menu demos), the arrow keys should change menu item focus.

## Current Behavior 😯
<!---
    Describe what happens instead of the expected behavior.
-->
After opening the menu, focus is on the `Paper` element that wraps the `MenuList` ul and the arrow keys have no effect. Hitting <kbd>tab</kbd> after opening the menu moves the focus from the `Paper` to the `MenuList` and then the arrow keys work as intended.

## Steps to Reproduce 🕹
<!---
    Provide a link to a live example (you can use codesandbox.io) and an unambiguous set of steps to reproduce this bug.
    Include code to reproduce, if relevant (which it most likely is).

    This codesandbox.io template _may_ be a good starting point:
    https://codesandbox.io/s/github/mui-org/material-ui/tree/master/examples/create-react-app

    If you're using typescript a better starting point would be
    https://codesandbox.io/s/github/mui-org/material-ui/tree/master/examples/create-react-app-with-typescript

    If YOU DO NOT take time to provide a codesandbox.io reproduction, should the COMMUNITY take time to help you?

-->
Link: https://codesandbox.io/s/material-demo-3obkw

1. Click on the button (or tab to it and hit <kbd>space</kbd> or <kbd>enter</kbd>
2. Try to use down arrow key to change focus (doesn't work)
3. Tab once
4. Use down arrow key to change focus (does work)

## Your Environment 🌎
<!---
    Include as many relevant details about the environment with which you experienced the bug.
    If you encounter issues with typescript please include version and tsconfig.
-->

| Tech         | Version |
|--------------|---------|
| Material-UI  | v4.1.3  |

## Possible Resolution

The easiest fix is to remove the `keepMounted` prop from the demo. I'm not sure what the reason was for adding it. Of course, ideally we would fix `Menu` such that the `autoFocus` works even with the `keepMounted` prop. I think this could be achieved by only specifying `autoFocus` (on MenuList or MenuItem) when `open` is `true`. Since `focus` is called when `autoFocus` changes to true (in addition to on mount when `autoFocus=true`), this should take care of the problem.





Issue discussion:
The PR that added keepMounted includes a rationale for it. But even if not it's still a documented prop and the component should support it. I want to revisit the keyboard navigation soon but we have other priorities that come first (see roadmap)
Ah, I found it:

> * aria-controls (was aria-owns) pointing to elements that aren't part of the DOM. They should be otherwise aria-controls doesn't add value. aria-controls is used in https://www.w3.org/TR/wai-aria-practices/examples/menu-button/menu-button-links.html

I was looking for something explicitly mentioning `keepMounted` on my first search through the pull request and missed this bullet point.

> But even if not it's still a documented prop and the component should support it.

I agree and it is now more important to support `keepMounted` well since the examples use it and developers will pattern their usage on the examples (which means they are much more likely to use `keepMounted` in the future and thus notice this accessibility issue).

Solution patch:
diff --git a/packages/material-ui/src/Menu/Menu.js b/packages/material-ui/src/Menu/Menu.js
--- a/packages/material-ui/src/Menu/Menu.js
+++ b/packages/material-ui/src/Menu/Menu.js
@@ -53,7 +53,7 @@ const Menu = React.forwardRef(function Menu(props, ref) {
     ...other
   } = props;
 
-  const autoFocus = autoFocusProp !== undefined ? autoFocusProp : !disableAutoFocusItem;
+  const autoFocus = (autoFocusProp !== undefined ? autoFocusProp : !disableAutoFocusItem) && open;
 
   const menuListActionsRef = React.useRef(null);
   const firstValidItemRef = React.useRef(null);


Test patch:
diff --git a/packages/material-ui/test/integration/Menu.test.js b/packages/material-ui/test/integration/Menu.test.js
--- a/packages/material-ui/test/integration/Menu.test.js
+++ b/packages/material-ui/test/integration/Menu.test.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import { assert } from 'chai';
+import { assert, expect } from 'chai';
 import TestUtils from 'react-dom/test-utils';
 import { createMount } from '@material-ui/core/test-utils';
 import Popover from '@material-ui/core/Popover';
@@ -10,368 +10,397 @@ import { setRef } from '../../src/utils/reactHelpers';
 import { stub } from 'sinon';
 import PropTypes from 'prop-types';
 import { createMuiTheme } from '@material-ui/core/styles';
+import { cleanup, render, fireEvent } from 'test/utils/createClientRender';
 
 describe('<Menu> integration', () => {
-  let mount;
-
-  before(() => {
-    // StrictModeViolation: test uses simulate
-    mount = createMount({ strict: false });
-  });
-
-  after(() => {
-    mount.cleanUp();
-  });
-
-  describe('mounted open', () => {
-    let wrapper;
-    let portalLayer;
+  describe('Using mount', () => {
+    let mount;
 
     before(() => {
-      wrapper = mount(<SimpleMenu transitionDuration={0} />);
+      // StrictModeViolation: test uses simulate
+      mount = createMount({ strict: false });
     });
 
-    it('should not be open', () => {
-      const popover = wrapper.find(Popover);
-      assert.strictEqual(popover.props().open, false, 'should have passed open=false to Popover');
-      const menuEl = document.getElementById('simple-menu');
-      assert.strictEqual(menuEl, null);
+    after(() => {
+      mount.cleanUp();
     });
 
-    it('should focus the list as nothing has been selected', () => {
-      wrapper.find('button').simulate('click');
-      portalLayer = document.querySelector('[data-mui-test="Modal"]');
-      assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('ul')[0]);
-    });
+    describe('mounted open', () => {
+      let wrapper;
+      let portalLayer;
 
-    it('should change focus to the first item when down arrow is pressed', () => {
-      TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
-        key: 'ArrowDown',
+      before(() => {
+        wrapper = mount(<SimpleMenu transitionDuration={0} />);
       });
-      assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[0]);
-    });
 
-    it('should change focus to the 2nd item when down arrow is pressed', () => {
-      TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
-        key: 'ArrowDown',
+      it('should not be open', () => {
+        const popover = wrapper.find(Popover);
+        assert.strictEqual(popover.props().open, false, 'should have passed open=false to Popover');
+        const menuEl = document.getElementById('simple-menu');
+        assert.strictEqual(menuEl, null);
       });
-      assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[1]);
-    });
 
-    it('should change focus to the 3rd item when down arrow is pressed', () => {
-      TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
-        key: 'ArrowDown',
+      it('should focus the list as nothing has been selected', () => {
+        wrapper.find('button').simulate('click');
+        portalLayer = document.querySelector('[data-mui-test="Modal"]');
+        assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('ul')[0]);
       });
-      assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[2]);
-    });
 
-    it('should switch focus from the 3rd item to the 1st item when down arrow is pressed', () => {
-      TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
-        key: 'ArrowDown',
+      it('should change focus to the first item when down arrow is pressed', () => {
+        TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
+          key: 'ArrowDown',
+        });
+        assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[0]);
       });
-      assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[0]);
-    });
 
-    it('should switch focus from the 1st item to the 3rd item when up arrow is pressed', () => {
-      TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
-        key: 'ArrowUp',
+      it('should change focus to the 2nd item when down arrow is pressed', () => {
+        TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
+          key: 'ArrowDown',
+        });
+        assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[1]);
       });
-      assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[2]);
-    });
 
-    it('should switch focus from the 3rd item to the 1st item when home key is pressed', () => {
-      TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
-        key: 'Home',
+      it('should change focus to the 3rd item when down arrow is pressed', () => {
+        TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
+          key: 'ArrowDown',
+        });
+        assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[2]);
       });
-      assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[0]);
-    });
 
-    it('should switch focus from the 1st item to the 3rd item when end key is pressed', () => {
-      TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
-        key: 'End',
+      it('should switch focus from the 3rd item to the 1st item when down arrow is pressed', () => {
+        TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
+          key: 'ArrowDown',
+        });
+        assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[0]);
       });
-      assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[2]);
-    });
 
-    it('should keep focus on the last item when a key with no associated action is pressed', () => {
-      TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
-        key: 'ArrowRight',
+      it('should switch focus from the 1st item to the 3rd item when up arrow is pressed', () => {
+        TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
+          key: 'ArrowUp',
+        });
+        assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[2]);
       });
-      assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[2]);
-    });
 
-    it('should change focus to the 2nd item when up arrow is pressed', () => {
-      TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
-        key: 'ArrowUp',
+      it('should switch focus from the 3rd item to the 1st item when home key is pressed', () => {
+        TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
+          key: 'Home',
+        });
+        assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[0]);
       });
-      assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[1]);
-    });
 
-    it('should select the 2nd item and close the menu', () => {
-      portalLayer.querySelectorAll('li')[1].click();
-      assert.strictEqual(wrapper.text(), 'selectedIndex: 1, open: false');
-    });
-  });
+      it('should switch focus from the 1st item to the 3rd item when end key is pressed', () => {
+        TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
+          key: 'End',
+        });
+        assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[2]);
+      });
 
-  describe('opening with a selected item', () => {
-    let wrapper;
+      it('should keep focus on the last item when a key with no associated action is pressed', () => {
+        TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
+          key: 'ArrowRight',
+        });
+        assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[2]);
+      });
 
-    before(() => {
-      wrapper = mount(<SimpleMenu transitionDuration={0} selectedIndex={2} />);
-    });
+      it('should change focus to the 2nd item when up arrow is pressed', () => {
+        TestUtils.Simulate.keyDown(portalLayer.querySelector('ul'), {
+          key: 'ArrowUp',
+        });
+        assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[1]);
+      });
 
-    it('should not be open', () => {
-      const popover = wrapper.find(Popover);
-      assert.strictEqual(popover.props().open, false);
-      const menuEl = document.getElementById('simple-menu');
-      assert.strictEqual(menuEl, null);
+      it('should select the 2nd item and close the menu', () => {
+        portalLayer.querySelectorAll('li')[1].click();
+        assert.strictEqual(wrapper.text(), 'selectedIndex: 1, open: false');
+      });
     });
 
-    it('should focus the 3rd selected item', () => {
-      wrapper.find('button').simulate('click');
-      const portalLayer = document.querySelector('[data-mui-test="Modal"]');
-      assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[2]);
-    });
+    describe('opening with a selected item', () => {
+      let wrapper;
 
-    it('should select the 2nd item and close the menu', () => {
-      const portalLayer = document.querySelector('[data-mui-test="Modal"]');
-      const item = portalLayer.querySelector('ul').children[1];
-      item.click();
-      assert.strictEqual(wrapper.text(), 'selectedIndex: 1, open: false');
-    });
+      before(() => {
+        wrapper = mount(<SimpleMenu transitionDuration={0} selectedIndex={2} />);
+      });
+
+      it('should not be open', () => {
+        const popover = wrapper.find(Popover);
+        assert.strictEqual(popover.props().open, false);
+        const menuEl = document.getElementById('simple-menu');
+        assert.strictEqual(menuEl, null);
+      });
+
+      it('should focus the 3rd selected item', () => {
+        wrapper.find('button').simulate('click');
+        const portalLayer = document.querySelector('[data-mui-test="Modal"]');
+        assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[2]);
+      });
+
+      it('should select the 2nd item and close the menu', () => {
+        const portalLayer = document.querySelector('[data-mui-test="Modal"]');
+        const item = portalLayer.querySelector('ul').children[1];
+        item.click();
+        assert.strictEqual(wrapper.text(), 'selectedIndex: 1, open: false');
+      });
 
-    it('should focus the 2nd selected item', () => {
-      wrapper.find('button').simulate('click');
-      const portalLayer = document.querySelector('[data-mui-test="Modal"]');
-      assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[1]);
+      it('should focus the 2nd selected item', () => {
+        wrapper.find('button').simulate('click');
+        const portalLayer = document.querySelector('[data-mui-test="Modal"]');
+        assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[1]);
+      });
     });
-  });
 
-  describe('Menu variant differences', () => {
-    const contentAnchorTracker = [false, false, false];
-    const focusTracker = [false, false, false];
-    let menuListFocusTracker = false;
-    const tabIndexTracker = [false, false, false];
-    const TrackingMenuItem = React.forwardRef(({ itemIndex, ...other }, ref) => {
-      return (
-        <MenuItem
-          onFocus={() => {
-            focusTracker[itemIndex] = true;
-          }}
-          ref={instance => {
-            setRef(ref, instance);
-            if (instance && !instance.stubbed) {
-              if (instance.tabIndex === 0) {
-                tabIndexTracker[itemIndex] = true;
-              } else if (instance.tabIndex > 0) {
-                tabIndexTracker[itemIndex] = instance.tabIndex;
+    describe('Menu variant differences', () => {
+      const contentAnchorTracker = [false, false, false];
+      const focusTracker = [false, false, false];
+      let menuListFocusTracker = false;
+      const tabIndexTracker = [false, false, false];
+      const TrackingMenuItem = React.forwardRef(({ itemIndex, ...other }, ref) => {
+        return (
+          <MenuItem
+            onFocus={() => {
+              focusTracker[itemIndex] = true;
+            }}
+            ref={instance => {
+              setRef(ref, instance);
+              if (instance && !instance.stubbed) {
+                if (instance.tabIndex === 0) {
+                  tabIndexTracker[itemIndex] = true;
+                } else if (instance.tabIndex > 0) {
+                  tabIndexTracker[itemIndex] = instance.tabIndex;
+                }
+                const offsetTop = instance.offsetTop;
+                stub(instance, 'offsetTop').get(() => {
+                  contentAnchorTracker[itemIndex] = true;
+                  return offsetTop;
+                });
+                instance.stubbed = true;
               }
-              const offsetTop = instance.offsetTop;
-              stub(instance, 'offsetTop').get(() => {
-                contentAnchorTracker[itemIndex] = true;
-                return offsetTop;
-              });
-              instance.stubbed = true;
-            }
-          }}
-          {...other}
-        />
-      );
-    });
-    TrackingMenuItem.propTypes = {
-      /**
-       * @ignore
-       */
-      itemIndex: PropTypes.number,
-    };
-    // Array.fill not supported by Chrome 41
-    const fill = (array, value) => {
-      for (let i = 0; i < array.length; i += 1) {
-        array[i] = value;
-      }
-    };
-    const mountTrackingMenu = (
-      variant,
-      {
-        selectedIndex,
-        selectedTabIndex,
-        invalidIndex,
-        autoFocusIndex,
-        disabledIndex,
-        autoFocus,
-        themeDirection,
-      } = {},
-    ) => {
-      const theme =
-        themeDirection !== undefined
-          ? createMuiTheme({
-              direction: themeDirection,
-            })
-          : undefined;
-
-      fill(contentAnchorTracker, false);
-      fill(focusTracker, false);
-      menuListFocusTracker = false;
-      fill(tabIndexTracker, false);
-      mount(
-        <Menu
-          variant={variant}
-          autoFocus={autoFocus}
-          anchorEl={document.body}
-          open
-          theme={theme}
-          MenuListProps={{
-            onFocus: () => {
-              menuListFocusTracker = true;
-            },
-          }}
-        >
-          {[0, 1, 2].map(itemIndex => {
-            if (itemIndex === invalidIndex) {
-              return null;
-            }
-            return (
-              <TrackingMenuItem
-                key={itemIndex}
-                disabled={itemIndex === disabledIndex ? true : undefined}
-                itemIndex={itemIndex}
-                selected={itemIndex === selectedIndex}
-                tabIndex={itemIndex === selectedIndex ? selectedTabIndex : undefined}
-                autoFocus={itemIndex === autoFocusIndex ? true : undefined}
-              >
-                Menu Item {itemIndex}
-              </TrackingMenuItem>
-            );
-          })}
-        </Menu>,
-      );
-    };
-
-    it('[variant=menu] adds coverage for rtl and Tab with no onClose', () => {
-      // This isn't adding very meaningful coverage apart from verifying it doesn't error, but
-      // it was so close to 100% that this has value in avoiding needing to drill into coverage
-      // details to see what isn't being tested.
-      mountTrackingMenu('menu', { themeDirection: 'rtl' });
-      assert.deepEqual(contentAnchorTracker, [true, false, false]);
-      assert.deepEqual(focusTracker, [false, false, false]);
-      assert.strictEqual(menuListFocusTracker, true);
-      assert.deepEqual(tabIndexTracker, [false, false, false]);
-
-      // Adds coverage for Tab with no onClose
-      TestUtils.Simulate.keyDown(document.activeElement, {
-        key: 'Tab',
+            }}
+            {...other}
+          />
+        );
+      });
+      TrackingMenuItem.propTypes = {
+        /**
+         * @ignore
+         */
+        itemIndex: PropTypes.number,
+      };
+      // Array.fill not supported by Chrome 41
+      const fill = (array, value) => {
+        for (let i = 0; i < array.length; i += 1) {
+          array[i] = value;
+        }
+      };
+      const mountTrackingMenu = (
+        variant,
+        {
+          selectedIndex,
+          selectedTabIndex,
+          invalidIndex,
+          autoFocusIndex,
+          disabledIndex,
+          autoFocus,
+          themeDirection,
+        } = {},
+      ) => {
+        const theme =
+          themeDirection !== undefined
+            ? createMuiTheme({
+                direction: themeDirection,
+              })
+            : undefined;
+
+        fill(contentAnchorTracker, false);
+        fill(focusTracker, false);
+        menuListFocusTracker = false;
+        fill(tabIndexTracker, false);
+        mount(
+          <Menu
+            variant={variant}
+            autoFocus={autoFocus}
+            anchorEl={document.body}
+            open
+            theme={theme}
+            MenuListProps={{
+              onFocus: () => {
+                menuListFocusTracker = true;
+              },
+            }}
+          >
+            {[0, 1, 2].map(itemIndex => {
+              if (itemIndex === invalidIndex) {
+                return null;
+              }
+              return (
+                <TrackingMenuItem
+                  key={itemIndex}
+                  disabled={itemIndex === disabledIndex ? true : undefined}
+                  itemIndex={itemIndex}
+                  selected={itemIndex === selectedIndex}
+                  tabIndex={itemIndex === selectedIndex ? selectedTabIndex : undefined}
+                  autoFocus={itemIndex === autoFocusIndex ? true : undefined}
+                >
+                  Menu Item {itemIndex}
+                </TrackingMenuItem>
+              );
+            })}
+          </Menu>,
+        );
+      };
+
+      it('[variant=menu] adds coverage for rtl and Tab with no onClose', () => {
+        // This isn't adding very meaningful coverage apart from verifying it doesn't error, but
+        // it was so close to 100% that this has value in avoiding needing to drill into coverage
+        // details to see what isn't being tested.
+        mountTrackingMenu('menu', { themeDirection: 'rtl' });
+        assert.deepEqual(contentAnchorTracker, [true, false, false]);
+        assert.deepEqual(focusTracker, [false, false, false]);
+        assert.strictEqual(menuListFocusTracker, true);
+        assert.deepEqual(tabIndexTracker, [false, false, false]);
+
+        // Adds coverage for Tab with no onClose
+        TestUtils.Simulate.keyDown(document.activeElement, {
+          key: 'Tab',
+        });
       });
-    });
 
-    it('[variant=menu] nothing selected', () => {
-      assert.deepEqual(contentAnchorTracker, [true, false, false]);
-      assert.deepEqual(focusTracker, [false, false, false]);
-      assert.strictEqual(menuListFocusTracker, true);
-      assert.deepEqual(tabIndexTracker, [false, false, false]);
-    });
+      it('[variant=menu] nothing selected', () => {
+        assert.deepEqual(contentAnchorTracker, [true, false, false]);
+        assert.deepEqual(focusTracker, [false, false, false]);
+        assert.strictEqual(menuListFocusTracker, true);
+        assert.deepEqual(tabIndexTracker, [false, false, false]);
+      });
 
-    it('[variant=menu] nothing selected, autoFocus on third', () => {
-      mountTrackingMenu('menu', { autoFocusIndex: 2 });
-      assert.deepEqual(contentAnchorTracker, [true, false, false]);
-      assert.deepEqual(focusTracker, [false, false, true]);
-      assert.strictEqual(menuListFocusTracker, true);
-      assert.deepEqual(tabIndexTracker, [false, false, false]);
-    });
+      it('[variant=menu] nothing selected, autoFocus on third', () => {
+        mountTrackingMenu('menu', { autoFocusIndex: 2 });
+        assert.deepEqual(contentAnchorTracker, [true, false, false]);
+        assert.deepEqual(focusTracker, [false, false, true]);
+        assert.strictEqual(menuListFocusTracker, true);
+        assert.deepEqual(tabIndexTracker, [false, false, false]);
+      });
 
-    it('[variant=selectedMenu] nothing selected', () => {
-      mountTrackingMenu('selectedMenu');
-      assert.deepEqual(contentAnchorTracker, [true, false, false]);
-      assert.deepEqual(focusTracker, [false, false, false]);
-      assert.strictEqual(menuListFocusTracker, true);
-      assert.deepEqual(tabIndexTracker, [false, false, false]);
-    });
+      it('[variant=selectedMenu] nothing selected', () => {
+        mountTrackingMenu('selectedMenu');
+        assert.deepEqual(contentAnchorTracker, [true, false, false]);
+        assert.deepEqual(focusTracker, [false, false, false]);
+        assert.strictEqual(menuListFocusTracker, true);
+        assert.deepEqual(tabIndexTracker, [false, false, false]);
+      });
 
-    it('[variant=selectedMenu] nothing selected, first index invalid', () => {
-      mountTrackingMenu('selectedMenu', { invalidIndex: 0 });
-      assert.deepEqual(contentAnchorTracker, [false, true, false]);
-      assert.deepEqual(focusTracker, [false, false, false]);
-      assert.strictEqual(menuListFocusTracker, true);
-      assert.deepEqual(tabIndexTracker, [false, false, false]);
-    });
+      it('[variant=selectedMenu] nothing selected, first index invalid', () => {
+        mountTrackingMenu('selectedMenu', { invalidIndex: 0 });
+        assert.deepEqual(contentAnchorTracker, [false, true, false]);
+        assert.deepEqual(focusTracker, [false, false, false]);
+        assert.strictEqual(menuListFocusTracker, true);
+        assert.deepEqual(tabIndexTracker, [false, false, false]);
+      });
 
-    it('[variant=menu] second item selected', () => {
-      mountTrackingMenu('menu', { selectedIndex: 1 });
-      assert.deepEqual(contentAnchorTracker, [true, false, false]);
-      assert.deepEqual(focusTracker, [false, false, false]);
-      assert.strictEqual(menuListFocusTracker, true);
-      assert.deepEqual(tabIndexTracker, [false, false, false]);
-    });
+      it('[variant=menu] second item selected', () => {
+        mountTrackingMenu('menu', { selectedIndex: 1 });
+        assert.deepEqual(contentAnchorTracker, [true, false, false]);
+        assert.deepEqual(focusTracker, [false, false, false]);
+        assert.strictEqual(menuListFocusTracker, true);
+        assert.deepEqual(tabIndexTracker, [false, false, false]);
+      });
 
-    it('[variant=selectedMenu] second item selected, explicit tabIndex', () => {
-      mountTrackingMenu('selectedMenu', { selectedIndex: 1, selectedTabIndex: 2 });
-      assert.deepEqual(contentAnchorTracker, [false, true, false]);
-      assert.deepEqual(focusTracker, [false, true, false]);
-      assert.strictEqual(menuListFocusTracker, true);
-      assert.deepEqual(tabIndexTracker, [false, 2, false]);
-    });
+      it('[variant=selectedMenu] second item selected, explicit tabIndex', () => {
+        mountTrackingMenu('selectedMenu', { selectedIndex: 1, selectedTabIndex: 2 });
+        assert.deepEqual(contentAnchorTracker, [false, true, false]);
+        assert.deepEqual(focusTracker, [false, true, false]);
+        assert.strictEqual(menuListFocusTracker, true);
+        assert.deepEqual(tabIndexTracker, [false, 2, false]);
+      });
 
-    it('[variant=selectedMenu] second item selected', () => {
-      mountTrackingMenu('selectedMenu', { selectedIndex: 1 });
-      assert.deepEqual(contentAnchorTracker, [false, true, false]);
-      assert.deepEqual(focusTracker, [false, true, false]);
-      assert.strictEqual(menuListFocusTracker, true);
-      assert.deepEqual(tabIndexTracker, [false, true, false]);
-    });
+      it('[variant=selectedMenu] second item selected', () => {
+        mountTrackingMenu('selectedMenu', { selectedIndex: 1 });
+        assert.deepEqual(contentAnchorTracker, [false, true, false]);
+        assert.deepEqual(focusTracker, [false, true, false]);
+        assert.strictEqual(menuListFocusTracker, true);
+        assert.deepEqual(tabIndexTracker, [false, true, false]);
+      });
 
-    it('[variant=selectedMenu] second item selected and disabled', () => {
-      mountTrackingMenu('selectedMenu', { selectedIndex: 1, disabledIndex: 1 });
-      assert.deepEqual(contentAnchorTracker, [true, false, false]);
-      assert.deepEqual(focusTracker, [false, false, false]);
-      assert.strictEqual(menuListFocusTracker, true);
-      assert.deepEqual(tabIndexTracker, [false, false, false]);
+      it('[variant=selectedMenu] second item selected and disabled', () => {
+        mountTrackingMenu('selectedMenu', { selectedIndex: 1, disabledIndex: 1 });
+        assert.deepEqual(contentAnchorTracker, [true, false, false]);
+        assert.deepEqual(focusTracker, [false, false, false]);
+        assert.strictEqual(menuListFocusTracker, true);
+        assert.deepEqual(tabIndexTracker, [false, false, false]);
+      });
+
+      it('[variant=selectedMenu] second item selected, no autoFocus', () => {
+        mountTrackingMenu('selectedMenu', { selectedIndex: 1, autoFocus: false });
+        assert.deepEqual(contentAnchorTracker, [false, true, false]);
+        assert.deepEqual(focusTracker, [false, false, false]);
+        assert.strictEqual(menuListFocusTracker, false);
+        assert.deepEqual(tabIndexTracker, [false, true, false]);
+      });
     });
 
-    it('[variant=selectedMenu] second item selected, no autoFocus', () => {
-      mountTrackingMenu('selectedMenu', { selectedIndex: 1, autoFocus: false });
-      assert.deepEqual(contentAnchorTracker, [false, true, false]);
-      assert.deepEqual(focusTracker, [false, false, false]);
-      assert.strictEqual(menuListFocusTracker, false);
-      assert.deepEqual(tabIndexTracker, [false, true, false]);
+    describe('closing', () => {
+      let wrapper;
+      let portalLayer;
+
+      beforeEach(() => {
+        wrapper = mount(<SimpleMenu transitionDuration={0} />);
+        wrapper.find('button').simulate('click');
+        portalLayer = document.querySelector('[data-mui-test="Modal"]');
+      });
+
+      it('should close the menu with tab', done => {
+        wrapper.setProps({
+          onExited() {
+            assert.strictEqual(document.getElementById('[data-mui-test="Menu"]'), null);
+            done();
+          },
+        });
+        assert.strictEqual(wrapper.text(), 'selectedIndex: null, open: true');
+        const list = portalLayer.querySelector('ul');
+        TestUtils.Simulate.keyDown(list, {
+          key: 'Tab',
+        });
+        assert.strictEqual(wrapper.text(), 'selectedIndex: null, open: false');
+      });
+
+      it('should close the menu using the backdrop', done => {
+        wrapper.setProps({
+          onExited() {
+            assert.strictEqual(document.getElementById('[data-mui-test="Menu"]'), null);
+            done();
+          },
+        });
+        assert.strictEqual(wrapper.text(), 'selectedIndex: null, open: true');
+        const backdrop = portalLayer.querySelector('[data-mui-test="Backdrop"]');
+        assert.strictEqual(backdrop != null, true);
+        backdrop.click();
+        assert.strictEqual(wrapper.text(), 'selectedIndex: null, open: false');
+      });
     });
   });
 
-  describe('closing', () => {
+  describe('Using RTL render with keepMounted', () => {
     let wrapper;
-    let portalLayer;
 
-    beforeEach(() => {
-      wrapper = mount(<SimpleMenu transitionDuration={0} />);
-      wrapper.find('button').simulate('click');
-      portalLayer = document.querySelector('[data-mui-test="Modal"]');
+    before(() => {
+      // Using render instead of createClientRender because createClientRender specifies an explicit
+      // base element that is the starting point for queries, but the menu would be rendered outside
+      // of that base element.
+      wrapper = render(<SimpleMenu transitionDuration={0} keepMounted />);
     });
 
-    it('should close the menu with tab', done => {
-      wrapper.setProps({
-        onExited() {
-          assert.strictEqual(document.getElementById('[data-mui-test="Menu"]'), null);
-          done();
-        },
-      });
-      assert.strictEqual(wrapper.text(), 'selectedIndex: null, open: true');
-      const list = portalLayer.querySelector('ul');
-      TestUtils.Simulate.keyDown(list, {
-        key: 'Tab',
-      });
-      assert.strictEqual(wrapper.text(), 'selectedIndex: null, open: false');
+    after(() => {
+      cleanup();
     });
 
-    it('should close the menu using the backdrop', done => {
-      wrapper.setProps({
-        onExited() {
-          assert.strictEqual(document.getElementById('[data-mui-test="Menu"]'), null);
-          done();
-        },
-      });
-      assert.strictEqual(wrapper.text(), 'selectedIndex: null, open: true');
-      const backdrop = portalLayer.querySelector('[data-mui-test="Backdrop"]');
-      assert.strictEqual(backdrop != null, true);
-      backdrop.click();
-      assert.strictEqual(wrapper.text(), 'selectedIndex: null, open: false');
+    it('should focus the list on open', () => {
+      const button = wrapper.getByLabelText('When device is locked');
+      const menu = wrapper.getByRole('menu');
+      expect(menu).to.not.be.focused;
+      button.focus();
+      fireEvent.click(button);
+      expect(menu).to.be.focused;
+      fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
+      expect(wrapper.getAllByRole('menuitem')[0]).to.be.focused;
     });
   });
 });