diff --git a/docs/pages/api-docs/autocomplete.md b/docs/pages/api-docs/autocomplete.md index dea6da27cf862a..3251614759305b 100644 --- a/docs/pages/api-docs/autocomplete.md +++ b/docs/pages/api-docs/autocomplete.md @@ -39,9 +39,9 @@ You can learn more about the difference by [reading this guide](/guides/minimizi | disableClearable | bool | false | If `true`, the input can't be cleared. | | disableCloseOnSelect | bool | false | If `true`, the popup won't close when a value is selected. | | disabled | bool | false | If `true`, the input will be disabled. | +| disabledItemsFocusable | bool | false | If `true`, will allow focus on disabled items. | | disableListWrap | bool | false | If `true`, the list box in the popup will not wrap focus. | | disablePortal | bool | false | Disable the portal behavior. The children stay within it's parent DOM hierarchy. | -| enableFocusForDisabledItems | bool | false | If `true`, will allow focus on disabled items. | | filterOptions | func | | A filter function that determines the options that are eligible.

**Signature:**
`function(options: T[], state: object) => undefined`
*options:* The options to render.
*state:* The state of the component. | | filterSelectedOptions | bool | false | If `true`, hide the selected options from the list box. | | forcePopupIcon | 'auto'
| bool
| 'auto' | Force the visibility display of the popup icon. | diff --git a/docs/pages/api-docs/menu-list.md b/docs/pages/api-docs/menu-list.md index cfdf7e400b44f2..bd1a752faba64b 100644 --- a/docs/pages/api-docs/menu-list.md +++ b/docs/pages/api-docs/menu-list.md @@ -30,8 +30,8 @@ the focus is placed inside the component it is fully keyboard accessible. | autoFocus | bool | false | If `true`, will focus the `[role="menu"]` container and move into tab order. | | autoFocusItem | bool | false | If `true`, will focus the first menuitem if `variant="menu"` or selected item if `variant="selectedMenu"`. | | children | node | | MenuList contents, normally `MenuItem`s. | +| disabledItemsFocusable | bool | false | If `true`, will allow focus on disabled items. | | disableListWrap | bool | false | If `true`, the menu items will not wrap focus. | -| enableFocusForDisabledItems | bool | false | If `true`, will allow focus on disabled items. | | variant | 'menu'
| 'selectedMenu'
| 'selectedMenu' | The variant to use. Use `menu` to prevent selected items from impacting the initial focus and the vertical alignment relative to the anchor element. | The `ref` is forwarded to the root element. diff --git a/docs/src/pages/components/autocomplete/LimitTags.js b/docs/src/pages/components/autocomplete/LimitTags.js index 8002327845d082..7f2bd393679983 100644 --- a/docs/src/pages/components/autocomplete/LimitTags.js +++ b/docs/src/pages/components/autocomplete/LimitTags.js @@ -21,7 +21,7 @@ export default function LimitTags() { option.title} defaultValue={[top100Films[13], top100Films[12], top100Films[11]]} diff --git a/docs/src/pages/components/autocomplete/LimitTags.tsx b/docs/src/pages/components/autocomplete/LimitTags.tsx index 8c7c8ea8a5af8a..e4dfbb476a8ce6 100644 --- a/docs/src/pages/components/autocomplete/LimitTags.tsx +++ b/docs/src/pages/components/autocomplete/LimitTags.tsx @@ -23,7 +23,7 @@ export default function LimitTags() { option.title} defaultValue={[top100Films[13], top100Films[12], top100Films[11]]} diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js index 50d9161ee45ec5..c7b1a2c1ae1552 100644 --- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js +++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js @@ -251,9 +251,9 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) { disableClearable = false, disableCloseOnSelect = false, disabled = false, + disabledItemsFocusable = false, disableListWrap = false, disablePortal = false, - enableFocusForDisabledItems = false, filterOptions, filterSelectedOptions = false, forcePopupIcon = 'auto', @@ -578,6 +578,10 @@ Autocomplete.propTypes = { * If `true`, the input will be disabled. */ disabled: PropTypes.bool, + /** + * If `true`, will allow focus on disabled items. + */ + disabledItemsFocusable: PropTypes.bool, /** * If `true`, the list box in the popup will not wrap focus. */ @@ -587,10 +591,6 @@ Autocomplete.propTypes = { * The children stay within it's parent DOM hierarchy. */ disablePortal: PropTypes.bool, - /** - * If `true`, will allow focus on disabled items. - */ - enableFocusForDisabledItems: PropTypes.bool, /** * A filter function that determines the options that are eligible. * diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts index 9a6ec38e55ec77..821e062b11c9c0 100644 --- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts +++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts @@ -67,13 +67,13 @@ export interface UseAutocompleteCommonProps { */ disableCloseOnSelect?: boolean; /** - * If `true`, the list box in the popup will not wrap focus. + * If `true`, will allow focus on disabled items. */ - disableListWrap?: boolean; + disabledItemsFocusable?: boolean; /** - * If `true`, will allow focus on disabled items. + * If `true`, the list box in the popup will not wrap focus. */ - enableFocusForDisabledItems?: boolean; + disableListWrap?: boolean; /** * A filter function that determines the options that are eligible. * @@ -184,6 +184,10 @@ export type AutocompleteCloseReason = 'toggleInput' | 'escape' | 'select-option' export type AutocompleteInputChangeReason = 'input' | 'reset' | 'clear'; export interface UseAutocompleteMultipleProps extends UseAutocompleteCommonProps { + /** + * The default input value. Use when the component is not controlled. + */ + defaultValue?: T[]; /** * If `true`, `value` must be an array and the menu will support multiple selections. */ @@ -195,10 +199,6 @@ export interface UseAutocompleteMultipleProps extends UseAutocompleteCommonPr * You can customize the equality behavior with the `getOptionSelected` prop. */ value?: T[]; - /** - * The default input value. Use when the component is not controlled. - */ - defaultValue?: T[]; /** * Callback fired when the value changes. * @@ -215,6 +215,10 @@ export interface UseAutocompleteMultipleProps extends UseAutocompleteCommonPr } export interface UseAutocompleteSingleProps extends UseAutocompleteCommonProps { + /** + * The default input value. Use when the component is not controlled. + */ + defaultValue?: T; /** * If `true`, `value` must be an array and the menu will support multiple selections. */ @@ -226,10 +230,6 @@ export interface UseAutocompleteSingleProps extends UseAutocompleteCommonProp * You can customize the equality behavior with the `getOptionSelected` prop. */ value?: T | null; - /** - * The default input value. Use when the component is not controlled. - */ - defaultValue?: T; /** * Callback fired when the value changes. * diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js index f50cecc5563c7f..2c98da8abca486 100644 --- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js +++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js @@ -73,8 +73,8 @@ export default function useAutocomplete(props) { defaultValue = props.multiple ? [] : null, disableClearable = false, disableCloseOnSelect = false, + disabledItemsFocusable = false, disableListWrap = false, - enableFocusForDisabledItems = false, filterOptions = defaultFilterOptions, filterSelectedOptions = false, freeSolo = false, @@ -313,11 +313,12 @@ export default function useAutocomplete(props) { const option = listboxRef.current.querySelector(`[data-option-index="${nextFocus}"]`); // Same logic as MenuList.js - const nextFocusDisabled = enableFocusForDisabledItems + const nextFocusDisabled = disabledItemsFocusable ? false : option && (option.disabled || option.getAttribute('aria-disabled') === 'true'); if ((option && !option.hasAttribute('tabindex')) || nextFocusDisabled) { + // Move to the next element. nextFocus += direction === 'next' ? 1 : -1; } else { return nextFocus; @@ -641,9 +642,16 @@ export default function useAutocomplete(props) { break; } if (highlightedIndexRef.current !== -1 && popupOpen) { + const option = filteredOptions[highlightedIndexRef.current]; + const disabled = getOptionDisabled ? getOptionDisabled(option) : false; + + if (disabled) { + return; + } + // We don't want to validate the form. event.preventDefault(); - selectNewValue(event, filteredOptions[highlightedIndexRef.current], 'select-option'); + selectNewValue(event, option, 'select-option'); // Move the selection to the end. if (autoComplete) { @@ -1012,13 +1020,13 @@ useAutocomplete.propTypes = { */ disableCloseOnSelect: PropTypes.bool, /** - * If `true`, the list box in the popup will not wrap focus. + * If `true`, will allow focus on disabled items. */ - disableListWrap: PropTypes.bool, + disabledItemsFocusable: PropTypes.bool, /** - * If `true`, will allow focus on disabled items. + * If `true`, the list box in the popup will not wrap focus. */ - enableFocusForDisabledItems: PropTypes.bool, + disableListWrap: PropTypes.bool, /** * A filter function that determins the options that are eligible. * diff --git a/packages/material-ui/src/ButtonBase/ButtonBase.js b/packages/material-ui/src/ButtonBase/ButtonBase.js index c872e7104b20d1..2d3636abed3862 100644 --- a/packages/material-ui/src/ButtonBase/ButtonBase.js +++ b/packages/material-ui/src/ButtonBase/ButtonBase.js @@ -159,7 +159,7 @@ const ButtonBase = React.forwardRef(function ButtonBase(props, ref) { false, ); - const handleFocus = useEventCallback(event => { + const handleFocus = useEventCallback((event) => { // Fix for https://github.com/facebook/react/issues/7769 if (!buttonRef.current) { buttonRef.current = event.currentTarget; @@ -212,7 +212,12 @@ const ButtonBase = React.forwardRef(function ButtonBase(props, ref) { } // Keyboard accessibility for non interactive elements - if (event.target === event.currentTarget && isNonNativeButton() && event.key === 'Enter') { + if ( + event.target === event.currentTarget && + isNonNativeButton() && + event.key === 'Enter' && + !disabled + ) { event.preventDefault(); if (onClick) { onClick(event); diff --git a/packages/material-ui/src/MenuList/MenuList.d.ts b/packages/material-ui/src/MenuList/MenuList.d.ts index 8d91ab6ed7643e..0033457b50670b 100644 --- a/packages/material-ui/src/MenuList/MenuList.d.ts +++ b/packages/material-ui/src/MenuList/MenuList.d.ts @@ -16,6 +16,10 @@ export interface MenuListProps extends StandardProps integration', () => { }); }); - it('should skip divider and disabled menu item when enableFocusForDisabledItems=false', () => { + it('should skip divider and disabled menu item', () => { const { getAllByRole } = render( - + Menu Item 1 Menu Item 2 @@ -325,90 +325,28 @@ describe(' integration', () => { const menuitems = getAllByRole('menuitem'); fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }); - expect(menuitems[0]).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }); - expect(menuitems[1]).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }); - expect(menuitems[3]).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }); - expect(menuitems[0]).to.have.focus; // and ArrowUp again - fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' }); - expect(menuitems[3]).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' }); - expect(menuitems[1]).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' }); - expect(menuitems[0]).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' }); - expect(menuitems[3]).to.have.focus; }); - it('should skip divider', () => { - const { getAllByRole } = render( - - Menu Item 1 - - Menu Item 2 - - Menu Item 3 - , - ); - const menuitems = getAllByRole('menuitem'); - - fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }); - - expect(menuitems[0]).to.have.focus; - - fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }); - - expect(menuitems[1]).to.have.focus; - - fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }); - - expect(menuitems[2]).to.have.focus; - - fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }); - - expect(menuitems[0]).to.have.focus; - - // and ArrowUp again - - fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' }); - - expect(menuitems[2]).to.have.focus; - - fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' }); - - expect(menuitems[1]).to.have.focus; - - fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' }); - - expect(menuitems[0]).to.have.focus; - - fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' }); - - expect(menuitems[2]).to.have.focus; - }); - it('should stay on a single item if it is the only focusable one', () => { const { getAllByRole } = render( - + Menu Item 1 Menu Item 2 Menu Item 3 @@ -418,29 +356,20 @@ describe(' integration', () => { const menuitems = getAllByRole('menuitem'); fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }); - expect(menuitems[1]).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }); - expect(menuitems[1]).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }); - expect(menuitems[1]).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' }); - expect(menuitems[1]).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' }); - expect(menuitems[1]).to.have.focus; }); - it('should keep focus on the menu if all items are disabled and enableFocusForDisabledItems=false', () => { + it('should keep focus on the menu if all items are disabled', () => { const { getByRole } = render( - + Menu Item 1 Menu Item 2 Menu Item 3 @@ -450,29 +379,20 @@ describe(' integration', () => { const menu = getByRole('menu'); fireEvent.keyDown(document.activeElement, { key: 'Home' }); - expect(menu).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }); - expect(menu).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }); - expect(menu).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'End' }); - expect(menu).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' }); - expect(menu).to.have.focus; }); - it('should allow focus on disabled items when enableFocusForDisabledItems=true', () => { + it('should allow focus on disabled items when disabledItemsFocusable=true', () => { const { getAllByRole } = render( - + Menu Item 1 Menu Item 2 Menu Item 3 @@ -483,23 +403,14 @@ describe(' integration', () => { const menuitems = getAllByRole('menuitem'); fireEvent.keyDown(document.activeElement, { key: 'Home' }); - expect(menuitems[0]).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }); - expect(menuitems[1]).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' }); - expect(menuitems[2]).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'End' }); - expect(menuitems[3]).to.have.focus; - fireEvent.keyDown(document.activeElement, { key: 'ArrowUp' }); - expect(menuitems[2]).to.have.focus; });