Repository: mui/material-ui (TypeScript)
Date: 2020-06-01 19:17:22+00:00
Task type: Bug Fix

Issue:
[AutoComplete] Options dropdown jumps back to beginning of list when an option is unchecked
By playing with the Customized Autocomplete example that reproduces the Github's lable picker, I noticed that if you scroll down the list of options and click on an option, the scroll position remains the same for when you check an option but it jumps back to the beginning of the list when an option is unchecked. Is there a way to prevent that?

https://codesandbox.io/s/material-demo-qqmtv?file=/demo.js


<!-- 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.
0
## Current Behavior 😯
Scroll jumps

## Expected Behavior 🤔
No jumping or a way to control if you want to reset the scroll position back to the beginning or not.

## Steps to Reproduce 🕹
1. Open the autocomplete menu
2. Scroll to the bottom of the list
3. Select an option
4. Unselect the same option

https://codesandbox.io/s/material-demo-qqmtv?file=/demo.js


## Your Environment 🌎
Codesandbox

| Tech        | Version |
| ----------- | ------- |
| Material-UI | v4.10.0  |
| React       |   16.13.1      |
| Browser     |  Chrome 81.0.4044.138  |



Issue discussion:

Solution patch:
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
@@ -275,8 +275,10 @@ export default function useAutocomplete(props) {
       }
     }
   }
