fix(mobile): suppress iOS long-press text-selection over the game surface (#60) #61
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/60-canvas-text-selection"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Fixes #60. On iPhone Safari, a touch-and-hold on the canvas / game area triggers the iOS text-selection gesture — a selection rectangle + loupe appear over the play field mid-game (operator playtest, 2026-06-21). This scopes selection/callout/tap-flash suppression to the game surface.
Approach — why
#app, not enumerated elementsThe tracker suggested
canvas, .game-container, body. I scoped to the game root#appinstead, because the suppression properties (user-select,-webkit-touch-callout,-webkit-tap-highlight-color) inherit, so a single root rule cascades tocanvas,#game-wrap, and#touch-controlswithout enumerating them.The AC3 "keep legitimate text selectable" constraint is satisfied for free by this scope:
?debug=audiooverlay — and it's appended todocument.bodyoutside#app, so it never inherits the suppression (verifieduser-select:auto).#app.When the enumerated form would be right instead: if
#appever gains a real DOM text region meant to stay selectable (e.g. a future HTML leaderboard or about-panel), the root-scope would over-suppress it — at that point switch to enumerating the game-surface elements (canvas +#game-wrap+#touch-controls) and leave the text region out. Today, root-scope is both simpler and correct.Diff
12 lines added to
client/index.html— one inherited rule block on#app:(
touch-action: nonewas already on the canvas, so double-tap-zoom / scroll-on-swipe were already handled.)Verification
Playwright iPhone-13 (
/tmp/cb60-verify.js, ephemeral):user-select:none+ transparent tap-highlight computed on#app,canvas(inherited),#touch-controls(inherited). ✅?debug=audiooverlay computeduser-select:autoand confirmed outside#app(closest('#app')null). ✅Honest boundary:
-webkit-touch-callout: noneis a Safari-only property — Chromium'sgetComputedStylenever reports it, so it cannot be asserted in the headless probe. It is grep-confirmed present in the built bundle (dist/index.html), and its behavioral effect (no long-press callout / loupe) is part of the iOS operator device loop — same instrument-vs-confirmation split as #58. The Chromium-observable suppression is what the probe proves; the iOS-only behavior is the operator's to confirm on-device.Scope
First of the mobile-session batch (#60 → #57 → #59). Composes cleanly with #57's upcoming layout work — this adds selection-suppression properties; #57 changes sizing/layout rules, no line overlap.
🤖 Generated with Claude Code
Surveyor review — APPROVE ✅
Verified at head
533fc39f4cfbaf3d0a8273a49c816ad838f71973, merge-base = current main tipa9fe11fd(clean, on current main). CSS-only, 12 lines on#app.The scope call is not just defensible — it's better than the tracker's enumeration. The tracker enumerated
canvas, .game-container, body; scoping to#app+ inheritance is superior precisely because it does not touchbody— so the?debug=audiooverlay (appended todocument.body, outside#app— verified in #58) keepsuser-select: autoand stays selectable. The enumerated…bodyform would have suppressed it. AC3 ("keep legit text selectable") is satisfied because of the narrower scope, not in spite of it. (implementer-extends-recommendation — a defensibly-better category call than the tracker's.)AC3 skeptic's pass (the load-bearing surface) — done independently: I enumerated every node inside
#app:canvas#board— game pixels, no DOM text#audio-hud—btn-mute(button label), twotype="range"sliders (no text content;user-selectis a no-op on a range thumb), a♪glyph span#touch-controls— all<button>labelsThere is no
type="text"input, no<textarea>, nocontenteditableanywhere in the client (grep-confirmed) — name/initials entry is canvas-drawn (keydown→canvas), so "canvas pixels, not DOM text" holds. Nothing legitimately-selectable or editable lives inside#app. The suppression breaks nothing.Verified (desktop-reachable axes):
vite buildclean. The inline<style>is kept verbatim indist/index.html;-webkit-touch-callout: noneconfirmed present in the built bundle at the#apprule (the Safari-only property the probe structurally can't assert — grep-verified as the PR states)./tmp/cb60-verify.js, Playwrightdevices['iPhone 13']):#app/canvas/#touch-controlsall computeuser-select: none(canvas + touch via inheritance ✓) andtap-highlight: rgba(0,0,0,0); the debug overlayexists: true, inApp: false, userSelect: auto— outside#app, stays selectable. Zero page errors. AC3-both-directions empirically confirmed (suppressed where it should be; selectable where it must be).Honest boundary handled right (probe-gated-merge-separation, same split as #58): the Chromium-observable bits (user-select / tap-highlight computed + overlay scope) are proven on desktop; the Safari-only long-press loupe/callout behavior is operator-device-gated — that verdict stays with the iOS device loop, and my stamp doesn't claim it. The mechanism is wired + scoped correctly, which is what desktop can prove.
must-fix: none. should-consider: none beyond the now-familiar uncommitted-probe note (
/tmp/cb60-verify.js) — same deferrable class as #58, where the operator device loop is the real gate and the probe is a desktop regression+scope check; consistent with the throwaway-probe convention you documented there. nit (precision):-webkit-tap-highlight-color: transparentalready existed on a control rule (built line 160) pre-PR; #61 broadens it to the whole surface — worth knowing it's a broadening, not a net-new suppression.Clean to merge. Stamping APPROVED below, pinned to the verified head. The iOS loupe/callout verdict stays with the operator's device loop.
APPROVED at head
533fc39f4cfbaf3d0a8273a49c816ad838f71973(on current main). CSS-only. AC3 skeptic's pass done independently — no text input / textarea / contenteditable anywhere in #app (only range sliders + button labels + canvas), so suppression breaks nothing; debug overlay is outside #app and stays selectable. The #app-scope is superior to the tracker's…bodyenumeration (preserves AC3 by not touching body). vite build clean +-webkit-touch-callout: noneconfirmed in the built bundle (the Safari-only prop the probe can't assert). iPhone-13 probe PASS (user-select:none on #app/canvas/touch via inheritance, tap-highlight transparent, overlay userSelect auto + outside #app, zero errors). The Safari-only loupe/callout behavior is operator-device-gated; this stamp doesn't claim it. Substance in the issue-comment above. Clean to merge.