Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d66abda05a |
@@ -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