Files
picpeak/frontend
Paul Nothaft 0b6b8fbdb0 fix(gallery): follow the input in use, not the device's primary pointer
Closes #1275. Follow-up to #1263.

`matchMedia('(hover: none) and (pointer: coarse)')` answers "what is this
device's primary pointer", which on anything with both inputs is the wrong
question. A touchscreen laptop reports fine+hover, so a finger tap was handled
as a click: the photo opened with no reveal step and the tile's own actions
needed a hover a finger cannot produce. An iPad with a trackpad reports the
opposite, so a mouse click was handled as a tap and opening a photo took two of
them while hovering did nothing.

Pointer events carry the answer per interaction. useInputMode holds one
module-level mode fed by a single window-level listener pair, so every tile
agrees and the listener count does not scale with the grid. The primary-pointer
query stays as the opening guess -- it is right for the two single-input cases
that are most of the traffic, a phone and a desktop -- and the first real
interaction corrects it on a hybrid.

pointermove matters as much as pointerdown: a mouse announces itself by
approaching, and the mode has to be right BEFORE the click, not as a
consequence of it. A pen is grouped with touch, since it taps rather than
hovers on most hardware and being wrong that way costs only a reveal step.

GalleryPremiumLayout's touch rules move off the media query onto a
data-input-mode attribute the layout sets, for the same reason: on a
touchscreen laptop the query stayed false and a finger could never reach the
checkbox or like button, and on an iPad with a trackpad it stayed true and both
were stuck on permanently.

The #1263 guarantee is unchanged and pinned by a test that walks all three
modes: a control that cannot be seen cannot be hit, whichever input is in use.

Verified in the running app under Chrome touch emulation, on a device
advertising a coarse primary pointer -- the iPad-with-trackpad case. A mouse
merely moving switched the grid to hover semantics and revealed the overlay,
and a subsequent tap switched it back; the premium layout's attribute followed,
with its checkbox reachable under touch and hidden-but-inert under mouse. The
mirror case (finger on a fine-primary device) cannot be staged in Chrome, which
couples touch emulation to a coarse primary pointer, so it rests on the jsdom
tests.

12 tests: 8 on the store, 4 more on PhotoCard. 3 of the 4 fail without the
per-interaction mode; the fourth is the #1263 no-regression guard and holds on
both sides by design.
2026-09-02 20:04:15 +02:00
..