Measured every overlay before touching anything. For a dealer running this on a laptop, all six were broken the same three ways. Stacked on #46.
What was measured
Before
After
Focus moves into the dialog
0 / 5
5 / 5
Tab stays inside (fwd + back)
0 / 5 — all leaked
5 / 5
Escape closes
1 / 5 (rulings only)
5 / 5
Focus restored on close
0 / 5
5 / 5
role + aria-modal
pot wizard had neither
5 / 5
Opening the pot wizard — the dialog that pays people — left focus on the button behind it, tabbing wandered through a page nobody can see or click, and there was no way back out except the mouse.
One implementation, not six
The overlays are opened and closed by five different modules, which is exactly why they each missed the same things. Rather than patch it into all of them, modal.js watches the hidden attribute they already toggle — focus in, Tab wrapped, Escape, focus restored, aria-modal set while open. An overlay added later gets all of it without anyone remembering to wire it up, which is the failure being fixed.
Escape presses the dialog's own cancel button rather than just hiding the overlay, so module cleanup still runs. Verified: Escape on the confirm dialog resolves its promise to false rather than leaving an await hanging forever.
Shortcuts.overlayOpen() had the same shape of bug — it listed the six overlay ids by hand. It asks the document now, so a sheet added later can't quietly start letting f fold the table behind it.
Focus you can see
Three focus rules existed in the whole 2700-line stylesheet, so tabbing relied on the browser's default ring — close to invisible on these dark custom-painted controls. :focus-visible rather than :focus, so a dealer tapping Fold isn't left with a ring on it.
That surfaced something small but real: .holdemBtn used transition: all, so the new outline faded in over 150ms and the ring lagged a step behind the Tab key. The transition names its properties now. After: 16/16 tab stops show a ring immediately, and the press state from #44 still animates.
Also
a mistyped negative stack read back as negative and flowed into putIn(), where Math.min(amount, -50) moves a negative amount and adds chips to the table. Clamped at zero.
accessible names for the chip colour, chip value and notes fields, which had none
Validation
11 new tests (99 total, all passing): negative stacks, negative and unreadable blinds, blinds nobody can cover, and saves that come back corrupt, truncated, or from before a field existed.
Playwright: all five overlays re-measured on the real page; shortcuts still fire with no dialog open and are still blocked with one open; touch guarantees from #44 unchanged (0 of 49 controls on touch-action: auto, 0 tap-highlighted). No console errors.
One note on process: my first run of the after-measurement showed no change at all. The cause was a stale server — a lingering dotnet process whose command line was .../net8.0/JoshHeaps.Net, which my pkill -f "JoshHeaps.Net.dll" pattern never matched, so Razor kept serving the old compiled page. Worth knowing if the same thing bites you locally.
Measured every overlay before touching anything. For a dealer running this on a laptop, all six were broken the same three ways. Stacked on #46.
## What was measured
| | Before | After |
| --- | --- | --- |
| Focus moves into the dialog | **0 / 5** | 5 / 5 |
| Tab stays inside (fwd + back) | **0 / 5** — all leaked | 5 / 5 |
| Escape closes | **1 / 5** (rulings only) | 5 / 5 |
| Focus restored on close | 0 / 5 | 5 / 5 |
| `role` + `aria-modal` | pot wizard had **neither** | 5 / 5 |
Opening the pot wizard — the dialog that *pays people* — left focus on the button behind it, tabbing wandered through a page nobody can see or click, and there was no way back out except the mouse.
## One implementation, not six
The overlays are opened and closed by five different modules, which is exactly why they each missed the same things. Rather than patch it into all of them, `modal.js` watches the `hidden` attribute they already toggle — focus in, Tab wrapped, Escape, focus restored, `aria-modal` set while open. **An overlay added later gets all of it without anyone remembering to wire it up**, which is the failure being fixed.
Escape presses the dialog's own cancel button rather than just hiding the overlay, so module cleanup still runs. Verified: Escape on the confirm dialog **resolves its promise to `false`** rather than leaving an `await` hanging forever.
`Shortcuts.overlayOpen()` had the same shape of bug — it listed the six overlay ids by hand. It asks the document now, so a sheet added later can't quietly start letting `f` fold the table behind it.
## Focus you can see
Three focus rules existed in the whole 2700-line stylesheet, so tabbing relied on the browser's default ring — close to invisible on these dark custom-painted controls. `:focus-visible` rather than `:focus`, so a dealer *tapping* Fold isn't left with a ring on it.
That surfaced something small but real: `.holdemBtn` used `transition: all`, so the new outline **faded in over 150ms** and the ring lagged a step behind the Tab key. The transition names its properties now. After: **16/16 tab stops show a ring immediately**, and the press state from #44 still animates.
## Also
- **a mistyped negative stack** read back as negative and flowed into `putIn()`, where `Math.min(amount, -50)` moves a *negative* amount and adds chips to the table. Clamped at zero.
- accessible names for the chip colour, chip value and notes fields, which had none
## Validation
11 new tests (**99 total, all passing**): negative stacks, negative and unreadable blinds, blinds nobody can cover, and saves that come back corrupt, truncated, or from before a field existed.
Playwright: all five overlays re-measured on the real page; shortcuts still fire with no dialog open and are still blocked with one open; touch guarantees from #44 unchanged (0 of 49 controls on `touch-action: auto`, 0 tap-highlighted). No console errors.
One note on process: my first run of the after-measurement showed no change at all. The cause was a stale server — a lingering `dotnet` process whose command line was `.../net8.0/JoshHeaps.Net`, which my `pkill -f "JoshHeaps.Net.dll"` pattern never matched, so Razor kept serving the old compiled page. Worth knowing if the same thing bites you locally.
Measured every overlay in the tool before touching anything. For a dealer running
this on a laptop, all six were broken in the same three ways.
- focus never moved into the dialog: 0 of 5 measured
- Tab walked straight out of the dialog into the page behind the scrim: 5 of 5
- Escape did nothing: only the rulings sheet handled it, 1 of 5
- the pot wizard — the dialog that pays people — had no role and no
accessible name at all
So opening the pot wizard left focus on the button behind it, tabbing wandered
through a page nobody can see or click, and there was no way back out except the
mouse.
One implementation, not six
The overlays are opened and closed by five different modules, which is why they
each missed the same things. Rather than patch this into all of them, modal.js
watches the `hidden` attribute they already toggle: focus moves to the first real
control on open, Tab and Shift-Tab wrap inside the dialog, Escape closes it, and
focus returns to whatever opened it. `aria-modal` is set while open and removed
on close. An overlay added later gets all of it without anyone remembering to
wire it up, which is the failure being fixed.
Escape presses the dialog's own cancel button rather than just hiding the
overlay, so the confirm dialog still resolves its promise — verified: Escape
resolves it to false rather than leaving an await hanging forever.
Shortcuts.overlayOpen() had the same shape of bug, listing the six overlay ids by
hand. It asks the document now, so a sheet added later can't quietly start
letting "f" fold the table behind it.
Focus you can see
Three focus rules existed for the whole stylesheet, so tabbing relied on the
browser default ring, which on these dark custom-painted controls is close to
invisible. `:focus-visible` keeps it to keyboard use so a dealer tapping Fold
isn't left with a ring on it.
That surfaced something small: `.holdemBtn` used `transition: all`, so the new
outline faded in over 150ms and the ring lagged a step behind the Tab key. The
transition names its properties now. Measured after: 16 of 16 tab stops show a
ring immediately, and the press state from before still animates.
Also
- a mistyped negative stack read back as negative, and flowed into putIn() where
Math.min(amount, -50) moves a negative amount and adds chips to the table.
Clamped at zero.
- accessible names for the chip colour, chip value and notes fields, which had none
11 new tests (99 total), covering negative stacks, negative and unreadable
blinds, blinds nobody can cover, and a save that comes back corrupt, truncated,
or from before a field existed.
Playwright: all five overlays re-measured — focus moves in 5/5, Tab and
Shift-Tab trapped 5/5, Escape closes 5/5, focus restored 5/5, role and
aria-modal present 5/5. Shortcuts still fire when no dialog is open and are
still blocked when one is. Touch guarantees from the earlier round unchanged:
0 of 49 controls on touch-action auto, 0 tap-highlighted.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
jheaps
merged commit 76f1952d61 into master2026-09-01 09:30:08 -06:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Measured every overlay before touching anything. For a dealer running this on a laptop, all six were broken the same three ways. Stacked on #46.
What was measured
role+aria-modalOpening the pot wizard — the dialog that pays people — left focus on the button behind it, tabbing wandered through a page nobody can see or click, and there was no way back out except the mouse.
One implementation, not six
The overlays are opened and closed by five different modules, which is exactly why they each missed the same things. Rather than patch it into all of them,
modal.jswatches thehiddenattribute they already toggle — focus in, Tab wrapped, Escape, focus restored,aria-modalset while open. An overlay added later gets all of it without anyone remembering to wire it up, which is the failure being fixed.Escape presses the dialog's own cancel button rather than just hiding the overlay, so module cleanup still runs. Verified: Escape on the confirm dialog resolves its promise to
falserather than leaving anawaithanging forever.Shortcuts.overlayOpen()had the same shape of bug — it listed the six overlay ids by hand. It asks the document now, so a sheet added later can't quietly start lettingffold the table behind it.Focus you can see
Three focus rules existed in the whole 2700-line stylesheet, so tabbing relied on the browser's default ring — close to invisible on these dark custom-painted controls.
:focus-visiblerather than:focus, so a dealer tapping Fold isn't left with a ring on it.That surfaced something small but real:
.holdemBtnusedtransition: all, so the new outline faded in over 150ms and the ring lagged a step behind the Tab key. The transition names its properties now. After: 16/16 tab stops show a ring immediately, and the press state from #44 still animates.Also
putIn(), whereMath.min(amount, -50)moves a negative amount and adds chips to the table. Clamped at zero.Validation
11 new tests (99 total, all passing): negative stacks, negative and unreadable blinds, blinds nobody can cover, and saves that come back corrupt, truncated, or from before a field existed.
Playwright: all five overlays re-measured on the real page; shortcuts still fire with no dialog open and are still blocked with one open; touch guarantees from #44 unchanged (0 of 49 controls on
touch-action: auto, 0 tap-highlighted). No console errors.One note on process: my first run of the after-measurement showed no change at all. The cause was a stale server — a lingering
dotnetprocess whose command line was.../net8.0/JoshHeaps.Net, which mypkill -f "JoshHeaps.Net.dll"pattern never matched, so Razor kept serving the old compiled page. Worth knowing if the same thing bites you locally.