Skip to content

Commit

Permalink
[Autocomplete] Prevent reset scroll position when new options are add…
Browse files Browse the repository at this point in the history
…ed (#35735)
  • Loading branch information
sai6855 authored Jan 31, 2023
1 parent 467a3af commit 14dba8d
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 20 deletions.
69 changes: 51 additions & 18 deletions packages/mui-base/src/AutocompleteUnstyled/useAutocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
unstable_useEventCallback as useEventCallback,
unstable_useControlled as useControlled,
unstable_useId as useId,
usePreviousProps,
} from '@mui/utils';

// https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript
Expand Down Expand Up @@ -193,24 +194,6 @@ export default function useAutocomplete(props) {
[getOptionLabel, inputValue, multiple, onInputChange, setInputValueState, clearOnBlur, value],
);

const prevValue = React.useRef();

React.useEffect(() => {
const valueChange = value !== prevValue.current;
prevValue.current = value;

if (focused && !valueChange) {
return;
}

// Only reset the input's value when freeSolo if the component's value changes.
if (freeSolo && !valueChange) {
return;
}

resetInputValue(null, value);
}, [value, resetInputValue, focused, prevValue, freeSolo]);

const [open, setOpenState] = useControlled({
controlled: openProp,
default: false,
Expand Down Expand Up @@ -247,6 +230,26 @@ export default function useAutocomplete(props) {
)
: [];

const previousProps = usePreviousProps({
filteredOptions,
value,
});

React.useEffect(() => {
const valueChange = value !== previousProps.value;

if (focused && !valueChange) {
return;
}

// Only reset the input's value when freeSolo if the component's value changes.
if (freeSolo && !valueChange) {
return;
}

resetInputValue(null, value);
}, [value, resetInputValue, focused, previousProps.value, freeSolo]);

const listboxAvailable = open && filteredOptions.length > 0 && !readOnly;

if (process.env.NODE_ENV !== 'production') {
Expand Down Expand Up @@ -461,11 +464,41 @@ export default function useAutocomplete(props) {
},
);

const checkHighlightedOptionExists = () => {
if (
highlightedIndexRef.current !== -1 &&
previousProps.filteredOptions &&
previousProps.filteredOptions.length !== filteredOptions.length &&
(multiple
? previousProps.value.every((val, i) => getOptionLabel(value[i]) === getOptionLabel(val))
: getOptionLabel(previousProps.value ?? '') === getOptionLabel(value ?? ''))
) {
const previousHighlightedOption = previousProps.filteredOptions[highlightedIndexRef.current];

if (previousHighlightedOption) {
const previousHighlightedOptionExists = filteredOptions.some((option) => {
return getOptionLabel(option) === getOptionLabel(previousHighlightedOption);
});

if (previousHighlightedOptionExists) {
return true;
}
}
}
return false;
};

const syncHighlightedIndex = React.useCallback(() => {
if (!popupOpen) {
return;
}

// Check if the previously highlighted option still exists in the updated filtered options list and if the value hasn't changed
// If it exists and the value hasn't changed, return, otherwise continue execution
if (checkHighlightedOptionExists()) {
return;
}

const valueItem = multiple ? value[0] : value;

// The popup is empty, reset
Expand Down
85 changes: 84 additions & 1 deletion packages/mui-joy/src/Autocomplete/Autocomplete.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1267,8 +1267,91 @@ describe('Joy <Autocomplete />', () => {

checkHighlightIs(listbox, 'two');

// three option is added and autocomplete re-renders, reset the highlight
// three option is added and autocomplete re-renders, restore the highlight
setProps({ options: ['one', 'two', 'three'] });
checkHighlightIs(listbox, 'two');
});

it('should keep focus when multiple options are selected and not reset to top option when options updated', () => {
const { setProps } = render(
<Autocomplete
open
multiple
defaultValue={['one', 'two']}
options={['one', 'two', 'three']}
autoFocus
/>,
);
const textbox = screen.getByRole('combobox');
const listbox = screen.getByRole('listbox');

fireEvent.keyDown(textbox, { key: 'ArrowDown' });
fireEvent.keyDown(textbox, { key: 'ArrowDown' });

checkHighlightIs(listbox, 'three');

// fourth option is added and autocomplete re-renders, restore the highlight
setProps({ options: ['one', 'two', 'three', 'four'] });
checkHighlightIs(listbox, 'three');
});

it('should keep focus when multiple options are selected by not resetting to the top option when options are updated and when options are provided as objects', () => {
const value = [{ label: 'one' }];
const options = [{ label: 'one' }, { label: 'two' }, { label: 'three' }];
const { setProps } = render(
<Autocomplete
multiple
options={options}
value={value}
isOptionEqualToValue={(option, val) => option.label === val.label}
autoFocus
open
/>,
);
const textbox = screen.getByRole('combobox');
const listbox = screen.getByRole('listbox');

fireEvent.keyDown(textbox, { key: 'ArrowDown' });
fireEvent.keyDown(textbox, { key: 'ArrowDown' });

checkHighlightIs(listbox, 'three');

// fourth option is added and autocomplete re-renders, restore the highlight
setProps({
options: [{ label: 'one' }, { label: 'two' }, { label: 'three' }, { label: 'four' }],
});
checkHighlightIs(listbox, 'three');
});

it('should keep focus on selected option when options updates and when options are provided as objects', () => {
const { setProps } = render(
<Autocomplete open options={[{ label: 'one' }, { label: 'two' }]} autoFocus />,
);
const textbox = screen.getByRole('combobox');
const listbox = screen.getByRole('listbox');

fireEvent.keyDown(textbox, { key: 'ArrowDown' }); // goes to 'one'
fireEvent.keyDown(textbox, { key: 'ArrowDown' }); // goes to 'two'

checkHighlightIs(listbox, 'two');

// three option is added and autocomplete re-renders, restore the highlight
setProps({ options: [{ label: 'one' }, { label: 'two' }, { label: 'three' }] });
checkHighlightIs(listbox, 'two');
});

it("should reset the highlight when previously highlighted option doesn't exists in new options", () => {
const { setProps } = render(<Autocomplete open options={['one', 'two']} autoFocus />);
const textbox = screen.getByRole('combobox');
const listbox = screen.getByRole('listbox');

fireEvent.keyDown(textbox, { key: 'ArrowDown' }); // goes to 'one'
fireEvent.keyDown(textbox, { key: 'ArrowDown' }); // goes to 'two'

checkHighlightIs(listbox, 'two');

// Options are updated and autocomplete re-renders; reset the highlight since two doesn't exist in the new options.
setProps({ options: ['one', 'three', 'four'] });
checkHighlightIs(listbox, null);
});

Expand Down
93 changes: 92 additions & 1 deletion packages/mui-material/src/Autocomplete/Autocomplete.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1607,8 +1607,99 @@ describe('<Autocomplete />', () => {

checkHighlightIs(listbox, 'two');

// three option is added and autocomplete re-renders, reset the highlight
// three option is added and autocomplete re-renders, restore the highlight
setProps({ options: ['one', 'two', 'three'] });
checkHighlightIs(listbox, 'two');
});

it('should keep focus when multiple options are selected and not reset to top option when options updated', () => {
const { setProps } = render(
<Autocomplete
open
multiple
defaultValue={['one', 'two']}
options={['one', 'two', 'three']}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);
const textbox = screen.getByRole('combobox');
const listbox = screen.getByRole('listbox');

fireEvent.keyDown(textbox, { key: 'ArrowDown' });
fireEvent.keyDown(textbox, { key: 'ArrowDown' });

checkHighlightIs(listbox, 'three');

// fourth option is added and autocomplete re-renders, restore the highlight
setProps({ options: ['one', 'two', 'three', 'four'] });
checkHighlightIs(listbox, 'three');
});

it('should keep focus when multiple options are selected by not resetting to the top option when options are updated and when options are provided as objects', () => {
const { setProps } = render(
<Autocomplete
open
multiple
defaultValue={[{ label: 'one' }]}
isOptionEqualToValue={(option, value) => option.label === value.label}
options={[{ label: 'one' }, { label: 'two' }, { label: 'three' }]}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);
const textbox = screen.getByRole('combobox');
const listbox = screen.getByRole('listbox');

fireEvent.keyDown(textbox, { key: 'ArrowDown' });
fireEvent.keyDown(textbox, { key: 'ArrowDown' });

checkHighlightIs(listbox, 'three');

// fourth option is added and autocomplete re-renders, restore the highlight
setProps({
options: [{ label: 'one' }, { label: 'two' }, { label: 'three' }, { label: 'four' }],
});
checkHighlightIs(listbox, 'three');
});

it('should keep focus on selected option when options updates and when options are provided as objects', () => {
const { setProps } = render(
<Autocomplete
open
options={[{ label: 'one' }, { label: 'two' }]}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);
const textbox = screen.getByRole('combobox');
const listbox = screen.getByRole('listbox');

fireEvent.keyDown(textbox, { key: 'ArrowDown' }); // goes to 'one'
fireEvent.keyDown(textbox, { key: 'ArrowDown' }); // goes to 'two'

checkHighlightIs(listbox, 'two');

// three option is added and autocomplete re-renders, restore the highlight
setProps({ options: [{ label: 'one' }, { label: 'two' }, { label: 'three' }] });
checkHighlightIs(listbox, 'two');
});

it("should reset the highlight when previously highlighted option doesn't exists in new options", () => {
const { setProps } = render(
<Autocomplete
open
options={['one', 'two']}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);
const textbox = screen.getByRole('combobox');
const listbox = screen.getByRole('listbox');

fireEvent.keyDown(textbox, { key: 'ArrowDown' }); // goes to 'one'
fireEvent.keyDown(textbox, { key: 'ArrowDown' }); // goes to 'two'

checkHighlightIs(listbox, 'two');

// Options are updated and autocomplete re-renders; reset the highlight since two doesn't exist in the new options.
setProps({ options: ['one', 'three', 'four'] });
checkHighlightIs(listbox, null);
});

Expand Down

0 comments on commit 14dba8d

Please sign in to comment.