Skip to content

Commit

Permalink
[Autocomplete] Add new AutocompleteInputChangeReason (#37301)
Browse files Browse the repository at this point in the history
Co-authored-by: Aarón García Hervás <aaron@mui.com>
  • Loading branch information
binh1298 and aarongarciah committed Jul 11, 2024
1 parent 701996c commit f899014
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 35 deletions.
13 changes: 13 additions & 0 deletions docs/data/material/migration/migrating-to-v6/migrating-to-v6.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,19 @@ This results in a reduction of the `@mui/material` package size by 2.5MB or 25%
Instead, using ESM-based CDNs such as [esm.sh](https://esm.sh/) is recommended.
For alternative installation methods, refer to the [CDN documentation](/material-ui/getting-started/installation/#cdn).

### Autocomplete

#### New reason values added to onInputChange

Three new possible values have been added to the `reason` argument in the `onInputChange` callback of the Autocomplete component.
These three were previously treated as `"reset"`, so if you are relying on that, you might need to adjust your code accordingly:

- `"blur"`: like `"reset"` but triggered when the focus is moved off the input. `clearOnBlur` must be `true`.
- `"selectOption"`: triggered when the input value changes after an option has been selected.
- `"removeOption"`: triggered in multiple selection when a chip gets removed due to the corresponding option being selected.

These are added on top of the existing ones: `"input"`, `"reset"`, and `"clear"`.

### Chip

Previously, the Chip component lost focus when the escape button was pressed, which differed from how other button-like components work.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
"typeDescriptions": {
"event": "The event source of the callback.",
"value": "The new value of the text input.",
"reason": "Can be: <code>&quot;input&quot;</code> (user input), <code>&quot;reset&quot;</code> (programmatic change), <code>&quot;clear&quot;</code>."
"reason": "Can be: <code>&quot;input&quot;</code> (user input), <code>&quot;reset&quot;</code> (programmatic change), <code>&quot;clear&quot;</code>, <code>&quot;blur&quot;</code>, <code>&quot;selectOption&quot;</code>, <code>&quot;removeOption&quot;</code>"
}
},
"onOpen": {
Expand Down
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 @@ -143,7 +143,7 @@
"typeDescriptions": {
"event": "The event source of the callback.",
"value": "The new value of the text input.",
"reason": "Can be: <code>&quot;input&quot;</code> (user input), <code>&quot;reset&quot;</code> (programmatic change), <code>&quot;clear&quot;</code>."
"reason": "Can be: <code>&quot;input&quot;</code> (user input), <code>&quot;reset&quot;</code> (programmatic change), <code>&quot;clear&quot;</code>, <code>&quot;blur&quot;</code>, <code>&quot;selectOption&quot;</code>, <code>&quot;removeOption&quot;</code>"
}
},
"onOpen": {
Expand Down
10 changes: 8 additions & 2 deletions packages/mui-base/src/useAutocomplete/useAutocomplete.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ export interface UseAutocompleteProps<
*
* @param {React.SyntheticEvent} event The event source of the callback.
* @param {string} value The new value of the text input.
* @param {string} reason Can be: `"input"` (user input), `"reset"` (programmatic change), `"clear"`.
* @param {string} reason Can be: `"input"` (user input), `"reset"` (programmatic change), `"clear"`, `"blur"`, `"selectOption"`, `"removeOption"`
*/
onInputChange?: (
event: React.SyntheticEvent,
Expand Down Expand Up @@ -329,7 +329,13 @@ export type AutocompleteCloseReason =
| 'selectOption'
| 'removeOption'
| 'blur';
export type AutocompleteInputChangeReason = 'input' | 'reset' | 'clear';
export type AutocompleteInputChangeReason =
| 'input'
| 'reset'
| 'clear'
| 'blur'
| 'selectOption'
| 'removeOption';

export type AutocompleteGetTagProps = ({ index }: { index: number }) => {
key: number;
Expand Down
10 changes: 5 additions & 5 deletions packages/mui-base/src/useAutocomplete/useAutocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export function useAutocomplete(props) {
const [focused, setFocused] = React.useState(false);

const resetInputValue = React.useCallback(
(event, newValue) => {
(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
const isOptionSelected = multiple ? value.length < newValue.length : newValue !== null;
Expand All @@ -176,7 +176,7 @@ export function useAutocomplete(props) {
setInputValueState(newInputValue);

if (onInputChange) {
onInputChange(event, newInputValue, 'reset');
onInputChange(event, newInputValue, reason);
}
},
[getOptionLabel, inputValue, multiple, onInputChange, setInputValueState, clearOnBlur, value],
Expand Down Expand Up @@ -236,7 +236,7 @@ export function useAutocomplete(props) {
return;
}

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

const listboxAvailable = open && filteredOptions.length > 0 && !readOnly;
Expand Down Expand Up @@ -681,7 +681,7 @@ export function useAutocomplete(props) {
}
}

resetInputValue(event, newValue);
resetInputValue(event, newValue, reason);

handleValue(event, newValue, reason, { option });
if (!disableCloseOnSelect && (!event || (!event.ctrlKey && !event.metaKey))) {
Expand Down Expand Up @@ -939,7 +939,7 @@ export function useAutocomplete(props) {
} else if (autoSelect && freeSolo && inputValue !== '') {
selectNewValue(event, inputValue, 'blur', 'freeSolo');
} else if (clearOnBlur) {
resetInputValue(event, value);
resetInputValue(event, value, 'blur');
}

handleClose(event, 'blur');
Expand Down
108 changes: 96 additions & 12 deletions packages/mui-joy/src/Autocomplete/Autocomplete.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1889,43 +1889,127 @@ describe('Joy <Autocomplete />', () => {
});

describe('prop: onInputChange', () => {
it('provides a reason on input change', () => {
it('provides a reason on input change', async () => {
const handleInputChange = spy();
const options = [{ name: 'foo' }];
render(
const { user } = render(
<Autocomplete
onInputChange={handleInputChange}
options={options}
getOptionLabel={(option) => option.name}
autoFocus
/>,
);
fireEvent.change(document.activeElement as HTMLInputElement, { target: { value: 'a' } });

await user.type(document.activeElement as HTMLInputElement, 'a');

expect(handleInputChange.callCount).to.equal(1);
expect(handleInputChange.args[0][1]).to.equal('a');
expect(handleInputChange.args[0][2]).to.equal('input');
});

it('provides a reason on select reset', () => {
it('provides a reason on select reset', async () => {
const handleInputChange = spy();
const options = [{ name: 'foo' }, { name: 'bar' }];
function MyComponent() {
const [value, setValue] = React.useState(options[0]);
return (
<React.Fragment>
<Autocomplete
onInputChange={handleInputChange}
openOnFocus
options={options}
getOptionLabel={(option) => option.name}
value={value}
/>
<button onClick={() => setValue(options[1])}>Reset</button>
</React.Fragment>
);
}
const { user } = render(<MyComponent />);

await user.click(screen.getByText('Reset'));

expect(handleInputChange.callCount).to.equal(4);
expect(handleInputChange.args[3][1]).to.equal(options[1].name);
expect(handleInputChange.args[3][2]).to.equal('reset');
});

it('provides a reason on clear', async () => {
const handleInputChange = spy();
const options = [{ name: 'foo' }];
render(
const { user } = render(
<Autocomplete
onInputChange={handleInputChange}
openOnFocus
options={options}
getOptionLabel={(option) => option.name}
defaultValue={options[0]}
autoFocus
/>,
);
const textbox = screen.getByRole('combobox');

fireEvent.keyDown(textbox, { key: 'ArrowDown' });
fireEvent.keyDown(textbox, { key: 'Enter' });
await user.click(screen.getByLabelText('Clear'));

expect(handleInputChange.callCount).to.equal(1);
expect(handleInputChange.args[0][1]).to.equal(options[0].name);
expect(handleInputChange.args[0][2]).to.equal('reset');
expect(handleInputChange.lastCall.args[1]).to.equal('');
expect(handleInputChange.lastCall.args[2]).to.equal('clear');
});

it('provides a reason on blur', async () => {
const handleInputChange = spy();
const options = [{ name: 'foo' }];
const { user } = render(
<Autocomplete
onInputChange={handleInputChange}
options={options}
getOptionLabel={(option) => option.name}
autoFocus
clearOnBlur
/>,
);
await user.type(screen.getByRole('combobox'), options[0].name);
await user.tab();

expect(handleInputChange.lastCall.args[1]).to.equal('');
expect(handleInputChange.lastCall.args[2]).to.equal('blur');
});

it('provides a reason on select option', async () => {
const handleInputChange = spy();
const options = [{ name: 'foo' }];
const { user } = render(
<Autocomplete
onInputChange={handleInputChange}
options={options}
getOptionLabel={(option) => option.name}
autoFocus
/>,
);

await user.click(screen.getByLabelText('Open'));
await user.click(screen.getByRole('option', { name: options[0].name }));

expect(handleInputChange.lastCall.args[1]).to.equal(options[0].name);
expect(handleInputChange.lastCall.args[2]).to.equal('selectOption');
});

it('provides a reason on remove option', async () => {
const handleInputChange = spy();
const options = [{ name: 'foo' }];
const { user } = render(
<Autocomplete
onInputChange={handleInputChange}
options={options}
getOptionLabel={(option) => option.name}
defaultValue={options}
autoFocus
multiple
/>,
);

await user.type(screen.getByRole('combobox'), `${options[0].name}{Enter}`);

expect(handleInputChange.lastCall.args[1]).to.equal('');
expect(handleInputChange.lastCall.args[2]).to.equal('removeOption');
});
});

Expand Down
2 changes: 1 addition & 1 deletion packages/mui-joy/src/Autocomplete/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1049,7 +1049,7 @@ Autocomplete.propTypes /* remove-proptypes */ = {
*
* @param {React.SyntheticEvent} event The event source of the callback.
* @param {string} value The new value of the text input.
* @param {string} reason Can be: `"input"` (user input), `"reset"` (programmatic change), `"clear"`.
* @param {string} reason Can be: `"input"` (user input), `"reset"` (programmatic change), `"clear"`, `"blur"`, `"selectOption"`, `"removeOption"`
*/
onInputChange: PropTypes.func,
/**
Expand Down
2 changes: 1 addition & 1 deletion packages/mui-material/src/Autocomplete/Autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -1082,7 +1082,7 @@ Autocomplete.propTypes /* remove-proptypes */ = {
*
* @param {React.SyntheticEvent} event The event source of the callback.
* @param {string} value The new value of the text input.
* @param {string} reason Can be: `"input"` (user input), `"reset"` (programmatic change), `"clear"`.
* @param {string} reason Can be: `"input"` (user input), `"reset"` (programmatic change), `"clear"`, `"blur"`, `"selectOption"`, `"removeOption"`
*/
onInputChange: PropTypes.func,
/**
Expand Down
Loading

0 comments on commit f899014

Please sign in to comment.