Repository: mui/material-ui (TypeScript)
Date: 2019-09-21 10:29:37+00:00
Task type: Bug Fix

Issue:
Menu initial focus does not follow WAI-ARIA recommendations
This is a follow-up to discussion in #16294 with @eps1lon and @ianschmitz, but I didn't want to add this into a closed issue or merged pull request.

In @ianschmitz's comment [here](https://github.com/mui-org/material-ui/pull/16294#issuecomment-503731492), there are two issues pointed out that I want to comment on:

* The lack of a roving tabindex
* Focus being placed on the `ul` instead of the first item in the list

## Thoughts on roving tabindex

Prior to my overhaul of the Menu focus navigation, it did have a roving tabindex. This was managed using state in the MenuList and had significant performance implications. If the roving tabindex is reintroduced, it should be done purely in the DOM without leveraging state (since using state triggers re-renders of the entire menu on focus changes). I, however, don't see value in adding the roving tabindex back in for Menu. As I understand it, the purpose of the roving tabindex is so that if you <kbd>tab</kbd> away from a composite widget (see #15597) and then <kbd>shift</kbd>+<kbd>tab</kbd> back to it, your focus location will be remembered. But for Menu, <kbd>tab</kbd> closes the menu and we always want to reset the initial focus location on open of the Menu, so the complexity of the roving tabindex doesn't seem to add any value.

## Reasons for focus being placed on the `ul` instead of the first item

This decision was in response to #14483 which voiced the following concern:

>  Highlighting the first option in the menu can trick the user into thinking it is already selected.

 At the time of deciding to change the focus behavior, I looked at a number of desktop applications (including Chrome) and the behavior of opening a menu with focus on the list (i.e. first <kbd>down arrow</kbd> places focus on first item rather than focus starting on the first item) seemed to be at least as common as starting focus on the first item. Also, the Material Design spec didn't indicate anything one way or the other.

