Give the seat holding the button its own middle back
On a fresh table the button is on seat 1, and seat 1 is where the first player goes. Tapping it did nothing. The dealer chip is a 22px disc, and a 22px disc is half the 44px a fingertip needs, so it carried an invisible pad to catch the tap. The pad was moved twice looking for somewhere to grow that wasn't already spoken for: outward first, where it hung over the seat next door and gave that seat the button; then inward, over "its own seat, which is a large target that can spare the corner". The seat could not spare it. Measured on a 390px phone, a 72x46 puck: the pad ran from the puck's own centre to its right edge and from its top to 3px off its bottom, and the chip's handler calls stopPropagation. The seat holding the button kept 52% of its face — 130 of 276 sampled points went to the chip, its exact middle among them. Every other seat kept 97%. The button moves every hand, so the dead puck went round the table with it. And the chip it went to had nothing to say. Its only answer is "give the button to this seat", and this is the seat that has it. The seat menu already knew that and hides "Give the button" there; mid-hand the same no-op would have taken the blinds off the felt and re-posted the same chips to announce that nothing happened. So the pad is gone rather than moved again — a fixed pad cannot be safe at every puck size, and no finger reaches any chip now: the ghosts are pointer-events: none without hover, and the one live chip is this one, which is now inert to the pointer and still there to read. Giving the button is the long-press menu on touch and the revealed ghost on a mouse, both unchanged. makeDealer returns early on the seat that already has it, so the keyboard can't re-post blinds to move the button nowhere either. The sweep had a check for exactly this — "a seat's centre belongs to that seat", written when the pad was reaching outward — and it ran clean through all of it, because `seat.contains(hit)` waves through anything inside the seat and the chip is inside the seat. The check written for this bug could not see the second half of it. A control inside a seat is a rival for the tap now, and against the old stylesheet it fails seven scenarios. 982 unit tests, 6 of them new; the layout sweep is 320 audits with the one pre-existing failure (a $1,300 chip behind seat 11 on a twelve-handed 360x640 all-in). Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
+27
-3
@@ -897,6 +897,14 @@ const LayoutAudit = {
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// It then made the same mistake one level down. `seat.contains(hit)` waved through
|
||||
// anything inside the seat, and the dealer chip is inside the seat: when its hit
|
||||
// pad was moved *inward* to stop it reaching the neighbours, it covered its own
|
||||
// seat's middle instead and this check called that fine. The chip's handler calls
|
||||
// stopPropagation, so those taps reached nothing — on a fresh table the button is
|
||||
// on seat 1, and seating the first player was the one thing on the felt that did
|
||||
// not work. A control inside the seat is a rival for the tap, not part of the seat.
|
||||
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
|
||||
@@ -916,13 +924,29 @@ const LayoutAudit = {
|
||||
if (x < 0 || y < 0 || x >= innerWidth || y >= innerHeight) return;
|
||||
|
||||
const hit = document.elementFromPoint(x, y);
|
||||
if (!hit || seat.contains(hit) || hit === seat) return;
|
||||
if (!hit) return;
|
||||
|
||||
// The nearest control at that point, if any. A control is a rival for the
|
||||
// tap wherever it lives — the seat's own chip swallows the event as surely
|
||||
// as the seat next door's does.
|
||||
const CONTROLS = "button, a, input, select, textarea, [role='button']";
|
||||
const rival = hit.closest ? hit.closest(CONTROLS) : null;
|
||||
|
||||
if (rival && seat.contains(rival)) {
|
||||
offenders.push(
|
||||
`seat ${index + 1}'s centre goes to its own ${this.describe(rival)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Its own name, stack or felt. That is the seat, and a tap there is a tap
|
||||
// on the seat.
|
||||
if (!rival && (hit === seat || seat.contains(hit))) return;
|
||||
|
||||
const other = hit.closest(".seat");
|
||||
const who = other ? `seat ${seats.indexOf(other) + 1}` : this.describe(hit);
|
||||
const control = hit.matches && hit.matches("button, a, input, select, [role='button']");
|
||||
offenders.push(
|
||||
`seat ${index + 1}'s centre goes to ${who}${control ? "'s " + this.describe(hit) : ""}`
|
||||
`seat ${index + 1}'s centre goes to ${who}${rival ? "'s " + this.describe(rival) : ""}`
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user