Seven keys drive this tool. Five of them are printed on the control they press -- F on Fold, C on Call, R on Raise, the return arrow on Deal and on Raise to, esc on Cancel. Two were printed on nothing at all: A opens the amount pad on All-in U takes back the last action A dealer who learned F, C and R from the buttons had no way to discover the other two existed. They are the two worth knowing: A is the fastest action in the tool and U is the one that saves a misclick. Both are on their controls now. The key lookup matches any of a button's classes, so the raise pad's four quick amounts can share one class for their looks while only All-in carries a key. And a legend in the sidebar, listing all seven with what they do, built from the same list the handlers are keyed off. There is a test that every key in the map has a line in the legend and that every line names a key that really is bound -- a shortcut with no line is one nobody can find, which is the whole bug this fixes. The panel is hidden on a touch device. A phone has no keys to press, and it is the same pointer question the touch-target rules ask rather than a guess about width. One thing I broke and fixed on the way. .actionKey pins itself to the top-right corner of a big action button. The undo control is a 30px pill with overflow hidden and no positioning context, so the U escaped it and floated above the action bar -- visible in the screenshot before I looked properly. It has its own inline class now, checked at six sizes: the cap sits inside the pill and inside the bar, and the pill is not clipped. 5 tests (249 total). Driven at six sizes across touch and pointer contexts: the undo pill shows U, the raise pad shows A on All-in and nothing on Min, half pot and Pot, the legend lists all seven, and it is display:none on a phone. Contrast clean, layout audit clean but for the landscape phone already known from #63, no console errors. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
98 lines
4.3 KiB
JavaScript
98 lines
4.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",
|
|
},
|
|
|
|
// What each one does, in a dealer's words, for the legend in the sidebar.
|
|
//
|
|
// Lives beside KEYS because the two must not drift: F, C and R are printed on the
|
|
// buttons they drive, but A and U were on nothing at all — a dealer who learned
|
|
// the first three from the bar had no way to find out the other two existed. A
|
|
// shortcut with no line here is a shortcut nobody can find, and there's a test
|
|
// that says so.
|
|
LEGEND: [
|
|
{ keys: ["F"], action: "fold", what: "Fold" },
|
|
{ keys: ["C"], action: "checkOrCall", what: "Check, or call the bet" },
|
|
{ keys: ["R"], action: "raise", what: "Open the amount pad to bet or raise" },
|
|
{ keys: ["A"], action: "allIn", what: "Put this player all-in" },
|
|
{ keys: ["U"], action: "undo", what: "Take back the last action" },
|
|
{ keys: ["Space", "Enter"], action: "advance", what: "Deal, or move to the next street" },
|
|
{ keys: ["Esc"], action: "cancel", what: "Close the amount pad without betting" },
|
|
],
|
|
|
|
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;
|
|
}
|