Files
learnharness/components/field-cycle.ts
T

37 lines
1.5 KiB
TypeScript
Raw Normal View History

"use client";
import { useEffect } from "react";
/**
* TAB CYCLES THE ACTIVE SURFACE'S TEXT FIELDS (owner, keyboard round
* 2026-10-06): forward-only, wrapping; Tab from outside the chain
* focuses the FIRST field — "navigate in". Shift+Tab is NOT ours (the
* global thinking-level cycle owns it — owner ruling the same round),
* so there is no reverse gear; the chain wraps.
*
* The event (not a prop bridge): the global Tab handler dispatches
* `lh:kbd-cycle-fields` and each composer-bearing surface answers with
* its own chain, gathered at event time from live DOM — the fields
* mount/unmount with the docks, so a query beats stale refs. The quiz
* dock's own Tab claim (rows ↔ thinking, while an option question is
* expanded) stops propagation at the document level, so this hook only
* ever hears Tab on surfaces WITHOUT an active claim — no double act.
*/
export function useFieldCycle(fields: () => readonly HTMLElement[]) {
useEffect(() => {
const onCycle = () => {
const list = fields().filter((el) => el.isConnected);
if (list.length === 0) return;
const active = document.activeElement;
const idx = active ? list.findIndex((el) => el === active || el.contains(active)) : -1;
if (idx < 0) {
list[0]!.focus();
return;
}
list[(idx + 1) % list.length]!.focus();
};
window.addEventListener("lh:kbd-cycle-fields", onCycle);
return () => window.removeEventListener("lh:kbd-cycle-fields", onCycle);
}, [fields]);
}