UNPKG

@lobehub/ui

Version:

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

1 lines • 7.57 kB
{"version":3,"file":"useScopeArrowNav.mjs","names":[],"sources":["../../../src/base-ui/FocusScope/useScopeArrowNav.ts"],"sourcesContent":["import { useEffect, useRef } from 'react';\n\nimport { hasModifier, isItemVisible, isTextInputTarget, scopeSelector } from './domUtils';\nimport { getActiveScopeId, setLastFocusedItem } from './store';\n\nexport interface UseScopeArrowNavOptions {\n enabled?: boolean;\n extra?: Record<string, (event: KeyboardEvent) => void>;\n itemSelector?: string;\n onItemFocus?: (el: HTMLElement) => void;\n scopeId: string;\n vimKeys?: boolean;\n}\n\n// Sibling instances for one scope id (mobile + desktop mounts) all see the same\n// KeyboardEvent; the first one claims it so focus only moves once.\nlet lastHandledEvent: KeyboardEvent | null = null;\n\nconst focusListeners = new Map<string, Set<(el: HTMLElement) => void>>();\n\nconst registerItemFocus = (scopeId: string, listener: (el: HTMLElement) => void) => {\n const set = focusListeners.get(scopeId) ?? new Set();\n set.add(listener);\n focusListeners.set(scopeId, set);\n return () => {\n set.delete(listener);\n if (set.size === 0) focusListeners.delete(scopeId);\n };\n};\n\nexport const notifyScopeItemFocus = (scopeId: string, el: HTMLElement) => {\n const id = el.dataset.id;\n if (id) setLastFocusedItem(scopeId, id);\n focusListeners.get(scopeId)?.forEach((listener) => listener(el));\n};\n\nexport const getScopeRoot = (scopeId: string): HTMLElement | null => {\n const selector = scopeSelector(scopeId);\n const active = document.activeElement;\n if (active instanceof HTMLElement) {\n const owned = active.closest<HTMLElement>(selector);\n if (owned) return owned;\n }\n const candidates = [...document.querySelectorAll<HTMLElement>(selector)];\n return candidates.find(isItemVisible) ?? candidates[0] ?? null;\n};\n\nexport const focusScopeItem = (scopeId: string, el: HTMLElement) => {\n el.focus({ preventScroll: true });\n el.scrollIntoView?.({ block: 'nearest' });\n notifyScopeItemFocus(scopeId, el);\n};\n\nexport const useScopeArrowNav = ({\n scopeId,\n itemSelector = '[data-scope-item]',\n enabled = true,\n vimKeys = false,\n onItemFocus,\n extra,\n}: UseScopeArrowNavOptions) => {\n const optionsRef = useRef({ extra, itemSelector, onItemFocus, scopeId });\n optionsRef.current = { extra, itemSelector, onItemFocus, scopeId };\n\n useEffect(() => {\n if (!onItemFocus) return;\n return registerItemFocus(scopeId, (el) => optionsRef.current.onItemFocus?.(el));\n }, [scopeId, !!onItemFocus]);\n\n useEffect(() => {\n if (!enabled || typeof window === 'undefined') return;\n\n const getItems = () => {\n const root = getScopeRoot(optionsRef.current.scopeId);\n if (!root) return [];\n return [...root.querySelectorAll<HTMLElement>(optionsRef.current.itemSelector)].filter(\n isItemVisible,\n );\n };\n\n const currentIndex = (items: HTMLElement[]) => {\n const active = document.activeElement;\n if (!(active instanceof HTMLElement)) return -1;\n const item = active.closest<HTMLElement>(optionsRef.current.itemSelector);\n return item ? items.indexOf(item) : -1;\n };\n\n const move = (direction: 1 | -1) => {\n const items = getItems();\n if (items.length === 0) return;\n const index = currentIndex(items);\n const next =\n index === -1\n ? direction === 1\n ? 0\n : items.length - 1\n : (index + direction + items.length) % items.length;\n focusScopeItem(optionsRef.current.scopeId, items[next]);\n };\n\n const edge = (which: 'first' | 'last') => {\n const items = getItems();\n if (items.length === 0) return;\n focusScopeItem(optionsRef.current.scopeId, items[which === 'first' ? 0 : items.length - 1]);\n };\n\n const isReachable = () => {\n const id = optionsRef.current.scopeId;\n if (getActiveScopeId() === id) return true;\n const active = document.activeElement;\n return active instanceof HTMLElement && !!active.closest(scopeSelector(id));\n };\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.defaultPrevented || lastHandledEvent === event) return;\n if (hasModifier(event) || isTextInputTarget(event.target) || !isReachable()) return;\n\n const builtin: Record<string, () => void> = {\n ArrowDown: () => move(1),\n ArrowUp: () => move(-1),\n End: () => edge('last'),\n Home: () => edge('first'),\n ...(vimKeys && { j: () => move(1), k: () => move(-1) }),\n };\n const extraHandler = optionsRef.current.extra?.[event.key];\n const handler = builtin[event.key] ?? (extraHandler && (() => extraHandler(event)));\n if (!handler) return;\n\n lastHandledEvent = event;\n event.preventDefault();\n handler();\n };\n\n window.addEventListener('keydown', onKeyDown, true);\n return () => window.removeEventListener('keydown', onKeyDown, true);\n }, [enabled, vimKeys]);\n};\n"],"mappings":"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"}