Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions packages/mui-material/src/Autocomplete/Autocomplete.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,47 @@ describe('<Autocomplete />', () => {
expect(handleChange.args[0][1]).to.deep.equal([]);
});

it('should not suppress focus events after clearing with Escape', async () => {
const handleOpen = spy();
const { user } = render(
<Autocomplete
clearOnEscape
openOnFocus
multiple
value={['one']}
options={['one', 'two']}
onChange={() => {}}
onOpen={handleOpen}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

const textbox = screen.getByRole('combobox');

// Opening on initial focus
expect(handleOpen.callCount).to.equal(1);

// Close the popup first so Escape takes the clear path
await user.keyboard('{Escape}');
// Popup was open, so first Escape closes it
handleOpen.resetHistory();

// Now Escape should clear (popup is closed, value is non-empty)
await user.keyboard('{Escape}');

// Focus is still on the input
expect(textbox).toHaveFocus();

// Blur and re-focus: onOpen should be called (ignoreFocus was NOT set)
act(() => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would user.tab and user.tab({shift}) also work here?

textbox.blur();
});
act(() => {
textbox.focus();
});
expect(handleOpen.callCount).to.equal(1);
});

it('should clear on escape if rendering single value', () => {
const handleChange = spy();
render(
Expand Down Expand Up @@ -1634,6 +1675,34 @@ describe('<Autocomplete />', () => {
fireEvent.focus(textbox);
expect(textbox).to.have.attribute('aria-expanded', 'true');
});

it('should suppress focus events when clearing with the clear button', async () => {
const handleOpen = spy();
const { user } = render(
<Autocomplete
openOnFocus
value="one"
options={['one', 'two']}
onChange={() => {}}
onOpen={handleOpen}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

// Opening on initial focus
expect(handleOpen.callCount).to.equal(1);

// Close popup
await user.keyboard('{Escape}');
handleOpen.resetHistory();

// Click the clear button
const clearButton = screen.getByTitle('Clear');
await user.click(clearButton);

// onOpen should NOT be called because ignoreFocus is set
expect(handleOpen.callCount).to.equal(0);
});
});

describe('listbox wrapping behavior', () => {
Expand Down Expand Up @@ -2576,6 +2645,89 @@ describe('<Autocomplete />', () => {
});

describe('prop: freeSolo', () => {
it('should reset input when controlled value changes to null', async () => {
function App() {
const [value, setValue] = React.useState('foo');
return (
<React.Fragment>
<Autocomplete
freeSolo
value={value}
options={['foo', 'bar']}
onChange={(event, newValue) => setValue(newValue)}
renderInput={(params) => <TextField {...params} />}
/>
<button onClick={() => setValue(null)} type="button">
Reset
</button>
</React.Fragment>
);
}
const { user } = render(<App />);
const textbox = screen.getByRole('combobox');
expect(textbox.value).to.equal('foo');

await user.click(screen.getByRole('button', { name: 'Reset' }));
expect(textbox.value).to.equal('');
});

it('should reset input when controlled value changes to null with clearOnBlur=false', async () => {
function App() {
const [value, setValue] = React.useState('foo');
return (
<React.Fragment>
<Autocomplete
freeSolo
clearOnBlur={false}
value={value}
options={['foo', 'bar']}
onChange={(event, newValue) => setValue(newValue)}
renderInput={(params) => <TextField {...params} />}
/>
<button onClick={() => setValue(null)} type="button">
Reset
</button>
</React.Fragment>
);
}
const { user } = render(<App />);
const textbox = screen.getByRole('combobox');
expect(textbox.value).to.equal('foo');

await user.click(screen.getByRole('button', { name: 'Reset' }));
expect(textbox.value).to.equal('');
});

it('should retain input when controlled multiple value changes with clearOnBlur=false', async () => {
function App() {
const [value, setValue] = React.useState(['one']);
return (
<React.Fragment>
<Autocomplete
multiple
freeSolo
clearOnBlur={false}
value={value}
options={['one', 'two']}
onChange={(event, newValue) => setValue(newValue)}
renderInput={(params) => <TextField {...params} />}
/>
<button onClick={() => setValue([])} type="button">
Reset
</button>
</React.Fragment>
);
}
const { user } = render(<App />);
const textbox = screen.getByRole('combobox');

await user.type(textbox, 'abc');
expect(textbox.value).to.equal('abc');

await user.click(screen.getByRole('button', { name: 'Reset' }));
expect(textbox.value).to.equal('abc');
});

it('pressing twice enter should not call onChange listener twice', () => {
const handleChange = spy();
const options = [{ name: 'foo' }];
Expand Down
15 changes: 10 additions & 5 deletions packages/mui-material/src/useAutocomplete/useAutocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,12 @@ function useAutocomplete(props) {

const resetInputValue = React.useCallback(
(event, newValue, reason) => {
// retain current `inputValue` if new option isn't selected and `clearOnBlur` is false
// When `multiple` is enabled, `newValue` is an array of all selected items including the newly selected item
// Retain the current `inputValue` when no new option is selected and `clearOnBlur` is false.
// In `multiple` mode, `newValue` is the next value array, so only length growth counts as a selection.
const isOptionSelected = multiple ? value.length < newValue.length : newValue !== null;
if (!isOptionSelected && !clearOnBlur) {
// A controlled single-value `freeSolo` reset to `null` should still clear the input.
const shouldClearOnReset = reason === 'reset' && freeSolo && !multiple && newValue === null;
if (!isOptionSelected && !clearOnBlur && !shouldClearOnReset) {
return;
}
const newInputValue = getInputValue(newValue, multiple, getOptionLabel, renderValue);
Expand All @@ -203,6 +205,7 @@ function useAutocomplete(props) {
onInputChange,
setInputValueState,
clearOnBlur,
freeSolo,
value,
renderValue,
],
Expand Down Expand Up @@ -826,7 +829,6 @@ function useAutocomplete(props) {
};

const handleClear = (event) => {
ignoreFocus.current = true;
setInputValueState('');

if (onInputChange) {
Expand Down Expand Up @@ -1272,7 +1274,10 @@ function useAutocomplete(props) {
getClearProps: () => ({
tabIndex: -1,
type: 'button',
onClick: handleClear,
onClick: (event) => {
ignoreFocus.current = true;
handleClear(event);
},
}),
getItemProps: ({ index = 0 } = {}) => ({
...(multiple && { key: index }),
Expand Down
Loading