From 10964001cf3b391011b7b48bd0d41f2e73ee261b Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 16 Jul 2026 15:56:54 -0500 Subject: docs(gallery): record the keyboard contract for the widget kit The kit already held both halves of the answer without stating it. GW.slideRule takes arrow keys bound to its own focusable element, which is why it never fights the gallery's global Escape handler: the browser arbitrates focus. But Emacs can't work that way. The SVG region is an image and never sees a keypress, so there the mode's keymap owns delivery. That's the same split as the tick contract, so I put it beside it. The target owns focus and delivery, the builder declares what it accepts through a KEYS table. Deliberately a table rather than a function over a DOM event: the Emacs port installs it into a keymap and never sees a keydown, so a function would force it to re-derive the widget's intent and the two bindings would drift. The five rules are each a bug someone ships otherwise. Never listen on document. Spend preventDefault only where there's a default worth killing. Let Tab and Escape bubble. press filters rather than trusts. Click and key both route through press. --- docs/prototypes/README.org | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) (limited to 'docs') diff --git a/docs/prototypes/README.org b/docs/prototypes/README.org index 651636a..1cb75c6 100644 --- a/docs/prototypes/README.org +++ b/docs/prototypes/README.org @@ -60,6 +60,51 @@ Widget-owned animation (the R17 scope trace, R31 radar sweep, R44 servo chase, R52 pseudo-PC, R53 day clock, R10 TIME page) runs inside the builder, gated on =prefers-reduced-motion=. +** Keyboard contract (widgets that take keys) + +Sibling of the tick contract above, and the same split: focus is an ambient +resource like the clock — exactly one thing holds it across the whole page — +so *the target owns focus and delivery, the builder declares what it accepts*. +Neither half works alone. Only the page knows which widget is live; only the +widget knows that Backspace means DEL. + +*The builder declares* a =KEYS= table on itself, mapping a canonical key name +to the argument its handle's =press= takes: + +: GW.abcKeypad.KEYS = { A:'A', ..., '0':'0', ..., Space:'SPC', Backspace:'DEL', Enter:'ENT' } + +One declaration, wired natively per target: + +- *Web* — the builder attaches a =keydown= listener *to its own focusable + element* (=tabindex=0=, focused on click). =GW.slideRule= is the precedent: + arrows step it, scoped to its own element. The browser's focus system does + the arbitration, which is why it doesn't fight the gallery's own global + Escape handler. +- *Emacs* — the SVG region is an image and never sees a keypress, so the mode + installs the same table into its keymap and calls =press=. This is why the + declaration is a table and not a function over a DOM event: a port that + can't read the widget's intent has to re-derive it, and then the two drift. +- *waybar* — GTK focus, same shape. + +Rules, each of them a bug someone will otherwise ship: + +- *Never listen on =document= or =window=.* A widget that does fights every + sibling and the page's own handlers; on the gallery, every keystroke + anywhere would type into whichever card bound last. +- *=preventDefault= only what has a default worth suppressing* — Space scrolls + the page, Backspace can navigate back. Nothing else. +- *Let Tab and Escape bubble.* Tab is how the page is navigable at all, and + the gallery's audit stepper owns Escape. A widget that swallows either + breaks something it can't see. +- *=press= filters, it does not trust.* A handle that appends whatever it is + handed will cheerfully append "F1". The table is the allowlist. +- *Click and key must land in the same place* — both route through =press=, so + the two paths cannot drift apart or fire different events. + +A widget with no =KEYS= table takes no keys, which is most of them: the kit is +click-first and that is what makes it port. Keys are additive, and in Emacs +they are the *more* native idiom, not the less. + ** Style options Widgets with named style axes take them as constructor opts backed by a -- cgit v1.2.3