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 = () => {