fix: seek jump back to current time (#22)

This commit is contained in:
Wesley Luyten
2025-09-23 15:26:45 -05:00
committed by GitHub
parent aefe890fee
commit a3f9630bd1
6 changed files with 68 additions and 34 deletions
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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)
@@ -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';
@@ -82,5 +82,5 @@ export const temporal = {
return () => events.forEach((event) => media.removeEventListener(event, eventHandler));
},
] as const,
},
},
};
@@ -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
@@ -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<typeof useTimeRangeRootState>
) => {
// 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<number | null>(null);
const currentTime = useRef<number | null>(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<HTMLDivElement>) => {
@@ -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);
},