@material-ui/core
Version:
React components that implement Google's Material Design.
258 lines (204 loc) • 7.63 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
var _react = _interopRequireDefault(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _reactDom = _interopRequireDefault(require("react-dom"));
var _ownerDocument = _interopRequireDefault(require("../utils/ownerDocument"));
var _List = _interopRequireDefault(require("../List"));
var _getScrollbarSize = _interopRequireDefault(require("../utils/getScrollbarSize"));
var _reactHelpers = require("../utils/reactHelpers");
function nextItem(list, item, disableListWrap) {
if (list === item) {
return list.firstChild;
}
if (item && item.nextElementSibling) {
return item.nextElementSibling;
}
return disableListWrap ? null : list.firstChild;
}
function previousItem(list, item, disableListWrap) {
if (list === item) {
return disableListWrap ? list.firstChild : list.lastChild;
}
if (item && item.previousElementSibling) {
return item.previousElementSibling;
}
return disableListWrap ? null : list.lastChild;
}
function textCriteriaMatches(nextFocus, textCriteria) {
if (textCriteria === undefined) {
return true;
}
var text = nextFocus.innerText;
if (text === undefined) {
// jsdom doesn't support innerText
text = nextFocus.textContent;
}
if (text === undefined) {
return false;
}
text = text.trim().toLowerCase();
if (text.length === 0) {
return false;
}
if (textCriteria.repeating) {
return text[0] === textCriteria.keys[0];
}
return text.indexOf(textCriteria.keys.join('')) === 0;
}
function moveFocus(list, currentFocus, disableListWrap, traversalFunction, textCriteria) {
var wrappedOnce = false;
var nextFocus = traversalFunction(list, currentFocus, currentFocus ? disableListWrap : false);
while (nextFocus) {
// Prevent infinite loop.
if (nextFocus === list.firstChild) {
if (wrappedOnce) {
return false;
}
wrappedOnce = true;
} // Move to the next element.
if (!nextFocus.hasAttribute('tabindex') || nextFocus.disabled || nextFocus.getAttribute('aria-disabled') === 'true' || !textCriteriaMatches(nextFocus, textCriteria)) {
nextFocus = traversalFunction(list, nextFocus, disableListWrap);
} else {
nextFocus.focus();
return true;
}
}
return false;
}
var useEnhancedEffect = typeof window === 'undefined' ? _react.default.useEffect : _react.default.useLayoutEffect;
var MenuList = _react.default.forwardRef(function MenuList(props, ref) {
var actions = props.actions,
_props$autoFocus = props.autoFocus,
autoFocus = _props$autoFocus === void 0 ? false : _props$autoFocus,
className = props.className,
onKeyDown = props.onKeyDown,
_props$disableListWra = props.disableListWrap,
disableListWrap = _props$disableListWra === void 0 ? false : _props$disableListWra,
other = (0, _objectWithoutProperties2.default)(props, ["actions", "autoFocus", "className", "onKeyDown", "disableListWrap"]);
var listRef = _react.default.useRef(null);
var textCriteriaRef = _react.default.useRef({
keys: [],
repeating: true,
previousKeyMatched: true,
lastTime: null
});
useEnhancedEffect(function () {
if (autoFocus) {
listRef.current.focus();
}
}, [autoFocus]);
_react.default.useImperativeHandle(actions, function () {
return {
adjustStyleForScrollbar: function adjustStyleForScrollbar(containerElement, theme) {
// Let's ignore that piece of logic if users are already overriding the width
// of the menu.
var noExplicitWidth = !listRef.current.style.width;
if (containerElement.clientHeight < listRef.current.clientHeight && noExplicitWidth) {
var scrollbarSize = "".concat((0, _getScrollbarSize.default)(true), "px");
listRef.current.style[theme.direction === 'rtl' ? 'paddingLeft' : 'paddingRight'] = scrollbarSize;
listRef.current.style.width = "calc(100% + ".concat(scrollbarSize, ")");
}
return listRef.current;
}
};
}, []);
var handleKeyDown = function handleKeyDown(event) {
var list = listRef.current;
var key = event.key;
/**
* @type {Element} - will always be defined since we are in a keydown handler
* attached to an element. A keydown event is either dispatched to the activeElement
* or document.body or document.documentElement. Only the first case will
* trigger this specific handler.
*/
var currentFocus = (0, _ownerDocument.default)(list).activeElement;
if (key === 'ArrowDown') {
event.preventDefault();
moveFocus(list, currentFocus, disableListWrap, nextItem);
} else if (key === 'ArrowUp') {
event.preventDefault();
moveFocus(list, currentFocus, disableListWrap, previousItem);
} else if (key === 'Home') {
event.preventDefault();
moveFocus(list, null, disableListWrap, nextItem);
} else if (key === 'End') {
event.preventDefault();
moveFocus(list, null, disableListWrap, previousItem);
} else if (key.length === 1) {
var criteria = textCriteriaRef.current;
var lowerKey = key.toLowerCase();
var currTime = performance.now();
if (criteria.keys.length > 0) {
// Reset
if (currTime - criteria.lastTime > 500) {
criteria.keys = [];
criteria.repeating = true;
criteria.previousKeyMatched = true;
} else if (criteria.repeating && lowerKey !== criteria.keys[0]) {
criteria.repeating = false;
}
}
criteria.lastTime = currTime;
criteria.keys.push(lowerKey);
var keepFocusOnCurrent = currentFocus && !criteria.repeating && textCriteriaMatches(currentFocus, criteria);
if (criteria.previousKeyMatched && (keepFocusOnCurrent || moveFocus(list, currentFocus, false, nextItem, criteria))) {
event.preventDefault();
} else {
criteria.previousKeyMatched = false;
}
}
if (onKeyDown) {
onKeyDown(event);
}
};
var handleOwnRef = _react.default.useCallback(function (instance) {
// #StrictMode ready
listRef.current = _reactDom.default.findDOMNode(instance);
}, []);
var handleRef = (0, _reactHelpers.useForkRef)(handleOwnRef, ref);
return _react.default.createElement(_List.default, (0, _extends2.default)({
role: "menu",
ref: handleRef,
className: className,
onKeyDown: handleKeyDown,
tabIndex: autoFocus ? 0 : -1
}, other));
});
process.env.NODE_ENV !== "production" ? MenuList.propTypes = {
/**
* @ignore
*/
actions: _propTypes.default.shape({
current: _propTypes.default.object
}),
/**
* If `true`, the list will be focused during the first mount.
* Focus will also be triggered if the value changes from false to true.
*/
autoFocus: _propTypes.default.bool,
/**
* MenuList contents, normally `MenuItem`s.
*/
children: _propTypes.default.node,
/**
* @ignore
*/
className: _propTypes.default.string,
/**
* If `true`, the menu items will not wrap focus.
*/
disableListWrap: _propTypes.default.bool,
/**
* @ignore
*/
onKeyDown: _propTypes.default.func
} : void 0;
var _default = MenuList;
exports.default = _default;