diff --git a/packages/mui-base/src/Select/Select.test.tsx b/packages/mui-base/src/Select/Select.test.tsx
index 852a8cf090ca63..5413df6f2a435f 100644
--- a/packages/mui-base/src/Select/Select.test.tsx
+++ b/packages/mui-base/src/Select/Select.test.tsx
@@ -1234,4 +1234,94 @@ describe('', () => {
expect(renderOption3Spy.callCount).to.equal(0);
expect(renderOption4Spy.callCount).to.equal(0);
});
+
+ it('1st option should have highlighted class irrespective of option selection', () => {
+ const { getByRole } = render(
+ ,
+ );
+
+ const select = getByRole('combobox');
+
+ act(() => {
+ select.focus();
+ });
+
+ fireEvent.keyDown(select, { key: 'ArrowDown' });
+
+ const firstOption = getByRole('option');
+
+ expect(firstOption).to.have.class(optionClasses.highlighted);
+ });
+
+ it('except for 1st option - other options should have highlighted class when option is selected by mouse click', () => {
+ const { getAllByRole, getByRole } = render(
+ ,
+ );
+
+ const select = getByRole('combobox');
+
+ act(() => {
+ select.focus();
+ });
+
+ fireEvent.keyDown(select, { key: 'ArrowDown' });
+
+ const options = getAllByRole('option');
+ const firstOption = options[0];
+ const secondOption = options[1];
+
+ expect(firstOption).to.have.class(optionClasses.highlighted);
+ // it doesn't have highlighted class as it is not selected
+ expect(secondOption).not.to.have.class(optionClasses.highlighted);
+
+ // selects option
+ fireEvent.click(secondOption);
+ expect(secondOption).to.have.class(optionClasses.highlighted);
+ // deselects option
+ fireEvent.click(secondOption);
+ expect(secondOption).not.to.have.class(optionClasses.highlighted);
+ });
+
+ it('deselected option should have highlighted class when option is deselected by keyboard', () => {
+ const { getAllByRole, getByRole } = render(
+ ,
+ );
+
+ const select = getByRole('combobox');
+
+ act(() => {
+ select.focus();
+ });
+
+ fireEvent.keyDown(select, { key: 'ArrowDown' });
+
+ const options = getAllByRole('option');
+ const firstOption = options[0];
+ const secondOption = options[1];
+
+ expect(firstOption).to.have.class(optionClasses.highlighted);
+ // it doesn't have highlighted class as it is not navigated yet
+ expect(secondOption).not.to.have.class(optionClasses.highlighted);
+
+ fireEvent.keyDown(select, { key: 'ArrowDown' }); // navigates to second option
+ expect(secondOption).to.have.class(optionClasses.highlighted);
+
+ fireEvent.keyDown(select, { key: ' ' }); // selects second option
+ expect(secondOption).to.have.class(optionClasses.highlighted);
+
+ fireEvent.keyDown(select, { key: ' ' }); // deselects second option
+ expect(secondOption).to.have.class(optionClasses.highlighted);
+ });
});
diff --git a/packages/mui-base/src/useList/listReducer.ts b/packages/mui-base/src/useList/listReducer.ts
index bfce751beb5567..1bf5df5e6bfc25 100644
--- a/packages/mui-base/src/useList/listReducer.ts
+++ b/packages/mui-base/src/useList/listReducer.ts
@@ -204,6 +204,7 @@ function handleItemSelection>(
item: ItemValue,
state: State,
context: ListActionContext,
+ reason: 'mouse' | 'keyboard',
): State {
const { itemComparer, isItemDisabled, selectionMode, items } = context;
const { selectedValues } = state;
@@ -217,10 +218,13 @@ function handleItemSelection>(
// if the item is already selected, remove it from the selection, otherwise add it
const newSelectedValues = toggleSelection(item, selectedValues, selectionMode, itemComparer);
+ const highlightedValue =
+ reason === 'mouse' && selectedValues.includes(items[itemIndex]) ? null : item;
+
return {
...state,
selectedValues: newSelectedValues,
- highlightedValue: item,
+ highlightedValue,
};
}
@@ -309,7 +313,7 @@ function handleKeyDown>(
return state;
}
- return handleItemSelection(state.highlightedValue, state, context);
+ return handleItemSelection(state.highlightedValue, state, context, 'keyboard');
default:
break;
@@ -423,6 +427,16 @@ function handleResetHighlight>(
};
}
+function handleItemHover>(
+ item: ItemValue,
+ state: State,
+): State {
+ return {
+ ...state,
+ highlightedValue: item,
+ };
+}
+
export function listReducer>(
state: State,
action: ListReducerAction & { context: ListActionContext },
@@ -433,7 +447,9 @@ export function listReducer>(
case ListActionTypes.keyDown:
return handleKeyDown(action.key, state, context);
case ListActionTypes.itemClick:
- return handleItemSelection(action.item, state, context);
+ return handleItemSelection(action.item, state, context, 'mouse');
+ case ListActionTypes.itemHover:
+ return handleItemHover(action.item, state);
case ListActionTypes.blur:
return handleBlur(state, context);
case ListActionTypes.textNavigation:
diff --git a/packages/mui-base/src/useOption/useOption.ts b/packages/mui-base/src/useOption/useOption.ts
index 556e82506e3344..953629a5f4f3a6 100644
--- a/packages/mui-base/src/useOption/useOption.ts
+++ b/packages/mui-base/src/useOption/useOption.ts
@@ -26,6 +26,7 @@ export function useOption(params: UseOptionParameters): UseOptionR
selected,
} = useListItem({
item: value,
+ handlePointerOverEvents: true,
});
const id = useId(idParam);
diff --git a/test/e2e/fixtures/Select/BaseSelect.tsx b/test/e2e/fixtures/Select/BaseSelect.tsx
new file mode 100644
index 00000000000000..49baaed199d0cc
--- /dev/null
+++ b/test/e2e/fixtures/Select/BaseSelect.tsx
@@ -0,0 +1,15 @@
+import * as React from 'react';
+import { Option, Select } from '@mui/base';
+
+function BaseSelect() {
+ return (
+
+ );
+}
+
+export default BaseSelect;
diff --git a/test/e2e/index.test.ts b/test/e2e/index.test.ts
index 817a7ec0ac4813..a649a91d64cd33 100644
--- a/test/e2e/index.test.ts
+++ b/test/e2e/index.test.ts
@@ -239,6 +239,48 @@ describe('e2e', () => {
});
});
+ describe('', () => {
+ it('should select hovered option when initial navigation through options starts from mouse move', async () => {
+ await renderFixture('Select/BaseSelect');
+
+ const combobox = (await screen.getByRole('combobox'))!;
+ await combobox.click();
+
+ const secondOption = (await screen.getByText('20'))!;
+
+ const dimensions = (await secondOption.boundingBox())!;
+
+ await page.mouse.move(dimensions.x + 10, dimensions.y + 10); // moves to 2nd option
+ await page.keyboard.down(' '); // selects 2nd option
+
+ const classNames = await secondOption.evaluate((element) => {
+ return Array.from(element.classList);
+ });
+
+ expect(classNames).to.include('Mui-selected');
+ });
+
+ it('should just highlight not select the option on mouse over', async () => {
+ await renderFixture('Select/BaseSelect');
+
+ const combobox = (await screen.getByRole('combobox'))!;
+ await combobox.click();
+
+ const secondOption = (await screen.getByText('20'))!;
+
+ const dimensions = (await secondOption.boundingBox())!;
+
+ await page.mouse.move(dimensions.x + 10, dimensions.y + 10); // moves to 2nd option
+
+ const classNames = await secondOption.evaluate((element) => {
+ return Array.from(element.classList);
+ });
+
+ expect(classNames).not.to.include('Mui-selected');
+ expect(classNames).to.include('MuiOption-highlighted');
+ });
+ });
+
describe('', () => {
// https://github.com/mui/material-ui/issues/32640
it('should handle suspense without error', async () => {