UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

111 lines (110 loc) • 4.02 kB
import { getActiveScopeId, setLastFocusedItem } from "./store.mjs"; import { hasModifier, isItemVisible, isTextInputTarget, scopeSelector } from "./domUtils.mjs"; import { useEffect, useRef } from "react"; //#region src/base-ui/FocusScope/useScopeArrowNav.ts let lastHandledEvent = null; const focusListeners = /* @__PURE__ */ new Map(); const registerItemFocus = (scopeId, listener) => { const set = focusListeners.get(scopeId) ?? /* @__PURE__ */ new Set(); set.add(listener); focusListeners.set(scopeId, set); return () => { set.delete(listener); if (set.size === 0) focusListeners.delete(scopeId); }; }; const notifyScopeItemFocus = (scopeId, el) => { const id = el.dataset.id; if (id) setLastFocusedItem(scopeId, id); focusListeners.get(scopeId)?.forEach((listener) => listener(el)); }; const getScopeRoot = (scopeId) => { const selector = scopeSelector(scopeId); const active = document.activeElement; if (active instanceof HTMLElement) { const owned = active.closest(selector); if (owned) return owned; } const candidates = [...document.querySelectorAll(selector)]; return candidates.find(isItemVisible) ?? candidates[0] ?? null; }; const focusScopeItem = (scopeId, el) => { el.focus({ preventScroll: true }); el.scrollIntoView?.({ block: "nearest" }); notifyScopeItemFocus(scopeId, el); }; const useScopeArrowNav = ({ scopeId, itemSelector = "[data-scope-item]", enabled = true, vimKeys = false, onItemFocus, extra }) => { const optionsRef = useRef({ extra, itemSelector, onItemFocus, scopeId }); optionsRef.current = { extra, itemSelector, onItemFocus, scopeId }; useEffect(() => { if (!onItemFocus) return; return registerItemFocus(scopeId, (el) => optionsRef.current.onItemFocus?.(el)); }, [scopeId, !!onItemFocus]); useEffect(() => { if (!enabled || typeof window === "undefined") return; const getItems = () => { const root = getScopeRoot(optionsRef.current.scopeId); if (!root) return []; return [...root.querySelectorAll(optionsRef.current.itemSelector)].filter(isItemVisible); }; const currentIndex = (items) => { const active = document.activeElement; if (!(active instanceof HTMLElement)) return -1; const item = active.closest(optionsRef.current.itemSelector); return item ? items.indexOf(item) : -1; }; const move = (direction) => { const items = getItems(); if (items.length === 0) return; const index = currentIndex(items); const next = index === -1 ? direction === 1 ? 0 : items.length - 1 : (index + direction + items.length) % items.length; focusScopeItem(optionsRef.current.scopeId, items[next]); }; const edge = (which) => { const items = getItems(); if (items.length === 0) return; focusScopeItem(optionsRef.current.scopeId, items[which === "first" ? 0 : items.length - 1]); }; const isReachable = () => { const id = optionsRef.current.scopeId; if (getActiveScopeId() === id) return true; const active = document.activeElement; return active instanceof HTMLElement && !!active.closest(scopeSelector(id)); }; const onKeyDown = (event) => { if (event.defaultPrevented || lastHandledEvent === event) return; if (hasModifier(event) || isTextInputTarget(event.target) || !isReachable()) return; const builtin = { ArrowDown: () => move(1), ArrowUp: () => move(-1), End: () => edge("last"), Home: () => edge("first"), ...vimKeys && { j: () => move(1), k: () => move(-1) } }; const extraHandler = optionsRef.current.extra?.[event.key]; const handler = builtin[event.key] ?? (extraHandler && (() => extraHandler(event))); if (!handler) return; lastHandledEvent = event; event.preventDefault(); handler(); }; window.addEventListener("keydown", onKeyDown, true); return () => window.removeEventListener("keydown", onKeyDown, true); }, [enabled, vimKeys]); }; //#endregion export { focusScopeItem, getScopeRoot, notifyScopeItemFocus, useScopeArrowNav }; //# sourceMappingURL=useScopeArrowNav.mjs.map