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:
shkim4 committed 2026-10-21 06:08:45 +09:00
1 parent af7f98d66b
commit 7cc3aa1ff8
10 files changed
+55 -69

No files matched your search

@@ -1,4 +1,5 @@
import { forwardRef, useId, useLayoutEffect, useRef, useState, type ButtonHTMLAttributes, type HTMLAttributes, type KeyboardEvent, type ReactNode } from "react"
import { moveRovingFocus } from "../shared/roving-focus"
import { SWViewerMenu } from "./SWViewerMenu"
import { SWViewerSwitch } from "./SWViewerControls"
import { SWViewerIcon } from "./SWViewerIcon"
@@ -111,15 +112,7 @@ function MenuButton({ item, iconOnly, onCommand }: { item: Extract<SWViewerHeade
function Tabs({ item, onCommand }: { item: Extract<SWViewerHeaderItem, { type: "tabs" }>; onCommand: (c: SWViewerCommand) => void }) {
const select = (value: string) => { item.onValueChange?.(value); onCommand({ id: item.id, itemId: item.id, value }) }
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
const keys = ["ArrowRight", "ArrowLeft", "ArrowDown", "ArrowUp", "Home", "End"]
if (!keys.includes(event.key)) return
const buttons = Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>('[role="radio"]:not(:disabled)'))
const index = buttons.indexOf(document.activeElement as HTMLButtonElement)
if (index < 0) return
event.preventDefault()
const next = event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : (index + (event.key === "ArrowRight" || event.key === "ArrowDown" ? 1 : -1) + buttons.length) % buttons.length
buttons[next].focus()
buttons[next].click()
moveRovingFocus(event, Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>('[role="radio"]:not(:disabled)')), { vertical: true, select: true })
}
const current = item.options.some(option => option.value === item.value) ? item.value : item.options.find(option => !option.disabled)?.value
return (