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
111 lines
4.5 KiB
JavaScript
111 lines
4.5 KiB
JavaScript
// The keyboard shortcuts, and the one rule that keeps them from stealing a press.
|
|
//
|
|
// Shortcuts talks to `document` directly, so these tests stand a small fake one up
|
|
// rather than a whole DOM: the only things it asks about are what has focus and
|
|
// whether an overlay is open.
|
|
const test = require("node:test");
|
|
const assert = require("node:assert");
|
|
const path = require("path");
|
|
|
|
const Shortcuts = require(path.join(
|
|
__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "js", "casino", "holdem", "shortcuts.js"));
|
|
|
|
// A stand-in for a focused element. `matches` answers the same selector list the
|
|
// real one would, which is the whole of what focusedControlOwnsKey looks at.
|
|
function element(tagName, { selectors = [], isContentEditable = false } = {}) {
|
|
return {
|
|
tagName,
|
|
isContentEditable,
|
|
matches: selector => selectors.some(s => selector.includes(s)),
|
|
};
|
|
}
|
|
|
|
const BUTTON = element("BUTTON", { selectors: ["button"] });
|
|
const SUMMARY = element("SUMMARY", { selectors: ["summary"] });
|
|
const LINK = element("A", { selectors: ["a[href]"] });
|
|
const DIV = element("DIV");
|
|
const TEXT_FIELD = element("INPUT");
|
|
|
|
// Runs one keypress against a fresh Shortcuts and reports which action fired.
|
|
function press(key, { focused = DIV, overlays = [] } = {}) {
|
|
const fired = [];
|
|
let defaultPrevented = false;
|
|
|
|
globalThis.document = {
|
|
activeElement: focused,
|
|
addEventListener() {},
|
|
querySelectorAll: () => overlays,
|
|
};
|
|
|
|
const handlers = {};
|
|
for (const action of Object.values(Shortcuts.KEYS)) {
|
|
handlers[action] = () => { fired.push(action); return true; };
|
|
}
|
|
Shortcuts.handlers = handlers;
|
|
Shortcuts.onKey({ key, preventDefault: () => { defaultPrevented = true; } });
|
|
|
|
return { fired: fired[0] || null, defaultPrevented };
|
|
}
|
|
|
|
test.after(() => { delete globalThis.document; });
|
|
|
|
test("Enter on a focused button is left to the button", () => {
|
|
assert.equal(press("Enter", { focused: BUTTON }).fired, null);
|
|
});
|
|
|
|
test("Space on a focused button is left to the button", () => {
|
|
assert.equal(press(" ", { focused: BUTTON }).fired, null);
|
|
});
|
|
|
|
test("a key left to the control is not swallowed either", () => {
|
|
// preventDefault would stop the browser activating the button, which is the
|
|
// half of the bug that turned "wrong action" into "no action".
|
|
assert.equal(press("Enter", { focused: BUTTON }).defaultPrevented, false);
|
|
});
|
|
|
|
test("summary and links own Enter and Space too", () => {
|
|
assert.equal(press("Enter", { focused: SUMMARY }).fired, null);
|
|
assert.equal(press(" ", { focused: LINK }).fired, null);
|
|
});
|
|
|
|
test("Space still advances when nothing pressable has focus", () => {
|
|
assert.equal(press(" ", { focused: DIV }).fired, "advance");
|
|
});
|
|
|
|
test("Enter still advances when nothing pressable has focus", () => {
|
|
assert.equal(press("Enter", { focused: DIV }).fired, "advance");
|
|
});
|
|
|
|
test("the letter shortcuts stay live with a button focused", () => {
|
|
// A button does nothing with "f", so there is no press to hand back — and a
|
|
// dealer whose hand is on the keys shouldn't lose them to a stray focus ring.
|
|
assert.equal(press("f", { focused: BUTTON }).fired, "fold");
|
|
assert.equal(press("c", { focused: BUTTON }).fired, "checkOrCall");
|
|
assert.equal(press("r", { focused: BUTTON }).fired, "raise");
|
|
assert.equal(press("a", { focused: BUTTON }).fired, "allIn");
|
|
assert.equal(press("u", { focused: BUTTON }).fired, "undo");
|
|
});
|
|
|
|
test("typing still silences everything but Escape", () => {
|
|
assert.equal(press("f", { focused: TEXT_FIELD }).fired, null);
|
|
assert.equal(press("Escape", { focused: TEXT_FIELD }).fired, "cancel");
|
|
});
|
|
|
|
test("an open overlay still owns the keyboard", () => {
|
|
assert.equal(press("f", { focused: DIV, overlays: [{ hidden: false }] }).fired, null);
|
|
assert.equal(press("f", { focused: DIV, overlays: [{ hidden: true }] }).fired, "fold");
|
|
});
|
|
|
|
test("a focused control wins before the overlay check, not after", () => {
|
|
// Both would decline here; the point is that neither path fires an action.
|
|
assert.equal(press("Enter", { focused: BUTTON, overlays: [{ hidden: false }] }).fired, null);
|
|
});
|
|
|
|
test("modified keys are never shortcuts", () => {
|
|
globalThis.document = { activeElement: DIV, addEventListener() {}, querySelectorAll: () => [] };
|
|
const fired = [];
|
|
Shortcuts.handlers = { advance: () => { fired.push("advance"); return true; } };
|
|
Shortcuts.onKey({ key: " ", ctrlKey: true, preventDefault() {} });
|
|
assert.deepEqual(fired, []);
|
|
});
|