@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
205 lines (197 loc) • 10.4 kB
JavaScript
;
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);
}