keyboard: the one-Esc law — a surfaced layer claims at the document level (keyboard 4 of 4)
Owner ruling (keyboard round 2026-10-06): Esc (or any key) does ONE action, on the topmost active layer. The plan-open-while-dictating double-act was the symptom: the plan popup listened at WINDOW level with no claim, and same-node window listeners all fire — the global chain (registered first) cancelled the dictation while the plan's own listener closed the plan. One Esc, two actions. The law (now the house pattern, documented at the chain): - field-level handlers claim at their element (focus in a field: the field's action) — unchanged, already correct - every SURFACED layer claims at the DOCUMENT level with preventDefault + stopPropagation — document fires before the window chain, so the chain stays silent under any surface - the window chain is the bottom-up fallback: dictation (a live capture, NOT a surface — it survives the Esc that closes a plan or overlay), then abort (escAbortTarget layers its own targets) Converted to the claim: plan popup, session overlay, chain table, settings dialog, setup dialog, the delete/abort dialogs (window-level stopPropagation was never enough — same-node listeners all fire), and the stats popover. Popovers (popover-dismiss), menus, model picker, and the sidebar context menu already claimed correctly. 1427 tests, tsc, lint, build green.
This commit is contained in:
@@ -639,13 +639,23 @@ export function PlanPopup({ messages, onClose }: { messages: readonly PopupMessa
|
||||
);
|
||||
const statusOf = (id: string): PlanNodeStatus => (statuses ? derivedNodeStatus(statuses, id) : "upcoming");
|
||||
|
||||
// Esc closes.
|
||||
// Esc closes — claimed at the DOCUMENT level (THE ONE-ESC LAW, owner
|
||||
// keyboard round 2026-10-06): a surfaced layer's Esc is exactly one
|
||||
// action. The window-level chain (dictation cancel → abort) is the
|
||||
// fallback for when NOTHING is surfaced; a window listener here could
|
||||
// never stop it — same-node listeners all fire — which was the
|
||||
// plan-open-while-dictating double-act (Esc closed the plan AND
|
||||
// cancelled the dictation).
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => document.removeEventListener("keydown", onKey);
|
||||
}, [onClose]);
|
||||
|
||||
// ── pan/zoom viewport (harvested semantics: fit floor = the default) ──
|
||||
|
||||
Reference in New Issue
Block a user