From 37ab2f51f8664657215adc2f54abdbf6a7afc72d Mon Sep 17 00:00:00 2001 From: Joe Heyming Date: Sun, 15 Oct 2023 11:15:17 -0700 Subject: [PATCH] [material-ui][Autocomplete] Unselect options when mouse leaves. What is the problem: - Currently, when your mouse leaves the Autocomplete Popover, the last hovered item is selected. What are we fixing: - We are adding a minor onMouseLeave event to each option. When you leave the option, it will remove any selection by selecting the -1 index. - We also added a prop: keepSelectedOnLeave if people still want this functionality. But we default to not having this functionality. I'm open to preserving backward compatibility, but it seems to make sense from reading the GitHub issues that it would be preferred as not default behavior. Note: It might be better performance wise, to add a toplevel mousemove and mouseleave event instead of a mousemove/mouseleave event per option. That way we wouldn't have to install so many event listeners. --- docs/pages/material-ui/api/autocomplete.json | 1 + .../api-docs/autocomplete/autocomplete.json | 3 +++ .../src/useAutocomplete/useAutocomplete.js | 14 ++++++++++++++ .../src/Autocomplete/Autocomplete.d.ts | 7 +++++++ .../mui-material/src/Autocomplete/Autocomplete.js | 8 ++++++++ .../src/Autocomplete/Autocomplete.test.js | 7 +++++++ 6 files changed, 40 insertions(+) diff --git a/docs/pages/material-ui/api/autocomplete.json b/docs/pages/material-ui/api/autocomplete.json index 452bda5816acb3..d611e9909f131b 100644 --- a/docs/pages/material-ui/api/autocomplete.json +++ b/docs/pages/material-ui/api/autocomplete.json @@ -84,6 +84,7 @@ "describedArgs": ["option", "value"] } }, + "keepSelectedOnLeave": { "type": { "name": "bool" }, "default": "false" }, "limitTags": { "type": { "name": "custom", "description": "integer" }, "default": "-1" }, "ListboxComponent": { "type": { "name": "elementType" }, "default": "'ul'" }, "ListboxProps": { "type": { "name": "object" } }, diff --git a/docs/translations/api-docs/autocomplete/autocomplete.json b/docs/translations/api-docs/autocomplete/autocomplete.json index b34ba880001cfb..a78e7a9af5f2f2 100644 --- a/docs/translations/api-docs/autocomplete/autocomplete.json +++ b/docs/translations/api-docs/autocomplete/autocomplete.json @@ -94,6 +94,9 @@ "description": "Used to determine if the option represents the given value. Uses strict equality by default. ⚠️ Both arguments need to be handled, an option can only match with one value.", "typeDescriptions": { "option": "The option to test.", "value": "The value to test against." } }, + "keepSelectedOnLeave": { + "description": "By default, options are selected on hover and deselected when unhovered. When keepSelectedOnLeave is true, we preserve the last selected option if the mouse leaves the Autocomplete popover." + }, "limitTags": { "description": "The maximum number of tags that will be visible when not focused. Set -1 to disable the limit." }, diff --git a/packages/mui-base/src/useAutocomplete/useAutocomplete.js b/packages/mui-base/src/useAutocomplete/useAutocomplete.js index 241fc98f92d444..c088e0b7c89b4c 100644 --- a/packages/mui-base/src/useAutocomplete/useAutocomplete.js +++ b/packages/mui-base/src/useAutocomplete/useAutocomplete.js @@ -105,6 +105,7 @@ export function useAutocomplete(props) { includeInputInList = false, inputValue: inputValueProp, isOptionEqualToValue = (option, value) => option === value, + keepSelectedOnLeave = false, multiple = false, onChange, onClose, @@ -975,6 +976,18 @@ export function useAutocomplete(props) { } }; + const handleOptionMouseLeave = (event) => { + if (keepSelectedOnLeave) { + return; + } + const index = -1; + setHighlightedIndex({ + event, + index, + reason: 'mouse', + }); + }; + const handleOptionMouseMove = (event) => { const index = Number(event.currentTarget.getAttribute('data-option-index')); if (highlightedIndexRef.current !== index) { @@ -1170,6 +1183,7 @@ export function useAutocomplete(props) { onMouseMove: handleOptionMouseMove, onClick: handleOptionClick, onTouchStart: handleOptionTouchStart, + onMouseLeave: handleOptionMouseLeave, 'data-option-index': index, 'aria-disabled': disabled, 'aria-selected': selected, diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.d.ts b/packages/mui-material/src/Autocomplete/Autocomplete.d.ts index 3df9132360da2d..a2fa38a29087ee 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.d.ts +++ b/packages/mui-material/src/Autocomplete/Autocomplete.d.ts @@ -182,6 +182,13 @@ export interface AutocompleteProps< * @default 'Loading…' */ loadingText?: React.ReactNode; + /** + * By default, options are selected on hover and deselected when unhovered. + * When keepSelectedOnLeave is `true`, we preserve the last selected option + * if the mouse leaves the Autocomplete popover. + * @default false + */ + keepSelectedOnLeave?: boolean; /** * The maximum number of tags that will be visible when not focused. * Set `-1` to disable the limit. diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.js b/packages/mui-material/src/Autocomplete/Autocomplete.js index a2e39002f3302b..0b616553e5a434 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.js +++ b/packages/mui-material/src/Autocomplete/Autocomplete.js @@ -418,6 +418,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { id: idProp, includeInputInList = false, inputValue: inputValueProp, + keepSelectedOnLeave = false, limitTags = -1, ListboxComponent = 'ul', ListboxProps, @@ -936,6 +937,13 @@ Autocomplete.propTypes /* remove-proptypes */ = { * @returns {boolean} */ isOptionEqualToValue: PropTypes.func, + /** + * By default, options are selected on hover and deselected when unhovered. + * When keepSelectedOnLeave is `true`, we preserve the last selected option + * if the mouse leaves the Autocomplete popover. + * @default false + */ + keepSelectedOnLeave: PropTypes.bool, /** * The maximum number of tags that will be visible when not focused. * Set `-1` to disable the limit. diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.test.js b/packages/mui-material/src/Autocomplete/Autocomplete.test.js index c2a42e332e7cf3..4f2a2fe06a11a6 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.test.js +++ b/packages/mui-material/src/Autocomplete/Autocomplete.test.js @@ -2702,6 +2702,13 @@ describe('', () => { expect(handleHighlightChange.lastCall.args[0]).not.to.equal(undefined); expect(handleHighlightChange.lastCall.args[1]).to.equal(options[0]); expect(handleHighlightChange.lastCall.args[2]).to.equal('mouse'); + + // when leaving the option, we want to unhighlight the last selected option. + fireEvent.mouseLeave(firstOption); + + expect(handleHighlightChange.lastCall.args[0]).not.to.equal(undefined); + expect(handleHighlightChange.lastCall.args[1]).to.equal(null); + expect(handleHighlightChange.lastCall.args[2]).to.equal('mouse'); }); it('should pass to onHighlightChange the correct value after filtering', () => {