"use strict"; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard"); var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); exports.__esModule = true; exports["default"] = void 0; var _matches = _interopRequireDefault(require("dom-helpers/matches")); var _querySelectorAll = _interopRequireDefault(require("dom-helpers/querySelectorAll")); var _addEventListener = _interopRequireDefault(require("dom-helpers/addEventListener")); var _react = _interopRequireWildcard(require("react")); var _propTypes = _interopRequireDefault(require("prop-types")); var _uncontrollable = require("uncontrollable"); var _usePrevious = _interopRequireDefault(require("@restart/hooks/usePrevious")); var _useForceUpdate = _interopRequireDefault(require("@restart/hooks/useForceUpdate")); var _useGlobalListener = _interopRequireDefault(require("@restart/hooks/useGlobalListener")); var _useEventCallback = _interopRequireDefault(require("@restart/hooks/useEventCallback")); var _DropdownContext = _interopRequireDefault(require("./DropdownContext")); var _DropdownMenu = _interopRequireDefault(require("./DropdownMenu")); var _DropdownToggle = _interopRequireDefault(require("./DropdownToggle")); var propTypes = { /** * A render prop that returns the root dropdown element. The `props` * argument should spread through to an element containing _both_ the * menu and toggle in order to handle keyboard events for focus management. * * @type {Function ({ * props: { * onKeyDown: (SyntheticEvent) => void, * }, * }) => React.Element} */ children: _propTypes["default"].node, /** * Determines the direction and location of the Menu in relation to it's Toggle. */ drop: _propTypes["default"].oneOf(['up', 'left', 'right', 'down']), /** * Controls the focus behavior for when the Dropdown is opened. Set to * `true` to always focus the first menu item, `keyboard` to focus only when * navigating via the keyboard, or `false` to disable completely * * The Default behavior is `false` **unless** the Menu has a `role="menu"` * where it will default to `keyboard` to match the recommended [ARIA Authoring practices](https://www.w3.org/TR/wai-aria-practices-1.1/#menubutton). */ focusFirstItemOnShow: _propTypes["default"].oneOf([false, true, 'keyboard']), /** * A css slector string that will return __focusable__ menu items. * Selectors should be relative to the menu component: * e.g. ` > li:not('.disabled')` */ itemSelector: _propTypes["default"].string, /** * Align the menu to the 'end' side of the placement side of the Dropdown toggle. The default placement is `top-start` or `bottom-start`. */ alignEnd: _propTypes["default"].bool, /** * Whether or not the Dropdown is visible. * * @controllable onToggle */ show: _propTypes["default"].bool, /** * Sets the initial show position of the Dropdown. */ defaultShow: _propTypes["default"].bool, /** * A callback fired when the Dropdown wishes to change visibility. Called with the requested * `show` value, the DOM event, and the source that fired it: `'click'`,`'keydown'`,`'rootClose'`, or `'select'`. * * ```ts static * function( * isOpen: boolean, * event: SyntheticEvent, * ): void * ``` * * @controllable show */ onToggle: _propTypes["default"].func }; function useRefWithUpdate() { var forceUpdate = (0, _useForceUpdate["default"])(); var ref = (0, _react.useRef)(null); var attachRef = (0, _react.useCallback)(function (element) { ref.current = element; // ensure that a menu set triggers an update for consumers forceUpdate(); }, [forceUpdate]); return [ref, attachRef]; } /** * @displayName Dropdown * @public */ function Dropdown(_ref) { var drop = _ref.drop, alignEnd = _ref.alignEnd, defaultShow = _ref.defaultShow, rawShow = _ref.show, rawOnToggle = _ref.onToggle, _ref$itemSelector = _ref.itemSelector, itemSelector = _ref$itemSelector === void 0 ? '* > *' : _ref$itemSelector, focusFirstItemOnShow = _ref.focusFirstItemOnShow, children = _ref.children; var _useUncontrolledProp = (0, _uncontrollable.useUncontrolledProp)(rawShow, defaultShow, rawOnToggle), show = _useUncontrolledProp[0], onToggle = _useUncontrolledProp[1]; // We use normal refs instead of useCallbackRef in order to populate the // the value as quickly as possible, otherwise the effect to focus the element // may run before the state value is set var _useRefWithUpdate = useRefWithUpdate(), menuRef = _useRefWithUpdate[0], setMenu = _useRefWithUpdate[1]; var menuElement = menuRef.current; var _useRefWithUpdate2 = useRefWithUpdate(), toggleRef = _useRefWithUpdate2[0], setToggle = _useRefWithUpdate2[1]; var toggleElement = toggleRef.current; var lastShow = (0, _usePrevious["default"])(show); var lastSourceEvent = (0, _react.useRef)(null); var focusInDropdown = (0, _react.useRef)(false); var toggle = (0, _react.useCallback)(function (nextShow, event) { onToggle(nextShow, event); }, [onToggle]); var context = (0, _react.useMemo)(function () { return { toggle: toggle, drop: drop, show: show, alignEnd: alignEnd, menuElement: menuElement, toggleElement: toggleElement, setMenu: setMenu, setToggle: setToggle }; }, [toggle, drop, show, alignEnd, menuElement, toggleElement, setMenu, setToggle]); if (menuElement && lastShow && !show) { focusInDropdown.current = menuElement.contains(document.activeElement); } var focusToggle = (0, _useEventCallback["default"])(function () { if (toggleElement && toggleElement.focus) { toggleElement.focus(); } }); var maybeFocusFirst = (0, _useEventCallback["default"])(function () { var type = lastSourceEvent.current; var focusType = focusFirstItemOnShow; if (focusType == null) { focusType = menuRef.current && (0, _matches["default"])(menuRef.current, '[role=menu]') ? 'keyboard' : false; } if (focusType === false || focusType === 'keyboard' && !/^key.+$/.test(type)) { return; } var first = (0, _querySelectorAll["default"])(menuRef.current, itemSelector)[0]; if (first && first.focus) first.focus(); }); (0, _react.useEffect)(function () { if (show) maybeFocusFirst();else if (focusInDropdown.current) { focusInDropdown.current = false; focusToggle(); } // only `show` should be changing }, [show, focusInDropdown, focusToggle, maybeFocusFirst]); (0, _react.useEffect)(function () { lastSourceEvent.current = null; }); var getNextFocusedChild = function getNextFocusedChild(current, offset) { if (!menuRef.current) return null; var items = (0, _querySelectorAll["default"])(menuRef.current, itemSelector); var index = items.indexOf(current) + offset; index = Math.max(0, Math.min(index, items.length)); return items[index]; }; (0, _useGlobalListener["default"])('keydown', function (event) { var _menuRef$current, _toggleRef$current; var key = event.key; var target = event.target; var fromMenu = (_menuRef$current = menuRef.current) == null ? void 0 : _menuRef$current.contains(target); var fromToggle = (_toggleRef$current = toggleRef.current) == null ? void 0 : _toggleRef$current.contains(target); // Second only to https://github.com/twbs/bootstrap/blob/8cfbf6933b8a0146ac3fbc369f19e520bd1ebdac/js/src/dropdown.js#L400 // in inscrutability var isInput = /input|textarea/i.test(target.tagName); if (isInput && (key === ' ' || key !== 'Escape' && fromMenu)) { return; } if (!fromMenu && !fromToggle) { return; } if (!menuRef.current && key === 'Tab') { return; } lastSourceEvent.current = event.type; switch (key) { case 'ArrowUp': { var next = getNextFocusedChild(target, -1); if (next && next.focus) next.focus(); event.preventDefault(); return; } case 'ArrowDown': event.preventDefault(); if (!show) { onToggle(true, event); } else { var _next = getNextFocusedChild(target, 1); if (_next && _next.focus) _next.focus(); } return; case 'Tab': // on keydown the target is the element being tabbed FROM, we need that // to know if this event is relevant to this dropdown (e.g. in this menu). // On `keyup` the target is the element being tagged TO which we use to check // if focus has left the menu (0, _addEventListener["default"])(document, 'keyup', function (e) { var _menuRef$current2; if (e.key === 'Tab' && !e.target || !((_menuRef$current2 = menuRef.current) != null && _menuRef$current2.contains(e.target))) { onToggle(false, event); } }, { once: true }); break; case 'Escape': event.preventDefault(); event.stopPropagation(); onToggle(false, event); break; default: } }); return /*#__PURE__*/_react["default"].createElement(_DropdownContext["default"].Provider, { value: context }, children); } Dropdown.displayName = 'ReactOverlaysDropdown'; Dropdown.propTypes = propTypes; Dropdown.Menu = _DropdownMenu["default"]; Dropdown.Toggle = _DropdownToggle["default"]; var _default = Dropdown; exports["default"] = _default; module.exports = exports.default;