Light up the seat that's holding the game up #57
@@ -582,6 +582,23 @@ button,
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
opacity: 0.32;
|
opacity: 0.32;
|
||||||
transition: opacity 0.15s ease;
|
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 {
|
.dealerChip:focus-visible {
|
||||||
@@ -2155,6 +2172,10 @@ body.hasActionBar #toastHost {
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
cursor: pointer;
|
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) {
|
@media (hover: hover) {
|
||||||
@@ -2872,3 +2893,9 @@ textarea:focus-visible {
|
|||||||
padding: 0.4rem 0.6rem;
|
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
|
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
|
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
|
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
|
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
|
nothing, the other treated low-alpha stops as opaque — so `uiAudit.js` is checked
|
||||||
against known-answer cases before it is believed.
|
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
|
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.
|
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
|
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) ----
|
// ---- Reach (touch target sizes) ----
|
||||||
|
|
||||||
touchTargets(minimum = 44) {
|
// What a fingertip can actually hit, which is not the same as the element's box.
|
||||||
const offenders = this.visibleControls()
|
//
|
||||||
|
// 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 => el.type !== "color")
|
||||||
.filter(el => {
|
.filter(el => !(exempt && el.matches(exempt)))
|
||||||
|
.forEach(el => {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
return rect.height < minimum - 0.5 || rect.width < minimum - 0.5;
|
// 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
|
||||||
.map(el => {
|
// here: they're 76x72 and overlap each other at the rim by design, so
|
||||||
const rect = el.getBoundingClientRect();
|
// measuring their reach would report the crowding as a fault.
|
||||||
return `${this.describe(el)} :: ${Math.round(rect.width)}x${Math.round(rect.height)}`;
|
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 };
|
return { name: `touch targets at least ${minimum}px`, offenders };
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user