UNPKG

@atlaskit/navigation-system

Version:

The latest navigation system for Atlassian apps.

97 lines (94 loc) • 3.85 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(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. const 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({ isMobile, enabled }) { const actionsRef = useRef(null); const triggerRef = useRef(null); const focusedElement = useRef(null); const actionIndex = useRef(0); const 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. const onFocusCapture = useCallback(event => { const target = event.target; focusedElement.current = target; const index = getActions(actionsRef.current).indexOf(target); if (index >= 0) { actionIndex.current = index; } }, []); useLayoutEffect(() => { if (!enabled) { return; } return bindAll(document, [{ type: 'focusin', 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: event => { if (!(event instanceof FocusEvent)) { return; } const 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(() => { 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(() => { var _actions$actionIndex$; const changed = previousIsMobile.current !== isMobile; previousIsMobile.current = isMobile; if (!enabled || !changed) { return; } const previous = focusedElement.current; focusedElement.current = null; if (!previous || document.activeElement !== document.body && document.activeElement !== previous) { return; } const actions = getActions(actionsRef.current); const replacement = isMobile ? triggerRef.current : (_actions$actionIndex$ = actions[actionIndex.current]) !== null && _actions$actionIndex$ !== void 0 ? _actions$actionIndex$ : actions[0]; replacement === null || replacement === void 0 ? void 0 : replacement.focus(); }, [enabled, isMobile]); return { actionsRef, triggerRef, onFocusCapture }; }