-  const setHighlightedIndex = useEventCallback((index, reason = 'auto', event) => {
+
+  const setHighlightedIndex = useEventCallback(({ event, index, reason = 'auto' }) => {
     highlightedIndexRef.current = index;
+
     // does the index exist?
     if (index === -1) {
       inputRef.current.removeAttribute('aria-activedescendant');
@@ -284,6 +286,10 @@ export default function useAutocomplete(props) {
       inputRef.current.setAttribute('aria-activedescendant', `${id}-option-${index}`);
     }
 
+    if (onHighlightChange) {
+      onHighlightChange(event, index === -1 ? null : filteredOptions[index], reason);
+    }
+
     if (!listboxRef.current) {
       return;
     }
@@ -300,10 +306,6 @@ export default function useAutocomplete(props) {
       return;
     }
 
-    if (onHighlightChange) {
-      onHighlightChange(event, filteredOptions[index], reason);
-    }
-
     if (index === -1) {
       listboxNode.scrollTop = 0;
       return;
@@ -338,85 +340,92 @@ export default function useAutocomplete(props) {
     }
   });
 
-  const changeHighlightedIndex = useEventCallback((diff, direction, reason = 'auto', event) => {
-    if (!popupOpen) {
-      return;
-    }
-
-    const getNextIndex = () => {
-      const maxIndex = filteredOptions.length - 1;
-
-      if (diff === 'reset') {
-        return defaultHighlighted;
-      }
-
-      if (diff === 'start') {
-        return 0;
-      }
-
-      if (diff === 'end') {
-        return maxIndex;
+  const changeHighlightedIndex = useEventCallback(
+    ({ event, diff, direction = 'next', reason = 'auto' }) => {
+      if (!popupOpen) {
+        return;
       }
 
-      const newIndex = highlightedIndexRef.current + diff;
+      const getNextIndex = () => {
+        const maxIndex = filteredOptions.length - 1;
 
-      if (newIndex < 0) {
-        if (newIndex === -1 && includeInputInList) {
-          return -1;
+        if (diff === 'reset') {
+          return defaultHighlighted;
         }
 
-        if ((disableListWrap && highlightedIndexRef.current !== -1) || Math.abs(diff) > 1) {
+        if (diff === 'start') {
           return 0;
         }
 
-        return maxIndex;
-      }
-
-      if (newIndex > maxIndex) {
-        if (newIndex === maxIndex + 1 && includeInputInList) {
-          return -1;
+        if (diff === 'end') {
+          return maxIndex;
         }
 
-        if (disableListWrap || Math.abs(diff) > 1) {
+        const newIndex = highlightedIndexRef.current + diff;
+
+        if (newIndex < 0) {
+          if (newIndex === -1 && includeInputInList) {
+            return -1;
+          }
+
+          if ((disableListWrap && highlightedIndexRef.current !== -1) || Math.abs(diff) > 1) {
+            return 0;
+          }
+
           return maxIndex;
         }
 
-        return 0;
-      }
+        if (newIndex > maxIndex) {
+          if (newIndex === maxIndex + 1 && includeInputInList) {
+            return -1;
+          }
 
-      return newIndex;
-    };
+          if (disableListWrap || Math.abs(diff) > 1) {
+            return maxIndex;
+          }
 
-    const nextIndex = validOptionIndex(getNextIndex(), direction);
-    setHighlightedIndex(nextIndex, reason, event);
+          return 0;
+        }
 
-    if (autoComplete && diff !== 'reset') {
-      if (nextIndex === -1) {
-        inputRef.current.value = inputValue;
-      } else {
-        const option = getOptionLabel(filteredOptions[nextIndex]);
-        inputRef.current.value = option;
-
-        // The portion of the selected suggestion that has not been typed by the user,
-        // a completion string, appears inline after the input cursor in the textbox.
-        const index = option.toLowerCase().indexOf(inputValue.toLowerCase());
-        if (index === 0 && inputValue.length > 0) {
-          inputRef.current.setSelectionRange(inputValue.length, option.length);
+        return newIndex;
+      };
+
+      const nextIndex = validOptionIndex(getNextIndex(), direction);
+      setHighlightedIndex({ index: nextIndex, reason, event });
+
+      // Sync the content of the input with the highlighted option.
+      if (autoComplete && diff !== 'reset') {
+        if (nextIndex === -1) {
+          inputRef.current.value = inputValue;
+        } else {
+          const option = getOptionLabel(filteredOptions[nextIndex]);
+          inputRef.current.value = option;
+
+          // The portion of the selected suggestion that has not been typed by the user,
+          // a completion string, appears inline after the input cursor in the textbox.
+          const index = option.toLowerCase().indexOf(inputValue.toLowerCase());
+          if (index === 0 && inputValue.length > 0) {
+            inputRef.current.setSelectionRange(inputValue.length, option.length);
+          }
         }
       }
-    }
-  });
+    },
+  );
 
-  React.useEffect(() => {
+  const syncHighlightedIndex = React.useCallback(() => {
     if (!popupOpen) {
       return;
     }
 
     const valueItem = multiple ? value[0] : value;
 
-    // The popup is empty
+    // The popup is empty, reset
     if (filteredOptions.length === 0 || valueItem == null) {
-      changeHighlightedIndex('reset', 'next');
+      changeHighlightedIndex({ diff: 'reset' });
+      return;
+    }
+
+    if (!listboxRef.current) {
       return;
     }
 
@@ -436,34 +445,53 @@ export default function useAutocomplete(props) {
       const itemIndex = findIndex(filteredOptions, (optionItem) =>
         getOptionSelected(optionItem, valueItem),
       );
-
       if (itemIndex === -1) {
-        changeHighlightedIndex('reset', 'next');
+        changeHighlightedIndex({ diff: 'reset' });
       } else {
-        setHighlightedIndex(itemIndex);
+        setHighlightedIndex({ index: itemIndex });
       }
       return;
     }
 
-    // Keep the index in the boundaries
+    // Prevent the highlighted index to leak outside the boundaries.
     if (highlightedIndexRef.current >= filteredOptions.length - 1) {
-      setHighlightedIndex(filteredOptions.length - 1);
+      setHighlightedIndex({ index: filteredOptions.length - 1 });
+      return;
     }
 
-    // Ignore filterOptions => options, getOptionSelected, getOptionLabel)
+    // Restore the focus to the previous index.
+    setHighlightedIndex({ index: highlightedIndexRef.current });
+    // Ignore filteredOptions (and options, getOptionSelected, getOptionLabel) not to break the scroll position
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [
+    // Only sync the highlighted index when the option switch between empty and not
     // eslint-disable-next-line react-hooks/exhaustive-deps
     filteredOptions.length === 0,
-    value,
-    popupOpen,
+    // Don't sync the highlighted index with the value when multiple
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    multiple ? false : value,
     filterSelectedOptions,
     changeHighlightedIndex,
     setHighlightedIndex,
+    popupOpen,
     inputValue,
     multiple,
   ]);
 
+  const handleListboxRef = useEventCallback((node) => {
+    setRef(listboxRef, node);
+
+    if (!node) {
+      return;
+    }
+
+    syncHighlightedIndex();
+  });
+
+  React.useEffect(() => {
+    syncHighlightedIndex();
+  }, [syncHighlightedIndex]);
+
   const handleOpen = (event) => {
     if (open) {
       return;
@@ -633,38 +661,43 @@ export default function useAutocomplete(props) {
         if (popupOpen && handleHomeEndKeys) {
           // Prevent scroll of the page
           event.preventDefault();
-          changeHighlightedIndex('start', 'next', 'keyboard', event);
+          changeHighlightedIndex({ diff: 'start', direction: 'next', reason: 'keyboard', event });
         }
         break;
       case 'End':
         if (popupOpen && handleHomeEndKeys) {
           // Prevent scroll of the page
           event.preventDefault();
-          changeHighlightedIndex('end', 'previous', 'keyboard', event);
+          changeHighlightedIndex({ diff: 'end', direction: 'previous', reason: 'keyboard', event });
         }
         break;
       case 'PageUp':
         // Prevent scroll of the page
         event.preventDefault();
-        changeHighlightedIndex(-pageSize, 'previous', 'keyboard', event);
+        changeHighlightedIndex({
+          diff: -pageSize,
+          direction: 'previous',
+          reason: 'keyboard',
+          event,
+        });
         handleOpen(event);
         break;
       case 'PageDown':
         // Prevent scroll of the page
         event.preventDefault();
-        changeHighlightedIndex(pageSize, 'next', 'keyboard', event);
+        changeHighlightedIndex({ diff: pageSize, direction: 'next', reason: 'keyboard', event });
         handleOpen(event);
         break;
       case 'ArrowDown':
         // Prevent cursor move
         event.preventDefault();
-        changeHighlightedIndex(1, 'next', 'keyboard', event);
+        changeHighlightedIndex({ diff: 1, direction: 'next', reason: 'keyboard', event });
         handleOpen(event);
         break;
       case 'ArrowUp':
         // Prevent cursor move
         event.preventDefault();
-        changeHighlightedIndex(-1, 'previous', 'keyboard', event);
+        changeHighlightedIndex({ diff: -1, direction: 'previous', reason: 'keyboard', event });
         handleOpen(event);
         break;
       case 'ArrowLeft':
@@ -796,8 +829,11 @@ export default function useAutocomplete(props) {
   };
 
   const handleOptionMouseOver = (event) => {
-    const index = Number(event.currentTarget.getAttribute('data-option-index'));
-    setHighlightedIndex(index, 'mouse', event);
+    setHighlightedIndex({
+      event,
+      index: Number(event.currentTarget.getAttribute('data-option-index')),
+      reason: 'mouse',
+    });
   };
 
   const handleOptionTouchStart = () => {
@@ -819,22 +855,6 @@ export default function useAutocomplete(props) {
     });
   };
 
-  const handleListboxRef = useEventCallback((node) => {
-    setRef(listboxRef, node);
-
-    if (!node) {
-      return;
-    }
-
-    // Automatically select the first option as the listbox become visible.
-    if (highlightedIndexRef.current === -1 && autoHighlight) {
-      changeHighlightedIndex('reset', 'next');
-    } else {
-      // Restore the focus to the correct option.
-      setHighlightedIndex(highlightedIndexRef.current);
-    }
-  });
-
   const handlePopupIndicator = (event) => {
     if (open) {
       handleClose(event, 'toggleInput');


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
@@ -5,7 +5,7 @@ import createMount from 'test/utils/createMount';
 import describeConformance from '@material-ui/core/test-utils/describeConformance';
 import consoleErrorMock, { consoleWarnMock } from 'test/utils/consoleErrorMock';
 import { spy } from 'sinon';
-import { createClientRender, fireEvent, screen } from 'test/utils/createClientRender';
+import { act, createClientRender, fireEvent, screen } from 'test/utils/createClientRender';
 import { createFilterOptions } from '../useAutocomplete/useAutocomplete';
 import Autocomplete from './Autocomplete';
 import TextField from '@material-ui/core/TextField';
@@ -60,7 +60,7 @@ describe('<Autocomplete />', () => {
           {...defaultProps}
           freeSolo
           loading
-          renderInput={(params) => <TextField autoFocus {...params} />}
+          renderInput={(params) => <TextField {...params} autoFocus />}
         />,
       );
       fireEvent.keyDown(screen.getByRole('textbox'), { key: 'ArrowDown' });
@@ -77,7 +77,7 @@ describe('<Autocomplete />', () => {
           freeSolo
           autoHighlight
           options={options}
-          renderInput={(params) => <TextField autoFocus {...params} />}
+          renderInput={(params) => <TextField {...params} autoFocus />}
         />,
       );
 
@@ -99,7 +99,7 @@ describe('<Autocomplete />', () => {
           value="one"
           autoHighlight
           options={options}
-          renderInput={(params) => <TextField autoFocus {...params} />}
+          renderInput={(params) => <TextField {...params} autoFocus />}
         />,
       );
 
@@ -120,10 +120,9 @@ describe('<Autocomplete />', () => {
           options={[]}
           autoHighlight
           loading
-          renderInput={(params) => <TextField autoFocus {...params} />}
+          renderInput={(params) => <TextField {...params} autoFocus />}
         />,
       );
-
       const textbox = getByRole('textbox');
       expect(textbox).not.to.have.attribute('aria-activedescendant');
 
@@ -140,7 +139,7 @@ describe('<Autocomplete />', () => {
           {...defaultProps}
           value="one"
           options={['one', 'two', 'three']}
-          renderInput={(params) => <TextField autoFocus {...params} />}
+          renderInput={(params) => <TextField {...params} autoFocus />}
         />,
       );
 
@@ -161,7 +160,7 @@ describe('<Autocomplete />', () => {
           defaultValue={['one']}
           options={['one', 'two', 'three']}
           disableCloseOnSelect
-          renderInput={(params) => <TextField autoFocus {...params} />}
+          renderInput={(params) => <TextField {...params} autoFocus />}
         />,
       );
       const textbox = getByRole('textbox');
@@ -178,6 +177,30 @@ describe('<Autocomplete />', () => {
     });
   });
 
+  describe('highlight synchronisation', () => {
+    it('should not update the highlight when multiple open and value change', () => {
+      const { setProps, getByRole } = render(
+        <Autocomplete
+          {...defaultProps}
+          value={['two']}
+          multiple
+          options={['one', 'two', 'three']}
+          renderInput={(params) => <TextField {...params} autoFocus />}
+        />,
+      );
+
+      function checkHighlightIs(expected) {
+        expect(getByRole('listbox').querySelector('li[data-focus]')).to.have.text(expected);
+      }
+
+      checkHighlightIs('two');
+      setProps({
+        value: [],
+      });
+      checkHighlightIs('two');
+    });
+  });
+
   describe('prop: limitTags', () => {
     it('show all items on focus', () => {
       const { container, getAllByRole, getByRole } = render(
@@ -238,10 +261,12 @@ describe('<Autocomplete />', () => {
 
       fireEvent.keyDown(textbox, { key: 'ArrowUp' });
       checkHighlightIs('three');
-      fireEvent.keyDown(textbox, { key: 'Enter' }); // selects the last option
+      fireEvent.keyDown(textbox, { key: 'Enter' }); // selects the last option (three)
       const input = getByRole('textbox');
-      input.blur();
-      input.focus(); // opens the listbox again
+      act(() => {
+        input.blur();
+        input.focus(); // opens the listbox again
+      });
       checkHighlightIs('two');
     });
   });
@@ -257,7 +282,7 @@ describe('<Autocomplete />', () => {
           autoSelect
           options={options}
           onChange={handleChange}
-          renderInput={(params) => <TextField autoFocus {...params} />}
+          renderInput={(params) => <TextField {...params} autoFocus />}
         />,
       );
       const textbox = screen.getByRole('textbox');
@@ -282,7 +307,7 @@ describe('<Autocomplete />', () => {
           value={[options[0]]}
           options={options}
           onChange={handleChange}
-          renderInput={(params) => <TextField autoFocus {...params} />}
+          renderInput={(params) => <TextField {...params} autoFocus />}
         />,
       );
       const textbox = screen.getByRole('textbox');
@@ -304,7 +329,7 @@ describe('<Autocomplete />', () => {
           freeSolo
           multiple
           onChange={handleChange}
-          renderInput={(params) => <TextField autoFocus {...params} />}
+          renderInput={(params) => <TextField {...params} autoFocus />}
         />,
       );
       fireEvent.change(document.activeElement, { target: { value: 'a' } });
@@ -364,15 +389,12 @@ describe('<Autocomplete />', () => {
       const [firstSelectedValue, secondSelectedValue] = screen.getAllByRole('button');
 
       fireEvent.keyDown(textbox, { key: 'ArrowLeft' });
-
       expect(secondSelectedValue).toHaveFocus();
 
       fireEvent.keyDown(secondSelectedValue, { key: 'ArrowLeft' });
-
       expect(firstSelectedValue).toHaveFocus();
 
       fireEvent.keyDown(firstSelectedValue, { key: 'Backspace' });
-
       expect(handleChange.callCount).to.equal(1);
       expect(handleChange.args[0][1]).to.deep.equal([options[1]]);
       expect(textbox).toHaveFocus();
@@ -396,17 +418,14 @@ describe('<Autocomplete />', () => {
     let textbox = screen.getByRole('textbox');
 
     fireEvent.keyDown(textbox, { key: 'Enter' });
-
     expect(handleSubmit.callCount).to.equal(1);
 
     fireEvent.change(textbox, { target: { value: 'o' } });
     fireEvent.keyDown(textbox, { key: 'ArrowDown' });
     fireEvent.keyDown(textbox, { key: 'Enter' });
-
     expect(handleSubmit.callCount).to.equal(1);
 
     fireEvent.keyDown(textbox, { key: 'Enter' });
-
     expect(handleSubmit.callCount).to.equal(2);
 
     setProps({ key: 'test-2', multiple: true, freeSolo: true });
@@ -414,11 +433,9 @@ describe('<Autocomplete />', () => {
 
     fireEvent.change(textbox, { target: { value: 'o' } });
     fireEvent.keyDown(textbox, { key: 'Enter' });
-
     expect(handleSubmit.callCount).to.equal(2);
 
     fireEvent.keyDown(textbox, { key: 'Enter' });
-
     expect(handleSubmit.callCount).to.equal(3);
 
     setProps({ key: 'test-3', freeSolo: true });
@@ -426,7 +443,6 @@ describe('<Autocomplete />', () => {
 
     fireEvent.change(textbox, { target: { value: 'o' } });
     fireEvent.keyDown(textbox, { key: 'Enter' });
-
     expect(handleSubmit.callCount).to.equal(4);
   });
 
@@ -625,7 +641,6 @@ describe('<Autocomplete />', () => {
 
       fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Escape' });
       fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Escape' });
-
       expect(handleChange.callCount).to.equal(1);
       expect(handleChange.args[0][1]).to.deep.equal([]);
     });
@@ -645,7 +660,6 @@ describe('<Autocomplete />', () => {
       );
 
       fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Escape' });
-
       expect(handleClose.callCount).to.equal(1);
     });
 
@@ -659,7 +673,6 @@ describe('<Autocomplete />', () => {
       );
 
       fireEvent.keyDown(screen.getByRole('textbox'), { key: 'ArrowDown' });
-
       expect(getByRole('textbox')).to.have.attribute(
         'aria-activedescendant',
         getAllByRole('option')[0].getAttribute('id'),
@@ -676,7 +689,6 @@ describe('<Autocomplete />', () => {
       );
 
       fireEvent.keyDown(screen.getByRole('textbox'), { key: 'ArrowUp' });
-
       const options = getAllByRole('option');
       expect(getByRole('textbox')).to.have.attribute(
         'aria-activedescendant',
@@ -720,7 +732,7 @@ describe('<Autocomplete />', () => {
     });
   });
 
-  describe('wrapping behavior', () => {
+  describe('listbox wrapping behavior', () => {
     it('wraps around when navigating the list by default', () => {
       const { getAllByRole } = render(
         <Autocomplete
@@ -864,73 +876,73 @@ describe('<Autocomplete />', () => {
         );
       });
     });
+  });
 
-    describe('prop: disabled', () => {
-      it('should disable the input', () => {
-        const { getByRole } = render(
-          <Autocomplete
-            {...defaultProps}
-            disabled
-            options={['one', 'two', 'three']}
-            renderInput={(params) => <TextField {...params} />}
-          />,
-        );
-        const input = getByRole('textbox');
-        expect(input.disabled).to.equal(true);
-      });
+  describe('prop: disabled', () => {
+    it('should disable the input', () => {
+      const { getByRole } = render(
+        <Autocomplete
+          {...defaultProps}
+          disabled
+          options={['one', 'two', 'three']}
+          renderInput={(params) => <TextField {...params} />}
+        />,
+      );
+      const input = getByRole('textbox');
+      expect(input.disabled).to.equal(true);
+    });
 
-      it('should disable the popup button', () => {
-        const { queryByTitle } = render(
-          <Autocomplete
-            {...defaultProps}
-            disabled
-            options={['one', 'two', 'three']}
-            renderInput={(params) => <TextField {...params} />}
-          />,
-        );
-        expect(queryByTitle('Open').disabled).to.equal(true);
-      });
+    it('should disable the popup button', () => {
+      const { queryByTitle } = render(
+        <Autocomplete
+          {...defaultProps}
+          disabled
+          options={['one', 'two', 'three']}
+          renderInput={(params) => <TextField {...params} />}
+        />,
+      );
+      expect(queryByTitle('Open').disabled).to.equal(true);
+    });
 
-      it('should not render the clear button', () => {
-        const { queryByTitle } = render(
-          <Autocomplete
-            {...defaultProps}
-            disabled
-            options={['one', 'two', 'three']}
-            renderInput={(params) => <TextField {...params} />}
-          />,
-        );
-        expect(queryByTitle('Clear')).to.equal(null);
-      });
+    it('should not render the clear button', () => {
+      const { queryByTitle } = render(
+        <Autocomplete
+          {...defaultProps}
+          disabled
+          options={['one', 'two', 'three']}
+          renderInput={(params) => <TextField {...params} />}
+        />,
+      );
+      expect(queryByTitle('Clear')).to.equal(null);
+    });
 
-      it('should not apply the hasClearIcon class', () => {
-        const { container } = render(
-          <Autocomplete
-            {...defaultProps}
-            disabled
-            options={['one', 'two', 'three']}
-            renderInput={(params) => <TextField {...params} />}
-          />,
-        );
-        expect(container.querySelector(`.${classes.root}`)).not.to.have.class(classes.hasClearIcon);
-        expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.hasPopupIcon);
-      });
+    it('should not apply the hasClearIcon class', () => {
+      const { container } = render(
+        <Autocomplete
+          {...defaultProps}
+          disabled
+          options={['one', 'two', 'three']}
+          renderInput={(params) => <TextField {...params} />}
+        />,
+      );
+      expect(container.querySelector(`.${classes.root}`)).not.to.have.class(classes.hasClearIcon);
+      expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.hasPopupIcon);
     });
+  });
 
-    describe('prop: disableClearable', () => {
-      it('should not render the clear button', () => {
-        const { queryByTitle, container } = render(
-          <Autocomplete
-            {...defaultProps}
-            disableClearable
-            options={['one', 'two', 'three']}
-            renderInput={(params) => <TextField {...params} />}
-          />,
-        );
-        expect(queryByTitle('Clear')).to.equal(null);
-        expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.hasPopupIcon);
-        expect(container.querySelector(`.${classes.root}`)).not.to.have.class(classes.hasClearIcon);
-      });
+  describe('prop: disableClearable', () => {
+    it('should not render the clear button', () => {
+      const { queryByTitle, container } = render(
+        <Autocomplete
+          {...defaultProps}
+          disableClearable
+          options={['one', 'two', 'three']}
+          renderInput={(params) => <TextField {...params} />}
+        />,
+      );
+      expect(queryByTitle('Clear')).to.equal(null);
+      expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.hasPopupIcon);
+      expect(container.querySelector(`.${classes.root}`)).not.to.have.class(classes.hasClearIcon);
     });
   });
 
@@ -1225,10 +1237,10 @@ describe('<Autocomplete />', () => {
     });
 
     it('should mantain list box open clicking on input when it is not empty', () => {
-      const handleChange = spy();
+      const handleHighlightChange = spy();
       const { getByRole, getAllByRole } = render(
         <Autocomplete
-          onHighlightChange={handleChange}
+          onHighlightChange={handleHighlightChange}
           options={['one']}
           renderInput={(params) => <TextField {...params} />}
         />,
@@ -1249,11 +1261,11 @@ describe('<Autocomplete />', () => {
     });
 
     it('should not toggle list box', () => {
-      const handleChange = spy();
+      const handleHighlightChange = spy();
       const { getByRole } = render(
         <Autocomplete
           value="one"
-          onHighlightChange={handleChange}
+          onHighlightChange={handleHighlightChange}
           options={['one']}
           renderInput={(params) => <TextField {...params} />}
         />,
@@ -1733,91 +1745,89 @@ describe('<Autocomplete />', () => {
 
   describe('prop: onHighlightChange', () => {
     it('should trigger event when default value is passed', () => {
-      const handleChange = spy();
+      const handleHighlightChange = spy();
       const options = ['one', 'two', 'three'];
       render(
         <Autocomplete
           defaultValue={options[0]}
-          onHighlightChange={handleChange}
+          onHighlightChange={handleHighlightChange}
           options={options}
           open
-          renderInput={(params) => <TextField autoFocus {...params} />}
+          renderInput={(params) => <TextField {...params} autoFocus />}
         />,
       );
-      expect(handleChange.callCount).to.equal(1);
-      expect(handleChange.args[0][0]).to.equal(undefined);
-      expect(handleChange.args[0][1]).to.equal(options[0]);
-      expect(handleChange.args[0][2]).to.equal('auto');
+      expect(handleHighlightChange.callCount).to.equal(1);
+      expect(handleHighlightChange.args[0][0]).to.equal(undefined);
+      expect(handleHighlightChange.args[0][1]).to.equal(options[0]);
+      expect(handleHighlightChange.args[0][2]).to.equal('auto');
     });
 
     it('should support keyboard event', () => {
-      const handleChange = spy();
+      const handleHighlightChange = spy();
       const options = ['one', 'two', 'three'];
       render(
         <Autocomplete
-          onHighlightChange={handleChange}
+          onHighlightChange={handleHighlightChange}
           options={options}
           open
-          renderInput={(params) => <TextField autoFocus {...params} />}
+          renderInput={(params) => <TextField {...params} autoFocus />}
         />,
       );
       const textbox = screen.getByRole('textbox');
 
       fireEvent.keyDown(textbox, { key: 'ArrowDown' });
-
-      expect(handleChange.callCount).to.equal(2);
-      expect(handleChange.args[1][0]).to.not.equal(undefined);
-      expect(handleChange.args[1][1]).to.equal(options[0]);
-      expect(handleChange.args[1][2]).to.equal('keyboard');
+      expect(handleHighlightChange.callCount).to.equal(3);
+      expect(handleHighlightChange.args[2][0]).to.not.equal(undefined);
+      expect(handleHighlightChange.args[2][1]).to.equal(options[0]);
+      expect(handleHighlightChange.args[2][2]).to.equal('keyboard');
 
       fireEvent.keyDown(textbox, { key: 'ArrowDown' });
-
-      expect(handleChange.callCount).to.equal(3);
-      expect(handleChange.args[2][0]).to.not.equal(undefined);
-      expect(handleChange.args[2][1]).to.equal(options[1]);
-      expect(handleChange.args[2][2]).to.equal('keyboard');
+      expect(handleHighlightChange.callCount).to.equal(4);
+      expect(handleHighlightChange.args[3][0]).to.not.equal(undefined);
+      expect(handleHighlightChange.args[3][1]).to.equal(options[1]);
+      expect(handleHighlightChange.args[3][2]).to.equal('keyboard');
     });
 
     it('should support mouse event', () => {
-      const handleChange = spy();
+      const handleHighlightChange = spy();
       const options = ['one', 'two', 'three'];
       const { getAllByRole } = render(
         <Autocomplete
-          onHighlightChange={handleChange}
+          onHighlightChange={handleHighlightChange}
           options={options}
           open
-          renderInput={(params) => <TextField autoFocus {...params} />}
+          renderInput={(params) => <TextField {...params} autoFocus />}
         />,
       );
       const firstOption = getAllByRole('option')[0];
       fireEvent.mouseOver(firstOption);
-      expect(handleChange.callCount).to.equal(2);
-      expect(handleChange.args[1][0]).to.not.equal(undefined);
-      expect(handleChange.args[1][1]).to.equal(options[0]);
-      expect(handleChange.args[1][2]).to.equal('mouse');
+      expect(handleHighlightChange.callCount).to.equal(3);
+      expect(handleHighlightChange.args[2][0]).to.not.equal(undefined);
+      expect(handleHighlightChange.args[2][1]).to.equal(options[0]);
+      expect(handleHighlightChange.args[2][2]).to.equal('mouse');
     });
 
     it('should pass to onHighlightChange the correct value after filtering', () => {
-      const handleChange = spy();
+      const handleHighlightChange = spy();
       const options = ['one', 'three', 'onetwo'];
       render(
         <Autocomplete
           {...defaultProps}
-          onHighlightChange={handleChange}
+          onHighlightChange={handleHighlightChange}
           options={options}
-          renderInput={(params) => <TextField autoFocus {...params} />}
+          renderInput={(params) => <TextField {...params} autoFocus />}
         />,
       );
       const textbox = screen.getByRole('textbox');
 
       fireEvent.change(document.activeElement, { target: { value: 'one' } });
-
       expect(screen.getAllByRole('option').length).to.equal(2);
 
       fireEvent.keyDown(textbox, { key: 'ArrowDown' });
       fireEvent.keyDown(textbox, { key: 'ArrowDown' });
-
-      expect(handleChange.args[handleChange.args.length - 1][1]).to.equal(options[2]);
+      expect(handleHighlightChange.args[handleHighlightChange.args.length - 1][1]).to.equal(
+        options[2],
+      );
     });
   });
 });