2026-10-05 05:18:26 +03:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ENTER SUBMITS — regardless of where focus is (owner round 6, 2026-10-05).
|
|
|
|
|
* The dictation reality: the learner dictates into the dock's field without
|
|
|
|
|
* ever focusing it, so Enter must submit from ANYWHERE — body focus, a
|
|
|
|
|
* scroll, a stray link. The field's own Enter handler still fires first
|
|
|
|
|
* when it IS focused (this listener sits at the window level, bubble's last
|
|
|
|
|
* stop) — one law, two entry points, no double submit (the focused field's
|
|
|
|
|
* handler claims the event via its own keydown; this hook skips typing
|
|
|
|
|
* surfaces entirely).
|
|
|
|
|
*
|
|
|
|
|
* What the window listener refuses to touch:
|
|
|
|
|
* - typing surfaces (input/textarea/select/contenteditable) — the focused
|
|
|
|
|
* surface owns its Enter (the dock field, the composer, settings);
|
|
|
|
|
* - modified Enter (⇧↵ is a newline, ⌘/Ctrl↵ belongs to the surface).
|
|
|
|
|
*
|
2026-10-10 09:43:12 +03:00
|
|
|
* BUTTONS — the pointer/keyboard split (owner, keyboard round 2026-10-10):
|
|
|
|
|
* a mouse click on a transcript control (a thinking toggle, a copy button)
|
|
|
|
|
* leaves focus ON that button; the learner's next Enter must SUBMIT, not
|
|
|
|
|
* re-activate the toggle — the report that born this rule. But two button
|
|
|
|
|
* populations keep native Enter-activates:
|
|
|
|
|
* 1. KEYBOARD-focused buttons — :focus-visible means focus arrived by
|
|
|
|
|
* keyboard (the arrow-walked menus; Tab is dead app-wide, so this
|
|
|
|
|
* only happens inside overlays);
|
|
|
|
|
* 2. buttons inside an OPEN OVERLAY — menus walk focus with arrows and
|
|
|
|
|
* Enter activates the walked button natively (menu-shell's contract),
|
|
|
|
|
* dialogs are their own surfaced layer (the ONE-ESC LAW's layers).
|
|
|
|
|
* The pointer-clicked transcript button fails both tests → Enter is ours
|
|
|
|
|
* (preventDefault also suppresses the browser's default activation, so
|
|
|
|
|
* the toggle does not fire underneath the submit).
|
|
|
|
|
*
|
2026-10-05 05:18:26 +03:00
|
|
|
* The submit callbacks self-guard (empty thinking / canSubmit) — this hook
|
|
|
|
|
* never checks answer validity, only focus honesty.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { useEffect, useRef } from "react";
|
|
|
|
|
import { isTypingSurface } from "@/src/core";
|
|
|
|
|
|
2026-10-10 09:43:12 +03:00
|
|
|
/** Overlay roots whose buttons keep native Enter (hand-rolled menus mark
|
|
|
|
|
* themselves data-lh-menu; Radix poppers/dialogs carry their own attrs). */
|
|
|
|
|
const OVERLAY_ROOTS = '[data-lh-menu], [data-radix-popper-content-wrapper], [role="menu"], [role="dialog"]';
|
|
|
|
|
|
|
|
|
|
/** Pure decision, exported for pins: does a focused BUTTON keep native
|
|
|
|
|
* Enter (true) or does Enter belong to submit (false)? */
|
|
|
|
|
export function buttonKeepsNativeEnter(el: HTMLElement): boolean {
|
|
|
|
|
// Keyboard-walked focus: the menu arrows landed here — Enter activates.
|
|
|
|
|
// (Guarded: a selector engine without the pseudo — linkedom in the
|
|
|
|
|
// test harness — degrades to "not keyboard focus", never throws on
|
|
|
|
|
// every Enter.)
|
|
|
|
|
try {
|
|
|
|
|
if (el.matches(":focus-visible")) return true;
|
|
|
|
|
} catch {
|
|
|
|
|
// unsupported pseudo — treat as pointer focus
|
|
|
|
|
}
|
|
|
|
|
// Inside an open overlay: the overlay owns its keys.
|
|
|
|
|
return !!el.closest(OVERLAY_ROOTS);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 05:18:26 +03:00
|
|
|
/** Mount-scoped: while the dock is mounted (a problem/question is active),
|
|
|
|
|
* plain Enter anywhere outside a typing surface or button submits it. */
|
|
|
|
|
export function useEnterSubmits(submit: () => void): void {
|
|
|
|
|
const submitRef = useRef(submit);
|
|
|
|
|
submitRef.current = submit;
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const onKey = (e: KeyboardEvent) => {
|
|
|
|
|
if (e.key !== "Enter") return;
|
|
|
|
|
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
|
|
|
|
|
if (isTypingSurface(e)) return;
|
2026-10-10 09:43:12 +03:00
|
|
|
const target = e.target as HTMLElement | null;
|
|
|
|
|
if (target?.tagName === "BUTTON" && buttonKeepsNativeEnter(target)) return;
|
2026-10-05 05:18:26 +03:00
|
|
|
e.preventDefault();
|
|
|
|
|
submitRef.current();
|
|
|
|
|
};
|
|
|
|
|
window.addEventListener("keydown", onKey);
|
|
|
|
|
return () => window.removeEventListener("keydown", onKey);
|
|
|
|
|
}, []);
|
|
|
|
|
}
|