Make the dealer puck hittable without making it bigger
Ran the touch-target check that had been sitting in the audit unused. The dealer puck is 22x22 — half what a fingertip needs — in ten places on the table, and tapping it hands the button to that seat and re-posts the blinds. Not a cosmetic control. It stays 22x22. A puck should look like a puck sitting on the rim of its seat, and growing it would take area from the seat itself, which a dealer taps far more often. Instead an invisible pad around it catches the tap: 43x44 hittable on every phone size, close to four times the area, with the pad pushed down and left away from the seat the chip juts towards. Verified that no seat tap is stolen — the occlusion audit is clean at every size. Three things the check itself got wrong, all found before acting on them It read element boxes, and boxes lie in both directions. A control padded out with a transparent ::after responds well past its painted size, and a checkbox wrapped in a label is hit by tapping anywhere on the label — so it reported the dedicated dealer toggle as a 24px fault when it sits inside a 320x44 label that already forwards the tap. It probes outward with elementFromPoint now, which is what a finger actually experiences. The probe walked a fixed 40px each way, so everything wider than 80px reported exactly 80. Every header button came back a suspiciously round "80x42". And it enforced 44px on desktop, where there is no fingertip. Those same header buttons are 64px tall on a phone and 42px with a mouse, which is not a defect. Keyed off hover:none now, the same signal used for the tablet field sizing. Also removed a ::after pad on the dedicated-dealer checkbox that was both inert -- pseudo-elements don't render on replaced elements -- and unnecessary, since the label already does the job. The puck is the one documented exemption: it is deliberately small and "Give the button" is in the seat menu at full size, so it's a shortcut rather than the only way through. The reason sits in the code next to the exemption. Phone sizes: dealer chip 22x22 drawn / 43x44 hittable, zero undersized controls even with the exemption and the pointer rule forced off. Layout and contrast audits clean at all seven sizes. 137 tests passing. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
@@ -582,6 +582,23 @@ button,
|
||||
z-index: 2;
|
||||
opacity: 0.32;
|
||||
transition: opacity 0.15s ease;
|
||||
/* A dealer puck should look like a puck — small, sitting on the rim of its seat.
|
||||
But tapping it hands the button to that seat and re-posts the blinds, and 22px
|
||||
is half the 44px a fingertip needs. The hit area grows without the chip doing
|
||||
so: the visual stays 22px and an invisible pad around it catches the tap.
|
||||
Deliberately not centred on the chip — it's pushed down and left, away from the
|
||||
neighbouring seat the chip juts towards, so the bigger target can't start
|
||||
stealing taps meant for the seat next door. */
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.dealerChip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
right: -7px;
|
||||
bottom: -15px;
|
||||
left: -14px;
|
||||
}
|
||||
|
||||
.dealerChip:focus-visible {
|
||||
@@ -2155,6 +2172,10 @@ body.hasActionBar #toastHost {
|
||||
font-size: 0.8rem;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
/* Same treatment as the dealer puck: a 30px dismiss button in a toast a dealer is
|
||||
swatting away mid-hand, given a fingertip's worth of hit area without becoming
|
||||
a bigger piece of furniture. */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
@@ -2872,3 +2893,9 @@ textarea:focus-visible {
|
||||
padding: 0.4rem 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.toastClose::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -7px;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ in a browser: whether a dealer can actually reach every control.
|
||||
node tests/holdem/runUiAudit.js # needs Playwright and the site running
|
||||
```
|
||||
|
||||
It checks WCAG AA contrast, touch-action and tap highlights, that fields stay selectable and are large
|
||||
It checks WCAG AA contrast, touch target sizes, touch-action and tap highlights, that fields stay selectable and are large
|
||||
enough not to trigger iOS zoom, that nothing important is covered or off-screen with
|
||||
no way to scroll to it, that no grid column has collapsed to nothing, and that no
|
||||
text is painted outside its box. It runs over the table, all five overlays and Quick
|
||||
@@ -51,6 +51,12 @@ directions — one read only `background-color` and scored text on a gradient ag
|
||||
nothing, the other treated low-alpha stops as opaque — so `uiAudit.js` is checked
|
||||
against known-answer cases before it is believed.
|
||||
|
||||
Touch targets are measured by probing outward with `elementFromPoint` rather than by
|
||||
reading the element box, because the box lies in both directions: a small control can
|
||||
be padded out with a transparent `::after` and respond well beyond its painted size,
|
||||
and a checkbox inside a label is hit by tapping the label. The 44px rule is only
|
||||
enforced where `hover: none` says there's no mouse.
|
||||
|
||||
It exists because these checks were being re-typed into a console every time
|
||||
something changed, and each time they only covered whatever had just been touched.
|
||||
That is how the standings table shipped with a player-name column zero pixels wide
|
||||
|
||||
+76
-8
@@ -386,16 +386,84 @@ const LayoutAudit = {
|
||||
|
||||
// ---- Reach (touch target sizes) ----
|
||||
|
||||
touchTargets(minimum = 44) {
|
||||
const offenders = this.visibleControls()
|
||||
// What a fingertip can actually hit, which is not the same as the element's box.
|
||||
//
|
||||
// Two things make the box misleading, and the first version of this check was
|
||||
// fooled by both. A control can be padded out with a transparent ::after, so it
|
||||
// responds well beyond its painted size — that's how a 22px dealer puck should
|
||||
// stay 22px and still take a thumb. And a checkbox wrapped in a label is hit by
|
||||
// tapping anywhere on the label, so a 24px box inside a 320x44 label is a
|
||||
// perfectly good target. Measuring the box reported both as faults.
|
||||
//
|
||||
// So it probes outwards with elementFromPoint until the point stops landing on the
|
||||
// control, which is the thing a finger actually experiences.
|
||||
effectiveTarget(el, limit = 48) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const cx = rect.left + rect.width / 2;
|
||||
const cy = rect.top + rect.height / 2;
|
||||
// The probe walks outward a fixed distance, so it can only ever report
|
||||
// 2 x limit. Anything already drawn larger than that reads as exactly the cap
|
||||
// — which had every header button reporting a suspiciously round 80px. Give
|
||||
// the reach room for the element itself plus the padding being looked for.
|
||||
limit = Math.max(limit, rect.width / 2 + 24, rect.height / 2 + 24);
|
||||
|
||||
// A wrapping or associated label forwards the tap, so it counts as the target.
|
||||
const label = el.closest("label") || (el.labels && el.labels[0]);
|
||||
const lands = (x, y) => {
|
||||
if (x < 0 || y < 0 || x >= innerWidth || y >= innerHeight) return false;
|
||||
const hit = document.elementFromPoint(x, y);
|
||||
if (!hit) return false;
|
||||
if (hit === el || el.contains(hit)) return true;
|
||||
return !!(label && (hit === label || label.contains(hit)));
|
||||
};
|
||||
|
||||
if (!lands(cx, cy)) return null; // centre isn't even hittable; occlusion's problem
|
||||
|
||||
const reach = (dx, dy) => {
|
||||
let far = 0;
|
||||
for (let d = 1; d <= limit; d++) {
|
||||
if (!lands(cx + dx * d, cy + dy * d)) break;
|
||||
far = d;
|
||||
}
|
||||
return far;
|
||||
};
|
||||
return { width: reach(-1, 0) + reach(1, 0), height: reach(0, -1) + reach(0, 1) };
|
||||
},
|
||||
|
||||
// The 44px guideline is about fingertips, so it only bites where there's no mouse.
|
||||
// Enforcing it everywhere flagged the header buttons at 42px tall on a desktop,
|
||||
// which is not a defect — the same buttons are 64px tall on a phone, where it
|
||||
// matters. Same reasoning as the field sizing: `hover: none` is what actually
|
||||
// distinguishes a finger from a pointer, not viewport width.
|
||||
//
|
||||
// `exempt` is for controls that are deliberately small *and* have a full-size path
|
||||
// to the same action. The dealer puck is the only one: it's a 22px chip sitting on
|
||||
// the rim of a seat, and growing it to 44px would take that area from the seat
|
||||
// itself — which a dealer taps far more often. "Give the button" is in the seat
|
||||
// menu at full size, so the puck is a shortcut, not the only way through.
|
||||
touchTargets(minimum = 44, { exempt = ".dealerChip", enforceOnPointerDevices = false } = {}) {
|
||||
const offenders = [];
|
||||
if (!enforceOnPointerDevices && matchMedia("(hover: hover)").matches) {
|
||||
return { name: `touch targets at least ${minimum}px`, offenders, skipped: "pointer device" };
|
||||
}
|
||||
this.visibleControls()
|
||||
.filter(el => el.type !== "color")
|
||||
.filter(el => {
|
||||
.filter(el => !(exempt && el.matches(exempt)))
|
||||
.forEach(el => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return rect.height < minimum - 0.5 || rect.width < minimum - 0.5;
|
||||
})
|
||||
.map(el => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return `${this.describe(el)} :: ${Math.round(rect.width)}x${Math.round(rect.height)}`;
|
||||
// Drawn big enough is big enough — no need to probe, and probing wide
|
||||
// elements is where the cap above used to produce nonsense. Seats sit
|
||||
// here: they're 76x72 and overlap each other at the rim by design, so
|
||||
// measuring their reach would report the crowding as a fault.
|
||||
if (rect.width >= minimum - 1 && rect.height >= minimum - 1) return;
|
||||
|
||||
const size = this.effectiveTarget(el);
|
||||
if (!size) return;
|
||||
if (size.width >= minimum - 1 && size.height >= minimum - 1) return;
|
||||
offenders.push(
|
||||
`${this.describe(el)} :: hittable ${size.width}x${size.height}` +
|
||||
` (drawn ${Math.round(rect.width)}x${Math.round(rect.height)})`
|
||||
);
|
||||
});
|
||||
return { name: `touch targets at least ${minimum}px`, offenders };
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user