UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

205 lines (197 loc) • 10.4 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports.MenuArrowKeyNavigationProvider = void 0; var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); var _react = _interopRequireWildcard(require("react")); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); } var hasEnabledItems = function hasEnabledItems(list) { return list.some(function (item) { return item.getAttribute('aria-disabled') !== 'true'; }); }; /** * This component is a wrapper of vertical menus which listens to keydown events of children * and handles up/down arrow key navigation */ var MenuArrowKeyNavigationProvider = exports.MenuArrowKeyNavigationProvider = function MenuArrowKeyNavigationProvider(_ref) { var children = _ref.children, handleClose = _ref.handleClose, disableArrowKeyNavigation = _ref.disableArrowKeyNavigation, keyDownHandlerContext = _ref.keyDownHandlerContext, closeOnTab = _ref.closeOnTab, onSelection = _ref.onSelection, editorRef = _ref.editorRef, popupsMountPoint = _ref.popupsMountPoint, disableCloseOnArrowClick = _ref.disableCloseOnArrowClick; var wrapperRef = (0, _react.useRef)(null); var _useState = (0, _react.useState)(-1), _useState2 = (0, _slicedToArray2.default)(_useState, 2), currentSelectedItemIndex = _useState2[0], setCurrentSelectedItemIndex = _useState2[1]; var element = popupsMountPoint ? [popupsMountPoint, editorRef.current] : [editorRef.current]; var _useState3 = (0, _react.useState)(element), _useState4 = (0, _slicedToArray2.default)(_useState3, 1), listenerTargetElement = _useState4[0]; var incrementIndex = (0, _react.useCallback)(function (list) { var currentIndex = currentSelectedItemIndex; var nextIndex = (currentIndex + 1) % list.length; // Skips disabled items. Previously this function relied on a list of enabled elements which caused a // difference between currentIndex and the item index in the menu. while (nextIndex !== currentIndex && list[nextIndex].getAttribute('aria-disabled') === 'true') { nextIndex = (nextIndex + 1) % list.length; } setCurrentSelectedItemIndex(nextIndex); return nextIndex; }, [currentSelectedItemIndex]); var decrementIndex = (0, _react.useCallback)(function (list) { var currentIndex = currentSelectedItemIndex; var nextIndex = (list.length + currentIndex - 1) % list.length; while (nextIndex !== currentIndex && list[nextIndex].getAttribute('aria-disabled') === 'true') { nextIndex = (list.length + nextIndex - 1) % list.length; } setCurrentSelectedItemIndex(nextIndex); return nextIndex; }, [currentSelectedItemIndex]); // this useEffect uses onSelection in it's dependency list which gets // changed as a result of the dropdown menu getting re-rendered in it's // parent component. Note that if onSelection gets updated to useMemo // this will no longer work. (0, _react.useEffect)(function () { var currentIndex = currentSelectedItemIndex; var list = getFocusableElements(wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current); var currentElement = list[currentIndex]; // eslint-disable-next-line @atlassian/perf-linting/no-chain-state-updates -- Ignored via go/ees017 (to be fixed) if (currentElement && currentElement.getAttribute('aria-disabled') === 'true') { var _list$focusIndex; // eslint-disable-next-line @atlassian/perf-linting/no-chain-state-updates -- Ignored via go/ees017 (to be fixed) var focusIndex = incrementIndex(list); // eslint-disable-next-line @atlassian/perf-linting/no-chain-state-updates -- Ignored via go/ees017 (to be fixed) (_list$focusIndex = list[focusIndex]) === null || _list$focusIndex === void 0 || _list$focusIndex.focus(); } }, [currentSelectedItemIndex, onSelection, incrementIndex, decrementIndex]); (0, _react.useLayoutEffect)(function () { // Backwards compatible behaviour: // - `true` disables all key handling (no listeners attached) // - a function is evaluated per event inside the handler if (disableArrowKeyNavigation === true) { return; } /** * To handle the key events on the list * @param event */ var handleKeyDown = function handleKeyDown(event) { var _wrapperRef$current; if (typeof disableArrowKeyNavigation === 'function' && disableArrowKeyNavigation(event)) { return; } var targetElement = event.target; // Tab key on menu items can be handled in the parent components of dropdown menus with KeydownHandlerContext if (event.key === 'Tab' && closeOnTab) { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion handleClose(event); keyDownHandlerContext === null || keyDownHandlerContext === void 0 || keyDownHandlerContext.handleTab(); return; } // To trap the focus inside the toolbar using left and right arrow keys var focusableElements = getFocusableElements(wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current); if (!focusableElements || (focusableElements === null || focusableElements === void 0 ? void 0 : focusableElements.length) === 0) { return; } if (targetElement instanceof HTMLElement && !((_wrapperRef$current = wrapperRef.current) !== null && _wrapperRef$current !== void 0 && _wrapperRef$current.contains(targetElement))) { setCurrentSelectedItemIndex(-1); } switch (event.key) { case 'ArrowDown': { if (hasEnabledItems(focusableElements)) { var _focusableElements$fo; var focusIndex = incrementIndex(focusableElements); (_focusableElements$fo = focusableElements[focusIndex]) === null || _focusableElements$fo === void 0 || _focusableElements$fo.focus(); event.preventDefault(); } break; } case 'ArrowUp': { if (hasEnabledItems(focusableElements)) { var _focusableElements$_f; var _focusIndex = decrementIndex(focusableElements); (_focusableElements$_f = focusableElements[_focusIndex]) === null || _focusableElements$_f === void 0 || _focusableElements$_f.focus(); event.preventDefault(); } break; } // ArrowLeft/Right on the menu should close the menus // then logic to retain the focus can be handled in the parent components with KeydownHandlerContext case 'ArrowLeft': if (targetElement instanceof HTMLElement && !targetElement.closest('.custom-key-handler-wrapper')) { return; } if (!disableCloseOnArrowClick) { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion handleClose(event); } if (targetElement instanceof HTMLElement && !targetElement.closest('[data-testid="editor-floating-toolbar"]')) { keyDownHandlerContext === null || keyDownHandlerContext === void 0 || keyDownHandlerContext.handleArrowLeft(); } break; case 'ArrowRight': if (targetElement instanceof HTMLElement && !targetElement.closest('.custom-key-handler-wrapper')) { return; } if (!disableCloseOnArrowClick) { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion handleClose(event); } if (targetElement instanceof HTMLElement && !targetElement.closest('[data-testid="editor-floating-toolbar"]')) { keyDownHandlerContext === null || keyDownHandlerContext === void 0 || keyDownHandlerContext.handleArrowRight(); } break; case 'Escape': // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion handleClose(event); break; case 'Enter': if (typeof onSelection === 'function') { onSelection(currentSelectedItemIndex); } break; default: return; } }; listenerTargetElement && listenerTargetElement.forEach(function (elem) { // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners elem && elem.addEventListener('keydown', handleKeyDown); }); return function () { listenerTargetElement && listenerTargetElement.forEach(function (elem) { // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners elem && elem.removeEventListener('keydown', handleKeyDown); }); }; }, [currentSelectedItemIndex, wrapperRef, handleClose, disableArrowKeyNavigation, keyDownHandlerContext, closeOnTab, onSelection, incrementIndex, decrementIndex, listenerTargetElement, disableCloseOnArrowClick]); return /*#__PURE__*/_react.default.createElement("div", { // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 className: "menu-key-handler-wrapper custom-key-handler-wrapper", ref: wrapperRef }, children); }; function getFocusableElements(rootNode) { if (!rootNode) { return []; } var focusableModalElements = rootNode.querySelectorAll('a[href], button:not([disabled]), textarea, input, select, div[tabindex="-1"]') || []; return Array.from(focusableModalElements); }