Repository: mui/material-ui (TypeScript)
Date: 2017-09-15 23:56:03+00:00
Task type: Bug Fix

Issue:
Select with keyboard controls does not work in Dialog
<!-- Checked checkbox should look like this: [x] -->
- [x] I have searched the [issues](https://github.com/callemall/material-ui/issues) of this repository and believe that this is not a duplicate.

Similar to issue #8169, but that doesn't mention keyboard controls. May be fixed by pull request #8227 but worth testing.

## Expected Behavior
Select should work exactly the same inside a dialog as on a normal page.

## Current Behavior
If you put a Select inside a dialog, the behaviour is very odd. The keyboard control doesn't work very well, tabbing to a select and pressing space opens the menu, but doesn't focus the menu so another tab is required to focus it. Then once closed, focus position is lost and starts at the beginning of the form again

## Steps to Reproduce (for bugs)
https://codesandbox.io/s/32v7k6ozo1

1. Put a select inside a dialog.


## Context
Trying to put forms inside of a Dialog control.

## Your Environment
| Tech         | Version |
|--------------|---------|
| Material-UI  |   1.0.0-beta.10      |
| React        |  15.6.1       |
| browser      |  all       |



Issue discussion:

Solution patch:
diff --git a/src/Menu/Menu.js b/src/Menu/Menu.js
--- a/src/Menu/Menu.js
+++ b/src/Menu/Menu.js
@@ -101,18 +101,41 @@ class Menu extends React.Component<AllProps, void> {
     transitionDuration: 'auto',
   };
 
-  menuList = undefined;
+  componentDidMount() {
+    if (this.props.open) {
+      this.focus();
+    }
+  }
 
-  handleEnter = (element: HTMLElement) => {
-    const menuList = findDOMNode(this.menuList);
+  componentDidUpdate(prevProps) {
+    if (!prevProps.open && this.props.open) {
+      // Needs to refocus as when a menu is rendered into another Modal,
+      // the first modal might change the focus to prevent any leak.
+      this.focus();
+    }
+  }
 
+  menuList = undefined;
+
+  focus = () => {
     if (this.menuList && this.menuList.selectedItem) {
       // $FlowFixMe
       findDOMNode(this.menuList.selectedItem).focus();
-    } else if (menuList && menuList.firstChild) {
+      return;
+    }
+
+    const menuList = findDOMNode(this.menuList);
+    if (menuList && menuList.firstChild) {
       // $FlowFixMe
       menuList.firstChild.focus();
     }
+  };
+
+  handleEnter = (element: HTMLElement) => {
+    const menuList = findDOMNode(this.menuList);
+
+    // Focus so the scroll computation of the Popover works as expected.
+    this.focus();
 
     // Let's ignore that piece of logic if users are already overriding the width
     // of the menu.
@@ -133,12 +156,11 @@ class Menu extends React.Component<AllProps, void> {
   handleListKeyDown = (event: SyntheticUIEvent<>, key: string) => {
     if (key === 'tab') {
       event.preventDefault();
+
       if (this.props.onRequestClose) {
-        return this.props.onRequestClose(event);
+        this.props.onRequestClose(event);
       }
     }
-
-    return false;
   };
 
   getContentAnchorEl = () => {
diff --git a/src/Menu/Menu.spec.js b/src/Menu/Menu.spec.js
--- a/src/Menu/Menu.spec.js
+++ b/src/Menu/Menu.spec.js
@@ -10,10 +10,16 @@ import Menu from './Menu';
 describe('<Menu />', () => {
   let shallow;
   let classes;
+  let mount;
 
   before(() => {
     shallow = createShallow({ dive: true });
     classes = getClasses(<Menu />);
+    mount = createMount();
+  });
+
+  after(() => {
+    mount.cleanUp();
   });
 
   it('should render a Popover', () => {
@@ -97,8 +103,23 @@ describe('<Menu />', () => {
     });
   });
 
+  it('should open during the initial mount', () => {
+    const wrapper = mount(
+      <Menu open classes={classes}>
+        <div />
+      </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 && menuEl.firstChild,
+      'should be the first menu item',
+    );
+  });
+
   describe('mount', () => {
-    let mount;
     let wrapper;
     let instance;
 
@@ -114,7 +135,6 @@ describe('<Menu />', () => {
     let findDOMNodeStub;
 
     before(() => {
-      mount = createMount();
       wrapper = mount(<Menu.Naked classes={classes} />);
       instance = wrapper.instance();
 
@@ -142,7 +162,6 @@ describe('<Menu />', () => {
     });
 
     after(() => {
-      mount.cleanUp();
       findDOMNodeStub.restore();
     });
 
diff --git a/src/Select/SelectInput.js b/src/Select/SelectInput.js
--- a/src/Select/SelectInput.js
+++ b/src/Select/SelectInput.js
@@ -311,6 +311,7 @@ class SelectInput extends React.Component<AllProps, State> {
             },
             classNameProp,
           )}
+          data-mui-test="SelectDisplay"
           aria-pressed={this.state.open ? 'true' : 'false'}
           tabIndex={disabled ? null : 0}
           role="button"
diff --git a/src/internal/Modal.js b/src/internal/Modal.js
--- a/src/internal/Modal.js
+++ b/src/internal/Modal.js
@@ -19,10 +19,8 @@ import Backdrop from './Backdrop';
 import Portal from './Portal';
 import type { TransitionCallback } from './Transition';
 
-/**
- * Modals don't open on the server so this won't break concurrency.
- * Could also put this on context.
- */
+// Modals don't open on the server so this won't break concurrency.
+// Could also put this on context.
 const modalManager = createModalManager();
 
 export const styles = (theme: Object) => ({
@@ -177,7 +175,7 @@ class Modal extends React.Component<AllProps, State> {
 
   componentDidMount() {
     this.mounted = true;
-    if (this.props.show === true) {
+    if (this.props.show) {
       this.handleShow();
     }
   }
@@ -198,6 +196,7 @@ class Modal extends React.Component<AllProps, State> {
     if (!prevProps.show && this.props.show) {
       this.handleShow();
     }
+    // We are waiting for the onExited callback to call handleHide.
   }
 
   componentWillUnmount() {


Test patch:
diff --git a/test/integration/Menu.spec.js b/test/integration/Menu.spec.js
--- a/test/integration/Menu.spec.js
+++ b/test/integration/Menu.spec.js
@@ -29,7 +29,7 @@ describe('<Menu> integration', () => {
 
     it('should not be open', () => {
       const popover = wrapper.find(Popover);
-      assert.strictEqual(popover.prop('open'), false, 'should have passed open=false to Popover');
+      assert.strictEqual(popover.props().open, false, 'should have passed open=false to Popover');
       const menuEl = document.getElementById('simple-menu');
       assert.strictEqual(menuEl, null, 'should not render the menu to the DOM');
     });
@@ -140,7 +140,7 @@ describe('<Menu> integration', () => {
 
     it('should not be open', () => {
       const popover = wrapper.find(Popover);
-      assert.strictEqual(popover.prop('open'), false, 'should have passed open=false to Popover');
+      assert.strictEqual(popover.props().open, false, 'should have passed open=false to Popover');
       const menuEl = document.getElementById('simple-menu');
       assert.strictEqual(menuEl, null, 'should not render the menu to the DOM');
     });
diff --git a/test/integration/Select.spec.js b/test/integration/Select.spec.js
new file mode 100644
--- /dev/null
+++ b/test/integration/Select.spec.js
@@ -0,0 +1,41 @@
+// @flow weak
+
+import React from 'react';
+import { assert } from 'chai';
+import { ReactWrapper } from 'enzyme';
+import { createMount } from 'src/test-utils';
+import SelectAndDialog from './fixtures/select/SelectAndDialog';
+
+describe('<Select> integration', () => {
+  let mount;
+
+  before(() => {
+    mount = createMount();
+  });
+
+  after(() => {
+    mount.cleanUp();
+  });
+
+  describe('with Dialog', () => {
+    let wrapper;
+
+    before(() => {
+      wrapper = mount(<SelectAndDialog />);
+    });
+
+    it('should focus the first item as nothing has been selected', () => {
+      const portal = wrapper.find('Modal').node.mountNode.firstChild;
+      const portalWrapper = new ReactWrapper(portal, portal);
+
+      const selectDisplay = portalWrapper.find('[data-mui-test="SelectDisplay"]');
+      selectDisplay.simulate('click');
+      const menuEl = document.querySelector('[data-mui-test="Menu"]');
+      assert.strictEqual(
+        document.activeElement,
+        menuEl && menuEl.children[1],
+        'should be the 2nd menu item',
+      );
+    });
+  });
+});
diff --git a/test/integration/fixtures/select/SelectAndDialog.js b/test/integration/fixtures/select/SelectAndDialog.js
new file mode 100644
--- /dev/null
+++ b/test/integration/fixtures/select/SelectAndDialog.js
@@ -0,0 +1,23 @@
+// @flow
+
+import React from 'react';
+import { MenuItem } from 'src/Menu';
+import Select from 'src/Select';
+import Dialog from 'src/Dialog';
+
+function SelectAndDialog() {
+  return (
+    <Dialog open>
+      <Select value={10}>
+        <MenuItem value="">
+          <em>None</em>
+        </MenuItem>
+        <MenuItem value={10}>Ten</MenuItem>
+        <MenuItem value={20}>Twenty</MenuItem>
+        <MenuItem value={30}>Thirty</MenuItem>
+      </Select>
+    </Dialog>
+  );
+}
+
+export default SelectAndDialog;