diff --git a/docs/pages/api-docs/autocomplete.md b/docs/pages/api-docs/autocomplete.md
index ae2b680e60784e..fb6ade645950ab 100644
--- a/docs/pages/api-docs/autocomplete.md
+++ b/docs/pages/api-docs/autocomplete.md
@@ -80,7 +80,6 @@ The `MuiAutocomplete` name can be used for providing [default props](/customizat
| PaperComponent | elementType | Paper | The component used to render the body of the popup. |
| PopperComponent | elementType | Popper | The component used to position the popup. |
| popupIcon | node | <ArrowDropDownIcon /> | The icon to display in place of the default popup icon. |
-| readOnly | bool | false | If `true`, the input is and search functionality disabled, but s still consistently styled. |
| renderGroup | func | | Render the group.
**Signature:**
`function(option: any) => ReactNode`
*option:* The group to render. |
| renderInput* | func | | Render the input.
**Signature:**
`function(params: object) => ReactNode`
|
| renderOption | func | | Render the option, use `getOptionLabel` by default.
**Signature:**
`function(props: object, option: T, state: object) => ReactNode`
*props:* The props to apply on the li element.
*option:* The option to render.
*state:* The state of the component. |
@@ -99,9 +98,7 @@ 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 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.
+| focused | .Mui-focused | Pseudo-class applied to the root element if focused.
| 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.
@@ -109,7 +106,6 @@ Any other props supplied will be provided to the root element (native element).
| inputRoot | .MuiAutocomplete-inputRoot | Styles applied to the Input element.
| input | .MuiAutocomplete-input | Styles applied to the input element.
| inputFocused | .MuiAutocomplete-inputFocused | Styles applied to the input element if tag focused.
-| inputDisabled | .MuiAutocomplete-inputDisabled | Styles applied to the input element readOnly={true}
| endAdornment | .MuiAutocomplete-endAdornment | Styles applied to the endAdornment element.
| clearIndicator | .MuiAutocomplete-clearIndicator | Styles applied to the clear indicator.
| popupIndicator | .MuiAutocomplete-popupIndicator | Styles applied to the popup indicator.
diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.d.ts b/packages/material-ui/src/Autocomplete/Autocomplete.d.ts
index b29d33bb6b19a5..a69f3eabd2f268 100644
--- a/packages/material-ui/src/Autocomplete/Autocomplete.d.ts
+++ b/packages/material-ui/src/Autocomplete/Autocomplete.d.ts
@@ -67,12 +67,8 @@ export interface AutocompleteProps<
root?: string;
/** Styles applied to the root element if `fullWidth={true}`. */
fullWidth?: string;
- /** Pseudo-class applied to the root element or option component `focused` class if keyboard or mouse focused. */
+ /** Pseudo-class applied to the root element if 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"`. */
@@ -87,8 +83,6 @@ export interface AutocompleteProps<
input?: string;
/** Styles applied to the input element if tag focused. */
inputFocused?: string;
- /** Styles applied to the input element readOnly={true} */
- inputDisabled?: string;
/** Styles applied to the endAdornment element. */
endAdornment?: string;
/** Styles applied to the clear indicator. */
@@ -145,11 +139,6 @@ export interface AutocompleteProps<
* @default false
*/
disablePortal?: boolean;
- /**
- * If `true`, the input is and search functionality disabled, but s still consistently styled.
- * @default false
- */
- readOnly?: boolean;
/**
* Force the visibility display of the popup icon.
* @default 'auto'
diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.js b/packages/material-ui/src/Autocomplete/Autocomplete.js
index 96cbd20b6dcdab..de3ec2bf0b1456 100644
--- a/packages/material-ui/src/Autocomplete/Autocomplete.js
+++ b/packages/material-ui/src/Autocomplete/Autocomplete.js
@@ -31,12 +31,8 @@ export const styles = (theme) => ({
fullWidth: {
width: '100%',
},
- /* Pseudo-class applied to the root element or option component `focused` class if keyboard or mouse focused. */
+ /* Pseudo-class applied to the root element if 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,
@@ -127,9 +123,6 @@ export const styles = (theme) => ({
padding: '2.5px 4px',
},
},
- '&$inputDisabled': {
- cursor: 'pointer',
- },
},
/* Styles applied to the input element. */
input: {
@@ -141,12 +134,6 @@ export const styles = (theme) => ({
inputFocused: {
opacity: 1,
},
- /* Styles applied to the input element readOnly={true} */
- inputDisabled: {
- cursor: 'pointer !important',
- color: 'transparent', // Hide the blinking cursor
- textShadow: `0 0 0 ${theme.palette.text.primary}`,
- },
/* Styles applied to the endAdornment element. */
endAdornment: {
// We use a position absolute to support wrapping tags.
@@ -219,21 +206,38 @@ export const styles = (theme) => ({
[theme.breakpoints.up('sm')]: {
minHeight: 'auto',
},
- '&$focused': {
+ '&[data-focus="true"]': {
backgroundColor: theme.palette.action.hover,
+ // Reset on touch devices, it doesn't add specificity
+ '@media (hover: none)': {
+ backgroundColor: 'transparent',
+ },
+ },
+ '&[aria-disabled="true"]': {
+ opacity: theme.palette.action.disabledOpacity,
+ pointerEvents: 'none',
},
- '&$selected': {
- backgroundColor: theme.palette.action.selected,
- '&$focused': {
+ '&.Mui-focusVisible': {
+ backgroundColor: theme.palette.action.focus,
+ },
+ '&[aria-selected="true"]': {
+ backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity),
+ '&[data-focus="true"]': {
backgroundColor: alpha(
- theme.palette.action.selected,
+ theme.palette.primary.main,
theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity,
),
+ // Reset on touch devices, it doesn't add specificity
+ '@media (hover: none)': {
+ backgroundColor: theme.palette.action.selected,
+ },
+ },
+ '&.Mui-focusVisible': {
+ backgroundColor: alpha(
+ theme.palette.primary.main,
+ theme.palette.action.selectedOpacity + theme.palette.action.focusOpacity,
+ ),
},
- },
- '&$disabled': {
- opacity: theme.palette.action.disabledOpacity,
- pointerEvents: 'none',
},
},
/* Styles applied to the group's label elements. */
@@ -274,7 +278,6 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
debug = false,
defaultValue = props.multiple ? [] : null,
disableClearable = false,
- readOnly = false,
disableCloseOnSelect = false,
disabled = false,
disabledItemsFocusable = false,
@@ -345,7 +348,6 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
setAnchorEl,
inputValue,
groupedOptions,
- highlightedOptionIndex,
} = useAutocomplete({ ...props, componentName: 'Autocomplete' });
let startAdornment;
@@ -396,45 +398,16 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
const renderGroup = renderGroupProp || defaultRenderGroup;
const defaultRenderOption = (props2, option) =>
{getOptionLabel(option)};
-
const renderOption = renderOptionProp || defaultRenderOption;
- const renderListOption = React.useCallback(
- (option, index) => {
- const optionProps = getOptionProps({ option, index });
- return renderOption(
- {
- ...optionProps,
- className: clsx(classes.option, {
- [classes.focused]: highlightedOptionIndex === optionProps.index,
- [classes.selected]: optionProps['aria-selected'],
- [classes.disabled]: optionProps['aria-disabled'],
- }),
- },
- option,
- {
- selected: optionProps['aria-selected'],
- inputValue,
- },
- );
- },
- [getOptionProps, highlightedOptionIndex, inputValue, classes, renderOption],
- );
+ const renderListOption = (option, index) => {
+ const optionProps = getOptionProps({ option, index });
- 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);
+ return renderOption({ ...optionProps, className: classes.option }, option, {
+ selected: optionProps['aria-selected'],
+ inputValue,
});
- }, [groupedOptions, groupBy, renderGroup, renderListOption]);
+ };
const hasClearIcon = !disableClearable && !disabled && dirty;
const hasPopupIcon = (!freeSolo || forcePopupIcon === true) && forcePopupIcon !== false;
@@ -463,9 +436,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
InputLabelProps: getInputLabelProps(),
InputProps: {
ref: setAnchorEl,
- className: clsx(classes.inputRoot, {
- [classes.inputDisabled]: readOnly,
- }),
+ className: classes.inputRoot,
startAdornment,
endAdornment: (
@@ -499,7 +470,6 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
inputProps: {
className: clsx(classes.input, {
[classes.inputFocused]: focusedTag === -1,
- [classes.inputDisabled]: readOnly,
}),
disabled,
...getInputProps(),
@@ -531,7 +501,18 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
{...getListboxProps()}
{...ListboxProps}
>
- {allGroupedOptions}
+ {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);
+ })}
) : null}
@@ -863,11 +844,6 @@ Autocomplete.propTypes = {
* @default
*/
popupIcon: PropTypes.node,
- /**
- * If `true`, the input is and search functionality disabled, but s still consistently styled.
- * @default false
- */
- readOnly: PropTypes.bool,
/**
* Render the group.
*
diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.test.js b/packages/material-ui/src/Autocomplete/Autocomplete.test.js
index 74f307679ea8ba..92a2ce6ebf9e45 100644
--- a/packages/material-ui/src/Autocomplete/Autocomplete.test.js
+++ b/packages/material-ui/src/Autocomplete/Autocomplete.test.js
@@ -17,9 +17,9 @@ import Autocomplete from './Autocomplete';
function checkHighlightIs(listbox, expected) {
if (expected) {
- expect(listbox.querySelector('li.Mui-focused')).to.have.text(expected);
+ expect(listbox.querySelector('li[data-focus]')).to.have.text(expected);
} else {
- expect(listbox.querySelector('li.Mui-focused')).to.equal(null);
+ expect(listbox.querySelector('li[data-focus]')).to.equal(null);
}
}
@@ -328,12 +328,12 @@ describe('
', () => {
it('should add new value when autoSelect & multiple on blur', () => {
const handleChange = spy();
- const options = ['one', 'two', 'three'];
+ const options = ['one', 'two'];
render(
', () => {
/>,
);
const textbox = screen.getByRole('textbox');
- fireEvent.change(textbox, { target: { value: 't' } });
- fireEvent.keyDown(textbox, { key: 'ArrowDown' });
act(() => {
+ fireEvent.change(textbox, { target: { value: 't' } });
+ fireEvent.keyDown(textbox, { key: 'ArrowDown' });
textbox.blur();
});
expect(handleChange.callCount).to.equal(1);
- expect(handleChange.args[0][1]).to.deep.equal(['one', 'two']);
+ expect(handleChange.args[0][1]).to.deep.equal(options);
});
it('should add new value when autoSelect & multiple & freeSolo on blur', () => {
@@ -533,7 +533,6 @@ 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);
+ 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);
- expect(() => {
- setProps({ key: 'test-2', multiple: true, freeSolo: true });
- textbox = screen.getByRole('textbox');
+ 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.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);
- }).not.toErrorDev();
+ fireEvent.keyDown(textbox, { key: 'Enter' });
+ expect(handleSubmit.callCount).to.equal(3);
- 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();
+ 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);
});
describe('prop: getOptionDisabled', () => {
@@ -1151,26 +1151,6 @@ describe('
', () => {
});
});
- describe('prop: readOnly', () => {
- it('should render inputDisabled class', () => {
- const handleChange = spy();
- const options = ['one', 'two', 'three'];
- render(
-
}
- />,
- );
- const textbox = screen.getByRole('textbox');
- fireEvent.click(textbox);
- expect(handleChange.callCount).to.equal(0);
- expect(textbox).toHaveFocus();
- expect(textbox).to.have.class(classes.inputDisabled);
- });
- });
-
describe('warnings', () => {
it('warn if getOptionLabel do not return a string', () => {
const handleChange = spy();
@@ -1272,8 +1252,6 @@ 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',
@@ -1424,15 +1402,17 @@ 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(3);
+ expect(document.activeElement.selectionStart).to.equal(1);
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/PaginationItem/PaginationItem.js b/packages/material-ui/src/PaginationItem/PaginationItem.js
index d7a5f593d61d9e..182fbe620e8de7 100644
--- a/packages/material-ui/src/PaginationItem/PaginationItem.js
+++ b/packages/material-ui/src/PaginationItem/PaginationItem.js
@@ -1,7 +1,8 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
-import { alpha, useTheme, withStyles, useThemeVariants } from '../styles';
+import { useTheme, withStyles, useThemeVariants } from '../styles';
+import { alpha } from '../styles/colorManipulator';
import ButtonBase from '../ButtonBase';
import { capitalize } from '../utils';
import FirstPageIcon from '../internal/svg-icons/FirstPage';
@@ -34,6 +35,9 @@ export const styles = (theme) => ({
backgroundColor: 'transparent',
},
},
+ '&$disabled': {
+ opacity: theme.palette.action.disabledOpacity,
+ },
'&$focusVisible': {
backgroundColor: theme.palette.action.focus,
},
@@ -61,9 +65,6 @@ export const styles = (theme) => ({
backgroundColor: theme.palette.action.selected,
},
},
- '&$disabled': {
- opacity: theme.palette.action.disabledOpacity,
- },
},
/* Styles applied applied to the root element if `size="small"`. */
sizeSmall: {
diff --git a/packages/material-ui/src/useAutocomplete/useAutocomplete.js b/packages/material-ui/src/useAutocomplete/useAutocomplete.js
index a588251a705c02..7e7598f853c67f 100644
--- a/packages/material-ui/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui/src/useAutocomplete/useAutocomplete.js
@@ -74,7 +74,6 @@ export default function useAutocomplete(props) {
defaultValue = props.multiple ? [] : null,
disableClearable = false,
disableCloseOnSelect = false,
- readOnly = false,
disabledItemsFocusable = false,
disableListWrap = false,
filterOptions = defaultFilterOptions,
@@ -129,7 +128,7 @@ export default function useAutocomplete(props) {
const [focusedTag, setFocusedTag] = React.useState(-1);
const defaultHighlighted = autoHighlight ? 0 : -1;
- const [highlightedOptionIndex, setHighlightedOptionIndex] = React.useState(defaultHighlighted);
+ const highlightedIndexRef = React.useRef(defaultHighlighted);
const [value, setValueState] = useControlled({
controlled: valueProp,
@@ -280,7 +279,7 @@ export default function useAutocomplete(props) {
}
const setHighlightedIndex = useEventCallback(({ event, index, reason = 'auto' }) => {
- setHighlightedOptionIndex(index);
+ highlightedIndexRef.current = index;
// does the index exist?
if (index === -1) {
@@ -298,9 +297,9 @@ export default function useAutocomplete(props) {
}
const prev = listboxRef.current.querySelector('[data-focus]');
-
if (prev) {
prev.removeAttribute('data-focus');
+ prev.classList.remove('Mui-focusVisible');
}
const listboxNode = listboxRef.current.parentElement.querySelector('[role="listbox"]');
@@ -322,6 +321,9 @@ export default function useAutocomplete(props) {
}
option.setAttribute('data-focus', 'true');
+ if (reason === 'keyboard') {
+ option.classList.add('Mui-focusVisible');
+ }
// Scroll active descendant into view.
// Logic copied from https://www.w3.org/TR/wai-aria-practices/examples/listbox/js/listbox.js
@@ -365,14 +367,14 @@ export default function useAutocomplete(props) {
return maxIndex;
}
- const newIndex = highlightedOptionIndex + diff;
+ const newIndex = highlightedIndexRef.current + diff;
if (newIndex < 0) {
if (newIndex === -1 && includeInputInList) {
return -1;
}
- if ((disableListWrap && highlightedOptionIndex !== -1) || Math.abs(diff) > 1) {
+ if ((disableListWrap && highlightedIndexRef.current !== -1) || Math.abs(diff) > 1) {
return 0;
}
@@ -435,7 +437,7 @@ export default function useAutocomplete(props) {
// Synchronize the value with the highlighted index
if (valueItem != null) {
- const currentOption = filteredOptions[highlightedOptionIndex];
+ const currentOption = filteredOptions[highlightedIndexRef.current];
// Keep the current highlighted index if possible
if (
@@ -458,13 +460,13 @@ export default function useAutocomplete(props) {
}
// Prevent the highlighted index to leak outside the boundaries.
- if (highlightedOptionIndex >= filteredOptions.length - 1) {
+ if (highlightedIndexRef.current >= filteredOptions.length - 1) {
setHighlightedIndex({ index: filteredOptions.length - 1 });
return;
}
// Restore the focus to the previous index.
- setHighlightedIndex({ index: highlightedOptionIndex });
+ setHighlightedIndex({ index: highlightedIndexRef.current });
// Ignore filteredOptions (and options, getOptionSelected, getOptionLabel) not to break the scroll position
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
@@ -719,8 +721,8 @@ export default function useAutocomplete(props) {
handleFocusTag(event, 'next');
break;
case 'Enter':
- if (highlightedOptionIndex !== -1 && popupOpen) {
- const option = filteredOptions[highlightedOptionIndex];
+ if (highlightedIndexRef.current !== -1 && popupOpen) {
+ const option = filteredOptions[highlightedIndexRef.current];
const disabled = getOptionDisabled ? getOptionDisabled(option) : false;
// We don't want to validate the form.
@@ -807,8 +809,8 @@ export default function useAutocomplete(props) {
return;
}
- if (autoSelect && highlightedOptionIndex !== -1 && popupOpen) {
- selectNewValue(event, filteredOptions[highlightedOptionIndex], 'blur');
+ if (autoSelect && highlightedIndexRef.current !== -1 && popupOpen) {
+ selectNewValue(event, filteredOptions[highlightedIndexRef.current], 'blur');
} else if (autoSelect && freeSolo && inputValue !== '') {
selectNewValue(event, inputValue, 'blur', 'freeSolo');
} else if (clearOnBlur) {
@@ -820,7 +822,6 @@ export default function useAutocomplete(props) {
const handleInputChange = (event) => {
const newValue = event.target.value;
- if (readOnly) return;
if (inputValue !== newValue) {
setInputValueState(newValue);
@@ -1005,8 +1006,7 @@ export default function useAutocomplete(props) {
const disabled = getOptionDisabled ? getOptionDisabled(option) : false;
return {
- index,
- key: `${id}-option-${index}`,
+ key: index,
tabIndex: -1,
role: 'option',
id: `${id}-option-${index}`,
@@ -1027,7 +1027,6 @@ export default function useAutocomplete(props) {
anchorEl,
setAnchorEl,
focusedTag,
- highlightedOptionIndex,
groupedOptions,
};
}