@atlaskit/navigation-system
Version:
The latest navigation system for Atlassian apps.
96 lines (93 loc) • 3.96 kB
JavaScript
import { useCallback, useEffect, useRef } from 'react';
import { bindAll } from 'bind-event-listener';
import { useLayoutEffect } from '@atlaskit/ds-lib/use-layout-effect';
function getActions(list) {
var _list$querySelectorAl;
return Array.from((_list$querySelectorAl = list === null || list === void 0 ? void 0 : list.querySelectorAll('button, a[href], input, select, textarea, [tabindex]')) !== null && _list$querySelectorAl !== void 0 ? _list$querySelectorAl : []).filter(function (element) {
// In-place and top-layer popups can be DOM descendants of the list.
// Their controls must not replace the index of the action that opened them.
var layer = element.closest('[popover], [role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"]');
return element.tabIndex >= 0 && !element.matches(':disabled') && element.getClientRects().length > 0 && !(layer && list !== null && list !== void 0 && list.contains(layer));
});
}
/**
* Keeps keyboard position when the regular actions move into or out of overflow.
*/
export function useTopNavEndFocus(_ref) {
var isMobile = _ref.isMobile,
enabled = _ref.enabled;
var actionsRef = useRef(null);
var triggerRef = useRef(null);
var focusedElement = useRef(null);
var actionIndex = useRef(0);
var previousIsMobile = useRef(isMobile);
// React capture follows owned portals as well as DOM descendants. The scope
// includes the overflow popup's focusable shell, but excludes persistent items.
var onFocusCapture = useCallback(function (event) {
var target = event.target;
focusedElement.current = target;
var index = getActions(actionsRef.current).indexOf(target);
if (index >= 0) {
actionIndex.current = index;
}
}, []);
useLayoutEffect(function () {
if (!enabled) {
return;
}
return bindAll(document, [{
type: 'focusin',
listener: function listener(event) {
// Owned React focus has already been recorded in capture, including
// portaled layers. Everything else cancels the pending handoff.
if (event.target !== focusedElement.current) {
focusedElement.current = null;
}
}
}, {
type: 'focusout',
listener: function listener(event) {
if (!(event instanceof FocusEvent)) {
return;
}
var element = focusedElement.current;
if (event.target !== element) {
return;
}
if (event.relatedTarget) {
focusedElement.current = null;
return;
}
// A popup can blur its trigger during unmount, before removing its DOM.
// Wait until that commit completes to distinguish removal from explicit blur.
queueMicrotask(function () {
if (focusedElement.current === element && element !== null && element !== void 0 && element.isConnected && element.getClientRects().length) {
focusedElement.current = null;
}
});
}
}]);
}, [enabled]);
// Restore after the removed popup has released its focus trap in passive cleanup.
useEffect(function () {
var _actions$actionIndex$;
var changed = previousIsMobile.current !== isMobile;
previousIsMobile.current = isMobile;
if (!enabled || !changed) {
return;
}
var previous = focusedElement.current;
focusedElement.current = null;
if (!previous || document.activeElement !== document.body && document.activeElement !== previous) {
return;
}
var actions = getActions(actionsRef.current);
var replacement = isMobile ? triggerRef.current : (_actions$actionIndex$ = actions[actionIndex.current]) !== null && _actions$actionIndex$ !== void 0 ? _actions$actionIndex$ : actions[0];
replacement === null || replacement === void 0 || replacement.focus();
}, [enabled, isMobile]);
return {
actionsRef: actionsRef,
triggerRef: triggerRef,
onFocusCapture: onFocusCapture
};
}