@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
241 lines (234 loc) • 12.4 kB
JavaScript
/**
* @jsxRuntime classic
* @jsx jsx
*/
import React, { useCallback, useLayoutEffect, useRef } from 'react';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
import { css, jsx } from '@emotion/react';
import { ELEMENT_BROWSER_ID } from '../../element-browser';
import { fullPageMessages } from '../../messages';
import { mediaInsertMessages } from '../../messages/media-insert';
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({
display: 'flex',
width: '100%',
alignItems: 'center'
});
/**
* 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) {
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
index = list.indexOf(document.activeElement);
index = (index + 1) % list.length;
}
selectedItemIndex.current = index;
}, []);
var decrementIndex = useCallback(function (list) {
var index = 0;
if (document.activeElement) {
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
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, _wrapperRef$current2;
// To trap the focus inside the horizontal toolbar for left and right arrow keys
var targetElement = event.target;
if (targetElement instanceof HTMLElement && !targetElement.closest("".concat(childComponentSelector))) {
return;
}
if (targetElement instanceof HTMLElement && (_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) || targetElement instanceof HTMLElement && (_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 elementBrowser = wrapperRef === null || wrapperRef === void 0 || (_wrapperRef$current = wrapperRef.current) === null || _wrapperRef$current === void 0 ? void 0 : _wrapperRef$current.querySelector("#".concat(ELEMENT_BROWSER_ID));
if (elementBrowser) {
// if element browser is open, then 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;
}
// If the target element is the media picker then navigation is handled by the media picker
if (targetElement instanceof HTMLElement && targetElement.closest("[aria-label=\"".concat(intl.formatMessage(mediaInsertMessages.mediaPickerPopupAriaLabel), "\"]"))) {
return;
}
if (targetElement instanceof HTMLElement && !((_wrapperRef$current2 = wrapperRef.current) !== null && _wrapperRef$current2 !== void 0 && _wrapperRef$current2.contains(targetElement))) {
selectedItemIndex.current = -1;
} else {
selectedItemIndex.current = targetElement instanceof HTMLElement && 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':
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
handleEscape(event);
break;
default:
}
};
var globalKeyDownHandler = function globalKeyDownHandler(event) {
// To focus the first element in the toolbar
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
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'
});
}
};
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
element === null || element === void 0 || element.addEventListener('keydown', handleKeyDown);
var editorViewDom = editorView === null || editorView === void 0 ? void 0 : editorView.dom;
if (isShortcutToFocusToolbar) {
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
editorViewDom === null || editorViewDom === void 0 || editorViewDom.addEventListener('keydown', globalKeyDownHandler);
}
return function () {
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
element === null || element === void 0 || element.removeEventListener('keydown', handleKeyDown);
if (isShortcutToFocusToolbar) {
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
editorViewDom === null || editorViewDom === void 0 || editorViewDom.removeEventListener('keydown', globalKeyDownHandler);
}
};
}, [selectedItemIndex, wrapperRef, editorView, disableArrowKeyNavigation, handleEscape, childComponentSelector, incrementIndex, decrementIndex, isShortcutToFocusToolbar, editorAppearance, useStickyToolbar, intl]);
return jsx("div", {
css: editorAppearance === 'comment' && centeredToolbarContainer
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766
,
className: "custom-key-handler-wrapper",
ref: wrapperRef,
role: "toolbar",
"aria-label": intl.formatMessage(fullPageMessages.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;
});
}