Repository: mui/material-ui (TypeScript)
Date: 2022-11-18 14:05:25+00:00
Task type: Bug Fix

Issue:
[Autocomplete] cursor resets highlighted item when using arrow keys
### Duplicates

- [X] I have searched the existing issues

### Latest version

- [X] I have tested the latest version

### Current behavior 😯

I begin with my mouse hovering over the first item. When I reach the bottom of the list (navigating via d-pad), the final down keypress results in `The Godfather`, the second item in the list, being selected (due to mouse hover).

https://mui.com/components/autocomplete/#combo-box

![mui_cursor_autocomplete](https://user-images.githubusercontent.com/1571918/145722387-16056280-06b4-4ce6-b9b8-520bbda8984a.gif)


### Expected behavior 🤔

Expected behavior is for the mouse to not interfere with the highlighted selection while using the d-pad. Here's an example of jQuery's autocomplete component.

https://jqueryui.com/autocomplete/#maxheight

![jquery_cursor_autocomplete](https://user-images.githubusercontent.com/1571918/145722451-366f625e-b362-45b9-bdcf-aa2b05d70d17.gif)


### Steps to reproduce 🕹

Steps:

1. Go to https://mui.com/components/autocomplete/#combo-box
2. Open autocomplete, hover over first item
3. Navigate down using d-pad
4. Once scrolling occurs, the highlighted item is erroneously selected

### Context 🔦

It increases the difficulty of using the autocomplete component. The user has to to move their cursor for the arrow keys to function as expected.

### Your environment 🌎

Repro'ed from docs site-- N/A


Issue discussion:
This indeed could be considered a bug, but I would say it's low priority. Firstly, the workaround is pretty simple (moving the mouse). Secondly, we are working on a change in Autocomplete internals that will be free of this issue. It likely won't be released before the next major version, though.
We may reconsider working on it in v5 if we see a significant need for it in the community (measured by upvotes on this issue).

Solution patch:
diff --git a/packages/mui-base/src/useAutocomplete/useAutocomplete.js b/packages/mui-base/src/useAutocomplete/useAutocomplete.js
--- a/packages/mui-base/src/useAutocomplete/useAutocomplete.js
+++ b/packages/mui-base/src/useAutocomplete/useAutocomplete.js
@@ -965,12 +965,15 @@ export default function useAutocomplete(props) {
     }
   };
 
-  const handleOptionMouseOver = (event) => {
-    setHighlightedIndex({
-      event,
-      index: Number(event.currentTarget.getAttribute('data-option-index')),
-      reason: 'mouse',
-    });
+  const handleOptionMouseMove = (event) => {
+    const index = Number(event.currentTarget.getAttribute('data-option-index'));
+    if (highlightedIndexRef.current !== index) {
+      setHighlightedIndex({
+        event,
+        index,
+        reason: 'mouse',
+      });
+    }
   };
 
   const handleOptionTouchStart = (event) => {
@@ -1144,7 +1147,7 @@ export default function useAutocomplete(props) {
         tabIndex: -1,
         role: 'option',
         id: `${id}-option-${index}`,
-        onMouseOver: handleOptionMouseOver,
+        onMouseMove: handleOptionMouseMove,
         onClick: handleOptionClick,
         onTouchStart: handleOptionTouchStart,
         'data-option-index': index,


Test patch:
diff --git a/packages/mui-joy/src/Autocomplete/Autocomplete.test.tsx b/packages/mui-joy/src/Autocomplete/Autocomplete.test.tsx
--- a/packages/mui-joy/src/Autocomplete/Autocomplete.test.tsx
+++ b/packages/mui-joy/src/Autocomplete/Autocomplete.test.tsx
@@ -2049,7 +2049,7 @@ describe('Joy <Autocomplete />', () => {
         <Autocomplete onHighlightChange={handleHighlightChange} options={options} open autoFocus />,
       );
       const firstOption = getAllByRole('option')[0];
-      fireEvent.mouseOver(firstOption);
+      fireEvent.mouseMove(firstOption);
       expect(handleHighlightChange.callCount).to.equal(
         // FIXME: highlighted index implementation should be implemented using React not the DOM.
         React.version.startsWith('18') ? 4 : 3,
diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.test.js b/packages/mui-material/src/Autocomplete/Autocomplete.test.js
--- a/packages/mui-material/src/Autocomplete/Autocomplete.test.js
+++ b/packages/mui-material/src/Autocomplete/Autocomplete.test.js
@@ -2520,7 +2520,7 @@ describe('<Autocomplete />', () => {
         />,
       );
       const firstOption = getAllByRole('option')[0];
-      fireEvent.mouseOver(firstOption);
+      fireEvent.mouseMove(firstOption);
       expect(handleHighlightChange.callCount).to.equal(
         // FIXME: highlighted index implementation should be implemented using React not the DOM.
         React.version.startsWith('18') ? 4 : 3,
diff --git a/test/e2e/fixtures/Autocomplete/HoverJoyAutocomplete.tsx b/test/e2e/fixtures/Autocomplete/HoverJoyAutocomplete.tsx
new file mode 100644
--- /dev/null
+++ b/test/e2e/fixtures/Autocomplete/HoverJoyAutocomplete.tsx
@@ -0,0 +1,15 @@
+import * as React from 'react';
+import Autocomplete from '@mui/joy/Autocomplete';
+
+function HoverJoyAutocomplete() {
+  return (
+    <Autocomplete
+      open
+      options={['one', 'two', 'three', 'four', 'five']}
+      sx={{ width: 300 }}
+      slotProps={{ listbox: { sx: { height: '100px' } } }}
+    />
+  );
+}
+
+export default HoverJoyAutocomplete;
diff --git a/test/e2e/fixtures/Autocomplete/HoverMaterialAutocomplete.tsx b/test/e2e/fixtures/Autocomplete/HoverMaterialAutocomplete.tsx
new file mode 100644
--- /dev/null
+++ b/test/e2e/fixtures/Autocomplete/HoverMaterialAutocomplete.tsx
@@ -0,0 +1,17 @@
+import * as React from 'react';
+import Autocomplete from '@mui/material/Autocomplete';
+import TextField from '@mui/material/TextField';
+
+function HoverMaterialAutocomplete() {
+  return (
+    <Autocomplete
+      open
+      options={['one', 'two', 'three', 'four', 'five']}
+      sx={{ width: 300 }}
+      ListboxProps={{ sx: { height: '100px' } }}
+      renderInput={(params) => <TextField {...params} />}
+    />
+  );
+}
+
+export default HoverMaterialAutocomplete;
diff --git a/test/e2e/index.test.ts b/test/e2e/index.test.ts
--- a/test/e2e/index.test.ts
+++ b/test/e2e/index.test.ts
@@ -193,6 +193,52 @@ describe('e2e', () => {
     });
   });
 
+  describe('<Autocomplete/>', () => {
+    it('[Material Autocomplete] should highlight correct option when initial navigation through options starts from mouse move', async () => {
+      await renderFixture('Autocomplete/HoverMaterialAutocomplete');
+
+      const combobox = (await screen.getByRole('combobox'))!;
+      await combobox.click();
+
+      const firstOption = (await screen.getByText('one'))!;
+
+      const dimensions = (await firstOption.boundingBox())!;
+
+      await page.mouse.move(dimensions.x + 10, dimensions.y + 10); // moves to 1st option
+      await page.keyboard.down('ArrowDown'); // moves to 2nd option
+      await page.keyboard.down('ArrowDown'); // moves to 3rd option
+      await page.keyboard.down('ArrowDown'); // moves to 4th option
+
+      const listbox = (await screen.getByRole('listbox'))!;
+      const focusedOption = (await listbox.$('.Mui-focused'))!;
+      const focusedOptionText = await focusedOption.innerHTML();
+
+      expect(focusedOptionText).to.equal('four');
+    });
+
+    it('[Joy Autocomplete] should highlight correct option when initial navigation through options starts from mouse move', async () => {
+      await renderFixture('Autocomplete/HoverJoyAutocomplete');
+
+      const combobox = (await screen.getByRole('combobox'))!;
+      await combobox.click();
+
+      const firstOption = (await screen.getByText('one'))!;
+
+      const dimensions = (await firstOption.boundingBox())!;
+
+      await page.mouse.move(dimensions.x + 10, dimensions.y + 10); // moves to 1st option
+      await page.keyboard.down('ArrowDown'); // moves to 2nd option
+      await page.keyboard.down('ArrowDown'); // moves to 3rd option
+      await page.keyboard.down('ArrowDown'); // moves to 4th option
+
+      const listbox = (await screen.getByRole('listbox'))!;
+      const focusedOption = (await listbox.$('.Joy-focused'))!;
+      const focusedOptionText = await focusedOption.innerHTML();
+
+      expect(focusedOptionText).to.equal('four');
+    });
+  });
+
   describe('<TextareaAutosize />', () => {
     // https://github.com/mui/material-ui/issues/32640
     it('should handle suspense without error', async () => {