UNPKG

@atlaskit/editor-common

Version:

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

211 lines (204 loc) • 10.7 kB
import _taggedTemplateLiteral from "@babel/runtime/helpers/taggedTemplateLiteral"; var _templateObject; /** @jsx jsx */ /* eslint-disable no-console */ import React, { useCallback, useLayoutEffect, useRef } from 'react'; import { css, jsx } from '@emotion/react'; import { fullPageMessages as messages } from '../../messages'; import { EDIT_AREA_ID } from '../../ui'; /* ** The context is used to handle the keydown events of submenus. ** Because the keyboard navigation is explicitly managed for main toolbar items ** Few key presses such as Tab,Arrow Right/Left need ot be handled here via context */ export var KeyDownHandlerContext = /*#__PURE__*/React.createContext({ handleArrowLeft: function handleArrowLeft() {}, handleArrowRight: function handleArrowRight() {}, handleTab: function handleTab() {} }); var centeredToolbarContainer = css(_templateObject || (_templateObject = _taggedTemplateLiteral(["\n display: flex;\n width: 100%;\n align-items: center;\n"]))); /** * This component is a wrapper of main toolbar which listens to keydown events of children * and handles left/right arrow key navigation for all focusable elements * @param * @returns */ export var ToolbarArrowKeyNavigationProvider = function ToolbarArrowKeyNavigationProvider(_ref) { var children = _ref.children, editorView = _ref.editorView, childComponentSelector = _ref.childComponentSelector, handleEscape = _ref.handleEscape, disableArrowKeyNavigation = _ref.disableArrowKeyNavigation, isShortcutToFocusToolbar = _ref.isShortcutToFocusToolbar, editorAppearance = _ref.editorAppearance, useStickyToolbar = _ref.useStickyToolbar, intl = _ref.intl; var wrapperRef = useRef(null); var selectedItemIndex = useRef(0); var incrementIndex = useCallback(function (list) { var index = 0; if (document.activeElement) { index = list.indexOf(document.activeElement); index = (index + 1) % list.length; } selectedItemIndex.current = index; }, []); var decrementIndex = useCallback(function (list) { var index = 0; if (document.activeElement) { index = list.indexOf(document.activeElement); index = (list.length + index - 1) % list.length; } selectedItemIndex.current = index; }, []); var handleArrowRight = function handleArrowRight() { var _filteredFocusableEle; var filteredFocusableElements = getFilteredFocusableElements(wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current); incrementIndex(filteredFocusableElements); (_filteredFocusableEle = filteredFocusableElements[selectedItemIndex.current]) === null || _filteredFocusableEle === void 0 || _filteredFocusableEle.focus(); }; var handleArrowLeft = function handleArrowLeft() { var _filteredFocusableEle2; var filteredFocusableElements = getFilteredFocusableElements(wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current); decrementIndex(filteredFocusableElements); (_filteredFocusableEle2 = filteredFocusableElements[selectedItemIndex.current]) === null || _filteredFocusableEle2 === void 0 || _filteredFocusableEle2.focus(); }; var handleTab = function handleTab() { var _filteredFocusableEle3; var filteredFocusableElements = getFilteredFocusableElements(wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current); (_filteredFocusableEle3 = filteredFocusableElements[selectedItemIndex.current]) === null || _filteredFocusableEle3 === void 0 || _filteredFocusableEle3.focus(); }; var handleTabLocal = function handleTabLocal() { var filteredFocusableElements = getFilteredFocusableElements(wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current); filteredFocusableElements.forEach(function (element) { element.setAttribute('tabindex', '-1'); }); filteredFocusableElements[selectedItemIndex.current].setAttribute('tabindex', '0'); }; var focusAndScrollToElement = function focusAndScrollToElement(element) { var scrollToElement = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; if (scrollToElement) { element === null || element === void 0 || element.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); } element.focus(); }; var submenuKeydownHandleContext = { handleArrowLeft: handleArrowLeft, handleArrowRight: handleArrowRight, handleTab: handleTab }; useLayoutEffect(function () { if (!wrapperRef.current || disableArrowKeyNavigation) { return; } var element = wrapperRef.current; /** * To handle the key events on the list * @param event */ var handleKeyDown = function handleKeyDown(event) { var _document$querySelect, _document$querySelect2, _wrapperRef$current; // To trap the focus inside the horizontal toolbar for left and right arrow keys var targetElement = event.target; // To filter out the events outside the child component if (!targetElement.closest("".concat(childComponentSelector))) { return; } // The key events are from child components such as dropdown menus / popups are ignored if ((_document$querySelect = document.querySelector('[data-role="droplistContent"], [data-test-id="color-picker-menu"], [data-emoji-picker-container="true"]')) !== null && _document$querySelect !== void 0 && _document$querySelect.contains(targetElement) || (_document$querySelect2 = document.querySelector('[data-test-id="color-picker-menu"]')) !== null && _document$querySelect2 !== void 0 && _document$querySelect2.contains(targetElement) || event.key === 'ArrowUp' || event.key === 'ArrowDown' || disableArrowKeyNavigation) { return; } var menuWrapper = document.querySelector('.menu-key-handler-wrapper'); if (menuWrapper) { // if menu wrapper exists, then a menu is open and arrow keys will be handled by MenuArrowKeyNavigationProvider return; } var filteredFocusableElements = getFilteredFocusableElements(wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current); if (!filteredFocusableElements || (filteredFocusableElements === null || filteredFocusableElements === void 0 ? void 0 : filteredFocusableElements.length) === 0) { return; } // This is kind of hack to reset the current focused toolbar item // to handle some use cases such as Tab in/out of main toolbar if (!((_wrapperRef$current = wrapperRef.current) !== null && _wrapperRef$current !== void 0 && _wrapperRef$current.contains(targetElement))) { selectedItemIndex.current = -1; } else { selectedItemIndex.current = filteredFocusableElements.indexOf(targetElement) > -1 ? filteredFocusableElements.indexOf(targetElement) : selectedItemIndex.current; } // do not scroll to focused element for sticky toolbar when navigating with arrows to avoid unnesessary scroll jump var allowScrollToElement = !(editorAppearance === 'comment' && !!useStickyToolbar); switch (event.key) { case 'ArrowRight': incrementIndex(filteredFocusableElements); focusAndScrollToElement(filteredFocusableElements[selectedItemIndex.current], allowScrollToElement); event.preventDefault(); break; case 'ArrowLeft': decrementIndex(filteredFocusableElements); focusAndScrollToElement(filteredFocusableElements[selectedItemIndex.current], allowScrollToElement); event.preventDefault(); break; case 'Tab': handleTabLocal(); break; case 'Escape': handleEscape(event); break; default: } }; var globalKeyDownHandler = function globalKeyDownHandler(event) { // To focus the first element in the toolbar if (isShortcutToFocusToolbar(event)) { var _filteredFocusableEle4, _filteredFocusableEle5; var filteredFocusableElements = getFilteredFocusableElements(wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current); (_filteredFocusableEle4 = filteredFocusableElements[0]) === null || _filteredFocusableEle4 === void 0 || _filteredFocusableEle4.focus(); (_filteredFocusableEle5 = filteredFocusableElements[0]) === null || _filteredFocusableEle5 === void 0 || _filteredFocusableEle5.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); } }; element === null || element === void 0 || element.addEventListener('keydown', handleKeyDown); var editorViewDom = editorView === null || editorView === void 0 ? void 0 : editorView.dom; if (isShortcutToFocusToolbar) { editorViewDom === null || editorViewDom === void 0 || editorViewDom.addEventListener('keydown', globalKeyDownHandler); } return function () { element === null || element === void 0 || element.removeEventListener('keydown', handleKeyDown); if (isShortcutToFocusToolbar) { editorViewDom === null || editorViewDom === void 0 || editorViewDom.removeEventListener('keydown', globalKeyDownHandler); } }; }, [selectedItemIndex, wrapperRef, editorView, disableArrowKeyNavigation, handleEscape, childComponentSelector, incrementIndex, decrementIndex, isShortcutToFocusToolbar, editorAppearance, useStickyToolbar]); return jsx("div", { css: editorAppearance === 'comment' && centeredToolbarContainer, className: "custom-key-handler-wrapper", ref: wrapperRef, role: "toolbar", "aria-label": intl.formatMessage(messages.toolbarLabel), "aria-controls": EDIT_AREA_ID }, jsx(KeyDownHandlerContext.Provider, { value: submenuKeydownHandleContext }, children)); }; function getFocusableElements(rootNode) { if (!rootNode) { return []; } var focusableModalElements = rootNode.querySelectorAll('a[href], button:not([disabled]), textarea, input, select, div[tabindex="-1"], div[tabindex="0"]') || []; return Array.from(focusableModalElements); } function getFilteredFocusableElements(rootNode) { // The focusable elements from child components such as dropdown menus / popups are ignored return getFocusableElements(rootNode).filter(function (elm) { var style = window.getComputedStyle(elm); // ignore invisible element to avoid losing focus var isVisible = style.visibility !== 'hidden' && style.display !== 'none'; return !elm.closest('[data-role="droplistContent"]') && !elm.closest('[data-emoji-picker-container="true"]') && !elm.closest('[data-test-id="color-picker-menu"]') && !elm.closest('.scroll-buttons') && isVisible; }); }