From d9d93088bffc86b2dd06a2436c170e765b92462e Mon Sep 17 00:00:00 2001 From: Sujin Lee Date: Wed, 28 Oct 2020 16:43:07 +0100 Subject: [PATCH 1/9] [Autocomplete] Add focused, selected, disabled css classes in Autocomplete-option --- .../src/Autocomplete/Autocomplete.js | 43 ++++++++++++------ .../src/Autocomplete/Autocomplete.test.js | 44 +++++++++---------- .../src/useAutocomplete/useAutocomplete.d.ts | 1 + .../src/useAutocomplete/useAutocomplete.js | 4 ++ 4 files changed, 57 insertions(+), 35 deletions(-) diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.js b/packages/material-ui/src/Autocomplete/Autocomplete.js index 7d68cd8e4b19bf..e0c68bb3109ce2 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.js +++ b/packages/material-ui/src/Autocomplete/Autocomplete.js @@ -2,6 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { withStyles } from '../styles'; +import { alpha } from '../styles/colorManipulator'; import Popper from '../Popper'; import ListSubheader from '../ListSubheader'; import Paper from '../Paper'; @@ -30,8 +31,6 @@ export const styles = (theme) => ({ fullWidth: { width: '100%', }, - /* Pseudo-class applied to the root element if focused. */ - focused: {}, /* Styles applied to the tag elements, e.g. the chips. */ tag: { margin: 3, @@ -205,20 +204,27 @@ export const styles = (theme) => ({ [theme.breakpoints.up('sm')]: { minHeight: 'auto', }, - '&[aria-selected="true"]': { - backgroundColor: theme.palette.action.selected, - }, - '&[data-focus="true"]': { + + '&$focused': { backgroundColor: theme.palette.action.hover, }, - '&:active': { + '&$selected': { backgroundColor: theme.palette.action.selected, + '&$focused': { + backgroundColor: alpha( + theme.palette.action.selected, + theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity, + ), + }, }, - '&[aria-disabled="true"]': { + '&$disabled': { opacity: theme.palette.action.disabledOpacity, pointerEvents: 'none', }, }, + focused: {}, + disabled: {}, + selected: {}, /* Styles applied to the group's label elements. */ groupLabel: { backgroundColor: theme.palette.background.paper, @@ -327,6 +333,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) { setAnchorEl, inputValue, groupedOptions, + focusedIndex, } = useAutocomplete({ ...props, componentName: 'Autocomplete' }); let startAdornment; @@ -381,11 +388,21 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) { const renderListOption = (option, index) => { const optionProps = getOptionProps({ option, index }); - - return renderOption({ ...optionProps, className: classes.option }, option, { - selected: optionProps['aria-selected'], - inputValue, - }); + return renderOption( + { + ...optionProps, + className: clsx(classes.option, { + [classes.focused]: focusedIndex === optionProps.key, + [classes.selected]: optionProps['aria-selected'], + [classes.disabled]: optionProps['aria-disabled'], + }), + }, + option, + { + selected: optionProps['aria-selected'], + inputValue, + }, + ); }; const hasClearIcon = !disableClearable && !disabled && dirty; diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.test.js b/packages/material-ui/src/Autocomplete/Autocomplete.test.js index 92a2ce6ebf9e45..f22ce408a7ea19 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.test.js +++ b/packages/material-ui/src/Autocomplete/Autocomplete.test.js @@ -533,6 +533,7 @@ describe('', () => { it('should trigger a form expectedly', () => { const handleSubmit = spy(); + const { setProps } = render( ', () => { renderInput={(props2) => } />, ); - let textbox = screen.getByRole('textbox'); - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleSubmit.callCount).to.equal(1); + let textbox = screen.getByRole('textbox'); - fireEvent.change(textbox, { target: { value: 'o' } }); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); fireEvent.keyDown(textbox, { key: 'Enter' }); expect(handleSubmit.callCount).to.equal(1); fireEvent.keyDown(textbox, { key: 'Enter' }); expect(handleSubmit.callCount).to.equal(2); - setProps({ key: 'test-2', multiple: true, freeSolo: true }); - textbox = screen.getByRole('textbox'); + expect(() => { + setProps({ key: 'test-2', multiple: true, freeSolo: true }); + textbox = screen.getByRole('textbox'); - fireEvent.change(textbox, { target: { value: 'o' } }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleSubmit.callCount).to.equal(2); - - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleSubmit.callCount).to.equal(3); + fireEvent.change(textbox, { target: { value: 'o' } }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + expect(handleSubmit.callCount).to.equal(2); - setProps({ key: 'test-3', freeSolo: true }); - textbox = screen.getByRole('textbox'); + fireEvent.keyDown(textbox, { key: 'Enter' }); + expect(handleSubmit.callCount).to.equal(3); + }).not.toErrorDev(); - fireEvent.change(textbox, { target: { value: 'o' } }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleSubmit.callCount).to.equal(4); + expect(() => { + setProps({ key: 'test-3', freeSolo: true }); + textbox = screen.getByRole('textbox'); + fireEvent.change(textbox, { target: { value: 'o' } }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + expect(handleSubmit.callCount).to.equal(4); + }).not.toErrorDev(); }); describe('prop: getOptionDisabled', () => { @@ -1252,6 +1252,8 @@ describe('', () => { />, ); }).toWarnDev([ + `Material-UI: The options provided combined with the \`groupBy\` method of Autocomplete returns duplicated headers.`, + 'You can solve the issue by sorting the options with the output of `groupBy`.', // strict mode renders twice 'returns duplicated headers', 'returns duplicated headers', @@ -1402,17 +1404,15 @@ describe('', () => { const textbox = screen.getByRole('textbox'); fireEvent.change(document.activeElement, { target: { value: 'O' } }); - expect(document.activeElement.value).to.equal('O'); fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - + fireEvent.change(document.activeElement, { target: { value: 'one' } }); expect(document.activeElement.value).to.equal('one'); - expect(document.activeElement.selectionStart).to.equal(1); + expect(document.activeElement.selectionStart).to.equal(3); expect(document.activeElement.selectionEnd).to.equal(3); fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(document.activeElement.value).to.equal('one'); expect(document.activeElement.selectionStart).to.equal(3); expect(document.activeElement.selectionEnd).to.equal(3); diff --git a/packages/material-ui/src/useAutocomplete/useAutocomplete.d.ts b/packages/material-ui/src/useAutocomplete/useAutocomplete.d.ts index dc7e5ec8ca89b1..b7899c841a6d6d 100644 --- a/packages/material-ui/src/useAutocomplete/useAutocomplete.d.ts +++ b/packages/material-ui/src/useAutocomplete/useAutocomplete.d.ts @@ -317,5 +317,6 @@ export default function useAutocomplete< anchorEl: null | HTMLElement; setAnchorEl: () => void; focusedTag: number; + focusedIndex: number; groupedOptions: T[]; }; diff --git a/packages/material-ui/src/useAutocomplete/useAutocomplete.js b/packages/material-ui/src/useAutocomplete/useAutocomplete.js index 52d55987294904..e5e75af772f937 100644 --- a/packages/material-ui/src/useAutocomplete/useAutocomplete.js +++ b/packages/material-ui/src/useAutocomplete/useAutocomplete.js @@ -129,6 +129,7 @@ export default function useAutocomplete(props) { const [focusedTag, setFocusedTag] = React.useState(-1); const defaultHighlighted = autoHighlight ? 0 : -1; const highlightedIndexRef = React.useRef(defaultHighlighted); + const [focusedIndex, setFocusedIndex] = React.useState(defaultHighlighted); const [value, setValueState] = useControlled({ controlled: valueProp, @@ -280,6 +281,7 @@ export default function useAutocomplete(props) { const setHighlightedIndex = useEventCallback(({ event, index, reason = 'auto' }) => { highlightedIndexRef.current = index; + setFocusedIndex(index); // does the index exist? if (index === -1) { @@ -297,6 +299,7 @@ export default function useAutocomplete(props) { } const prev = listboxRef.current.querySelector('[data-focus]'); + if (prev) { prev.removeAttribute('data-focus'); } @@ -1023,6 +1026,7 @@ export default function useAutocomplete(props) { anchorEl, setAnchorEl, focusedTag, + focusedIndex, groupedOptions, }; } From 44c460690052130445e2a48646af0c4b0f1759d0 Mon Sep 17 00:00:00 2001 From: Sujin Lee Date: Wed, 28 Oct 2020 17:10:20 +0100 Subject: [PATCH 2/9] [Autocomplete] update proptypes --- packages/material-ui/src/Autocomplete/Autocomplete.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.js b/packages/material-ui/src/Autocomplete/Autocomplete.js index e0c68bb3109ce2..a41f922ea88f6e 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.js +++ b/packages/material-ui/src/Autocomplete/Autocomplete.js @@ -204,7 +204,6 @@ export const styles = (theme) => ({ [theme.breakpoints.up('sm')]: { minHeight: 'auto', }, - '&$focused': { backgroundColor: theme.palette.action.hover, }, From 81aaa6f7ce18ad0b289ee8f0d73c0a20636e9cd9 Mon Sep 17 00:00:00 2001 From: Sujin Lee Date: Thu, 29 Oct 2020 16:32:01 +0100 Subject: [PATCH 3/9] [Autocomplete] use useMemo to render grouped options --- .../src/Autocomplete/Autocomplete.js | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.js b/packages/material-ui/src/Autocomplete/Autocomplete.js index a41f922ea88f6e..ba9139c824b3ad 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.js +++ b/packages/material-ui/src/Autocomplete/Autocomplete.js @@ -404,6 +404,24 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) { ); }; + const renderGroupedOptions = React.useMemo( + () => (groupedOptions) => { + return groupedOptions.map((option, index) => { + if (groupBy) { + return renderGroup({ + key: option.key, + group: option.group, + children: option.options.map((option2, index2) => + renderListOption(option2, option.index + index2), + ), + }); + } + return renderListOption(option, index); + }); + }, + [groupedOptions], + ); + const hasClearIcon = !disableClearable && !disabled && dirty; const hasPopupIcon = (!freeSolo || forcePopupIcon === true) && forcePopupIcon !== false; @@ -496,18 +514,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) { {...getListboxProps()} {...ListboxProps} > - {groupedOptions.map((option, index) => { - if (groupBy) { - return renderGroup({ - key: option.key, - group: option.group, - children: option.options.map((option2, index2) => - renderListOption(option2, option.index + index2), - ), - }); - } - return renderListOption(option, index); - })} + {renderGroupedOptions(groupedOptions)} ) : null} From 645d58d0fe0cf828ad1aa5752d5d8c16a3dd9b64 Mon Sep 17 00:00:00 2001 From: Sujin Lee Date: Thu, 29 Oct 2020 18:18:28 +0100 Subject: [PATCH 4/9] [Autocomplete] add optionFocused, optionSelected, optionDisabled classes --- docs/pages/api-docs/autocomplete.md | 3 ++ .../src/Autocomplete/Autocomplete.d.ts | 6 +++ .../src/Autocomplete/Autocomplete.js | 44 ++++++++++--------- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/docs/pages/api-docs/autocomplete.md b/docs/pages/api-docs/autocomplete.md index fb6ade645950ab..3283962b0b2687 100644 --- a/docs/pages/api-docs/autocomplete.md +++ b/docs/pages/api-docs/autocomplete.md @@ -117,6 +117,9 @@ Any other props supplied will be provided to the root element (native element). | loading | .MuiAutocomplete-loading | Styles applied to the loading wrapper. | noOptions | .MuiAutocomplete-noOptions | Styles applied to the no option wrapper. | option | .MuiAutocomplete-option | Styles applied to the option elements. +| optionFocused | .MuiAutocomplete-optionFocused | Styles applied to the option if keyboard or mouse focused. +| optionSelected | .MuiAutocomplete-optionSelected | Styles applied to the option if option is selected. +| optionDisabled | .MuiAutocomplete-optionDisabled | Styles applied to the option if option is disabled. | groupLabel | .MuiAutocomplete-groupLabel | Styles applied to the group's label elements. | groupUl | .MuiAutocomplete-groupUl | Styles applied to the group's ul elements. diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.d.ts b/packages/material-ui/src/Autocomplete/Autocomplete.d.ts index a69f3eabd2f268..fcd53020e66bfa 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.d.ts +++ b/packages/material-ui/src/Autocomplete/Autocomplete.d.ts @@ -105,6 +105,12 @@ export interface AutocompleteProps< noOptions?: string; /** Styles applied to the option elements. */ option?: string; + /** Styles applied to the option if keyboard or mouse focused. */ + optionFocused?: string; + /** Styles applied to the option if option is selected. */ + optionSelected?: string; + /** Styles applied to the option if option is disabled. */ + optionDisabled?: string; /** Styles applied to the group's label elements. */ groupLabel?: string; /** Styles applied to the group's ul elements. */ diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.js b/packages/material-ui/src/Autocomplete/Autocomplete.js index ba9139c824b3ad..ade97af94db687 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.js +++ b/packages/material-ui/src/Autocomplete/Autocomplete.js @@ -31,6 +31,8 @@ export const styles = (theme) => ({ fullWidth: { width: '100%', }, + /* Pseudo-class applied to the root element if focused. */ + focused: {}, /* Styles applied to the tag elements, e.g. the chips. */ tag: { margin: 3, @@ -204,26 +206,28 @@ export const styles = (theme) => ({ [theme.breakpoints.up('sm')]: { minHeight: 'auto', }, + }, + /* Styles applied to the option if keyboard or mouse focused. */ + optionFocused: { + backgroundColor: theme.palette.action.hover, + }, + + /* Styles applied to the option if option is selected. */ + optionSelected: { + backgroundColor: theme.palette.action.selected, '&$focused': { - backgroundColor: theme.palette.action.hover, - }, - '&$selected': { - backgroundColor: theme.palette.action.selected, - '&$focused': { - backgroundColor: alpha( - theme.palette.action.selected, - theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity, - ), - }, - }, - '&$disabled': { - opacity: theme.palette.action.disabledOpacity, - pointerEvents: 'none', + backgroundColor: alpha( + theme.palette.action.selected, + theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity, + ), }, }, - focused: {}, - disabled: {}, - selected: {}, + + /* Styles applied to the option if option is disabled. */ + optionDisabled: { + opacity: theme.palette.action.disabledOpacity, + pointerEvents: 'none', + }, /* Styles applied to the group's label elements. */ groupLabel: { backgroundColor: theme.palette.background.paper, @@ -391,9 +395,9 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) { { ...optionProps, className: clsx(classes.option, { - [classes.focused]: focusedIndex === optionProps.key, - [classes.selected]: optionProps['aria-selected'], - [classes.disabled]: optionProps['aria-disabled'], + [classes.optionFocused]: focusedIndex === optionProps.key, + [classes.optionSelected]: optionProps['aria-selected'], + [classes.optionDisabled]: optionProps['aria-disabled'], }), }, option, From 3bd5c6c319b188f632105efade5c15e9d2acc934 Mon Sep 17 00:00:00 2001 From: Sujin Lee Date: Thu, 29 Oct 2020 19:39:48 +0100 Subject: [PATCH 5/9] [Autocomplete] add Pseudo-class focused, selected, disabled classes and delete data-focus attribute --- docs/pages/api-docs/autocomplete.md | 4 +++- packages/material-ui/src/Autocomplete/Autocomplete.d.ts | 6 +++++- packages/material-ui/src/Autocomplete/Autocomplete.js | 9 ++++++++- .../material-ui/src/Autocomplete/Autocomplete.test.js | 6 ++++-- .../material-ui/src/useAutocomplete/useAutocomplete.js | 8 -------- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/docs/pages/api-docs/autocomplete.md b/docs/pages/api-docs/autocomplete.md index 3283962b0b2687..0ed362ba1b8e02 100644 --- a/docs/pages/api-docs/autocomplete.md +++ b/docs/pages/api-docs/autocomplete.md @@ -98,7 +98,9 @@ Any other props supplied will be provided to the root element (native element). |:-----|:-------------|:------------| | root | .MuiAutocomplete-root | Styles applied to the root element. | fullWidth | .MuiAutocomplete-fullWidth | Styles applied to the root element if `fullWidth={true}`. -| focused | .Mui-focused | Pseudo-class applied to the root element if focused. +| focused | .Mui-focused | Pseudo-class applied to the root element or option component `focused` class if keyboard or mouse focused. +| disabled | .Mui-disabled | Pseudo-class applied to the option component `disabled` class if option is disabled. +| selected | .Mui-selected | Pseudo-class applied to the option component `selected` class if option is selected. | tag | .MuiAutocomplete-tag | Styles applied to the tag elements, e.g. the chips. | tagSizeSmall | .MuiAutocomplete-tagSizeSmall | Styles applied to the tag elements, e.g. the chips if `size="small"`. | hasPopupIcon | .MuiAutocomplete-hasPopupIcon | Styles applied when the popup icon is rendered. diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.d.ts b/packages/material-ui/src/Autocomplete/Autocomplete.d.ts index fcd53020e66bfa..3affa7e543d4dd 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.d.ts +++ b/packages/material-ui/src/Autocomplete/Autocomplete.d.ts @@ -67,8 +67,12 @@ export interface AutocompleteProps< root?: string; /** Styles applied to the root element if `fullWidth={true}`. */ fullWidth?: string; - /** Pseudo-class applied to the root element if focused. */ + /** Pseudo-class applied to the root element or option component `focused` class if keyboard or mouse focused. */ focused?: string; + /** Pseudo-class applied to the option component `disabled` class if option is disabled. */ + disabled?: string; + /** Pseudo-class applied to the option component `selected` class if option is selected. */ + selected?: string; /** Styles applied to the tag elements, e.g. the chips. */ tag?: string; /** Styles applied to the tag elements, e.g. the chips if `size="small"`. */ diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.js b/packages/material-ui/src/Autocomplete/Autocomplete.js index ade97af94db687..83a53a39515abd 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.js +++ b/packages/material-ui/src/Autocomplete/Autocomplete.js @@ -31,8 +31,12 @@ export const styles = (theme) => ({ fullWidth: { width: '100%', }, - /* Pseudo-class applied to the root element if focused. */ + /* Pseudo-class applied to the root element or option component `focused` class if keyboard or mouse focused. */ focused: {}, + /* Pseudo-class applied to the option component `disabled` class if option is disabled. */ + disabled: {}, + /* Pseudo-class applied to the option component `selected` class if option is selected. */ + selected: {}, /* Styles applied to the tag elements, e.g. the chips. */ tag: { margin: 3, @@ -398,6 +402,9 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) { [classes.optionFocused]: focusedIndex === optionProps.key, [classes.optionSelected]: optionProps['aria-selected'], [classes.optionDisabled]: optionProps['aria-disabled'], + [classes.focused]: focusedIndex === optionProps.key, + [classes.selected]: optionProps['aria-selected'], + [classes.disabled]: optionProps['aria-selected'], }), }, option, diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.test.js b/packages/material-ui/src/Autocomplete/Autocomplete.test.js index f22ce408a7ea19..10a04fcc8093ee 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.test.js +++ b/packages/material-ui/src/Autocomplete/Autocomplete.test.js @@ -17,9 +17,11 @@ import Autocomplete from './Autocomplete'; function checkHighlightIs(listbox, expected) { if (expected) { - expect(listbox.querySelector('li[data-focus]')).to.have.text(expected); + expect(listbox.querySelector('li.MuiAutocomplete-optionFocused.Mui-focused')).to.have.text( + expected, + ); } else { - expect(listbox.querySelector('li[data-focus]')).to.equal(null); + expect(listbox.querySelector('li.MuiAutocomplete-optionFocused.Mui-focused')).to.equal(null); } } diff --git a/packages/material-ui/src/useAutocomplete/useAutocomplete.js b/packages/material-ui/src/useAutocomplete/useAutocomplete.js index e5e75af772f937..3e2439668592b2 100644 --- a/packages/material-ui/src/useAutocomplete/useAutocomplete.js +++ b/packages/material-ui/src/useAutocomplete/useAutocomplete.js @@ -298,12 +298,6 @@ export default function useAutocomplete(props) { return; } - const prev = listboxRef.current.querySelector('[data-focus]'); - - if (prev) { - prev.removeAttribute('data-focus'); - } - const listboxNode = listboxRef.current.parentElement.querySelector('[role="listbox"]'); // "No results" @@ -322,8 +316,6 @@ export default function useAutocomplete(props) { return; } - option.setAttribute('data-focus', 'true'); - // Scroll active descendant into view. // Logic copied from https://www.w3.org/TR/wai-aria-practices/examples/listbox/js/listbox.js // From 46e7e2ea3cc0de140951e854415e07b6471d09d1 Mon Sep 17 00:00:00 2001 From: Sujin Lee Date: Thu, 29 Oct 2020 19:54:19 +0100 Subject: [PATCH 6/9] [Autocomplete] fix prototypes test failing --- packages/material-ui/src/Autocomplete/Autocomplete.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.js b/packages/material-ui/src/Autocomplete/Autocomplete.js index 83a53a39515abd..803f8bb5ac29e8 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.js +++ b/packages/material-ui/src/Autocomplete/Autocomplete.js @@ -215,7 +215,6 @@ export const styles = (theme) => ({ optionFocused: { backgroundColor: theme.palette.action.hover, }, - /* Styles applied to the option if option is selected. */ optionSelected: { backgroundColor: theme.palette.action.selected, @@ -226,7 +225,6 @@ export const styles = (theme) => ({ ), }, }, - /* Styles applied to the option if option is disabled. */ optionDisabled: { opacity: theme.palette.action.disabledOpacity, From 0e5091695c5073b14d4ecf7378983ed38ea9b684 Mon Sep 17 00:00:00 2001 From: Sujin Lee Date: Thu, 29 Oct 2020 20:13:48 +0100 Subject: [PATCH 7/9] [Autocomplete] change renderGoupedOptions --- .../src/Autocomplete/Autocomplete.js | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.js b/packages/material-ui/src/Autocomplete/Autocomplete.js index 803f8bb5ac29e8..08a5453d6c6f17 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.js +++ b/packages/material-ui/src/Autocomplete/Autocomplete.js @@ -413,23 +413,20 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) { ); }; - const renderGroupedOptions = React.useMemo( - () => (groupedOptions) => { - return groupedOptions.map((option, index) => { - if (groupBy) { - return renderGroup({ - key: option.key, - group: option.group, - children: option.options.map((option2, index2) => - renderListOption(option2, option.index + index2), - ), - }); - } - return renderListOption(option, index); - }); - }, - [groupedOptions], - ); + const allGroupedOptions = React.useMemo(() => { + return groupedOptions.map((option, index) => { + if (groupBy) { + return renderGroup({ + key: option.key, + group: option.group, + children: option.options.map((option2, index2) => + renderListOption(option2, option.index + index2), + ), + }); + } + return renderListOption(option, index); + }); + }, [groupedOptions]); const hasClearIcon = !disableClearable && !disabled && dirty; const hasPopupIcon = (!freeSolo || forcePopupIcon === true) && forcePopupIcon !== false; @@ -523,7 +520,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) { {...getListboxProps()} {...ListboxProps} > - {renderGroupedOptions(groupedOptions)} + {allGroupedOptions} ) : null} From 3a9a1034c787a9442ec3a19494f04967a0763081 Mon Sep 17 00:00:00 2001 From: Sujin Lee Date: Thu, 29 Oct 2020 21:11:20 +0100 Subject: [PATCH 8/9] [Autocomplete] use highlightedIndex instead of focusedIndex --- packages/material-ui/src/Autocomplete/Autocomplete.js | 6 +++--- packages/material-ui/src/useAutocomplete/useAutocomplete.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.js b/packages/material-ui/src/Autocomplete/Autocomplete.js index 08a5453d6c6f17..3c1652bfc0fa51 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.js +++ b/packages/material-ui/src/Autocomplete/Autocomplete.js @@ -338,7 +338,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) { setAnchorEl, inputValue, groupedOptions, - focusedIndex, + highlightedIndex, } = useAutocomplete({ ...props, componentName: 'Autocomplete' }); let startAdornment; @@ -397,10 +397,10 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) { { ...optionProps, className: clsx(classes.option, { - [classes.optionFocused]: focusedIndex === optionProps.key, + [classes.optionFocused]: highlightedIndex === optionProps.key, [classes.optionSelected]: optionProps['aria-selected'], [classes.optionDisabled]: optionProps['aria-disabled'], - [classes.focused]: focusedIndex === optionProps.key, + [classes.focused]: highlightedIndex === optionProps.key, [classes.selected]: optionProps['aria-selected'], [classes.disabled]: optionProps['aria-selected'], }), diff --git a/packages/material-ui/src/useAutocomplete/useAutocomplete.js b/packages/material-ui/src/useAutocomplete/useAutocomplete.js index 3e2439668592b2..b34aade0f64720 100644 --- a/packages/material-ui/src/useAutocomplete/useAutocomplete.js +++ b/packages/material-ui/src/useAutocomplete/useAutocomplete.js @@ -1018,7 +1018,7 @@ export default function useAutocomplete(props) { anchorEl, setAnchorEl, focusedTag, - focusedIndex, + highlightedIndex: highlightedIndexRef.current, groupedOptions, }; } From a713a50154089d381bdf17aab7e055f591447ca8 Mon Sep 17 00:00:00 2001 From: Sujin Lee Date: Thu, 29 Oct 2020 22:09:34 +0100 Subject: [PATCH 9/9] [Autocomplete] use useState for highlightedOptionIndex --- .../src/Autocomplete/Autocomplete.js | 8 ++--- .../src/useAutocomplete/useAutocomplete.d.ts | 2 +- .../src/useAutocomplete/useAutocomplete.js | 34 +++++++++++-------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.js b/packages/material-ui/src/Autocomplete/Autocomplete.js index 3c1652bfc0fa51..fa58106e46cc33 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.js +++ b/packages/material-ui/src/Autocomplete/Autocomplete.js @@ -338,7 +338,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) { setAnchorEl, inputValue, groupedOptions, - highlightedIndex, + highlightedOptionIndex, } = useAutocomplete({ ...props, componentName: 'Autocomplete' }); let startAdornment; @@ -397,12 +397,12 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) { { ...optionProps, className: clsx(classes.option, { - [classes.optionFocused]: highlightedIndex === optionProps.key, + [classes.optionFocused]: highlightedOptionIndex === optionProps.key, [classes.optionSelected]: optionProps['aria-selected'], [classes.optionDisabled]: optionProps['aria-disabled'], - [classes.focused]: highlightedIndex === optionProps.key, + [classes.focused]: highlightedOptionIndex === optionProps.key, [classes.selected]: optionProps['aria-selected'], - [classes.disabled]: optionProps['aria-selected'], + [classes.disabled]: optionProps['aria-disabled'], }), }, option, diff --git a/packages/material-ui/src/useAutocomplete/useAutocomplete.d.ts b/packages/material-ui/src/useAutocomplete/useAutocomplete.d.ts index b7899c841a6d6d..c412ccaf9b71b9 100644 --- a/packages/material-ui/src/useAutocomplete/useAutocomplete.d.ts +++ b/packages/material-ui/src/useAutocomplete/useAutocomplete.d.ts @@ -317,6 +317,6 @@ export default function useAutocomplete< anchorEl: null | HTMLElement; setAnchorEl: () => void; focusedTag: number; - focusedIndex: number; + highlightedOptionIndex: number; groupedOptions: T[]; }; diff --git a/packages/material-ui/src/useAutocomplete/useAutocomplete.js b/packages/material-ui/src/useAutocomplete/useAutocomplete.js index b34aade0f64720..e2f8634f0757ad 100644 --- a/packages/material-ui/src/useAutocomplete/useAutocomplete.js +++ b/packages/material-ui/src/useAutocomplete/useAutocomplete.js @@ -128,8 +128,7 @@ export default function useAutocomplete(props) { const [focusedTag, setFocusedTag] = React.useState(-1); const defaultHighlighted = autoHighlight ? 0 : -1; - const highlightedIndexRef = React.useRef(defaultHighlighted); - const [focusedIndex, setFocusedIndex] = React.useState(defaultHighlighted); + const [highlightedOptionIndex, setHighlightedOptionIndex] = React.useState(defaultHighlighted); const [value, setValueState] = useControlled({ controlled: valueProp, @@ -280,8 +279,7 @@ export default function useAutocomplete(props) { } const setHighlightedIndex = useEventCallback(({ event, index, reason = 'auto' }) => { - highlightedIndexRef.current = index; - setFocusedIndex(index); + setHighlightedOptionIndex(index); // does the index exist? if (index === -1) { @@ -298,6 +296,12 @@ export default function useAutocomplete(props) { return; } + const prev = listboxRef.current.querySelector('[data-focus]'); + + if (prev) { + prev.removeAttribute('data-focus'); + } + const listboxNode = listboxRef.current.parentElement.querySelector('[role="listbox"]'); // "No results" @@ -316,6 +320,8 @@ export default function useAutocomplete(props) { return; } + option.setAttribute('data-focus', 'true'); + // Scroll active descendant into view. // Logic copied from https://www.w3.org/TR/wai-aria-practices/examples/listbox/js/listbox.js // @@ -358,14 +364,14 @@ export default function useAutocomplete(props) { return maxIndex; } - const newIndex = highlightedIndexRef.current + diff; + const newIndex = highlightedOptionIndex + diff; if (newIndex < 0) { if (newIndex === -1 && includeInputInList) { return -1; } - if ((disableListWrap && highlightedIndexRef.current !== -1) || Math.abs(diff) > 1) { + if ((disableListWrap && highlightedOptionIndex !== -1) || Math.abs(diff) > 1) { return 0; } @@ -428,7 +434,7 @@ export default function useAutocomplete(props) { // Synchronize the value with the highlighted index if (valueItem != null) { - const currentOption = filteredOptions[highlightedIndexRef.current]; + const currentOption = filteredOptions[highlightedOptionIndex]; // Keep the current highlighted index if possible if ( @@ -451,13 +457,13 @@ export default function useAutocomplete(props) { } // Prevent the highlighted index to leak outside the boundaries. - if (highlightedIndexRef.current >= filteredOptions.length - 1) { + if (highlightedOptionIndex >= filteredOptions.length - 1) { setHighlightedIndex({ index: filteredOptions.length - 1 }); return; } // Restore the focus to the previous index. - setHighlightedIndex({ index: highlightedIndexRef.current }); + setHighlightedIndex({ index: highlightedOptionIndex }); // Ignore filteredOptions (and options, getOptionSelected, getOptionLabel) not to break the scroll position // eslint-disable-next-line react-hooks/exhaustive-deps }, [ @@ -712,8 +718,8 @@ export default function useAutocomplete(props) { handleFocusTag(event, 'next'); break; case 'Enter': - if (highlightedIndexRef.current !== -1 && popupOpen) { - const option = filteredOptions[highlightedIndexRef.current]; + if (highlightedOptionIndex !== -1 && popupOpen) { + const option = filteredOptions[highlightedOptionIndex]; const disabled = getOptionDisabled ? getOptionDisabled(option) : false; // We don't want to validate the form. @@ -800,8 +806,8 @@ export default function useAutocomplete(props) { return; } - if (autoSelect && highlightedIndexRef.current !== -1 && popupOpen) { - selectNewValue(event, filteredOptions[highlightedIndexRef.current], 'blur'); + if (autoSelect && highlightedOptionIndex !== -1 && popupOpen) { + selectNewValue(event, filteredOptions[highlightedOptionIndex], 'blur'); } else if (autoSelect && freeSolo && inputValue !== '') { selectNewValue(event, inputValue, 'blur', 'freeSolo'); } else if (clearOnBlur) { @@ -1018,7 +1024,7 @@ export default function useAutocomplete(props) { anchorEl, setAnchorEl, focusedTag, - highlightedIndex: highlightedIndexRef.current, + highlightedOptionIndex, groupedOptions, }; }