// 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; }