seek-bar's interpolation interval can be stopped and never restarted, on any platform #164

Closed
opened 2026-08-20 21:10:07 +00:00 by logan · 1 comment
Collaborator

Split out of #53, where yonlu found it by reading the component. It is
a real defect independent of that report's symptom, and it is not
Android-specific, so it gets its own issue.

handleInput() stops the interval and nothing can restart it.

// Stops progress while user is dragging the thumb
private handleInput() {
  this.stopProgress();
}

stopProgress() clears the timer and mutates no reactive state, so
Lit schedules no update, updated() does not run, and the tail of
updated() that would call startProgress() never executes. The
interval is then only restarted by:

  • a change event (handleChange calls startProgress() directly), or
  • the next accepted backend report, which re-renders via the store.

So any input that is not followed by a change freezes the
interpolation
until the next 1 Hz report rescues it. A drag cancelled
outside the element, a pointer cancelled by a scroll gesture, or a
touch that lands on the track and is treated as a scrub all produce
that. On a touch device those are ordinary gestures.

Why it is usually invisible, and when it is not. While playback is
running a report arrives every second and papers over it, so the worst
case is a bar that stalls for up to a second. It is permanent whenever
reports are not arriving — which is exactly the state a paused or
stalled player is in, and is the state anyone investigating "the bar
is not moving" is already looking at. It also means the component has
two owners for one timer, which is what makes the behaviour hard to
reason about from either one.

Direction. Give updated() sole ownership of the interval, so
there is one place that decides whether it runs. That means the drag
has to be reactive state rather than a bare side effect. The one thing
to be careful about is that a flag set on input must not be able to
strand: a drag that never produces a change has to clear it some
other way, or the fix converts an up-to-one-second stall into a
permanent one.

Worth doing together with the second half: while a drag is in
progress, an incoming report currently overwrites seekValue, so the
thumb is pulled out from under the finger once a second.

No test covers any of this. seek-bar has no test at any tier —
component, e2e or otherwise — which is why a component this
load-bearing can be broken on one platform with nothing failing.

Split out of #53, where yonlu found it by reading the component. It is a real defect independent of that report's symptom, and it is not Android-specific, so it gets its own issue. **`handleInput()` stops the interval and nothing can restart it.** ```ts // Stops progress while user is dragging the thumb private handleInput() { this.stopProgress(); } ``` `stopProgress()` clears the timer and mutates **no reactive state**, so Lit schedules no update, `updated()` does not run, and the tail of `updated()` that would call `startProgress()` never executes. The interval is then only restarted by: - a `change` event (`handleChange` calls `startProgress()` directly), or - the next accepted backend report, which re-renders via the store. So **any `input` that is not followed by a `change` freezes the interpolation** until the next 1 Hz report rescues it. A drag cancelled outside the element, a pointer cancelled by a scroll gesture, or a touch that lands on the track and is treated as a scrub all produce that. On a touch device those are ordinary gestures. **Why it is usually invisible, and when it is not.** While playback is running a report arrives every second and papers over it, so the worst case is a bar that stalls for up to a second. It is permanent whenever reports are *not* arriving — which is exactly the state a paused or stalled player is in, and is the state anyone investigating "the bar is not moving" is already looking at. It also means the component has two owners for one timer, which is what makes the behaviour hard to reason about from either one. **Direction.** Give `updated()` sole ownership of the interval, so there is one place that decides whether it runs. That means the drag has to be reactive state rather than a bare side effect. The one thing to be careful about is that a flag set on `input` must not be able to strand: a drag that never produces a `change` has to clear it some other way, or the fix converts an up-to-one-second stall into a permanent one. Worth doing together with the second half: while a drag *is* in progress, an incoming report currently overwrites `seekValue`, so the thumb is pulled out from under the finger once a second. **No test covers any of this.** `seek-bar` has no test at any tier — component, e2e or otherwise — which is why a component this load-bearing can be broken on one platform with nothing failing.
logan self-assigned this 2026-08-20 21:10:14 +00:00
logan added the
Status
In Progress
label 2026-08-20 21:10:14 +00:00
Author
Collaborator

Taking this on fix/53-seek-bar-never-moves, the branch opened for #53
— this was split out of it and the two share the component and the
tests being added.

Giving updated() sole ownership of the interval, with the drag as
@state, plus document pointerup/pointercancel listeners while the
drag is live so a gesture that never produces a change cannot strand
the flag — which would turn a one-second stall into a permanent one.
Reports are ignored while dragging, so the thumb is not pulled out from
under the finger.

And the missing coverage: seek-bar gets a component-tier test at last,
including the mid-playback mount #53 asked for.

Taking this on `fix/53-seek-bar-never-moves`, the branch opened for #53 — this was split out of it and the two share the component and the tests being added. Giving `updated()` sole ownership of the interval, with the drag as `@state`, plus document `pointerup`/`pointercancel` listeners while the drag is live so a gesture that never produces a `change` cannot strand the flag — which would turn a one-second stall into a permanent one. Reports are ignored while dragging, so the thumb is not pulled out from under the finger. And the missing coverage: `seek-bar` gets a component-tier test at last, including the mid-playback mount #53 asked for.
logan closed this issue 2026-08-20 21:31:17 +00:00
gitea-actions bot removed the
Status
In Progress
label 2026-08-20 21:31:25 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yonlu/yellowjacket#164