refactor(sw): share roving focus and outside pointer handling
- components/shared/roving-focus.ts (moveRovingFocus): the same Arrow / Home / End algorithm of SWFooter, SWToolGroup, the SWViewerHeader tabs and SWHomeMenu; each caller passes its own keys, wrap and select behavior. - components/shared/use-outside-pointer.ts (useOutsidePointerDown): the same pointerdown effect of the SWHeader dropdowns, layer menu, document title and More. Escape, focus return and positioning stay in each component. Both modules are internal: the package entry and the public declarations are unchanged.
This commit is contained in:
1 parent
af7f98d66b
commit
7cc3aa1ff8
10 files changed
+55
-69
No files matched your search
@@ -0,0 +1,20 @@
|
||||
import type { KeyboardEvent } from "react"
|
||||
|
||||
/**
|
||||
* Internal (not exported from the package): roving focus among `items`, shared by SWFooter, SWToolGroup, the
|
||||
* SWViewerHeader tabs and SWHomeMenu (2026-10-20; the four copies had the same algorithm). ArrowRight / ArrowLeft —
|
||||
* and ArrowDown / ArrowUp when `vertical` — move and wrap; Home / End jump. Nothing happens unless the focused element
|
||||
* is one of `items`. `select` clicks the new item (radio groups). Returns whether the key was handled.
|
||||
*/
|
||||
export function moveRovingFocus(event: KeyboardEvent<HTMLElement>, items: HTMLElement[], { vertical = false, select = false } = {}) {
|
||||
const forward = event.key === "ArrowRight" || (vertical && event.key === "ArrowDown")
|
||||
const backward = event.key === "ArrowLeft" || (vertical && event.key === "ArrowUp")
|
||||
if (!forward && !backward && event.key !== "Home" && event.key !== "End") return false
|
||||
const index = items.indexOf(document.activeElement as HTMLElement)
|
||||
if (index < 0) return false
|
||||
event.preventDefault()
|
||||
const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : (index + (forward ? 1 : -1) + items.length) % items.length
|
||||
items[next].focus()
|
||||
if (select) items[next].click()
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useEffect, useRef, type RefObject } from "react"
|
||||
|
||||
/**
|
||||
* Internal (not exported from the package): while `open`, a pointerdown outside `rootRef` calls `onOutside` (the latest
|
||||
* one). Shared by the SWHeader dropdowns, layer menu, document title and More (2026-10-20; the four copies were the same
|
||||
* effect). Escape, focus return and positioning stay in each component.
|
||||
*/
|
||||
export function useOutsidePointerDown(open: boolean, rootRef: RefObject<HTMLElement | null>, onOutside: () => void) {
|
||||
const latest = useRef(onOutside)
|
||||
latest.current = onOutside
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const onPointerDown = (event: PointerEvent) => {
|
||||
if (!rootRef.current?.contains(event.target as Node)) latest.current()
|
||||
}
|
||||
document.addEventListener("pointerdown", onPointerDown)
|
||||
return () => document.removeEventListener("pointerdown", onPointerDown)
|
||||
}, [open, rootRef])
|
||||
}
|
||||
Reference in new issue
Block a user