At the time, I was completely ignorant of the WAI-ARIA documentation which clearly states [here](https://www.w3.org/TR/wai-aria-practices/#menu):

>  When a menu opens, or when a menubar receives focus, keyboard focus is placed on the first item. 

I became considerably less ignorant about the WAI-ARIA documentation while writing up #15597. If I had known then (when I was reworking the menu focus logic) what I know now, I would not have changed the default behavior. I do think the new behavior is reasonable and would be worth retaining as an option (perhaps an `initialFocusOnList` property?), but the default should be in accord with the WAI-ARIA documentation.

One of the reasons I was excited to change the default behavior is that it made it easier to support disabled menu items and dividers appropriately, since I didn't have to worry about the first item being one of those and therefore needing to put the initial focus elsewhere. However, this can be handled in a fairly straightforward manner if this is done in `MenuList` leveraging the [moveFocus](https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/MenuList/MenuList.js#L51) function. So if we continue supporting the current behavior via a property, `MenuList` would use the property within its [autoFocus effect](https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/MenuList/MenuList.js#L100) to decide whether to set focus to the list or call moveFocus to set focus to the first focusable child.





Issue discussion:
Thanks for summarizing @ryancogswell.

> I, however, don't see value in adding the roving tabindex back in for Menu. As I understand it, the purpose of the roving tabindex is so that if you tab away from a composite widget (see #15597) and then shift+tab back to it, your focus location will be remembered. But for Menu, tab closes the menu and we always want to reset the initial focus location on open of the Menu, so the complexity of the roving tabindex doesn't seem to add any value.

I agree with you for the case of `Menu`. Are you suggesting we shouldn't implement roving index for `MenuList`? Not having a roving index can limit the usefulness of `MenuList`, as it offers a poor experience for keyboard users when entering/leaving the list, especially with many items. 

For example we have large lists in some of our application (think 100s of items - in our case it's layers of a map). We use `MenuList` as it provides a great experience for keyboard users - if they're tabbing through the application they don't have to tab through 100s of items in the list, they can tab to the list and use arrow keys if that's what they're interested in, or they can tab to the next element in the app. With the current approach, when interacting with the list to select an item, when returning to the list they have to cycle through with their arrow keys through potentially hundreds of items to get back to the selected item.

With all that said we may be using the current a11y benefits of the 3.x `MenuList` more than the average application and I can respect that there may not be an appetite to have a great keyboard user experience if it requires a lot of development effort.

>  If the roving tabindex is reintroduced, it should be done purely in the DOM without leveraging state (since using state triggers re-renders of the entire menu on focus changes)

This may be low effort? If so it would be a great way to implement and keeps us as close to spec as possible while giving a great experience for keyboard users.
> Are you suggesting we shouldn't implement roving index for `MenuList`? Not having a roving index can limit the usefulness of `MenuList`

@ianschmitz I have considered `MenuList` to primarily be an implementation detail of `Menu` and that it's main usefulness was for creating alternative `Menu` implementations. Using it as a list for its a11y features is really a workaround for the fact that `List` isn't treated as a composite widget (which is what  #15597 is about).

> This may be low effort?

It is definitely much lower effort to add the roving tabindex back in to `MenuList` (via the DOM) than to add composite widget support to `List`. It would likely mean using a ref to point at the DOM element for which we last called `focus()` and then whenever we call `focus()` we would also set `tabindex=0` and set `tabindex=-1` for the DOM element in the ref and then update the ref. The bulk of the work would be creating appropriate tests. Might need to query the DOM for the element with `tabindex=0` if the ref isn't yet set when focus is changing.
I was playing with the [Tree View](https://material-ui.com/components/tree-view/) component, and the accessibility is implemented beautifully. The roving tabindex works as expected. The focusing works exactly as expected. You can focus outside of the component, and focus back in and return to the previously focused list item. Tree views are arguably a much trickier thing to get right a11y wise, so perhaps we could use that implementation as inspiration to solve `Menu`/`MenuList` a11y?
I got distracted by the test suite not being able to catch this properly. There are some minor issues to resolve to get a proper a11y test suite for this and then  we can work on this. Sorry this takes longer than expected.

I just want to make sure that we have a smooth transition once we get better focus handling primitives from react.
No worries. I wanted to let you know that the Tree View seemed to be fully WCAG AA compliant from my knowledge and was a pleasure to use. Good work to whomever developed that!
That would be @joshwooding. Really great work!
Thanks for the kind words 😊 That TreeView was going to be WCAG AA compliant even if it killed me 😂

Solution patch:
diff --git a/docs/pages/api/menu.md b/docs/pages/api/menu.md
--- a/docs/pages/api/menu.md
+++ b/docs/pages/api/menu.md
@@ -25,10 +25,10 @@ You can learn more about the difference by [reading this guide](/guides/minimizi
 | Name | Type | Default | Description |
 |:-----|:-----|:--------|:------------|
 | <span class="prop-name">anchorEl</span> | <span class="prop-type">object<br>&#124;&nbsp;func</span> |  | The DOM element used to set the position of the menu. |
-| <span class="prop-name">autoFocus</span> | <span class="prop-type">bool</span> |  | If `true` (default), the menu list (possibly a particular item depending on the menu variant) will receive focus on open. |
+| <span class="prop-name">autoFocus</span> | <span class="prop-type">bool</span> | <span class="prop-default">true</span> | If `true` (Default) will focus the `[role="menu"]` if no focusable child is found. Disabled children are not focusable. If you set this prop to `false` focus will be placed on the parent modal container. This has severe accessibility implications and should only be considered if you manage focus otherwise. |
 | <span class="prop-name">children</span> | <span class="prop-type">node</span> |  | Menu contents, normally `MenuItem`s. |
 | <span class="prop-name">classes</span> | <span class="prop-type">object</span> |  | Override or extend the styles applied to the component. See [CSS API](#css) below for more details. |
-| <span class="prop-name">disableAutoFocusItem</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | Same as `autoFocus=false`. |
+| <span class="prop-name">disableAutoFocusItem</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | When opening the menu will not focus the active item but the `[role="menu"]` unless `autoFocus` is also set to `false`. Not using the default means not following WAI-ARIA authoring practices. Please be considerate about possible accessibility implications. |
 | <span class="prop-name">MenuListProps</span> | <span class="prop-type">object</span> | <span class="prop-default">{}</span> | Props applied to the [`MenuList`](/api/menu-list/) element. |
 | <span class="prop-name">onClose</span> | <span class="prop-type">func</span> |  | Callback fired when the component requests to be closed.<br><br>**Signature:**<br>`function(event: object, reason: string) => void`<br>*event:* The event source of the callback.<br>*reason:* Can be:`"escapeKeyDown"`, `"backdropClick"`, `"tabKeyDown"`. |
 | <span class="prop-name">onEnter</span> | <span class="prop-type">func</span> |  | Callback fired before the Menu enters. |
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
@@ -37,7 +37,7 @@ export const styles = {
 
 const Menu = React.forwardRef(function Menu(props, ref) {
   const {
-    autoFocus: autoFocusProp,
+    autoFocus = true,
     children,
     classes,
     disableAutoFocusItem = false,
@@ -53,13 +53,12 @@ const Menu = React.forwardRef(function Menu(props, ref) {
   } = props;
   const theme = useTheme();
 
-  const autoFocus = (autoFocusProp !== undefined ? autoFocusProp : !disableAutoFocusItem) && open;
+  const autoFocusItem = autoFocus && !disableAutoFocusItem && open;
 
   const menuListActionsRef = React.useRef(null);
-  const firstValidItemRef = React.useRef(null);
-  const firstSelectedItemRef = React.useRef(null);
+  const contentAnchorRef = React.useRef(null);
 
-  const getContentAnchorEl = () => firstSelectedItemRef.current || firstValidItemRef.current;
+  const getContentAnchorEl = () => contentAnchorRef.current;
 
   const handleEntering = (element, isAppearing) => {
     if (menuListActionsRef.current) {
@@ -81,13 +80,20 @@ const Menu = React.forwardRef(function Menu(props, ref) {
     }
   };
 
-  let firstValidElementIndex = null;
-  let firstSelectedIndex = null;
-
-  const items = React.Children.map(children, (child, index) => {
+  /**
+   * the index of the item should receive focus
+   * in a `variant="selectedMenu"` it's the first `selected` item
+   * otherwise it's the very first item.
+   */
+  let activeItemIndex = -1;
+  // since we inject focus related props into children we have to do a lookahead
+  // to check if there is a `selected` item. We're looking for the last `selected`
+  // item and use the first valid item as a fallback
+  React.Children.map(children, (child, index) => {
     if (!React.isValidElement(child)) {
-      return null;
+      return;
     }
+
     if (process.env.NODE_ENV !== 'production') {
       if (child.type === React.Fragment) {
         console.error(
@@ -98,42 +104,36 @@ const Menu = React.forwardRef(function Menu(props, ref) {
         );
       }
     }
-    if (firstValidElementIndex === null) {
-      firstValidElementIndex = index;
+
+    if (!child.props.disabled) {
+      if (variant === 'selectedMenu' && child.props.selected) {
+        activeItemIndex = index;
+      } else if (activeItemIndex === -1) {
+        activeItemIndex = index;
+      }
     }
-    let newChildProps = null;
-    if (
-      variant === 'selectedMenu' &&
-      firstSelectedIndex === null &&
-      child.props.selected &&
-      !child.props.disabled
-    ) {
-      firstSelectedIndex = index;
-      newChildProps = {};
-      if (autoFocus) {
+  });
+
+  const items = React.Children.map(children, (child, index) => {
+    if (index === activeItemIndex) {
+      const newChildProps = {};
+      if (autoFocusItem) {
         newChildProps.autoFocus = true;
       }
-      if (child.props.tabIndex === undefined) {
+      if (child.props.tabIndex === undefined && variant === 'selectedMenu') {
         newChildProps.tabIndex = 0;
       }
       newChildProps.ref = instance => {
         // #StrictMode ready
-        firstSelectedItemRef.current = ReactDOM.findDOMNode(instance);
+        contentAnchorRef.current = ReactDOM.findDOMNode(instance);
         setRef(child.ref, instance);
       };
-    } else if (index === firstValidElementIndex) {
-      newChildProps = {
-        ref: instance => {
-          // #StrictMode ready
-          firstValidItemRef.current = ReactDOM.findDOMNode(instance);
-          setRef(child.ref, instance);
-        },
-      };
-    }
 
-    if (newChildProps !== null) {
-      return React.cloneElement(child, newChildProps);
+      if (newChildProps !== null) {
+        return React.cloneElement(child, newChildProps);
+      }
     }
+
     return child;
   });
 
@@ -161,7 +161,7 @@ const Menu = React.forwardRef(function Menu(props, ref) {
         data-mui-test="Menu"
         onKeyDown={handleListKeyDown}
         actions={menuListActionsRef}
-        autoFocus={autoFocus && firstSelectedIndex === null}
+        autoFocus={autoFocus && (activeItemIndex === -1 || disableAutoFocusItem)}
         {...MenuListProps}
         className={clsx(classes.list, MenuListProps.className)}
       >
@@ -177,7 +177,10 @@ Menu.propTypes = {
    */
   anchorEl: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
   /**
-   * If `true` (default), the menu list (possibly a particular item depending on the menu variant) will receive focus on open.
+   * If `true` (Default) will focus the `[role="menu"]` if no focusable child is found. Disabled
+   * children are not focusable. If you set this prop to `false` focus will be placed
+   * on the parent modal container. This has severe accessibility implications
+   * and should only be considered if you manage focus otherwise.
    */
   autoFocus: PropTypes.bool,
   /**
@@ -190,8 +193,10 @@ Menu.propTypes = {
    */
   classes: PropTypes.object.isRequired,
   /**
-   * Same as `autoFocus=false`.
-   * @deprecated Use `autoFocus` instead.
+   * When opening the menu will not focus the active item but the `[role="menu"]`
+   * unless `autoFocus` is also set to `false`. Not using the default means not
+   * following WAI-ARIA authoring practices. Please be considerate about possible
+   * accessibility implications.
    */
   disableAutoFocusItem: PropTypes.bool,
   /**


Test patch:
diff --git a/packages/material-ui/src/Menu/Menu.test.js b/packages/material-ui/src/Menu/Menu.test.js
--- a/packages/material-ui/src/Menu/Menu.test.js
+++ b/packages/material-ui/src/Menu/Menu.test.js
@@ -152,13 +152,12 @@ describe('<Menu />', () => {
   it('should open during the initial mount', () => {
     const wrapper = mount(
       <Menu {...defaultProps} open>
-        <div tabIndex={-1} />
+        <div role="menuitem" tabIndex={-1} />
       </Menu>,
     );
     const popover = wrapper.find(Popover);
     assert.strictEqual(popover.props().open, true);
-    const menuEl = document.querySelector('[data-mui-test="Menu"]');
-    assert.strictEqual(document.activeElement, menuEl);
+    assert.strictEqual(wrapper.find('[role="menuitem"]').props().autoFocus, true);
   });
 
   it('should not focus list if autoFocus=false', () => {
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
@@ -2,10 +2,11 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { expect } from 'chai';
+import { useFakeTimers } from 'sinon';
 import Button from '@material-ui/core/Button';
 import MenuItem from '@material-ui/core/MenuItem';
 import Menu from '@material-ui/core/Menu';
-import { cleanup, createClientRender, fireEvent, wait } from 'test/utils/createClientRender';
+import { cleanup, createClientRender, fireEvent } from 'test/utils/createClientRender';
 
 const options = [
   'Show some love to Material-UI',
@@ -13,7 +14,7 @@ const options = [
   'Hide sensitive notification content',
 ];
 
-function SimpleMenu(props) {
+function ButtonMenu(props) {
   const { selectedIndex: selectedIndexProp, ...other } = props;
   const [anchorEl, setAnchorEl] = React.useState(null);
   const [selectedIndex, setSelectedIndex] = React.useState(selectedIndexProp || null);
@@ -43,7 +44,15 @@ function SimpleMenu(props) {
       >
         {`selectedIndex: ${selectedIndex}, open: ${open}`}
       </Button>
-      <Menu id="lock-menu" anchorEl={anchorEl} open={open} onClose={handleClose} {...other}>
+      <Menu
+        id="lock-menu"
+        anchorEl={anchorEl}
+        keepMounted
+        open={open}
+        onClose={handleClose}
+        transitionDuration={0}
+        {...other}
+      >
         {options.map((option, index) => (
           <MenuItem
             key={option}
@@ -58,52 +67,58 @@ function SimpleMenu(props) {
   );
 }
 
-SimpleMenu.propTypes = { selectedIndex: PropTypes.number };
+ButtonMenu.propTypes = { selectedIndex: PropTypes.number };
 
 describe('<Menu /> integration', () => {
+  /**
+   * @type {ReturnType<useFakeTimers>}
+   */
+  let clock;
   const render = createClientRender({ strict: false });
 
+  beforeEach(() => {
+    clock = useFakeTimers();
+  });
+
   afterEach(() => {
+    clock.restore();
     cleanup();
   });
 
-  it('is not part of the DOM by default', () => {
-    const { queryByRole } = render(<SimpleMenu transitionDuration={0} />);
+  it('is part of the DOM by default but hidden', () => {
+    const { queryByRole: getByRole } = render(<ButtonMenu />);
 
-    expect(queryByRole('menu')).to.be.null;
+    // note: this will fail once testing-library ignores inaccessible roles :)
+    expect(getByRole('menu')).to.be.ariaHidden;
   });
 
-  it('is not part of the DOM by default even with a selectedIndex', () => {
-    const { queryByRole } = render(<SimpleMenu transitionDuration={0} selectedIndex={2} />);
+  it('does not gain any focus when mounted ', () => {
+    const { getByRole } = render(<ButtonMenu />);
 
-    expect(queryByRole('menu')).to.be.null;
+    expect(getByRole('menu')).to.not.contain(document.activeElement);
   });
 
-  it('should focus the list on open', () => {
-    const { getByLabelText, getByRole } = render(<SimpleMenu transitionDuration={0} keepMounted />);
-    const button = getByLabelText('open menu');
-    const menu = getByRole('menu');
-
-    expect(menu).to.not.be.focused;
+  it('should focus the first item on open', () => {
+    const { getByLabelText, getAllByRole } = render(<ButtonMenu />);
 
+    const button = getByLabelText('open menu');
     button.focus();
-    fireEvent.click(button);
-    expect(menu).to.be.focused;
-  });
+    button.click();
 
-  it('should focus the list as nothing has been selected and changes focus according to keyboard navigation', () => {
-    const { getAllByRole, queryByRole, getByLabelText } = render(
-      <SimpleMenu transitionDuration={0} />,
-    );
-    const button = getByLabelText('open menu');
+    expect(getAllByRole('menuitem')[0]).to.be.focused;
+  });
 
-    expect(queryByRole('menu')).to.be.null;
+  it('changes focus according to keyboard navigation', () => {
+    const { getAllByRole, getByLabelText } = render(<ButtonMenu />);
 
+    const button = getByLabelText('open menu');
     button.focus();
-    fireEvent.click(button);
-    expect(queryByRole('menu')).to.be.focused;
+    button.click();
 
     fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
+    expect(getAllByRole('menuitem')[1]).to.be.focused;
+
+    fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' });
     expect(getAllByRole('menuitem')[0]).to.be.focused;
 
     fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' });
@@ -120,15 +135,12 @@ describe('<Menu /> integration', () => {
   });
 
   it('focuses the selected item when opening', () => {
-    const { getAllByRole, getByLabelText } = render(
-      <SimpleMenu transitionDuration={0} selectedIndex={2} />,
-    );
-    const button = getByLabelText('open menu');
-
-    expect(document.body).to.be.focused;
+    const { getAllByRole, getByLabelText } = render(<ButtonMenu selectedIndex={2} />);
 
+    const button = getByLabelText('open menu');
     button.focus();
-    fireEvent.click(button);
+    button.click();
+
     expect(getAllByRole('menuitem')[2]).to.be.focused;
   });
 
@@ -137,8 +149,8 @@ describe('<Menu /> integration', () => {
       return <Menu anchorEl={document.body} open {...props} />;
     }
 
-    specify('[variant=menu] will focus the menu if nothing is selected', () => {
-      const { getAllByRole, getByRole } = render(
+    specify('[variant=menu] will focus the first item if nothing is selected', () => {
+      const { getAllByRole } = render(
         <OpenMenu variant="menu">
           <MenuItem />
           <MenuItem />
@@ -146,14 +158,14 @@ describe('<Menu /> integration', () => {
         </OpenMenu>,
       );
 
-      expect(getByRole('menu')).to.be.focused;
+      expect(getAllByRole('menuitem')[0]).to.be.focused;
       expect(getAllByRole('menuitem')[0]).to.have.property('tabIndex', -1);
       expect(getAllByRole('menuitem')[1]).to.have.property('tabIndex', -1);
       expect(getAllByRole('menuitem')[2]).to.have.property('tabIndex', -1);
     });
 
-    specify('[variant=selectedMenu] will focus the menu if nothing is selected', () => {
-      const { getAllByRole, getByRole } = render(
+    specify('[variant=selectedMenu] will focus the first item if nothing is selected', () => {
+      const { getAllByRole } = render(
         <OpenMenu variant="selectedMenu">
           <MenuItem />
           <MenuItem />
@@ -161,15 +173,15 @@ describe('<Menu /> integration', () => {
         </OpenMenu>,
       );
 
-      expect(getByRole('menu')).to.be.focused;
-      expect(getAllByRole('menuitem')[0]).to.have.property('tabIndex', -1);
+      expect(getAllByRole('menuitem')[0]).to.be.focused;
+      expect(getAllByRole('menuitem')[0]).to.have.property('tabIndex', 0);
       expect(getAllByRole('menuitem')[1]).to.have.property('tabIndex', -1);
       expect(getAllByRole('menuitem')[2]).to.have.property('tabIndex', -1);
     });
 
     // no case for variant=selectedMenu
-    specify('[variant=menu] ignores `autoFocus` on `MenuItem`', () => {
-      const { getAllByRole, getByRole } = render(
+    specify('[variant=menu] prioritizes `autoFocus` on `MenuItem`', () => {
+      const { getAllByRole } = render(
         <OpenMenu variant="menu">
           <MenuItem />
           <MenuItem />
@@ -177,14 +189,14 @@ describe('<Menu /> integration', () => {
         </OpenMenu>,
       );
 
-      expect(getByRole('menu')).to.be.focused;
+      expect(getAllByRole('menuitem')[2]).to.be.focused;
       expect(getAllByRole('menuitem')[0]).to.have.property('tabIndex', -1);
       expect(getAllByRole('menuitem')[1]).to.have.property('tabIndex', -1);
       expect(getAllByRole('menuitem')[2]).to.have.property('tabIndex', -1);
     });
 
     specify('[variant=menu] ignores `selected` on `MenuItem`', () => {
-      const { getAllByRole, getByRole } = render(
+      const { getAllByRole } = render(
         <OpenMenu variant="menu">
           <MenuItem />
           <MenuItem selected />
@@ -192,7 +204,7 @@ describe('<Menu /> integration', () => {
         </OpenMenu>,
       );
 
-      expect(getByRole('menu')).to.be.focused;
+      expect(getAllByRole('menuitem')[0]).to.be.focused;
       expect(getAllByRole('menuitem')[0]).to.have.property('tabIndex', -1);
       expect(getAllByRole('menuitem')[1]).to.have.property('tabIndex', -1);
       expect(getAllByRole('menuitem')[2]).to.have.property('tabIndex', -1);
@@ -228,23 +240,32 @@ describe('<Menu /> integration', () => {
       expect(getAllByRole('menuitem')[2]).to.have.property('tabIndex', -1);
     });
 
-    // no case for menu
-    specify('[variant=selectedMenu] focuses the menu if the selected menuitem is disabled', () => {
-      const { getAllByRole, getByRole } = render(
-        <OpenMenu variant="selectedMenu">
-          <MenuItem />
-          <MenuItem disabled selected />
-          <MenuItem />
-        </OpenMenu>,
-      );
-
-      expect(getByRole('menu')).to.be.focused;
-      expect(getAllByRole('menuitem')[0]).to.have.property('tabIndex', -1);
-      expect(getAllByRole('menuitem')[1]).to.have.property('tabIndex', -1);
-      expect(getAllByRole('menuitem')[2]).to.have.property('tabIndex', -1);
-    });
+    // falling back to the menu immediately so that we don't have to come up
+    // with custom fallbacks (e.g. what happens if the first item is also selected)
+    // it's debatable whether disabled items should still be focusable
+    specify(
+      '[variant=selectedMenu] focuses the first non-disabled item if the selected menuitem is disabled',
+      () => {
+        const { getAllByRole } = render(
+          <OpenMenu variant="selectedMenu">
+            <MenuItem disabled />
+            <MenuItem />
+            <MenuItem disabled selected />
+            <MenuItem />
+          </OpenMenu>,
+        );
+
+        expect(getAllByRole('menuitem')[1]).to.be.focused;
+        expect(getAllByRole('menuitem')[0]).to.have.property('tabIndex', -1);
+        expect(getAllByRole('menuitem')[1]).to.have.property('tabIndex', 0);
+        expect(getAllByRole('menuitem')[2]).to.have.property('tabIndex', -1);
+        expect(getAllByRole('menuitem')[3]).to.have.property('tabIndex', -1);
+      },
+    );
 
     // no case for menu
+    // TODO: should this even change focus? I would guess that autoFocus={false}
+    // means "developer: I take care of focus don't steal it from me"
     specify('[variant=selectedMenu] focuses no part of the menu when `autoFocus={false}`', () => {
       const { getAllByRole, getByTestId } = render(
         <OpenMenu autoFocus={false} variant="selectedMenu" PaperProps={{ 'data-testid': 'Paper' }}>
@@ -259,36 +280,53 @@ describe('<Menu /> integration', () => {
       expect(getAllByRole('menuitem')[1]).to.have.property('tabIndex', 0);
       expect(getAllByRole('menuitem')[2]).to.have.property('tabIndex', -1);
     });
-  });
 
-  it('closes the menu when Tabbing while the list is active', async () => {
-    const { queryByRole, getByLabelText } = render(<SimpleMenu transitionDuration={0} />);
-    const button = getByLabelText('open menu');
+    specify('[variant=selectedMenu] focuses nothing when it is closed and mounted', () => {
+      const { getByRole } = render(<ButtonMenu selectedIndex={1} variant="selectedMenu" />);
 
-    expect(document.body).to.be.focused;
+      expect(getByRole('menu')).not.to.contain(document.activeElement);
+    });
 
-    button.focus();
-    fireEvent.click(button);
-    expect(queryByRole('menu')).to.be.focused;
+    specify(
+      '[variant=selectedMenu] focuses the selected item when opening when it was already mounted',
+      () => {
+        const { getAllByRole, getByRole } = render(
+          <ButtonMenu selectedIndex={1} variant="selectedMenu" />,
+        );
+
+        getByRole('button').focus();
+        getByRole('button').click();
+
+        expect(getAllByRole('menuitem')[1]).to.be.focused;
+        expect(getAllByRole('menuitem')[0]).to.have.property('tabIndex', -1);
+        expect(getAllByRole('menuitem')[1]).to.have.property('tabIndex', 0);
+        expect(getAllByRole('menuitem')[2]).to.have.property('tabIndex', -1);
+      },
+    );
+  });
 
-    fireEvent.keyDown(document.activeElement, { key: 'Tab' });
+  it('closes the menu when Tabbing while the list is active', () => {
+    const { getByRole } = render(<ButtonMenu />);
 
+    getByRole('button').focus();
+    getByRole('button').click();
+
+    fireEvent.keyDown(document.activeElement, { key: 'Tab' });
     // react-transition-group uses one commit per state transition so we need to wait a bit
-    await wait(() => expect(queryByRole('menu')).to.be.null, { timeout: 10 });
-  });
+    clock.tick(0);
 
-  it('closes the menu when the backdrop is clicked', async () => {
-    const { queryByRole, getByLabelText } = render(<SimpleMenu transitionDuration={0} />);
-    const button = getByLabelText('open menu');
+    expect(getByRole('menu')).to.be.ariaHidden;
+  });
 
-    expect(queryByRole('menu')).to.be.null;
+  it('closes the menu when the backdrop is clicked', () => {
+    const { getByRole } = render(<ButtonMenu />);
 
-    button.focus();
-    fireEvent.click(button);
-    expect(queryByRole('menu')).to.be.focused;
+    getByRole('button').focus();
+    getByRole('button').click();
 
-    fireEvent.click(document.querySelector('[data-mui-test="Backdrop"]'));
+    document.querySelector('[data-mui-test="Backdrop"]').click();
+    clock.tick(0);
 
-    await wait(() => expect(queryByRole('menu')).to.be.null, { timeout: 10 });
+    expect(getByRole('menu')).to.be.ariaHidden;
   });
 });
diff --git a/packages/material-ui/test/integration/NestedMenu.test.js b/packages/material-ui/test/integration/NestedMenu.test.js
--- a/packages/material-ui/test/integration/NestedMenu.test.js
+++ b/packages/material-ui/test/integration/NestedMenu.test.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import { expect } from 'chai';
-import { cleanup, createClientRender } from 'test/utils/createClientRender';
+import { cleanup, createClientRender, within } from 'test/utils/createClientRender';
 import Menu from '@material-ui/core/Menu';
 import MenuItem from '@material-ui/core/MenuItem';
 
@@ -51,18 +51,18 @@ describe('<NestedMenu> integration', () => {
     expect(queryAllByRole('menu')).to.have.length(0);
   });
 
-  it('should focus the list as nothing has been selected', () => {
+  it('should focus the first item of the first menu when nothing has been selected', () => {
     const { getByRole } = render(<NestedMenu firstMenuOpen />);
 
     expect(getByRole('menu')).to.have.id('first-menu');
-    expect(getByRole('menu')).to.be.focused;
+    expect(within(getByRole('menu')).getAllByRole('menuitem')[0]).to.be.focused;
   });
 
-  it('should focus the list of second menu', () => {
+  it('should focus the first item of the second menu when nothing has been selected', () => {
     const { getByRole } = render(<NestedMenu secondMenuOpen />);
 
     expect(getByRole('menu')).to.have.id('second-menu');
-    expect(getByRole('menu')).to.be.focused;
+    expect(within(getByRole('menu')).getAllByRole('menuitem')[0]).to.be.focused;
   });
 
   it('should open the first menu after it was closed', () => {
@@ -72,7 +72,7 @@ describe('<NestedMenu> integration', () => {
     setProps({ firstMenuOpen: true });
 
     expect(getByRole('menu')).to.have.id('first-menu');
-    expect(getByRole('menu')).to.be.focused;
+    expect(within(getByRole('menu')).getAllByRole('menuitem')[0]).to.be.focused;
   });
 
   it('should be able to open second menu again', () => {
@@ -82,6 +82,6 @@ describe('<NestedMenu> integration', () => {
     setProps({ secondMenuOpen: true });
 
     expect(getByRole('menu')).to.have.id('second-menu');
-    expect(getByRole('menu')).to.be.focused;
+    expect(within(getByRole('menu')).getAllByRole('menuitem')[0]).to.be.focused;
   });
 });
diff --git a/test/utils/initMatchers.js b/test/utils/initMatchers.js
--- a/test/utils/initMatchers.js
+++ b/test/utils/initMatchers.js
@@ -35,7 +35,7 @@ chai.use((chaiAPI, utils) => {
       currentNode !== document.documentElement &&
       ariaHidden === false
     ) {
-      ariaHidden = element.getAttribute('aria-hidden') === 'true';
+      ariaHidden = currentNode.getAttribute('aria-hidden') === 'true';
       previousNode = currentNode;
       currentNode = currentNode.parentElement;
     }