diff --git a/packages/material-ui/src/List/List.js b/packages/material-ui/src/List/List.js index a54ac9feb5c517..9a6cbbab58a828 100644 --- a/packages/material-ui/src/List/List.js +++ b/packages/material-ui/src/List/List.js @@ -1,8 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; import clsx from 'clsx'; import withStyles from '../styles/withStyles'; import ListContext from './ListContext'; +import handleListKeyDown from '../utils/handleListKeyDown'; +import { useForkRef } from '../utils/reactHelpers'; export const styles = { /* Styles applied to the root element. */ @@ -35,13 +38,27 @@ const List = React.forwardRef(function List(props, ref) { className, component: Component, dense, + disableListWrap, disablePadding, + onKeyDown, subheader, ...other } = props; const context = React.useMemo(() => ({ dense }), [dense]); + const listRef = React.useRef(); + const selectedItemRef = React.useRef(); + + const handleKeyDown = event => + handleListKeyDown(event, listRef, selectedItemRef, disableListWrap, onKeyDown); + + const handleOwnRef = React.useCallback(refArg => { + // StrictMode ready + listRef.current = ReactDOM.findDOMNode(refArg); + }, []); + const handleRef = useForkRef(handleOwnRef, ref); + return ( @@ -89,10 +107,18 @@ List.propTypes = { * `dense` context. */ dense: PropTypes.bool, + /** + * If `true`, the menu items will not wrap focus. + */ + disableListWrap: PropTypes.bool, /** * If `true`, vertical padding will be removed from the list. */ disablePadding: PropTypes.bool, + /** + * @ignore + */ + onKeyDown: PropTypes.func, /** * The content of the subheader, normally `ListSubheader`. */ diff --git a/packages/material-ui/src/MenuList/MenuList.js b/packages/material-ui/src/MenuList/MenuList.js index 8fef8929e6cebe..eef515b18093c3 100644 --- a/packages/material-ui/src/MenuList/MenuList.js +++ b/packages/material-ui/src/MenuList/MenuList.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import warning from 'warning'; import ownerDocument from '../utils/ownerDocument'; +import handleListKeyDown from '../utils/handleListKeyDown'; import List from '../List'; import getScrollbarSize from '../utils/getScrollbarSize'; import { useForkRef } from '../utils/reactHelpers'; @@ -92,46 +93,8 @@ const MenuList = React.forwardRef(function MenuList(props, ref) { } }; - const handleKeyDown = event => { - const list = listRef.current; - const key = event.key; - const currentFocus = ownerDocument(list).activeElement; - - if ( - (key === 'ArrowUp' || key === 'ArrowDown') && - (!currentFocus || (currentFocus && !list.contains(currentFocus))) - ) { - if (selectedItemRef.current) { - selectedItemRef.current.focus(); - } else { - list.firstChild.focus(); - } - } else if (key === 'ArrowDown') { - event.preventDefault(); - if (currentFocus.nextElementSibling) { - currentFocus.nextElementSibling.focus(); - } else if (!disableListWrap) { - list.firstChild.focus(); - } - } else if (key === 'ArrowUp') { - event.preventDefault(); - if (currentFocus.previousElementSibling) { - currentFocus.previousElementSibling.focus(); - } else if (!disableListWrap) { - list.lastChild.focus(); - } - } else if (key === 'Home') { - event.preventDefault(); - list.firstChild.focus(); - } else if (key === 'End') { - event.preventDefault(); - list.lastChild.focus(); - } - - if (onKeyDown) { - onKeyDown(event); - } - }; + const handleKeyDown = event => + handleListKeyDown(event, listRef, selectedItemRef, disableListWrap, onKeyDown); const handleItemFocus = event => { const list = listRef.current; diff --git a/packages/material-ui/src/utils/handleListKeyDown.js b/packages/material-ui/src/utils/handleListKeyDown.js new file mode 100644 index 00000000000000..2765a652116d38 --- /dev/null +++ b/packages/material-ui/src/utils/handleListKeyDown.js @@ -0,0 +1,48 @@ +import ownerDocument from './ownerDocument'; + +export default function handleListKeyDown( + event, + listRef, + selectedItemRef, + disableListWrap, + onKeyDown, +) { + const list = listRef.current; + const key = event.key; + const currentFocus = ownerDocument(list).activeElement; + + if ( + (key === 'ArrowUp' || key === 'ArrowDown') && + (!currentFocus || (currentFocus && !list.contains(currentFocus))) + ) { + if (selectedItemRef.current) { + selectedItemRef.current.focus(); + } else { + list.firstChild.focus(); + } + } else if (key === 'ArrowDown') { + event.preventDefault(); + if (currentFocus.nextElementSibling) { + currentFocus.nextElementSibling.focus(); + } else if (!disableListWrap) { + list.firstChild.focus(); + } + } else if (key === 'ArrowUp') { + event.preventDefault(); + if (currentFocus.previousElementSibling) { + currentFocus.previousElementSibling.focus(); + } else if (!disableListWrap) { + list.lastChild.focus(); + } + } else if (key === 'Home') { + event.preventDefault(); + list.firstChild.focus(); + } else if (key === 'End') { + event.preventDefault(); + list.lastChild.focus(); + } + + if (onKeyDown) { + onKeyDown(event); + } +}