Found while checking the keyboard could still reach the dealer button: it could reach it and then couldn't press it. Enter and Space were bound to "advance" for the whole document, with an exception only for text fields. Those two keys are how a keyboard presses the control it is sitting on, so every button on the page lost them -- and not to nothing. Measured: Tab to Resolve Pot, press Enter -> the table dealt the next hand Tab to Resolve Pot, press Space -> Resolve Pot, correctly Tab to a dealer button, Enter -> nothing at all The inconsistency is the shortcut handler's return value. It calls preventDefault only when it handled the key, so when "advance" applied it ran instead of the button, and when it didn't apply the browser's own activation got through. A control that does something else every other press is worse than one that does nothing. Enter and Space now go to a focused button, [role=button], link or summary, and the shortcut only claims them when nothing pressable has focus. The letters stay live with a button focused -- a button does nothing with "f", and a dealer whose hand is on the keys shouldn't lose them to a stray focus ring. Space still advances the street: dealt, f, f, c, c, Space -> flop. The action bar rebuilds itself after every press, so focus falls back to the body and the dealer's Space is never quietly aimed at the Fold they just clicked. 11 tests (163 total), and they fail on the old handler -- 4 of them, exactly the Enter/Space-on-a-control cases. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
81 lines
3.3 KiB
JavaScript
81 lines
3.3 KiB
JavaScript
// Keyboard control for a dealer running this on a laptop.
|
|
//
|
|
// The phone has gestures; the desktop had a mouse and nothing else. These are the
|
|
// same actions the bar already offers, reachable without moving a hand to the
|
|
// trackpad between every decision — which is the whole point when the bar is being
|
|
// tapped thirty times a hand.
|
|
const Shortcuts = {
|
|
// Deliberately single letters with no modifier: a dealer's spare hand rests on
|
|
// the keyboard, and a chord is slower than reaching for the button would be.
|
|
KEYS: {
|
|
f: "fold",
|
|
c: "checkOrCall",
|
|
r: "raise",
|
|
a: "allIn",
|
|
" ": "advance",
|
|
enter: "advance",
|
|
u: "undo",
|
|
escape: "cancel",
|
|
},
|
|
|
|
mount(handlers) {
|
|
this.handlers = handlers;
|
|
document.addEventListener("keydown", e => this.onKey(e));
|
|
},
|
|
|
|
// Nothing fires while the dealer is typing a name, an amount, or a note — the
|
|
// "c" in "Cass" must not fold the table.
|
|
isTyping() {
|
|
const el = document.activeElement;
|
|
if (!el) return false;
|
|
const tag = el.tagName;
|
|
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || el.isContentEditable;
|
|
},
|
|
|
|
// An overlay owns the keyboard while it's up. Asking the document which overlays
|
|
// are open, rather than listing them, means a sheet added later can't quietly
|
|
// start letting "f" fold the table behind it — the same reason Modal watches for
|
|
// them instead of being told about each one.
|
|
overlayOpen() {
|
|
return [...document.querySelectorAll('[id$="Overlay"]')].some(el => !el.hidden);
|
|
},
|
|
|
|
// Enter and Space are how a keyboard presses the control it is already sitting on.
|
|
// Claiming them for "advance" didn't just block that press — it ran a different
|
|
// action instead. Measured: Tab to Resolve Pot, press Enter, and the table dealt
|
|
// the next hand. Space on the same button worked, because "advance" happened to
|
|
// decline that time and the browser's own activation got through. A control that
|
|
// does something else every other press is worse than one that does nothing.
|
|
//
|
|
// Only these two keys are handed back. The letter shortcuts stay live with a
|
|
// button focused, because a button does nothing with "f" and the dealer's hand
|
|
// shouldn't have to leave them.
|
|
focusedControlOwnsKey(key) {
|
|
if (key !== "enter" && key !== " ") return false;
|
|
const el = document.activeElement;
|
|
return !!(el && el.matches && el.matches('button, [role="button"], a[href], summary'));
|
|
},
|
|
|
|
onKey(e) {
|
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
|
|
const key = e.key.length === 1 ? e.key.toLowerCase() : e.key.toLowerCase();
|
|
const action = this.KEYS[key];
|
|
if (!action) return;
|
|
|
|
if (this.focusedControlOwnsKey(key)) return;
|
|
|
|
// Escape is the exception: it should still close the raise pad even if focus
|
|
// happens to be sitting in its amount field.
|
|
if (this.isTyping() && !(action === "cancel")) return;
|
|
if (this.overlayOpen()) return;
|
|
|
|
const handled = this.handlers[action] && this.handlers[action]();
|
|
if (handled) e.preventDefault();
|
|
},
|
|
};
|
|
|
|
if (typeof module !== "undefined" && module.exports) {
|
|
module.exports = Shortcuts;
|
|
}
|