feat(packages): add pauseOnDrag to time slider#1596
Conversation
|
| Name | Link |
|---|---|
| 🔨 Latest commit | e20eabe |
|
@shiminshen is attempting to deploy a commit to the Mux Team on Vercel. A member of the Team first needs to authorize it. |
|
Hi @shiminshen! Thanks for your contribution 🙌 I created the PR with your changes, as external PRs are currently restricted. I gave it a try and it looks correct to me. Would you mind rebasing on main and giving it another quick test, just to make Also, Cursor Bot detected some potential issues; would you mind reviewing those as well? Thanks again! |
Opt-in option to pause playback while the user is dragging the time slider thumb, resuming on release if it was playing before. Reduces the jittery feel during long scrubs and avoids audio glitching. Closes videojs#1440. Mirrors Vidstack's API: - React: `<TimeSliderRoot pauseWhileDragging />` (default false) - HTML: `<media-time-slider pause-while-dragging>` (boolean attribute) Implementation: - Wraps the existing `onDragStart` / `onDragEnd` callbacks. User-provided callbacks still fire — pause/resume runs alongside them. - Reads playback state via the existing `selectPlayback` selector + a new `PlayerController` / `useLatestRef` hookup, mirroring how time and buffer are already wired. - Tracks `wasPlayingBeforeDrag` so a player paused before the drag doesn't get auto-played on release (avoids accidental autoplay). - `play()` rejection on resume (autoplay policy, etc.) is swallowed here — the existing error feature surfaces it. Tests: - HTML: `pauseWhileDragging` default + attribute reflection. - React: 4 cases — default no-op, pause/resume when playing, no-resume when already paused, user-callback forwarding. Behavioral tests use the existing slider-mock pattern (captures `onDragStart` / `onDragEnd` via a hoisted ref).
Address Cursor Bot findings on videojs#1596: - Resume on drag-end based on the captured `wasPlayingBeforeDrag` flag only, not the current `pauseWhileDragging` prop/attribute. If the flag turns off between dragStart and dragEnd the user still expects the auto-pause to undo itself on release. - Resume on unmount/disconnect if a drag paused playback — createSlider's destroy() does not fire onDragEnd, so without this the player would stay paused when the slider is removed mid-drag. Tests cover both edge cases (React).
e20eabe to
80d201d
Compare
|
Thanks @R-Delfino95! Rebased on
Two new React tests cover both edge cases. Existing tests still pass; lint and the React/HTML time-slider typecheck are clean. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 80d201d. Configure here.
createSlider's destroy() does not fire onDragEnd, so destroying the element while a drag had paused playback would leave the player paused. disconnectedCallback covered DOM removal, but destroy() can also be called directly (e.g. keep-alive) without going through disconnect. Extract the resume logic into a private helper and call it from both disconnectedCallback and destroyCallback before super so the PlayerController is still attached.
There was a problem hiding this comment.
Thank you, Renzo. Before a deeper review I'd like to see more conformance with our core design.
- Lets rename this to
pauseOnDrag. Little more terse. - Move core state logic and props to the
TimeSliderCoreclass. Please see other core classes and interfaces as reference. Make sure they're documented. - The PR title
feat(html,react)contains an invalid scope. Here it'd befeat(packages)
| onDragStart: handleDragStart, | ||
| onDragEnd: handleDragEnd, |
There was a problem hiding this comment.
We can inline the handlers here to avoid naming/defining them above and separating from config.
| } satisfies PropertyDeclarationMap<Exclude<keyof TimeSliderCore.Props, 'value' | 'min' | 'max'>> & { | ||
| pauseWhileDragging: { type: BooleanConstructor; attribute: string }; | ||
| }; |
There was a problem hiding this comment.
This union & of types shouldn't happen, types come from TimeSliderCore.Props
There was a problem hiding this comment.
Fixed in 2ae34cc — types come from TimeSliderCore.Props now.
| /** | ||
| * When true, pause playback while the user is dragging the thumb, | ||
| * resuming on release if it was playing before. Default `false`. | ||
| */ | ||
| pauseWhileDragging = false; |
There was a problem hiding this comment.
Descriptions don't belong here, they should be derived from implementing TimeSliderCore.Props. See how above none of them have it. Default value comes from core too.
There was a problem hiding this comment.
Removed in 2ae34cc — description and default come from core now.
…rops Move the pauseWhileDragging prop type and default value into TimeSliderCore so HTML and React layers can derive them. Drops the `&`-intersection workaround on the satisfies clause, the duplicate JSDoc, and the inline `false` default. Also inlines the React drag handlers into the useSlider config to keep the config and handlers co-located.
…gic into TimeSliderCore Drag pause/resume state now lives in TimeSliderCore as documented startDrag/ endDrag methods (following the PlayButtonCore.toggle pattern), so the HTML and React layers only delegate. The HTML attribute is now pause-on-drag.
Tests, typecheck and lint all pass. |

Closes #1440
What it does
Adds opt-in
pauseWhileDraggingmirroring Vidstack's API:<TimeSliderRoot pauseWhileDragging />(defaultfalse)<media-time-slider pause-while-dragging>(boolean attribute)Wraps the existing
onDragStart/onDragEndcallbacks — user-provided callbacks still fire alongside. Reads playback state via the existingselectPlaybackselector +PlayerController/useLatestRef, mirroring howtimeandbufferare already wired.Tracks
wasPlayingBeforeDraginternally so a player paused before the drag doesn't get accidentally auto-played on release.Scope choice
Opt-in default (
false), matching Vidstack'spauseWhileDraggingprop. v8 and Plyr are always-on (with platform disables for mobile/STV iOS audio glitching). I went opt-in because:play()rejection on resume (autoplay policy, ad-paused state) is swallowed here — the existing error feature surfaces it via the player error state.Verified locally
pnpm -F @videojs/react test— 224/224 pass (+4 new behavioral tests: default no-op, pause/resume when playing, no-resume when already paused, user-callback forwarding)pnpm -F @videojs/html test— 120/120 pass (+1 new: attribute reflection)pnpm lint/pnpm typecheck/pnpm check:workspace— all cleanOpen question
The React test uses a hoisted-ref pattern to capture
onDragStart/onDragEndfrom the createSlider mock. If you'd prefer not extending the slider mock that way, happy to drop the React behavioral tests and lean on Playwright E2E for slider behavior (matches the existing convention in the HTML element tests).Note
Medium Risk
Touches playback pause/play during seek UI interactions; default-off limits blast radius, but resume and mid-drag teardown paths affect player state.
Overview
Adds an opt-in
pauseOnDragoption (defaultfalse) on the time slider so playback can pause while scrubbing and resume on release only if it was playing when the drag started.TimeSliderCoregainsstartDrag/endDragusingMediaPlaybackState, with#wasPlayingBeforeDragso already-paused media is not auto-played; resume still runs ifpauseOnDragis turned off mid-drag. Failedplay()on resume is swallowed.React (
TimeSliderRoot) and HTML (media-time-slider/pause-on-drag) wire drag lifecycle through the core viaselectPlayback, still calling useronDragStart/onDragEnd. Both callendDragon unmount/disconnect/destroy so a mid-drag teardown does not leave the player paused.Tests cover core drag behavior, HTML attribute reflection, and React integration (including unmount resume and callback forwarding).
Reviewed by Cursor Bugbot for commit 71ef8a8. Bugbot is set up for automated code reviews on this repo. Configure here.