feat(android): the touch model reaches the other three lists
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m33s
CI / e2e (pull_request) Successful in 9m20s

Plan 019 phases 3 and 4, which finish #63. The queue panel and both
playlist detail views get tap-to-play and hold-to-select; the playlist
views get swipe-to-queue as well.

Phase 3 was not the pure wiring the plan expected, in two places.

A tap on a queue row plays that position. Copying track-list's tap --
which sets the queue to the list the row is in -- would rebuild the
queue from the queue, discarding its source, its shuffle order and
anything inserted by hand. It reads as a no-op and is not one.

And the queue panel has no swipe, deliberately. A right swipe means add
to the queue everywhere else it exists, and a queue row is already in
the queue; the only thing it could mean there is remove, which is the
same gesture with the opposite effect one screen away. Removing a queue
row is on the row, on its sheet since #60, and now on its selection
bar. The assertion is that its rows do not opt in.

The reveal became utils/swipe-to-queue.ts rather than being copied into
three lists, keyed on a data-swipe attribute so one stylesheet carries
the touch-action half of the device fix to rows that are called two
different things.

Phase 4 was already true and is now asserted: a claimed tap has its
click swallowed, so an explore-link inside a row never sees one and
tap-to-play wins with no rule of its own. Its test was vacuous when
written -- the tap helper sent no click, so there was nothing to
swallow -- which also weakened phase 1's. It sends one now.

Escape leaves selection mode, from selection-bar rather than from each
of the four hosts, since that element exists only while the mode does.
The platform's back gesture deliberately does not reach it: the shell
owns the history stack and four lists reaching for history is four
stacks. That is #200.

Verified on the reference phone: a queue row taps to its own index and
refuses a swipe, a playlist row queues on a swipe and plays its
playlist on a tap, and a hold raises the bar without the menu.

Closes #63
This commit is contained in:
2026-08-22 01:41:21 -04:00
parent 4e667759c4
commit 29feb4b94b
11 changed files with 1402 additions and 291 deletions
+16 -8
View File
@@ -266,12 +266,20 @@ describe('a finger swiped right across a track row', () => {
expect(rows(el)[0]?.getAttribute('aria-selected')).toBe('false');
});
it('declares pan-y on the row, which is half of what makes it work', () => {
// The other half is the module's non-passive `preventDefault`.
// Neither works alone on Chrome 113 and both are irrelevant here,
// so this reads the stylesheet rather than the rendering — the
// regression is someone tidying the declaration away, and nothing
// in this browser looks different when they do.
it('declares pan-y on the row, which is half of what makes it work', async () => {
// The other half is the gesture module's non-passive
// `preventDefault`. Neither works alone on Chrome 113 and both are
// irrelevant here, so this reads the stylesheet and the attribute
// rather than the rendering the regression is someone tidying
// one of them away, and nothing in this browser looks different
// when they do.
const el = await mountList();
expect(
rows(el)[0]?.hasAttribute('data-swipe'),
'the row opts into the shared rule',
).toBe(true);
const sheets = (
customElements.get('track-list') as unknown as {
styles: { cssText: string }[];
@@ -280,9 +288,9 @@ describe('a finger swiped right across a track row', () => {
const css = sheets.map((s) => s.cssText).join('\n');
const rule = css
.split('}')
.find((block) => /\.track-row\s*\{/.test(block));
.find((block) => /\[data-swipe\]\s*\{/.test(block));
expect(rule, 'the row rule is still there to read').toBeTruthy();
expect(rule, 'the shared rule is in this component').toBeTruthy();
expect(rule).toContain('touch-action: pan-y');
expect(css, 'never none: it takes the scrolling too').not.toContain(
'touch-action: none',