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
2 changes: 1 addition & 1 deletion docs/translations/api-docs/autocomplete/autocomplete.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"description": "If <code>true</code>, the first option is automatically highlighted."
},
"autoSelect": {
"description": "If <code>true</code>, the selected option becomes the value of the input when the Autocomplete loses focus unless the user chooses a different option or changes the character string in the input.<br>When using the <code>freeSolo</code> mode, the typed value will be the input value if the Autocomplete loses focus without highlighting an option."
"description": "If <code>true</code>, the value is updated when the input loses focus under one of these conditions:<br>- An option highlighted via keyboard navigation or <code>autoHighlight</code> is selected. Hover and touch highlights are ignored. - Otherwise, in <code>freeSolo</code> mode, the typed text becomes the value."
},
"blurOnSelect": {
"description": "<p>Control if the input should be blurred when an option is selected:</p>\n<ul>\n<li><code>false</code> the input is not blurred.</li>\n<li><code>true</code> the input is always blurred.</li>\n<li><code>touch</code> the input is blurred after a touch event.</li>\n<li><code>mouse</code> the input is blurred after a mouse event.</li>\n</ul>\n"
Expand Down
9 changes: 4 additions & 5 deletions packages/mui-material/src/Autocomplete/Autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -789,12 +789,11 @@ Autocomplete.propTypes /* remove-proptypes */ = {
*/
autoHighlight: PropTypes.bool,
/**
* If `true`, the selected option becomes the value of the input
* when the Autocomplete loses focus unless the user chooses
* a different option or changes the character string in the input.
* If `true`, the value is updated when the input loses focus under one of these conditions:
*
* When using the `freeSolo` mode, the typed value will be the input value
* if the Autocomplete loses focus without highlighting an option.
* - An option highlighted via keyboard navigation or `autoHighlight` is selected.
* Hover and touch highlights are ignored.
* - Otherwise, in `freeSolo` mode, the typed text becomes the value.
* @default false
*/
autoSelect: PropTypes.bool,
Expand Down
350 changes: 350 additions & 0 deletions packages/mui-material/src/Autocomplete/Autocomplete.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,154 @@ describe('<Autocomplete />', () => {
expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal('a');
});

it('should prefer typed text over a mouse-hovered option on blur with freeSolo', async () => {
const handleChange = spy();
const options = ['The Shawshank Redemption', 'The Godfather'];
const { user } = render(
<Autocomplete
autoSelect
freeSolo
openOnFocus
options={options}
onChange={handleChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

await user.type(screen.getByRole('combobox'), 'The');
await user.pointer({ target: screen.getByRole('option', { name: 'The Godfather' }) });
await user.tab();

expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal('The');
});

it('should not select a touch-highlighted option on blur', async () => {
Comment thread
ZeeshanTamboli marked this conversation as resolved.
const handleChange = spy();
const options = ['one', 'two', 'three'];
const { user } = render(
<Autocomplete
autoSelect
openOnFocus
options={options}
onChange={handleChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

await user.pointer({
keys: '[TouchA>]',
target: screen.getByRole('option', { name: 'two' }),
});
await user.tab();

expect(handleChange.callCount).to.equal(0);
});

it('should not select a mouse-hovered option on blur even if already highlighted', async () => {
const handleChange = spy();
const options = ['one', 'two', 'three'];
const { user } = render(
<Autocomplete
autoSelect
autoHighlight
openOnFocus
options={options}
onChange={handleChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

// First option is programmatically highlighted by autoHighlight.
// Hovering it should still mark it as mouse-initiated and prevent
// autoSelect from committing it on blur.
await user.pointer({ target: screen.getByRole('option', { name: 'one' }) });
await user.tab();

expect(handleChange.callCount).to.equal(0);
});

it('should not select a mouse-hovered option on blur', async () => {
const handleChange = spy();
const options = ['one', 'two', 'three'];
const { user } = render(
<Autocomplete
autoSelect
openOnFocus
options={options}
onChange={handleChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

await user.pointer({ target: screen.getByRole('option', { name: 'two' }) });
await user.tab();

expect(handleChange.callCount).to.equal(0);
});

it('should not select a mouse-hovered option on outside click blur', async () => {
const handleChange = spy();
const options = ['one', 'two', 'three'];
const { user } = render(
<React.Fragment>
<Autocomplete
autoSelect
openOnFocus
options={options}
onChange={handleChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>
<button type="button">Outside</button>
</React.Fragment>,
);

await user.pointer({ target: screen.getByRole('option', { name: 'two' }) });
await user.click(screen.getByRole('button', { name: 'Outside' }));

expect(handleChange.callCount).to.equal(0);
});

it('should select a keyboard-highlighted option on blur', async () => {
const handleChange = spy();
const options = ['one', 'two', 'three'];
const { user } = render(
<Autocomplete
autoSelect
openOnFocus
options={options}
onChange={handleChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

await user.keyboard('{ArrowDown}');
await user.tab();

expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal('one');
});

it('should select the first option on blur when autoHighlight is true', async () => {
const handleChange = spy();
const options = ['one', 'two', 'three'];
const { user } = render(
<Autocomplete
autoSelect
autoHighlight
openOnFocus
options={options}
onChange={handleChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

await user.tab();

expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal('one');
});
});

describe('prop: multiple', () => {
Expand Down Expand Up @@ -2804,6 +2952,208 @@ describe('<Autocomplete />', () => {
expect(handleChange.args[0][1]).to.equal('あ');
});

it('should prefer typed text over auto-highlighted match on Enter', async () => {
const handleChange = spy();
const options = ['The Shawshank Redemption', 'The Godfather'];
const { user } = render(
<Autocomplete
freeSolo
autoHighlight
openOnFocus
options={options}
onChange={handleChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

await user.type(screen.getByRole('combobox'), 'The{Enter}');

expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal('The');
});

it('should prefer typed text after editing a selected value', async () => {
const handleChange = spy();
const options = ['The Shawshank Redemption', 'The Godfather'];
const { user } = render(
<Autocomplete
freeSolo
defaultValue="The Godfather"
options={options}
onChange={handleChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

// Edit the text (still partially matches the selected value's option)
// and press Enter — should create free text, not re-select the old value
await user.keyboard('{Backspace}{Backspace}{Backspace}{Backspace}{Backspace}{Enter}');

expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal('The Godf');
});

it('should create freeSolo text after one edit and Enter', async () => {
const handleChange = spy();
const options = ['The Shawshank Redemption', 'The Godfather'];
const { user } = render(
<Autocomplete
freeSolo
defaultValue="The Godfather"
options={options}
onChange={handleChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

await user.keyboard('{Backspace}');
expect(screen.getByRole('option', { name: 'The Godfather' })).not.to.equal(null);
await user.keyboard('{Enter}');

expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal('The Godfathe');
});

it('should select the highlighted option on Enter after keyboard navigation', async () => {
const handleChange = spy();
const options = ['The Shawshank Redemption', 'The Godfather'];
const { user } = render(
<Autocomplete
freeSolo
options={options}
onChange={handleChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

await user.type(screen.getByRole('combobox'), 'The');
await user.keyboard('{ArrowDown}{Enter}');

expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal('The Shawshank Redemption');
});

it('should select a mouse-hovered option on Enter after typing', async () => {
const handleChange = spy();
const options = ['The Shawshank Redemption', 'The Godfather'];
const { user } = render(
<Autocomplete
freeSolo
options={options}
onChange={handleChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

await user.type(screen.getByRole('combobox'), 'The');
await user.pointer({ target: screen.getByRole('option', { name: 'The Godfather' }) });
await user.keyboard('{Enter}');

expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal('The Godfather');
});

it('should not select a touch-highlighted option after scroll on Enter', async () => {
const handleChange = spy();
const handleClose = spy();
const options = ['one', 'two', 'three'];
const { user } = render(
<Autocomplete
openOnFocus
options={options}
onChange={handleChange}
onClose={handleClose}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);
const optionOne = screen.getByRole('option', { name: 'one' });

// user.pointer({ keys: '[TouchA>]' }) fires pointerdown which moves focus
// on real devices, touchStart does not move focus
// therefore fireEvent is more correct here
fireEvent.touchStart(optionOne);
fireEvent.scroll(screen.getByRole('listbox'));
await user.keyboard('{Enter}');

expect(handleChange.callCount).to.equal(0);
expect(handleClose.callCount).to.equal(1);
});

it('should allow Enter to select after touch-scroll then typing', async () => {
const handleChange = spy();
const options = ['one', 'two', 'three'];
const { user } = render(
<Autocomplete
autoHighlight
openOnFocus
options={options}
onChange={handleChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

// Touch-scroll makes the highlight stale
await user.pointer({
keys: '[TouchA>]',
target: screen.getByRole('option', { name: 'one' }),
});
fireEvent.scroll(screen.getByRole('listbox'));

// Typing clears the stale scroll flag; autoHighlight re-highlights
await user.type(screen.getByRole('combobox'), 't');
await user.keyboard('{Enter}');

expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal('two');
});

it('should select an option on tap without scroll', async () => {
const handleChange = spy();
const options = ['one', 'two', 'three'];
const { user } = render(
<Autocomplete
open
options={options}
onChange={handleChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

await user.pointer([
{ keys: '[TouchA]', target: screen.getByRole('option', { name: 'one' }) },
]);

expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal('one');
});

it('should not misclassify scroll as touch after close and reopen', async () => {
const handleChange = spy();
const options = ['one', 'two', 'three'];
const { user } = render(
<Autocomplete
openOnFocus
options={options}
onChange={handleChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

// Touch an option, then close by pressing Escape
await user.pointer({
keys: '[TouchA>]',
target: screen.getByRole('option', { name: 'one' }),
});
await user.keyboard('{Escape}');

// Reopen (first ArrowDown) and navigate (second ArrowDown), then Enter.
// The touch state should not leak into this new popup session.
await user.keyboard('{ArrowDown}{ArrowDown}{Enter}');

expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal('one');
});

it('should render endAdornment only when clear icon or popup icon is available', () => {
const view = render(
<Autocomplete freeSolo options={[]} renderInput={(params) => <TextField {...params} />} />,
Expand Down
Loading
Loading