@atlaskit/navigation-system
Version:
The latest navigation system for Atlassian apps.
102 lines (98 loc) • 4.19 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useTopNavEndFocus = useTopNavEndFocus;
var _react = require("react");
var _bindEventListener = require("bind-event-listener");
var _useLayoutEffect = require("@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.
*/
function useTopNavEndFocus(_ref) {
var isMobile = _ref.isMobile,
enabled = _ref.enabled;
var actionsRef = (0, _react.useRef)(null);
var triggerRef = (0, _react.useRef)(null);
var focusedElement = (0, _react.useRef)(null);
var actionIndex = (0, _react.useRef)(0);
var previousIsMobile = (0, _react.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 = (0, _react.useCallback)(function (event) {
var target = event.target;
focusedElement.current = target;
var index = getActions(actionsRef.current).indexOf(target);
if (index >= 0) {
actionIndex.current = index;
}
}, []);
(0, _useLayoutEffect.useLayoutEffect)(function () {
if (!enabled) {
return;
}
return (0, _bindEventListener.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.
(0, _react.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
};
}