The card picker greys out a card that is already dealt and hangs the reason off a `title`. Measured on an iPhone with three hands out: seven of the fifty-two were dead, none of them could take focus, and tapping one changed nothing at all — no toast, no text, not a byte of DOM. The tool's answer to "who has the ace of spades?" existed and was reachable only with a mouse. The header's two buttons were the same, on a new dealer's first screen, and Export CSV was greyed out with no explanation written anywhere. `disabled` is the wrong attribute whenever there is something to say: it drops the control out of the tab order, so a screen reader never lands on it to hear the description, and it eats the tap on a touch screen, where the hover that would have shown the `title` does not exist. `aria-disabled` announces the same state and leaves the control reachable, so one capture listener can turn the press into a sentence — in a toast, or inside a sheet that is standing on top of the toast. It paid for itself in the picker's keyboard too. The arrows hunted past dealt cards because a `disabled` button cannot hold focus, and the hunting did the exact thing the grid's no-wrap rule was written to prevent: Right from A-spades over a dealt A-hearts landed on A-diamonds, so the column shifted and the next press of Down walked a different suit. A cell that can hold focus needs no hunting, and the APG asks for that inside a composite widget anyway. The line that answers is one line by measurement, not by taste. Its box is 318px on a Galaxy S8 at 20px of root text; the first wording drew 332px and wrapped, shoving fifty-two targets 21px down mid-tap, and twenty-four capital Ws draw 434px whatever the wording, so the box is what holds. Two things the sweep found on the way. It opened the card picker by un-hiding an overlay whose grid had never been painted, so for its whole life it audited an empty sheet; it now opens it the way a dealer does, with cards out and the answer showing. And the slot buttons above the grid were named to a screen reader by the em dash they draw when empty. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
133 lines
6.5 KiB
JavaScript
133 lines
6.5 KiB
JavaScript
// The half of the interface the stylesheet does not draw.
|
|
//
|
|
// The browser draws the checkboxes, the menu a <select> opens, the scrollbars, the
|
|
// caret, the spellcheck underline and the autofill wash, and it draws all of them for a
|
|
// light page unless the page says otherwise. This one never did: measured,
|
|
// `color-scheme: normal` on a #141414 ground, with three checkboxes in the Table Setup
|
|
// panel rendering as bright white squares — the loudest objects on the screen, brighter
|
|
// than the gold — and two more in the seat sheet doing the same over the felt.
|
|
//
|
|
// The comment on the pot wizard's hand-painted checkbox called it "the last raw browser
|
|
// control in the tool". Six others were still the platform's, and three of those were
|
|
// still 13x13 against a 24px floor this stylesheet had already written down once and
|
|
// applied to exactly one box.
|
|
//
|
|
// Painting is the expensive way to fix that, and the pot wizard's box shows the bill:
|
|
// a tick drawn as a `background` on a pseudo-element is not a colour forced-colors mode
|
|
// knows how to replace, so it simply disappears — no visible checked state at all, on
|
|
// the dialog that pays the table. Declaring the scheme fixes six controls natively and
|
|
// costs nothing; the seventh stays painted on purpose and pays its own way.
|
|
const test = require("node:test");
|
|
const { describe } = require("node:test");
|
|
const assert = require("node:assert");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const CSS = fs.readFileSync(
|
|
path.join(__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "css", "casino", "holdem.css"), "utf8");
|
|
|
|
const ruleFor = selector => {
|
|
const at = CSS.indexOf(`\n${selector} {`);
|
|
assert.ok(at >= 0, `there is no \`${selector}\` rule`);
|
|
return CSS.slice(at, CSS.indexOf("}", at));
|
|
};
|
|
|
|
// Every block of a given at-rule, braces balanced, joined.
|
|
//
|
|
// This used to take the first one and stop, which held for exactly as long as there was
|
|
// one of them. A second forced-colors block was added nearer the top of the file for a
|
|
// control that dropped out of the OS palette, and the bill-paying test below started
|
|
// reading that block instead and reporting the pot wizard's answers missing — a false
|
|
// failure about a rule nobody had touched. There is no reason a stylesheet should
|
|
// answer a media query in one place, so this asks about all of them.
|
|
const atRule = query => {
|
|
const open = `@media ${query} {`;
|
|
const blocks = [];
|
|
for (let at = CSS.indexOf(open); at >= 0; at = CSS.indexOf(open, at + 1)) {
|
|
let depth = 0;
|
|
for (let i = CSS.indexOf("{", at); i < CSS.length; i++) {
|
|
if (CSS[i] === "{") depth++;
|
|
else if (CSS[i] === "}" && --depth === 0) { blocks.push(CSS.slice(at, i + 1)); break; }
|
|
}
|
|
assert.ok(blocks.length && depth === 0, `\`${open}\` never closes`);
|
|
}
|
|
assert.ok(blocks.length, `there is no \`@media ${query}\` block`);
|
|
return blocks.join("\n");
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("the page says what colour it is", () => {
|
|
const root = ruleFor(":root");
|
|
|
|
test("dark, to the browser and not only to the eye", () => {
|
|
assert.match(root, /color-scheme:\s*dark/);
|
|
});
|
|
|
|
// The checked colour is the green this tool already uses for yes, so the one
|
|
// painted checkbox and the six native ones agree rather than nearly agree.
|
|
test("and the checked colour is the tool's own green, not the platform's blue", () => {
|
|
assert.match(root, /--accent-on:\s*#1e8449/);
|
|
assert.match(root, /accent-color:\s*var\(--accent-on\)/);
|
|
assert.match(ruleFor(".potWizardChoice input:checked"), /background:\s*#1e8449/);
|
|
});
|
|
});
|
|
|
|
describe("a checkbox is one size", () => {
|
|
test("every one of them, not the one that was noticed", () => {
|
|
assert.match(ruleFor('input[type="checkbox"]'), /width:\s*24px/);
|
|
assert.match(ruleFor('input[type="checkbox"]'), /height:\s*24px/);
|
|
});
|
|
|
|
test("and no box carries a size of its own any more", () => {
|
|
assert.ok(!/#dedicatedDealerToggle\s*\{/.test(CSS),
|
|
"one checkbox is still sized by id, which is how five got left behind");
|
|
assert.ok(!/width:\s*22px/.test(ruleFor(".potWizardChoice input")),
|
|
"the painted box restates its size instead of taking the one above");
|
|
});
|
|
|
|
// A stated width is only a suggestion to a flex item. The coarse-pointer block had
|
|
// already been bitten by this and said so; the mouse case had not.
|
|
test("a swatch in a crowded row keeps the width it was given", () => {
|
|
assert.match(ruleFor(".chipColorInput"), /flex:\s*none/);
|
|
});
|
|
});
|
|
|
|
describe("what painting a control costs, and who pays it", () => {
|
|
// If a future round paints another one, this is the bill it has to settle too.
|
|
test("nothing gives up the platform's drawing without answering forced colours", () => {
|
|
const forced = atRule("(forced-colors: active)");
|
|
const painted = [...CSS.matchAll(/\n([^\n{}]+)\s*\{[^}]*appearance:\s*none/g)]
|
|
.map(m => m[1].trim());
|
|
assert.ok(painted.length > 0, "nothing is painted, so this test is measuring nothing");
|
|
painted.forEach(selector => {
|
|
assert.ok(forced.includes(selector),
|
|
`\`${selector}\` drops the platform's control but has no forced-colors answer`);
|
|
});
|
|
});
|
|
|
|
// A tick drawn as a background is gone in forced-colors mode; a system keyword is
|
|
// the only colour guaranteed to survive, because it *is* the palette.
|
|
test("the tick that is drawn rather than typed survives the OS repainting it", () => {
|
|
const forced = atRule("(forced-colors: active)");
|
|
assert.match(forced, /\.potWizardChoice input::before\s*\{[^}]*background:\s*CanvasText/);
|
|
});
|
|
|
|
test("and so does the row's own chosen state, which is a gradient", () => {
|
|
const forced = atRule("(forced-colors: active)");
|
|
assert.match(forced, /\.potWizardChoice\.chosen\s*\{[^}]*outline:\s*2px solid Highlight/);
|
|
});
|
|
});
|
|
|
|
describe("the other page of the same tool", () => {
|
|
const QUICKSPLIT = fs.readFileSync(
|
|
path.join(__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "css", "casino", "quicksplit.css"), "utf8");
|
|
|
|
// It is the darker of the two pages and is opened from the felt mid-hand. It has no
|
|
// checkboxes to go white, but scrollbars, caret and selection were still drawn for
|
|
// a light page.
|
|
test("Quick Split says what colour it is too", () => {
|
|
assert.match(QUICKSPLIT, /:root\s*\{[^}]*color-scheme:\s*dark/);
|
|
});
|
|
});
|