import { nextTick } from 'vue' export interface UseRovingFocusOptions { /** Selectable item ids, in DOM order. Already excludes items skipped entirely (e.g. disabled tabs). */ ids: () => string[] /** The id that is currently active/selected. */ activeId: () => string | undefined /** Activates an id (e.g. emit `update:modelValue`). The composable focuses the matching element afterward. */ activate: (id: string) => void /** Returns the focusable element for an id, queried lazily so it reflects the post-activation DOM. */ getElement: (id: string) => HTMLElement | null | undefined /** Arrow-key axis: `'horizontal'` (Left/Right, default) or `'vertical'` (Up/Down). */ orientation?: 'horizontal' | 'vertical' } export interface UseRovingFocusReturn { /** Bind to each item's `@keydown`. */ handleKeydown: (event: KeyboardEvent) => void } /** * WAI-ARIA roving-tabindex navigation with automatic activation (the tabs pattern): * Left/Right (or Up/Down when `orientation` is `'vertical'`) move to the adjacent item * and activate it immediately, wrapping past the ends; Home/End jump to the first/last * item. Only the active item should carry `tabindex="0"`; give every other item * `tabindex="-1"`. Used by BaseTabs. */ export function useRovingFocus(options: UseRovingFocusOptions): UseRovingFocusReturn { const orientation = options.orientation ?? 'horizontal' const prevKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft' const nextKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight' function moveTo(id: string | undefined) { if (id === undefined) return options.activate(id) void nextTick(() => options.getElement(id)?.focus()) } function handleKeydown(event: KeyboardEvent) { const ids = options.ids() if (ids.length === 0) return const currentIndex = ids.indexOf(options.activeId() ?? '') if (event.key === 'Home') { event.preventDefault() moveTo(ids[0]) return } if (event.key === 'End') { event.preventDefault() moveTo(ids[ids.length - 1]) return } if (event.key === prevKey) { event.preventDefault() moveTo(ids[currentIndex <= 0 ? ids.length - 1 : currentIndex - 1]) return } if (event.key === nextKey) { event.preventDefault() moveTo(ids[currentIndex === -1 || currentIndex >= ids.length - 1 ? 0 : currentIndex + 1]) } } return { handleKeydown } }