Repository: mui/material-ui (TypeScript)
Date: 2019-11-09 22:56:06+00:00
Task type: Bug Fix

Issue:
[Autocomplete] Multi-select pop-up not shown when already focused
## Current Behavior 😯

When clicking on the input rendered by the Autocomplete, it pops up with the available selections. The user selects a value, after which the input shows the selected option as a chip. When the user then clicks inside of the input again - the popup does not appear. Instead, the user has to either select the arrow pointing down, or click outside of the select, after which it loses focus, only to then click inside of the input again to trigger the popup.

Video:
![Screen Recording 2019-11-05 at 11 20 34](https://user-images.githubusercontent.com/15650071/68200133-8d8a5380-ffbf-11e9-92ec-ff7c08361801.gif)

## Expected Behavior 🤔

After the user selects the first option, the user should be able to click inside the (already) focused input to trigger the popup. The user should not have to lose focus on the input to trigger the popup again.

## Steps to Reproduce 🕹
Using the MUI Labs page: https://material-ui.com/components/autocomplete/#multiple-values
Using any of the three multi-selects, I am using the filteredSelectedOptions.

Steps:
1. Click inside of the input, on the right of the Inception chip.
2. Select any value inside of the popup.
3. Click inside of the input again, to the right of your selected option.
4. You should not see a popup appear.

## 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.
-->
My company is using this input for forms. The form allows the user to select multiple values. If the user is not able to select another value after clicking inside the already focused text field, it might lead the user to believe this isn't a multi-select.

## Your Environment 🌎
Using the online docs page on  https://material-ui.com/components/autocomplete/#multiple-values with Google Chrome version 78.0.3904.70 official build.



Issue discussion:
@FlorisWarmenhoven Thank you for the report. What do you think of implementing this toggle behavior when the input is empty, and only?

I have tried something like this to get a sense of the UX it produces
```diff
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
index e60cf0804..28caff93c 100644
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -635,6 +635,12 @@ export default function useAutocomplete(props) {
     }
   };

+  const handleClick = () => {
+    if (inputValue === '') {
+      handlePopupIndicator();
+    }
+  };
+
   let dirty = freeSolo && inputValue.length > 0;
   dirty = dirty || (multiple ? value.length > 0 : value !== null);

@@ -672,6 +678,7 @@ export default function useAutocomplete(props) {
       value: inputValue,
       onBlur: handleBlur,
       onFocus: handleFocus,
+      onClick: handleClick,
       onChange: handleInputChange,
       // if open then this is handled imperativeley so don't let react override
       // only have an opinion about this when closed
```

I think it's better. Regarding the actual implementation, the above changes are not enough
as the focus event also open the popup.
I think that we would like to look at #18101 at the same time, to make sure we have a global solution.
I might be misunderstanding you - but if you only handle this toggling behavior when the input field is empty - then my initial bug report still stands:

I expect the popup to always toggle when the user clicks inside of the input field. I don't see this as an enhancement, I see this as core functionality.

Googling searchable selects and picking the first option I see: 
https://selectize.github.io/selectize.js/
If you scroll down a tiny bit to the email input, you can type and select an email - after which you can immediately click inside the select field to trigger the popup again.
@FlorisWarmenhoven selectize.js seems to rely on the equivalent to our `disableCloseOnSelect={true} filterSelectedOptions={true}` options.

input value empty != autocomplete empty.
Terribly sorry @oliviertassinari, but I'm not familiar with either code bases. I was purely looking at functionality.

Are you saying that the functionality I expect (where clicking the input after selecting a value, it should open the popup again), is not something you agree with? 

Solution patch:
diff --git a/docs/src/pages/components/autocomplete/CustomizedHook.js b/docs/src/pages/components/autocomplete/CustomizedHook.js
--- a/docs/src/pages/components/autocomplete/CustomizedHook.js
+++ b/docs/src/pages/components/autocomplete/CustomizedHook.js
@@ -124,7 +124,7 @@ const Listbox = styled('ul')`
 
 export default function CustomizedHook() {
   const {
-    getComboboxProps,
+    getRootProps,
     getInputLabelProps,
     getInputProps,
     getTagProps,
@@ -143,7 +143,7 @@ export default function CustomizedHook() {
 
   return (
     <div>
-      <div {...getComboboxProps()}>
+      <div {...getRootProps()}>
         <Label {...getInputLabelProps()}>Customized hook</Label>
         <InputWrapper ref={setAnchorEl} className={focused ? 'focused' : ''}>
           {value.map((option, index) => (
diff --git a/docs/src/pages/components/autocomplete/CustomizedHook.tsx b/docs/src/pages/components/autocomplete/CustomizedHook.tsx
--- a/docs/src/pages/components/autocomplete/CustomizedHook.tsx
+++ b/docs/src/pages/components/autocomplete/CustomizedHook.tsx
@@ -124,7 +124,7 @@ const Listbox = styled('ul')`
 
 export default function CustomizedHook() {
   const {
-    getComboboxProps,
+    getRootProps,
     getInputLabelProps,
     getInputProps,
     getTagProps,
@@ -143,7 +143,7 @@ export default function CustomizedHook() {
 
   return (
     <div>
-      <div {...getComboboxProps()}>
+      <div {...getRootProps()}>
         <Label {...getInputLabelProps()}>Customized hook</Label>
         <InputWrapper ref={setAnchorEl} className={focused ? 'focused' : ''}>
           {value.map((option: FilmOptionType, index: number) => (
diff --git a/docs/src/pages/components/autocomplete/UseAutocomplete.js b/docs/src/pages/components/autocomplete/UseAutocomplete.js
--- a/docs/src/pages/components/autocomplete/UseAutocomplete.js
+++ b/docs/src/pages/components/autocomplete/UseAutocomplete.js
@@ -36,7 +36,7 @@ const useStyles = makeStyles(theme => ({
 export default function UseAutocomplete() {
   const classes = useStyles();
   const {
-    getComboboxProps,
+    getRootProps,
     getInputLabelProps,
     getInputProps,
     getListboxProps,
@@ -49,7 +49,7 @@ export default function UseAutocomplete() {
 
   return (
     <div>
-      <div {...getComboboxProps()}>
+      <div {...getRootProps()}>
         <label className={classes.label} {...getInputLabelProps()}>
           useAutocomplete
         </label>
diff --git a/docs/src/pages/components/autocomplete/UseAutocomplete.tsx b/docs/src/pages/components/autocomplete/UseAutocomplete.tsx
--- a/docs/src/pages/components/autocomplete/UseAutocomplete.tsx
+++ b/docs/src/pages/components/autocomplete/UseAutocomplete.tsx
@@ -38,7 +38,7 @@ const useStyles = makeStyles((theme: Theme) =>
 export default function UseAutocomplete() {
   const classes = useStyles();
   const {
-    getComboboxProps,
+    getRootProps,
     getInputLabelProps,
     getInputProps,
     getListboxProps,
@@ -51,7 +51,7 @@ export default function UseAutocomplete() {
 
   return (
     <div>
-      <div {...getComboboxProps()}>
+      <div {...getRootProps()}>
         <label className={classes.label} {...getInputLabelProps()}>
           useAutocomplete
         </label>
diff --git a/docs/src/pages/components/autocomplete/autocomplete.md b/docs/src/pages/components/autocomplete/autocomplete.md
--- a/docs/src/pages/components/autocomplete/autocomplete.md
+++ b/docs/src/pages/components/autocomplete/autocomplete.md
@@ -55,7 +55,7 @@ The Autocomplete component uses this hook internally.
 import useAutocomplete from '@material-ui/lab/useAutocomplete';
 ```
 
-- 📦 [4 kB gzipped](/size-snapshot).
+- 📦 [4.5 kB gzipped](/size-snapshot).
 
 {{"demo": "pages/components/autocomplete/UseAutocomplete.js", "defaultCodeOpen": false}}
 
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
@@ -210,7 +210,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
   const PopperComponent = disablePortal ? DisablePortal : PopperComponentProp;
 
   const {
-    getComboboxProps,
+    getRootProps,
     getInputProps,
     getInputLabelProps,
     getPopupIndicatorProps,
@@ -282,7 +282,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
           },
           className,
         )}
-        {...getComboboxProps()}
+        {...getRootProps()}
         {...other}
       >
         {renderInput({
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
@@ -153,7 +153,7 @@ export interface UseAutocompleteProps {
 export default function useAutocomplete(
   props: UseAutocompleteProps,
 ): {
-  getComboboxProps: () => {};
+  getRootProps: () => {};
   getInputProps: () => {};
   getInputLabelProps: () => {};
   getClearProps: () => {};
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
@@ -93,6 +93,7 @@ export default function useAutocomplete(props) {
     }
   });
 
+  const firstFocus = React.useRef(true);
   const inputRef = React.useRef(null);
   const listboxRef = React.useRef(null);
   const [anchorEl, setAnchorEl] = React.useState(null);
@@ -531,6 +532,14 @@ export default function useAutocomplete(props) {
           // We don't want to validate the form.
           event.preventDefault();
           selectNewValue(event, filteredOptions[highlightedIndexRef.current]);
+
+          // Move the selection to the end.
+          if (autoComplete) {
+            inputRef.current.setSelectionRange(
+              inputRef.current.value.length,
+              inputRef.current.value.length,
+            );
+          }
         } else if (freeSolo && inputValue !== '') {
           selectNewValue(event, inputValue);
         }
@@ -572,6 +581,7 @@ export default function useAutocomplete(props) {
 
   const handleBlur = event => {
     setFocused(false);
+    firstFocus.current = true;
 
     if (debug && inputValue !== '') {
       return;
@@ -640,6 +650,30 @@ export default function useAutocomplete(props) {
     }
   };
 
+  const handleMouseDown = event => {
+    if (event.target.nodeName !== 'INPUT') {
+      // Prevent blur
+      event.preventDefault();
+    }
+  };
+
+  const handleClick = () => {
+    if (
+      firstFocus.current &&
+      inputRef.current.selectionEnd - inputRef.current.selectionStart === 0
+    ) {
+      inputRef.current.select();
+    }
+
+    firstFocus.current = false;
+  };
+
+  const handleInputMouseDown = () => {
+    if (inputValue === '') {
+      handlePopupIndicator();
+    }
+  };
+
   let dirty = freeSolo && inputValue.length > 0;
   dirty = dirty || (multiple ? value.length > 0 : value !== null);
 
@@ -663,11 +697,13 @@ export default function useAutocomplete(props) {
   }
 
   return {
-    getComboboxProps: () => ({
+    getRootProps: () => ({
       'aria-owns': popupOpen ? `${id}-popup` : null,
       role: 'combobox',
       'aria-expanded': popupOpen,
       onKeyDown: handleKeyDown,
+      onMouseDown: handleMouseDown,
+      onClick: handleClick,
     }),
     getInputLabelProps: () => ({
       id: `${id}-label`,
@@ -678,6 +714,7 @@ export default function useAutocomplete(props) {
       onBlur: handleBlur,
       onFocus: handleFocus,
       onChange: handleInputChange,
+      onMouseDown: handleInputMouseDown,
       // if open then this is handled imperativeley so don't let react override
       // only have an opinion about this when closed
       'aria-activedescendant': popupOpen ? undefined : null,
@@ -693,16 +730,10 @@ export default function useAutocomplete(props) {
     getClearProps: () => ({
       tabIndex: -1,
       onClick: handleClear,
-      onMouseDown: event => {
-        event.preventDefault();
-      },
     }),
     getPopupIndicatorProps: () => ({
       tabIndex: -1,
       onClick: handlePopupIndicator,
-      onMouseDown: event => {
-        event.preventDefault();
-      },
     }),
     getTagProps: ({ index }) => ({
       key: index,
@@ -716,6 +747,7 @@ export default function useAutocomplete(props) {
       'aria-labelledby': `${id}-label`,
       ref: handleListboxRef,
       onMouseDown: event => {
+        // Prevent blur
         event.preventDefault();
       },
     }),


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
@@ -497,4 +497,56 @@ describe('<Autocomplete />', () => {
       expect(handleChange.callCount).to.equal(1);
     });
   });
+
+  describe('prop: autoComplete', () => {
+    it('add a completion string', () => {
+      const { getByRole } = render(
+        <Autocomplete
+          autoComplete
+          options={['one', 'two']}
+          renderInput={params => <TextField autoFocus {...params} />}
+        />,
+      );
+      const textbox = getByRole('textbox');
+      fireEvent.change(textbox, { target: { value: 'O' } });
+      expect(textbox.value).to.equal('O');
+      fireEvent.keyDown(textbox, { key: 'ArrowDown' });
+      expect(textbox.value).to.equal('one');
+      expect(textbox.selectionStart).to.equal(1);
+      expect(textbox.selectionEnd).to.equal(3);
+      fireEvent.keyDown(textbox, { key: 'Enter' });
+      expect(textbox.value).to.equal('one');
+      expect(textbox.selectionStart).to.equal(3);
+      expect(textbox.selectionEnd).to.equal(3);
+    });
+  });
+
+  describe('click input', () => {
+    it('toggles if empty', () => {
+      const { getByRole } = render(
+        <Autocomplete options={['one', 'two']} renderInput={params => <TextField {...params} />} />,
+      );
+      const textbox = getByRole('textbox');
+      const combobox = getByRole('combobox');
+      expect(combobox).to.have.attribute('aria-expanded', 'false');
+      fireEvent.mouseDown(textbox);
+      expect(combobox).to.have.attribute('aria-expanded', 'true');
+      fireEvent.mouseDown(textbox);
+      expect(combobox).to.have.attribute('aria-expanded', 'false');
+    });
+
+    it('selects all the first time', () => {
+      const { getByRole } = render(
+        <Autocomplete
+          value="one"
+          options={['one', 'two']}
+          renderInput={params => <TextField {...params} />}
+        />,
+      );
+      const textbox = getByRole('textbox');
+      fireEvent.click(textbox);
+      expect(textbox.selectionStart).to.equal(0);
+      expect(textbox.selectionEnd).to.equal(3);
+    });
+  });
 });