Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a2fc3e540 | ||
|
|
b64da7a787 |
@@ -3138,7 +3138,18 @@ body.hasActionBar #toastHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panelBody .holdemBtnSmall,
|
.panelBody .holdemBtnSmall,
|
||||||
.handHistoryActions .holdemBtn {
|
.handHistoryActions .holdemBtn,
|
||||||
|
/* The card picker's Clear and Done sat at 40px: the touch rule was scoped to
|
||||||
|
.panelBody and the picker is a sheet, so it never reached them. Clear wipes a
|
||||||
|
player's cards, which is not a thing to make a dealer aim at. */
|
||||||
|
.cardPickerHeader .holdemBtn,
|
||||||
|
/* Undo was 30px, and it is the control a dealer reaches for having just recorded
|
||||||
|
the wrong thing with five people waiting — the worst place to be fourteen pixels
|
||||||
|
short of a fingertip. Growing the box costs bar height, which comes out of the
|
||||||
|
felt, but measured at 390x844 the felt is 366px against a 220px floor, so there
|
||||||
|
is room for it. A hit-area pad was tried first and does not work here: the pill
|
||||||
|
clips its overflow to keep a long label on one line, which clips the pad too. */
|
||||||
|
.actionUndo {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3525,7 +3536,9 @@ textarea:focus-visible {
|
|||||||
|
|
||||||
.panelBody .holdemBtnSmall,
|
.panelBody .holdemBtnSmall,
|
||||||
.handHistoryActions .holdemBtn,
|
.handHistoryActions .holdemBtn,
|
||||||
.toastAction {
|
.toastAction,
|
||||||
|
.cardPickerHeader .holdemBtn,
|
||||||
|
.actionUndo {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+82
-4
@@ -93,10 +93,44 @@ const LayoutAudit = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// The modal that is currently on top, if any.
|
||||||
|
//
|
||||||
|
// Every check below asks "is this thing reachable", and while a sheet or dialog is
|
||||||
|
// open the answer for everything behind it is legitimately no — that is what a
|
||||||
|
// modal is for. Without knowing that, the audit reported the pot wizard covering
|
||||||
|
// the seats as ten faults and the rulings sheet as seven, which is the noise that
|
||||||
|
// made it useless in exactly the states nobody had audited. Now it scopes to what
|
||||||
|
// is actually on top and says so.
|
||||||
|
MODAL_SELECTOR: "#potWizardOverlay, #rulingsOverlay, #seatMenuOverlay, #cardPickerOverlay, " +
|
||||||
|
"#confirmOverlay, #addChipsOverlay, #seatEditorOverlay",
|
||||||
|
|
||||||
|
openModal() {
|
||||||
|
const open = [...document.querySelectorAll(this.MODAL_SELECTOR)]
|
||||||
|
.filter(el => !el.hidden && getComputedStyle(el).display !== "none");
|
||||||
|
// Last in the DOM wins, which is how they stack.
|
||||||
|
return open.length > 0 ? open[open.length - 1] : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// A seat sheet on a phone isn't an overlay element, it's a body class — but it
|
||||||
|
// covers the felt just the same.
|
||||||
|
modalScope() {
|
||||||
|
const overlay = this.openModal();
|
||||||
|
if (overlay) return overlay;
|
||||||
|
if (document.body.classList.contains("seatSheetOpen")) {
|
||||||
|
return document.getElementById("seatEditorPanel") || null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
occlusion(selector = "#actionBar button, #holdemHeader button, .seat") {
|
occlusion(selector = "#actionBar button, #holdemHeader button, .seat") {
|
||||||
const offenders = [];
|
const offenders = [];
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
[...document.querySelectorAll(selector)].forEach(el => {
|
// While something is open on top, only what is inside it can be judged. The
|
||||||
|
// rest of the page is meant to be unreachable.
|
||||||
|
const scope = this.modalScope();
|
||||||
|
const candidates = [...document.querySelectorAll(selector)]
|
||||||
|
.filter(el => !scope || scope.contains(el));
|
||||||
|
candidates.forEach(el => {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
if (rect.width === 0 || rect.height === 0) return;
|
if (rect.width === 0 || rect.height === 0) return;
|
||||||
|
|
||||||
@@ -504,10 +538,25 @@ const LayoutAudit = {
|
|||||||
if (!enforceOnPointerDevices && matchMedia("(hover: hover)").matches) {
|
if (!enforceOnPointerDevices && matchMedia("(hover: hover)").matches) {
|
||||||
return { name: `touch targets at least ${minimum}px`, offenders, skipped: "pointer device" };
|
return { name: `touch targets at least ${minimum}px`, offenders, skipped: "pointer device" };
|
||||||
}
|
}
|
||||||
|
const unmeasured = [];
|
||||||
this.visibleControls()
|
this.visibleControls()
|
||||||
.filter(el => el.type !== "color")
|
.filter(el => el.type !== "color")
|
||||||
.filter(el => !(exempt && el.matches(exempt)))
|
.filter(el => !(exempt && el.matches(exempt)))
|
||||||
.forEach(el => {
|
.forEach(el => {
|
||||||
|
// A control inside a scrolling sheet can sit below its container's
|
||||||
|
// visible area while still having a viewport rect. elementFromPoint at
|
||||||
|
// its centre then returns the sheet, effectiveTarget gives up, and the
|
||||||
|
// check used to pass it in silence — which is how "Confirm Winner(s)",
|
||||||
|
// the button that pays the pot out, went unmeasured for 36 rounds.
|
||||||
|
// Bring it into view before probing.
|
||||||
|
const scroller = el.closest("[style*='overflow'], .modalBody, #potWizardBody, #rulingsSheet, #seatEditorPanel");
|
||||||
|
if (scroller && scroller.scrollHeight > scroller.clientHeight) {
|
||||||
|
const box = scroller.getBoundingClientRect();
|
||||||
|
const own = el.getBoundingClientRect();
|
||||||
|
if (own.bottom > box.bottom || own.top < box.top) {
|
||||||
|
el.scrollIntoView({ block: "center" });
|
||||||
|
}
|
||||||
|
}
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
// Drawn big enough is big enough — no need to probe, and probing wide
|
// 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
|
// elements is where the cap above used to produce nonsense. Seats sit
|
||||||
@@ -515,15 +564,35 @@ const LayoutAudit = {
|
|||||||
// measuring their reach would report the crowding as a fault.
|
// measuring their reach would report the crowding as a fault.
|
||||||
if (rect.width >= minimum - 1 && rect.height >= minimum - 1) return;
|
if (rect.width >= minimum - 1 && rect.height >= minimum - 1) return;
|
||||||
|
|
||||||
|
// A checkbox inside a label is tapped by tapping the label, so the
|
||||||
|
// label is the target. Measured rather than probed, because probing
|
||||||
|
// fails on anything scrolled off the viewport — which is how a 24px
|
||||||
|
// checkbox with a 335x44 label around it came up as unprobeable
|
||||||
|
// instead of as the perfectly good target it is.
|
||||||
|
const label = el.closest("label");
|
||||||
|
if (label) {
|
||||||
|
const lr = label.getBoundingClientRect();
|
||||||
|
if (lr.width >= minimum - 1 && lr.height >= minimum - 1) return;
|
||||||
|
}
|
||||||
|
|
||||||
const size = this.effectiveTarget(el);
|
const size = this.effectiveTarget(el);
|
||||||
if (!size) return;
|
// Still can't measure it. That is not a pass — it is the check failing
|
||||||
|
// to run, and quietly returning here is exactly the kind of exemption
|
||||||
|
// that hides real faults. Say so instead.
|
||||||
|
if (!size) {
|
||||||
|
unmeasured.push(
|
||||||
|
`${this.describe(el)} :: could not be probed` +
|
||||||
|
` (drawn ${Math.round(rect.width)}x${Math.round(rect.height)})`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (size.width >= minimum - 1 && size.height >= minimum - 1) return;
|
if (size.width >= minimum - 1 && size.height >= minimum - 1) return;
|
||||||
offenders.push(
|
offenders.push(
|
||||||
`${this.describe(el)} :: hittable ${size.width}x${size.height}` +
|
`${this.describe(el)} :: hittable ${size.width}x${size.height}` +
|
||||||
` (drawn ${Math.round(rect.width)}x${Math.round(rect.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, unmeasured };
|
||||||
},
|
},
|
||||||
|
|
||||||
// Tapping the middle of a seat must give you that seat.
|
// Tapping the middle of a seat must give you that seat.
|
||||||
@@ -537,6 +606,13 @@ const LayoutAudit = {
|
|||||||
// Reported separately from occlusion because the cause is different: not an
|
// Reported separately from occlusion because the cause is different: not an
|
||||||
// overlay on top of the table, but two pieces of the table fighting over a tap.
|
// overlay on top of the table, but two pieces of the table fighting over a tap.
|
||||||
seatOwnership() {
|
seatOwnership() {
|
||||||
|
// Seats behind an open sheet aren't fighting each other for taps — nothing is
|
||||||
|
// meant to reach them at all. Asking the question there produced six offenders
|
||||||
|
// per modal, every one of them the modal doing its job.
|
||||||
|
if (this.modalScope()) {
|
||||||
|
return { name: "a seat's centre belongs to that seat", offenders: [],
|
||||||
|
skipped: "a sheet is open over the table" };
|
||||||
|
}
|
||||||
const seats = [...document.querySelectorAll(".seat")];
|
const seats = [...document.querySelectorAll(".seat")];
|
||||||
const offenders = [];
|
const offenders = [];
|
||||||
|
|
||||||
@@ -617,7 +693,9 @@ const LayoutAudit = {
|
|||||||
viewport: `${innerWidth}x${innerHeight}`,
|
viewport: `${innerWidth}x${innerHeight}`,
|
||||||
failed: checks.filter(c => c.offenders.length > 0),
|
failed: checks.filter(c => c.offenders.length > 0),
|
||||||
// Things worth knowing that aren't faults — see occlusion() on toasts.
|
// Things worth knowing that aren't faults — see occlusion() on toasts.
|
||||||
warnings: checks.flatMap(c => c.warnings || []),
|
// Controls a check couldn't probe go here too: not a fault, but not a pass
|
||||||
|
// either, and a silent skip is how faults hide.
|
||||||
|
warnings: checks.flatMap(c => [...(c.warnings || []), ...(c.unmeasured || [])]),
|
||||||
passed: checks.filter(c => c.offenders.length === 0).map(c => c.name),
|
passed: checks.filter(c => c.offenders.length === 0).map(c => c.name),
|
||||||
clean: checks.every(c => c.offenders.length === 0),
|
clean: checks.every(c => c.offenders.length === 0),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user