UNPKG

@atlaskit/navigation-system

Version:

The latest navigation system for Atlassian apps.

96 lines (93 loc) • 3.96 kB
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 }; }