diff --git a/docs/data/material/migration/migrating-to-v6/migrating-to-v6.md b/docs/data/material/migration/migrating-to-v6/migrating-to-v6.md
index f5cdcb86b76..aa8b17a4cac 100644
--- a/docs/data/material/migration/migrating-to-v6/migrating-to-v6.md
+++ b/docs/data/material/migration/migrating-to-v6/migrating-to-v6.md
@@ -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.
diff --git a/docs/translations/api-docs-joy/autocomplete/autocomplete.json b/docs/translations/api-docs-joy/autocomplete/autocomplete.json
index 1472da2b7fa..f3af4583bd8 100644
--- a/docs/translations/api-docs-joy/autocomplete/autocomplete.json
+++ b/docs/translations/api-docs-joy/autocomplete/autocomplete.json
@@ -148,7 +148,7 @@
"typeDescriptions": {
"event": "The event source of the callback.",
"value": "The new value of the text input.",
- "reason": "Can be: "input"
(user input), "reset"
(programmatic change), "clear"
."
+ "reason": "Can be: "input"
(user input), "reset"
(programmatic change), "clear"
, "blur"
, "selectOption"
, "removeOption"
"
}
},
"onOpen": {
diff --git a/docs/translations/api-docs/autocomplete/autocomplete.json b/docs/translations/api-docs/autocomplete/autocomplete.json
index 9eaf631c685..6ec4dc19a1d 100644
--- a/docs/translations/api-docs/autocomplete/autocomplete.json
+++ b/docs/translations/api-docs/autocomplete/autocomplete.json
@@ -143,7 +143,7 @@
"typeDescriptions": {
"event": "The event source of the callback.",
"value": "The new value of the text input.",
- "reason": "Can be: "input"
(user input), "reset"
(programmatic change), "clear"
."
+ "reason": "Can be: "input"
(user input), "reset"
(programmatic change), "clear"
, "blur"
, "selectOption"
, "removeOption"
"
}
},
"onOpen": {
diff --git a/packages/mui-base/src/useAutocomplete/useAutocomplete.d.ts b/packages/mui-base/src/useAutocomplete/useAutocomplete.d.ts
index b9286b06d70..8c4e7eec43b 100644
--- a/packages/mui-base/src/useAutocomplete/useAutocomplete.d.ts
+++ b/packages/mui-base/src/useAutocomplete/useAutocomplete.d.ts
@@ -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,
@@ -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;
diff --git a/packages/mui-base/src/useAutocomplete/useAutocomplete.js b/packages/mui-base/src/useAutocomplete/useAutocomplete.js
index 45caa7dd212..2280e75b87e 100644
--- a/packages/mui-base/src/useAutocomplete/useAutocomplete.js
+++ b/packages/mui-base/src/useAutocomplete/useAutocomplete.js
@@ -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;
@@ -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],
@@ -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;
@@ -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))) {
@@ -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');
diff --git a/packages/mui-joy/src/Autocomplete/Autocomplete.test.tsx b/packages/mui-joy/src/Autocomplete/Autocomplete.test.tsx
index 045c7fac2b9..0faa8ba03dc 100644
--- a/packages/mui-joy/src/Autocomplete/Autocomplete.test.tsx
+++ b/packages/mui-joy/src/Autocomplete/Autocomplete.test.tsx
@@ -1889,10 +1889,10 @@ describe('Joy ', () => {
});
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(
', () => {
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 (
+
+ option.name}
+ value={value}
+ />
+
+
+ );
+ }
+ const { user } = render();
+
+ 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(
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(
+ 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(
+ 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(
+ 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');
});
});
diff --git a/packages/mui-joy/src/Autocomplete/Autocomplete.tsx b/packages/mui-joy/src/Autocomplete/Autocomplete.tsx
index 96bbf5621d6..d78662a90f3 100644
--- a/packages/mui-joy/src/Autocomplete/Autocomplete.tsx
+++ b/packages/mui-joy/src/Autocomplete/Autocomplete.tsx
@@ -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,
/**
diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.js b/packages/mui-material/src/Autocomplete/Autocomplete.js
index d02f3daaa10..5f3f0014350 100644
--- a/packages/mui-material/src/Autocomplete/Autocomplete.js
+++ b/packages/mui-material/src/Autocomplete/Autocomplete.js
@@ -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,
/**
diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.test.js b/packages/mui-material/src/Autocomplete/Autocomplete.test.js
index 44a037bd962..7fda0169707 100644
--- a/packages/mui-material/src/Autocomplete/Autocomplete.test.js
+++ b/packages/mui-material/src/Autocomplete/Autocomplete.test.js
@@ -2525,10 +2525,10 @@ describe('', () => {
});
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(
', () => {
renderInput={(params) => }
/>,
);
- fireEvent.change(document.activeElement, { target: { value: 'a' } });
+
+ await user.type(document.activeElement, '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 (
+
+ option.name}
+ renderInput={(params) => }
+ value={value}
+ />
+
+
+ );
+ }
+ const { user } = render();
+
+ await user.click(screen.getByText('Reset'));
+
+ expect(handleInputChange.lastCall.args[1]).to.equal(options[1].name);
+ expect(handleInputChange.lastCall.args[2]).to.equal('reset');
+ });
+
+ it('provides a reason on clear', async () => {
const handleInputChange = spy();
const options = [{ name: 'foo' }];
- render(
+ const { user } = render(
option.name}
renderInput={(params) => }
+ defaultValue={options[0]}
/>,
);
- 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(
+ option.name}
+ renderInput={(params) => }
+ 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(
+ option.name}
+ renderInput={(params) => }
+ />,
+ );
+
+ 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(
+ option.name}
+ renderInput={(params) => }
+ defaultValue={options}
+ 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');
});
});