From a3f9630bd1eb34a16f339ffd30071b8adc864ca0 Mon Sep 17 00:00:00 2001 From: Wesley Luyten Date: Tue, 23 Sep 2025 15:26:45 -0500 Subject: [PATCH] fix: seek jump back to current time (#22) --- packages/core/media-store/src/index.ts | 2 +- packages/core/media-store/src/media-store.ts | 2 +- .../media-store/src/state-mediators/index.ts | 2 +- .../{utils => state-mediators}/temporal.ts | 2 +- .../html/src/components/media-time-range.ts | 63 ++++++++++++------- .../react/react/src/components/TimeRange.tsx | 31 ++++++--- 6 files changed, 68 insertions(+), 34 deletions(-) rename packages/core/media-store/src/{utils => state-mediators}/temporal.ts (99%) diff --git a/packages/core/media-store/src/index.ts b/packages/core/media-store/src/index.ts index 1a7577e9..32c61ecf 100644 --- a/packages/core/media-store/src/index.ts +++ b/packages/core/media-store/src/index.ts @@ -2,7 +2,7 @@ export * from './factory'; export { createMediaStore } from './media-store'; export * from './state-mediators/playable'; export * from './state-mediators/audible'; -export * from './utils/temporal'; +export * from './state-mediators/temporal'; export * from './component-state-definitions/play-button'; export * from './component-state-definitions/mute-button'; export * from './component-state-definitions/volume-range'; diff --git a/packages/core/media-store/src/media-store.ts b/packages/core/media-store/src/media-store.ts index 035400a5..98242da8 100644 --- a/packages/core/media-store/src/media-store.ts +++ b/packages/core/media-store/src/media-store.ts @@ -4,7 +4,7 @@ import { createMediaStore as factory } from './factory'; import { audible } from './state-mediators/audible'; import { fullscreenable } from './state-mediators/fullscreenable'; import { playable } from './state-mediators/playable'; -import { temporal } from './utils/temporal'; +import { temporal } from './state-mediators/temporal'; // Example of default media store with default state mediator definitions. (CJP) // NOTE: We can also change the API to take an array of stateMediators (or either/both) (CJP) diff --git a/packages/core/media-store/src/state-mediators/index.ts b/packages/core/media-store/src/state-mediators/index.ts index 1c6ab4ae..b19a68cd 100644 --- a/packages/core/media-store/src/state-mediators/index.ts +++ b/packages/core/media-store/src/state-mediators/index.ts @@ -1,4 +1,4 @@ export { audible } from './audible'; export { playable } from './playable'; -export { temporal } from '../utils/temporal'; +export { temporal } from './temporal'; export { fullscreenable } from './fullscreenable'; diff --git a/packages/core/media-store/src/utils/temporal.ts b/packages/core/media-store/src/state-mediators/temporal.ts similarity index 99% rename from packages/core/media-store/src/utils/temporal.ts rename to packages/core/media-store/src/state-mediators/temporal.ts index 2f0bea54..5958b37c 100644 --- a/packages/core/media-store/src/utils/temporal.ts +++ b/packages/core/media-store/src/state-mediators/temporal.ts @@ -82,5 +82,5 @@ export const temporal = { return () => events.forEach((event) => media.removeEventListener(event, eventHandler)); }, ] as const, - }, + }, }; diff --git a/packages/html/html/src/components/media-time-range.ts b/packages/html/html/src/components/media-time-range.ts index 00ebbd18..af8ea100 100644 --- a/packages/html/html/src/components/media-time-range.ts +++ b/packages/html/html/src/components/media-time-range.ts @@ -26,13 +26,15 @@ export class TimeRangeRootBase extends HTMLElement { currentTime: number; duration: number; requestSeek: (time: number) => void; + pointerPosition: number | null; + hovering: boolean; + dragging: boolean; } | undefined; _trackElement: HTMLElement | null = null; - _pointerPosition: number | null = null; - _hovering: boolean = false; - _dragging: boolean = false; + _seekingTime: number | null = null; + _currentTime: number | null = null; constructor() { super(); @@ -45,6 +47,11 @@ export class TimeRangeRootBase extends HTMLElement { this.addEventListener('pointerleave', this); } + _setState(state: any): void { + this._state = { ...this._state, ...state }; + this._update(useTimeRangeRootProps(this._state!, this), this._state!); + } + handleEvent(event: Event): void { const { type } = event; const state = this._state; @@ -71,9 +78,11 @@ export class TimeRangeRootBase extends HTMLElement { private _handlePointerDown(event: PointerEvent) { event.preventDefault(); - this._dragging = true; + this._setState({ dragging: true }); + const seekTime = calculateSeekTimeFromPointerEvent(event, this._state!.duration); this._state!.requestSeek(seekTime); + this._seekingTime = seekTime; // Capture pointer events this.setPointerCapture(event.pointerId); @@ -84,30 +93,32 @@ export class TimeRangeRootBase extends HTMLElement { const rect = this._trackElement.getBoundingClientRect(); const ratio = calculatePointerRatio(event.clientX, rect); - this._pointerPosition = ratio; + this._setState({ pointerPosition: ratio }); - if (this._dragging) { + if (this._state!.dragging) { const seekTime = calculateSeekTimeFromRatio(ratio, this._state!.duration); this._state!.requestSeek(seekTime); + this._seekingTime = seekTime; } } private _handlePointerUp(event: PointerEvent) { this.releasePointerCapture(event.pointerId); - if (this._dragging && this._trackElement && this._pointerPosition !== null) { - const seekTime = calculateSeekTimeFromRatio(this._pointerPosition, this._state!.duration); + if (this._state!.dragging && this._trackElement && this._state!.pointerPosition !== null) { + const seekTime = calculateSeekTimeFromRatio(this._state!.pointerPosition, this._state!.duration); this._state!.requestSeek(seekTime); + this._seekingTime = seekTime; } - this._dragging = false; + this._setState({ dragging: false }); } private _handlePointerEnter() { - this._hovering = true; + this._setState({ hovering: true }); } private _handlePointerLeave() { - this._hovering = false; + this._setState({ hovering: false }); } get currentTime(): number { @@ -119,24 +130,32 @@ export class TimeRangeRootBase extends HTMLElement { } _update(props: any, state: any): void { - this._state = state; + this._state = { ...this._state, ...state }; - // Find track element this._trackElement = this.querySelector('media-time-range-track') as HTMLElement; - - // Calculate slider fill percentage - const sliderFill = - this._dragging && this._pointerPosition !== null - ? this._pointerPosition - : state.duration > 0 - ? (state.currentTime / state.duration) * 100 - : 0; + + // When dragging, use pointer position for immediate feedback; + // While seeking, use seeking time so it doesn't jump back to the current time; + // Otherwise, use current time; + let sliderFill = 0; + if (state.dragging && state.pointerPosition !== null) { + sliderFill = state.pointerPosition; + } else if (state.duration > 0) { + if (this._seekingTime !== null && this._currentTime === state.currentTime) { + sliderFill = (this._seekingTime / state.duration) * 100; + } else { + sliderFill = (state.currentTime / state.duration) * 100; + this._seekingTime = null; + } + } + + this._currentTime = state.currentTime; // Update CSS custom properties this.style.setProperty('--slider-fill', `${Math.round(sliderFill)}%`); this.style.setProperty( '--slider-pointer', - this._hovering && this._pointerPosition !== null ? `${Math.round(this._pointerPosition)}%` : '0%' + this._state!.hovering && this._state!.pointerPosition !== null ? `${Math.round(this._state!.pointerPosition)}%` : '0%' ); // Update ARIA attributes diff --git a/packages/react/react/src/components/TimeRange.tsx b/packages/react/react/src/components/TimeRange.tsx index 289107d0..c8322489 100644 --- a/packages/react/react/src/components/TimeRange.tsx +++ b/packages/react/react/src/components/TimeRange.tsx @@ -1,7 +1,7 @@ import type { ConnectedComponent } from '../utils/component-factory'; import type { PointerEvent, PropsWithChildren } from 'react'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo, useState, useRef } from 'react'; import { timeRangeStateDefinition } from '@vjs-10/media-store'; import { shallowEqual, useMediaSelector, useMediaStore } from '@vjs-10/react-media-store'; @@ -79,13 +79,25 @@ export const useTimeRangeRootProps = ( props: PropsWithChildren<{ [k: string]: any }>, state: ReturnType ) => { - // When dragging, use pointer position for immediate feedback; otherwise use current time - const sliderFill = - state.dragging && state.pointerPosition !== null - ? state.pointerPosition - : state.duration > 0 - ? (state.currentTime / state.duration) * 100 - : 0; + const seekingTime = useRef(null); + const currentTime = useRef(null); + + // When dragging, use pointer position for immediate feedback; + // While seeking, use seeking time so it doesn't jump back to the current time; + // Otherwise, use current time; + let sliderFill = 0; + if (state.dragging && state.pointerPosition !== null) { + sliderFill = state.pointerPosition; + } else if (state.duration > 0) { + if (seekingTime.current !== null && currentTime.current === state.currentTime) { + sliderFill = (seekingTime.current / state.duration) * 100; + } else { + sliderFill = (state.currentTime / state.duration) * 100; + seekingTime.current = null; + } + } + + currentTime.current = state.currentTime; const handlePointerDown = useCallback( (e: PointerEvent) => { @@ -93,6 +105,7 @@ export const useTimeRangeRootProps = ( state.setDragging(true); const seekTime = calculateSeekTimeFromPointerEvent(e, state.duration); state.requestSeek(seekTime); + seekingTime.current = seekTime; // Capture pointer events to ensure we receive move and up events even if pointer leaves element e.currentTarget.setPointerCapture(e.pointerId); @@ -111,6 +124,7 @@ export const useTimeRangeRootProps = ( if (state.dragging) { const seekTime = calculateSeekTimeFromRatio(ratio, state.duration); state.requestSeek(seekTime); + seekingTime.current = seekTime; } }, [state.trackRef, state.setPointerPosition, state.dragging, state.requestSeek, state.duration] @@ -123,6 +137,7 @@ export const useTimeRangeRootProps = ( if (state.dragging && state.trackRef && state.pointerPosition !== null) { const seekTime = calculateSeekTimeFromRatio(state.pointerPosition, state.duration); state.requestSeek(seekTime); + seekingTime.current = seekTime; } state.setDragging(false); },