keyboard: home's Tab targets its OWN composer — the smoke catch (keyboard 3 follow-up)
The smoke found it: Tab from body on home preventDefault'd but never focused — home does not render the shared Composer component; its composer is the bespoke topic textarea, and the selector matched nothing. The chain is now topicInputRef itself. Also verified live this round: the one-Esc law on plan/chains/settings (exactly one action, nothing stacked), Enter-submits from the Other input (a real session filed the custom answer immediately), and Shift+Tab staying the thinking-level cycle everywhere. 1427 tests, tsc, lint, build green.
This commit is contained in:
+10
-6
@@ -52,12 +52,6 @@ export function Home({
|
|||||||
const [setupOpen, setSetupOpen] = useState(false);
|
const [setupOpen, setSetupOpen] = useState(false);
|
||||||
// Voice dictation (STT wave, 2026-09-17): finals append at the end of the
|
// Voice dictation (STT wave, 2026-09-17): finals append at the end of the
|
||||||
// topic draft; the strip lives in the stats bar's middle slot.
|
// topic draft; the strip lives in the stats bar's middle slot.
|
||||||
// Tab navigates INTO the composer (owner, keyboard round 2026-10-06):
|
|
||||||
// the only text field on this surface — the cycle's whole chain.
|
|
||||||
const cycleComposer = useCallback((): readonly HTMLElement[] =>
|
|
||||||
Array.from(document.querySelectorAll<HTMLElement>('[data-testid="composer"] textarea')),
|
|
||||||
[]);
|
|
||||||
useFieldCycle(cycleComposer);
|
|
||||||
const dictation = useDictation({
|
const dictation = useDictation({
|
||||||
// Ref-first: the join reads the mirror so the send resuming after
|
// Ref-first: the join reads the mirror so the send resuming after
|
||||||
// `await finish()` sees the dictated tail.
|
// `await finish()` sees the dictated tail.
|
||||||
@@ -72,6 +66,16 @@ export function Home({
|
|||||||
* the keyboard from the stop. Falling edge only; this surface's only
|
* the keyboard from the stop. Falling edge only; this surface's only
|
||||||
* dictation target IS this textarea, so no case-split is needed. */
|
* dictation target IS this textarea, so no case-split is needed. */
|
||||||
const topicInputRef = useRef<HTMLTextAreaElement>(null);
|
const topicInputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
// Tab navigates INTO the topic composer (owner, keyboard round
|
||||||
|
// 2026-10-06): the only text field on this surface — and home's
|
||||||
|
// composer is its OWN textarea, not the shared Composer component,
|
||||||
|
// so the chain is the ref itself (the smoke caught a selector that
|
||||||
|
// matched nothing here).
|
||||||
|
const cycleTopicField = useCallback(
|
||||||
|
(): readonly HTMLElement[] => (topicInputRef.current ? [topicInputRef.current] : []),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
useFieldCycle(cycleTopicField);
|
||||||
const dictationWasActiveRef = useRef(false);
|
const dictationWasActiveRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dictation.active) {
|
if (dictation.active) {
|
||||||
|
|||||||
Reference in New Issue
Block a user