A Go-like game designed to be playe by two-to-eight players competing for territory!
bit-blossom docs memories project window-level-input-guards.md
1.9 kB
Markdown
at main

Window-level input guards #

attachEndTurn in ui.ts binds its Enter/Space handler to window, not to the End Turn button. That one decision has now produced three separate bugs, each found after the guard was believed correct:

  1. 2026-09-06 — button.disabled suppresses the button's own click but has no effect on a window listener, so Enter/Space kept ending turns after a win. Fixed by moving the real legality check into game.ts.
  2. 2026-09-07 — adding a <select> meant Space (which opens the dropdown) also ended the turn. Fixed by widening the guard from "is the End Turn button focused" to "is any form control focused."
  3. 2026-09-08 — the concede <dialog>. showModal() makes the background content inert (pointer events, focus, tab order) but has NO effect on a listener bound to window: a keydown inside the dialog still bubbles through the dialog to document to window. Clicking the dialog's non-focusable body text moved focus off Cancel onto the <dialog> element, defeating the form-control check, so Enter ended the turn behind the open modal — and because the dialog's player name is set once at open time, confirming afterwards would have conceded a different player than the one named. Fixed by bailing on any open dialog.

Two rules follow:

  • Modal inertness is not listener protection. Nothing about showModal(), inert, or pointer-events: none stops an event from reaching a window/document listener. Guard explicitly.
  • A guard on a window-level listener is a claim, and it needs a dispatched event behind it, never code inspection. Assert on the event's own defaultPrevented (see the technique bullet in build-and-serve.md) plus the state that must not have changed. All three bugs above passed a reading of the code; two of them were caught only by dispatching, and the third by predicting the dispatch would fail.