Repository: mui/material-ui (TypeScript) — Feature

Issue:
[Autocomplete] On mobile selecting option keeps keyboard open
<!-- Provide a general summary of the issue in the Title above -->
When using the autocomplete input on mobile the keyboard doesn't disappear after selecting an item from the dropdown list. Even when selecting an item and seeing it populate the input then clicking the enter key on the keyboard is still there.

<!--
  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] The issue is present in the latest release.
- [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.

## Current Behavior 😯

Keyboard doesn't disappear on mobile when selecting something from the autocomplete input.

<!-- Describe what happens instead of the expected behavior. -->

## Expected Behavior 🤔

After typing and selecting a match in the list the keyboard should disappear.

<!-- Describe what should happen. -->

## 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?
-->

Steps:

1. Go to [material-ui.com](https://material-ui.com/components/autocomplete/) on mobile
2. Select an autocomplete input and start typing
3. Click item from list of matches

## Context 🔦

<!--
  What are you trying to accomplish? How has this issue affected you?
  Providing context helps us come up with a solution that is most useful in the real world.
-->

I'm making a progressive web app and would like to stay within the material ui component library instead of moving to use react-select or downshift for autocomplete search

## 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 (lab) | v4.0.0-alpha.35  |
| Browser     | Chrome 78.0.3904.108 |
| OS | Android 9 |



Discussion:
@oliviertassinari On desktop as well as mobile, the underlying `<TextField />` keeps focused after an option is selected. Is this not the normal behaviour? 
@scott-mcnulty I have wondered about this point during the initial implementation of the component. Keeping the focus was done on purpose to be closer to a regular input experience and not to interrupt the experience when working on a multi-select input.

> downshift

I believe you would have to implement the logic on your own going with this solution. The solution would be almost identical to what you would need to write to add such support to the Autocomplete.

> react-select

If you have a desktop with a touch screen, it will blur the input, even if you have used your mouse or keyboard to select the value ❌.

---

I think that we can add an opt-in prop for such behavior (default false):
```ts
blurOnSelect?: false | true | 'touch' | 'pointer';
```

```diff
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
index 38f9ee430..6722376bd 100644
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -71,6 +71,7 @@ export default function useAutocomplete(props) {
     autoComplete = false,
     autoHighlight = false,
     autoSelect = false,
+    blurOnSelect = 'touch',
     clearOnEscape = false,
     debug = false,
     defaultValue,
@@ -91,8 +92,8 @@ export default function useAutocomplete(props) {
     multiple = false,
     onChange,
     onClose,
-    onOpen,
     onInputChange,
+    onOpen,
     open: openProp,
     options = [],
     value: valueProp,
@@ -666,9 +667,25 @@ export default function useAutocomplete(props) {
     setHighlightedIndex(index, 'mouse');
   };

+  const isTouch = React.useRef(false);
+
+  const handleOptionTouchStart = () => {
+    isTouch.current = true;
+  };
+
   const handleOptionClick = event => {
     const index = Number(event.currentTarget.getAttribute('data-option-index'));
     selectNewValue(event, filteredOptions[index]);
+
+    if (
+      blurOnSelect === true ||
+      (blurOnSelect === 'touch' && isTouch.current) ||
+      (blurOnSelect === 'pointer' && !isTouch.current)
+    ) {
+      inputRef.current.blur();
+    }
+
+    isTouch.current = false;
   };

   const handleTagDelete = index => event => {
@@ -814,6 +831,7 @@ export default function useAutocomplete(props) {
         id: `${id}-option-${index}`,
         onMouseOver: handleOptionMouseOver,
         onClick: handleOptionClick,
+        onTouchStart: handleOptionTouchStart,
         'data-option-index': index,
         'aria-disabled': disabled,
         'aria-selected': selected,
```

What do you think of this diff? Do you want to work on a pull request?
@scott-mcnulty If you don't mind I could work on this issue.
@m4theushw Ok, this sounds good, but let's wait @scott-mcnulty's perspective on the matter first :).
I like the idea of the blurOnSelect prop you suggested @oliviertassinari -- seems like a good way to go

@m4theushw If you'd like to work on this be my guest :slightly_smiling_face: 

Thank you guys for your help!

Resolving patch:
diff --git a/docs/pages/api/autocomplete.md b/docs/pages/api/autocomplete.md
--- a/docs/pages/api/autocomplete.md
+++ b/docs/pages/api/autocomplete.md
@@ -27,6 +27,7 @@ You can learn more about the difference by [reading this guide](/guides/minimizi
 | <span class="prop-name">autoComplete</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the portion of the selected suggestion that has not been typed by the user, known as the completion string, appears inline after the input cursor in the textbox. The inline completion string is visually highlighted and has a selected state. |
 | <span class="prop-name">autoHighlight</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the first option is automatically highlighted. |
 | <span class="prop-name">autoSelect</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the selected option becomes the value of the input when the Autocomplete loses focus unless the user chooses a different option or changes the character string in the input. |
+| <span class="prop-name">blurOnSelect</span> | <span class="prop-type">'mouse'<br>&#124;&nbsp;'touch'<br>&#124;&nbsp;bool</span> | <span class="prop-default">false</span> | Control if the input should be blurred when an option is selected:<br>- `false` the input is not blurred. - `true` the input is always blurred. - `touch` the input is blurred after a touch event. - `mouse` the input is blurred after a mouse event. |
 | <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">clearOnEscape</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, clear all values when the user presses escape and the popup is closed. |
 | <span class="prop-name">clearText</span> | <span class="prop-type">string</span> | <span class="prop-default">'Clear'</span> | Override the default text for the *clear* icon button.<br>For localization purposes, you can use the provided [translations](/guides/localization/). |
diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
--- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
+++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
@@ -218,6 +218,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
     autoComplete = false,
     autoHighlight = false,
     autoSelect = false,
+    blurOnSelect = false,
     classes,
     className,
     clearOnEscape = false,
@@ -466,6 +467,15 @@ Autocomplete.propTypes = {
    * a different option or changes the character string in the input.
    */
   autoSelect: PropTypes.bool,
+  /**
+   * Control if the input should be blurred when an option is selected:
+   *
+   * - `false` the input is not blurred.
+   * - `true` the input is always blurred.
+   * - `touch` the input is blurred after a touch event.
+   * - `mouse` the input is blurred after a mouse event.
+   */
+  blurOnSelect: PropTypes.oneOfType([PropTypes.oneOf(['mouse', 'touch']), PropTypes.bool]),
   /**
    * Override or extend the styles applied to the component.
    * See [CSS API](#css) below for more details.
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
@@ -35,6 +35,15 @@ export interface UseAutocompleteProps {
    * a different option or changes the character string in the input.
    */
   autoSelect?: boolean;
+  /**
+   * Control if the input should be blurred when an option is selected:
+   *
+   * - `false` the input is not blurred.
+   * - `true` the input is always blurred.
+   * - `touch` the input is blurred after a touch event.
+   * - `mouse` the input is blurred after a mouse event.
+   */
+  blurOnSelect?: 'touch' | 'mouse' | true | false;
   /**
    * If `true`, clear all values when the user presses escape and the popup is closed.
    */
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -71,6 +71,7 @@ export default function useAutocomplete(props) {
     autoComplete = false,
     autoHighlight = false,
     autoSelect = false,
+    blurOnSelect = false,
     clearOnEscape = false,
     debug = false,
     defaultValue,
@@ -666,9 +667,25 @@ export default function useAutocomplete(props) {
     setHighlightedIndex(index, 'mouse');
   };
 
+  const isTouch = React.useRef(false);
+
+  const handleOptionTouchStart = () => {
+    isTouch.current = true;
+  };
+
   const handleOptionClick = event => {
     const index = Number(event.currentTarget.getAttribute('data-option-index'));
     selectNewValue(event, filteredOptions[index]);
+
+    if (
+      blurOnSelect === true ||
+      (blurOnSelect === 'touch' && isTouch.current) ||
+      (blurOnSelect === 'mouse' && !isTouch.current)
+    ) {
+      inputRef.current.blur();
+    }
+
+    isTouch.current = false;
   };
 
   const handleTagDelete = index => event => {
@@ -814,6 +831,7 @@ export default function useAutocomplete(props) {
         id: `${id}-option-${index}`,
         onMouseOver: handleOptionMouseOver,
         onClick: handleOptionClick,
+        onTouchStart: handleOptionTouchStart,
         'data-option-index': index,
         'aria-disabled': disabled,
         'aria-selected': selected,


Test patch:
diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
--- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
+++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
@@ -564,8 +564,8 @@ describe('<Autocomplete />', () => {
       fireEvent.click(input);
 
       const listbox = getByRole('listbox');
-      const firstItem = listbox.querySelector('li');
-      fireEvent.click(firstItem);
+      const firstOption = listbox.querySelector('li');
+      fireEvent.click(firstOption);
 
       expect(handleChange.args[0][1]).to.equal('one');
     });
@@ -779,4 +779,77 @@ describe('<Autocomplete />', () => {
       expect(handleInputChange.args[0][2]).to.equal('reset');
     });
   });
+
+  describe('prop: blurOnSelect', () => {
+    it('[blurOnSelect=true] should blur the input when clicking or touching options', () => {
+      const options = [{ name: 'foo' }];
+      const { getByRole, queryByTitle } = render(
+        <Autocomplete
+          options={options}
+          getOptionLabel={option => option.name}
+          renderInput={params => <TextField {...params} autoFocus />}
+          blurOnSelect
+        />,
+      );
+      const textbox = getByRole('textbox');
+      let firstOption = getByRole('option');
+      expect(textbox).to.have.focus;
+      fireEvent.click(firstOption);
+      expect(textbox).to.not.have.focus;
+
+      const opener = queryByTitle('Open');
+      fireEvent.click(opener);
+      expect(textbox).to.have.focus;
+      firstOption = getByRole('option');
+      fireEvent.touchStart(firstOption);
+      fireEvent.click(firstOption);
+      expect(textbox).to.not.have.focus;
+    });
+
+    it('[blurOnSelect="touch"] should only blur the input when an option is touched', () => {
+      const options = [{ name: 'foo' }];
+      const { getByRole, queryByTitle } = render(
+        <Autocomplete
+          options={options}
+          getOptionLabel={option => option.name}
+          renderInput={params => <TextField {...params} autoFocus />}
+          blurOnSelect="touch"
+        />,
+      );
+      const textbox = getByRole('textbox');
+      let firstOption = getByRole('option');
+      fireEvent.click(firstOption);
+      expect(textbox).to.have.focus;
+
+      const opener = queryByTitle('Open');
+      fireEvent.click(opener);
+      firstOption = getByRole('option');
+      fireEvent.touchStart(firstOption);
+      fireEvent.click(firstOption);
+      expect(textbox).to.not.have.focus;
+    });
+
+    it('[blurOnSelect="mouse"] should only blur the input when an option is clicked', () => {
+      const options = [{ name: 'foo' }];
+      const { getByRole, queryByTitle } = render(
+        <Autocomplete
+          options={options}
+          getOptionLabel={option => option.name}
+          renderInput={params => <TextField {...params} autoFocus />}
+          blurOnSelect="mouse"
+        />,
+      );
+      const textbox = getByRole('textbox');
+      let firstOption = getByRole('option');
+      fireEvent.touchStart(firstOption);
+      fireEvent.click(firstOption);
+      expect(textbox).to.have.focus;
+
+      const opener = queryByTitle('Open');
+      fireEvent.click(opener);
+      firstOption = getByRole('option');
+      fireEvent.click(firstOption);
+      expect(textbox).to.not.have.focus;
+    });
+  });
 });