diff --git a/docs/pages/api-docs/autocomplete.md b/docs/pages/api-docs/autocomplete.md
index 9b9b34aed76166..9ed93814f8528f 100644
--- a/docs/pages/api-docs/autocomplete.md
+++ b/docs/pages/api-docs/autocomplete.md
@@ -69,6 +69,7 @@ The `MuiAutocomplete` name can be used for providing [default props](/customizat
| noOptionsText | node | 'No options' | Text to display when there are no options.
For localization purposes, you can use the provided [translations](/guides/localization/). |
| onChange | func | | Callback fired when the value changes.
**Signature:**
`function(event: object, value: T, reason: string) => void`
*event:* The event source of the callback.
*value:* The new value of the component.
*reason:* One of "create-option", "select-option", "remove-option", "blur" or "clear". |
| onClose | func | | Callback fired when the popup requests to be closed. Use in controlled mode (see open).
**Signature:**
`function(event: object, reason: string) => void`
*event:* The event source of the callback.
*reason:* Can be: `"toggleInput"`, `"escape"`, `"select-option"`, `"blur"`. |
+| onHighlightChange | func | | Callback fired when the highlight option changes.
**Signature:**
`function(event: object, option: T, reason: string) => void`
*event:* The event source of the callback.
*option:* The highlighted option.
*reason:* Can be: `"keyboard"`, `"auto"`, `"mouse"`. |
| onInputChange | func | | Callback fired when the input value changes.
**Signature:**
`function(event: object, value: string, reason: string) => void`
*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"`. |
| onOpen | func | | Callback fired when the popup requests to be opened. Use in controlled mode (see open).
**Signature:**
`function(event: object) => void`
*event:* The event source of the callback. |
| open | bool | | Control the popup` open state. |
diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
index 3fd03bfc45aace..f6c58d99a54206 100644
--- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
+++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
@@ -279,6 +279,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
noOptionsText = 'No options',
onChange,
onClose,
+ onHighlightChange,
onInputChange,
onOpen,
open,
@@ -729,6 +730,14 @@ Autocomplete.propTypes = {
* @param {string} reason Can be: `"toggleInput"`, `"escape"`, `"select-option"`, `"blur"`.
*/
onClose: PropTypes.func,
+ /**
+ * Callback fired when the highlight option changes.
+ *
+ * @param {object} event The event source of the callback.
+ * @param {T} option The highlighted option.
+ * @param {string} reason Can be: `"keyboard"`, `"auto"`, `"mouse"`.
+ */
+ onHighlightChange: PropTypes.func,
/**
* Callback fired when the input value changes.
*
diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
index 1eade23ae29bba..b222c51665b0d6 100644
--- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
+++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
@@ -1549,4 +1549,66 @@ describe('', () => {
expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.fullWidth);
});
});
+
+ describe('prop: onHighlightChange', () => {
+ it('should trigger event when default value is passed', () => {
+ const handleChange = spy();
+ const options = ['one', 'two', 'three'];
+ render(
+ }
+ />,
+ );
+ expect(handleChange.callCount).to.equal(1);
+ expect(handleChange.args[0][0]).to.equal(undefined);
+ expect(handleChange.args[0][1]).to.equal(options[0]);
+ expect(handleChange.args[0][2]).to.equal('auto');
+ });
+
+ it('should support keyboard event', () => {
+ const handleChange = spy();
+ const options = ['one', 'two', 'three'];
+ render(
+ }
+ />,
+ );
+ fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
+ expect(handleChange.callCount).to.equal(2);
+ expect(handleChange.args[1][0]).to.not.equal(undefined);
+ expect(handleChange.args[1][1]).to.equal(options[0]);
+ expect(handleChange.args[1][2]).to.equal('keyboard');
+ fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
+ expect(handleChange.callCount).to.equal(3);
+ expect(handleChange.args[2][0]).to.not.equal(undefined);
+ expect(handleChange.args[2][1]).to.equal(options[1]);
+ expect(handleChange.args[2][2]).to.equal('keyboard');
+ });
+
+ it('should support mouse event', () => {
+ const handleChange = spy();
+ const options = ['one', 'two', 'three'];
+ const { getAllByRole } = render(
+ }
+ />,
+ );
+ const firstOption = getAllByRole('option')[0];
+ fireEvent.mouseOver(firstOption);
+ expect(handleChange.callCount).to.equal(2);
+ expect(handleChange.args[1][0]).to.not.equal(undefined);
+ expect(handleChange.args[1][1]).to.equal(options[0]);
+ expect(handleChange.args[1][2]).to.equal('mouse');
+ });
+ });
});
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
index 22d3e470954b7c..4d6132cdbf1f1e 100644
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
@@ -170,6 +170,18 @@ export interface UseAutocompleteCommonProps {
* @param {object} event The event source of the callback.
*/
onOpen?: (event: React.ChangeEvent<{}>) => void;
+ /**
+ * Callback fired when the highlight option changes.
+ *
+ * @param {object} event The event source of the callback.
+ * @param {T} option The highlighted option.
+ * @param {string} reason Can be: `"keyboard"`, `"auto"`, `"mouse"`.
+ */
+ onHighlightChange?: (
+ event: React.ChangeEvent<{}>,
+ option: T | null,
+ reason: AutocompleteHighlightChangeReason
+ ) => void;
/**
* Control the popup` open state.
*/
@@ -189,6 +201,8 @@ export interface UseAutocompleteCommonProps {
selectOnFocus?: boolean;
}
+export type AutocompleteHighlightChangeReason = 'keyboard' | 'mouse' | 'auto';
+
export type AutocompleteChangeReason =
| 'create-option'
| 'select-option'
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
index f01076949360ff..f53b23e23b8d4c 100644
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -99,6 +99,7 @@ export default function useAutocomplete(props) {
multiple = false,
onChange,
onClose,
+ onHighlightChange,
onInputChange,
onOpen,
open: openProp,
@@ -120,7 +121,7 @@ export default function useAutocomplete(props) {
const defaultHighlighted = autoHighlight ? 0 : -1;
const highlightedIndexRef = React.useRef(defaultHighlighted);
- const setHighlightedIndex = useEventCallback((index, mouse = false) => {
+ const setHighlightedIndex = useEventCallback((index, reason = 'auto', event) => {
highlightedIndexRef.current = index;
// does the index exist?
if (index === -1) {
@@ -145,6 +146,10 @@ export default function useAutocomplete(props) {
return;
}
+ if (onHighlightChange) {
+ onHighlightChange(event, options[index], reason);
+ }
+
if (index === -1) {
listboxNode.scrollTop = 0;
return;
@@ -163,7 +168,7 @@ export default function useAutocomplete(props) {
//
// Consider this API instead once it has a better browser support:
// .scrollIntoView({ scrollMode: 'if-needed', block: 'nearest' });
- if (listboxNode.scrollHeight > listboxNode.clientHeight && !mouse) {
+ if (listboxNode.scrollHeight > listboxNode.clientHeight && reason !== 'mouse') {
const element = option;
const scrollBottom = listboxNode.clientHeight + listboxNode.scrollTop;
@@ -332,7 +337,7 @@ export default function useAutocomplete(props) {
}
}
- const changeHighlightedIndex = useEventCallback((diff, direction) => {
+ const changeHighlightedIndex = useEventCallback((diff, direction, reason = 'auto', event) => {
if (!popupOpen) {
return;
}
@@ -382,7 +387,7 @@ export default function useAutocomplete(props) {
};
const nextIndex = validOptionIndex(getNextIndex(), direction);
- setHighlightedIndex(nextIndex);
+ setHighlightedIndex(nextIndex, reason, event);
if (autoComplete && diff !== 'reset') {
if (nextIndex === -1) {
@@ -627,38 +632,38 @@ export default function useAutocomplete(props) {
if (popupOpen) {
// Prevent scroll of the page
event.preventDefault();
- changeHighlightedIndex('start', 'next');
+ changeHighlightedIndex('start', 'next', 'keyboard', event);
}
break;
case 'End':
if (popupOpen) {
// Prevent scroll of the page
event.preventDefault();
- changeHighlightedIndex('end', 'previous');
+ changeHighlightedIndex('end', 'previous', 'keyboard', event);
}
break;
case 'PageUp':
// Prevent scroll of the page
event.preventDefault();
- changeHighlightedIndex(-pageSize, 'previous');
+ changeHighlightedIndex(-pageSize, 'previous', 'keyboard', event);
handleOpen(event);
break;
case 'PageDown':
// Prevent scroll of the page
event.preventDefault();
- changeHighlightedIndex(pageSize, 'next');
+ changeHighlightedIndex(pageSize, 'next', 'keyboard', event);
handleOpen(event);
break;
case 'ArrowDown':
// Prevent cursor move
event.preventDefault();
- changeHighlightedIndex(1, 'next');
+ changeHighlightedIndex(1, 'next', 'keyboard', event);
handleOpen(event);
break;
case 'ArrowUp':
// Prevent cursor move
event.preventDefault();
- changeHighlightedIndex(-1, 'previous');
+ changeHighlightedIndex(-1, 'previous', 'keyboard', event);
handleOpen(event);
break;
case 'ArrowLeft':
@@ -791,7 +796,7 @@ export default function useAutocomplete(props) {
const handleOptionMouseOver = (event) => {
const index = Number(event.currentTarget.getAttribute('data-option-index'));
- setHighlightedIndex(index, 'mouse');
+ setHighlightedIndex(index, 'mouse', event);
};
const handleOptionTouchStart = () => {