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(' + + , + ); + + 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('