mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
refactor(packages): dry up core, html, and react UI architecture (#699)
This commit is contained in:
@@ -4,6 +4,7 @@ import type { MediaBufferState } from '../../../core/media/state';
|
||||
import { definePlayerFeature } from '../../feature';
|
||||
|
||||
export const bufferFeature = definePlayerFeature({
|
||||
name: 'buffer',
|
||||
state: (): MediaBufferState => ({
|
||||
buffered: [],
|
||||
seekable: [],
|
||||
|
||||
@@ -8,6 +8,7 @@ const IDLE_DELAY = 2000;
|
||||
const TAP_THRESHOLD = 250;
|
||||
|
||||
export const controlsFeature = definePlayerFeature({
|
||||
name: 'controls',
|
||||
state: (): MediaControlsState => ({
|
||||
userActive: true,
|
||||
controlsVisible: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { exitPiP, isPiPActive } from '../../presentation/pip';
|
||||
import type { WebKitVideoElement } from '../../presentation/types';
|
||||
|
||||
export const fullscreenFeature = definePlayerFeature({
|
||||
name: 'fullscreen',
|
||||
state: ({ target }): MediaFullscreenState => ({
|
||||
fullscreen: false,
|
||||
fullscreenAvailability: 'unavailable',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { enterPiP, exitPiP, isPiPActive, isPiPSupported } from '../../presentati
|
||||
import type { WebKitVideoElement } from '../../presentation/types';
|
||||
|
||||
export const pipFeature = definePlayerFeature({
|
||||
name: 'pip',
|
||||
state: ({ target }): MediaPictureInPictureState => ({
|
||||
pip: false,
|
||||
pipAvailability: 'unavailable',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { definePlayerFeature } from '../../feature';
|
||||
const DEFAULT_RATES: readonly number[] = [1, 1.2, 1.5, 1.7, 2];
|
||||
|
||||
export const playbackRateFeature = definePlayerFeature({
|
||||
name: 'playbackRate',
|
||||
state: ({ target }): MediaPlaybackRateState => ({
|
||||
playbackRates: DEFAULT_RATES,
|
||||
playbackRate: 1,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { MediaPlaybackState } from '../../../core/media/state';
|
||||
import { definePlayerFeature } from '../../feature';
|
||||
|
||||
export const playbackFeature = definePlayerFeature({
|
||||
name: 'playback',
|
||||
state: ({ target }): MediaPlaybackState => ({
|
||||
paused: true,
|
||||
ended: false,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { MediaSourceState } from '../../../core/media/state';
|
||||
import { definePlayerFeature } from '../../feature';
|
||||
|
||||
export const sourceFeature = definePlayerFeature({
|
||||
name: 'source',
|
||||
state: ({ target, signals }): MediaSourceState => ({
|
||||
source: null,
|
||||
canPlay: false,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { MediaTextCue, MediaTextTrackState } from '../../../core/media/stat
|
||||
import { definePlayerFeature } from '../../feature';
|
||||
|
||||
export const textTrackFeature = definePlayerFeature({
|
||||
name: 'textTrack',
|
||||
state: (): MediaTextTrackState => ({
|
||||
chaptersCues: [],
|
||||
thumbnailCues: [],
|
||||
|
||||
@@ -6,6 +6,7 @@ import { hasMetadata } from '../../media/predicate';
|
||||
import { signalKeys } from '../signal-keys';
|
||||
|
||||
export const timeFeature = definePlayerFeature({
|
||||
name: 'time',
|
||||
state: ({ target, signals }): MediaTimeState => ({
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { MediaFeatureAvailability, MediaVolumeState } from '../../../core/m
|
||||
import { definePlayerFeature } from '../../feature';
|
||||
|
||||
export const volumeFeature = definePlayerFeature({
|
||||
name: 'volume',
|
||||
state: ({ target }): MediaVolumeState => ({
|
||||
volume: 1,
|
||||
muted: false,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { State } from '@videojs/store';
|
||||
import { listen } from '@videojs/utils/dom';
|
||||
import type { PopoverInteraction } from '../../../core/ui/popover/popover-core';
|
||||
import type { PopoverInput } from '../../../core/ui/popover/popover-core';
|
||||
import type { UIFocusEvent, UIPointerEvent } from '../event';
|
||||
import type { TransitionHandler } from '../transition';
|
||||
import type { TransitionApi } from '../transition';
|
||||
|
||||
export type PopoverOpenChangeReason = 'click' | 'hover' | 'focus' | 'escape' | 'outside-click' | 'blur';
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface PopoverChangeDetails {
|
||||
}
|
||||
|
||||
export interface PopoverOptions {
|
||||
transition: TransitionHandler;
|
||||
transition: TransitionApi;
|
||||
onOpenChange: (open: boolean, details: PopoverChangeDetails) => void;
|
||||
/** Fires after open/close animations complete. */
|
||||
onOpenChangeComplete?: (open: boolean) => void;
|
||||
@@ -37,8 +37,8 @@ export interface PopoverPopupProps {
|
||||
onFocusOut: (event: UIFocusEvent) => void;
|
||||
}
|
||||
|
||||
export interface PopoverHandle {
|
||||
interaction: State<PopoverInteraction>;
|
||||
export interface PopoverApi {
|
||||
input: State<PopoverInput>;
|
||||
triggerProps: PopoverTriggerProps;
|
||||
popupProps: PopoverPopupProps;
|
||||
readonly triggerElement: HTMLElement | null;
|
||||
@@ -49,7 +49,7 @@ export interface PopoverHandle {
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export function createPopover(options: PopoverOptions): PopoverHandle {
|
||||
export function createPopover(options: PopoverOptions): PopoverApi {
|
||||
const { transition, onOpenChange, closeOnEscape, closeOnOutsideClick } = options;
|
||||
|
||||
const state = transition.state;
|
||||
@@ -299,7 +299,7 @@ export function createPopover(options: PopoverOptions): PopoverHandle {
|
||||
popupEl = el;
|
||||
|
||||
if (el) {
|
||||
// If the interaction is already open (e.g., React mount after state
|
||||
// If the popover is already open (e.g., React mount after state
|
||||
// change), show the popover now. In `applyOpen` the element may not
|
||||
// have been in the DOM yet, so the earlier `tryShowPopover` was a no-op.
|
||||
if (state.current.active) {
|
||||
@@ -316,7 +316,7 @@ export function createPopover(options: PopoverOptions): PopoverHandle {
|
||||
}
|
||||
|
||||
return {
|
||||
interaction: state,
|
||||
input: state,
|
||||
triggerProps,
|
||||
popupProps,
|
||||
get triggerElement() {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { vi } from 'vitest';
|
||||
import { createTransitionHandler } from '../../transition';
|
||||
import { createTransition } from '../../transition';
|
||||
import { createPopover, type PopoverChangeDetails } from '../popover';
|
||||
|
||||
export function createTestPopover(overrides?: Partial<Parameters<typeof createPopover>[0]>) {
|
||||
const onOpenChange = vi.fn<(open: boolean, details: PopoverChangeDetails) => void>();
|
||||
const transition = overrides?.transition ?? createTransitionHandler();
|
||||
const transition = overrides?.transition ?? createTransition();
|
||||
const popover = createPopover({
|
||||
transition,
|
||||
onOpenChange,
|
||||
|
||||
@@ -5,16 +5,16 @@ import { createTestPopover } from './popover-helpers';
|
||||
describe('createPopover', () => {
|
||||
it('starts closed', () => {
|
||||
const { popover } = createTestPopover();
|
||||
expect(popover.interaction.current).toEqual({ active: false, status: 'idle' });
|
||||
expect(popover.input.current).toEqual({ active: false, status: 'idle' });
|
||||
});
|
||||
|
||||
describe('open/close', () => {
|
||||
it('updates interaction state and calls onOpenChange when opening', () => {
|
||||
it('updates input state and calls onOpenChange when opening', () => {
|
||||
const { popover, onOpenChange } = createTestPopover();
|
||||
|
||||
popover.open();
|
||||
|
||||
expect(popover.interaction.current.active).toBe(true);
|
||||
expect(popover.input.current.active).toBe(true);
|
||||
expect(onOpenChange).toHaveBeenCalledWith(true, { reason: 'click' });
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('createPopover', () => {
|
||||
|
||||
popover.open();
|
||||
|
||||
expect(popover.interaction.current).toEqual({ active: true, status: 'starting' });
|
||||
expect(popover.input.current).toEqual({ active: true, status: 'starting' });
|
||||
});
|
||||
|
||||
it('calls onOpenChange when closing', () => {
|
||||
@@ -35,7 +35,7 @@ describe('createPopover', () => {
|
||||
popover.close();
|
||||
|
||||
// active stays true until close animation completes
|
||||
expect(popover.interaction.current.active).toBe(true);
|
||||
expect(popover.input.current.active).toBe(true);
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false, { reason: 'click' });
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('createPopover', () => {
|
||||
popover.open();
|
||||
popover.close();
|
||||
|
||||
expect(popover.interaction.current).toEqual({ active: true, status: 'ending' });
|
||||
expect(popover.input.current).toEqual({ active: true, status: 'ending' });
|
||||
});
|
||||
|
||||
it('does not call onOpenChange if already open', () => {
|
||||
@@ -95,7 +95,7 @@ describe('createPopover', () => {
|
||||
|
||||
popover.triggerProps.onClick(event);
|
||||
|
||||
expect(popover.interaction.current.active).toBe(true);
|
||||
expect(popover.input.current.active).toBe(true);
|
||||
expect(onOpenChange).toHaveBeenCalledWith(true, expect.objectContaining({ reason: 'click' }));
|
||||
});
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('createPopover', () => {
|
||||
popover.triggerProps.onClick({ preventDefault: vi.fn() } as unknown as UIEvent);
|
||||
|
||||
// active stays true until close animation completes
|
||||
expect(popover.interaction.current.active).toBe(true);
|
||||
expect(popover.input.current.active).toBe(true);
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false, expect.objectContaining({ reason: 'click' }));
|
||||
});
|
||||
|
||||
@@ -122,8 +122,8 @@ describe('createPopover', () => {
|
||||
// Click during close animation should re-open
|
||||
popover.triggerProps.onClick({ preventDefault: vi.fn() } as unknown as UIEvent);
|
||||
|
||||
expect(popover.interaction.current.active).toBe(true);
|
||||
expect(popover.interaction.current.status).not.toBe('ending');
|
||||
expect(popover.input.current.active).toBe(true);
|
||||
expect(popover.input.current.status).not.toBe('ending');
|
||||
expect(onOpenChange).toHaveBeenCalledWith(true, expect.objectContaining({ reason: 'click' }));
|
||||
});
|
||||
});
|
||||
@@ -156,7 +156,7 @@ describe('createPopover', () => {
|
||||
popover.open();
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
expect(popover.interaction.current.active).toBe(false);
|
||||
expect(popover.input.current.active).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -165,13 +165,13 @@ describe('createPopover', () => {
|
||||
const { popover } = createTestPopover();
|
||||
const callback = vi.fn();
|
||||
|
||||
popover.interaction.subscribe(callback);
|
||||
popover.input.subscribe(callback);
|
||||
|
||||
popover.open();
|
||||
flush();
|
||||
|
||||
expect(callback).toHaveBeenCalled();
|
||||
expect(popover.interaction.current.active).toBe(true);
|
||||
expect(popover.input.current.active).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { listen } from '@videojs/utils/dom';
|
||||
import { throttle } from '@videojs/utils/function';
|
||||
import { clamp, roundToStep } from '@videojs/utils/number';
|
||||
import { isNull } from '@videojs/utils/predicate';
|
||||
import type { SliderInteraction } from '../../core/ui/slider/slider-core';
|
||||
import type { SliderInput } from '../../core/ui/slider/slider-core';
|
||||
import { getPercentFromPointerEvent } from '../utils/pointer';
|
||||
import type { UIKeyboardEvent, UIPointerEvent } from './event';
|
||||
|
||||
@@ -50,8 +50,8 @@ export interface SliderThumbProps {
|
||||
onBlur: () => void;
|
||||
}
|
||||
|
||||
export interface SliderHandle {
|
||||
interaction: State<SliderInteraction>;
|
||||
export interface SliderApi {
|
||||
input: State<SliderInput>;
|
||||
rootProps: SliderRootProps;
|
||||
thumbProps: SliderThumbProps;
|
||||
destroy: () => void;
|
||||
@@ -60,8 +60,8 @@ export interface SliderHandle {
|
||||
/** Intentional drag threshold — number of pointermove events before drag starts. */
|
||||
const DRAG_THRESHOLD = 2;
|
||||
|
||||
export function createSlider(options: SliderOptions): SliderHandle {
|
||||
const state = createState<SliderInteraction>({
|
||||
export function createSlider(options: SliderOptions): SliderApi {
|
||||
const input = createState<SliderInput>({
|
||||
pointerPercent: 0,
|
||||
dragPercent: 0,
|
||||
dragging: false,
|
||||
@@ -97,10 +97,10 @@ export function createSlider(options: SliderOptions): SliderHandle {
|
||||
|
||||
function endDrag(): void {
|
||||
if (!isDragging) {
|
||||
state.patch({ pointing: false, pointerPercent: 0 });
|
||||
input.patch({ pointing: false, pointerPercent: 0 });
|
||||
} else {
|
||||
isDragging = false;
|
||||
state.patch({ dragging: false, pointing: false, pointerPercent: 0 });
|
||||
input.patch({ dragging: false, pointing: false, pointerPercent: 0 });
|
||||
options.onDragEnd?.();
|
||||
}
|
||||
|
||||
@@ -128,17 +128,17 @@ export function createSlider(options: SliderOptions): SliderHandle {
|
||||
|
||||
if (!isDragging && moveCount >= DRAG_THRESHOLD) {
|
||||
isDragging = true;
|
||||
state.patch({ dragging: true, dragPercent: percent, pointerPercent: percent });
|
||||
input.patch({ dragging: true, dragPercent: percent, pointerPercent: percent });
|
||||
options.onDragStart?.();
|
||||
options.onValueChange?.(percent);
|
||||
throttledCommit?.(percent);
|
||||
} else if (isDragging) {
|
||||
state.patch({ dragPercent: percent, pointerPercent: percent });
|
||||
input.patch({ dragPercent: percent, pointerPercent: percent });
|
||||
options.onValueChange?.(percent);
|
||||
throttledCommit?.(percent);
|
||||
} else {
|
||||
// Below drag threshold — update hover preview only.
|
||||
state.patch({ pointerPercent: percent });
|
||||
input.patch({ pointerPercent: percent });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ export function createSlider(options: SliderOptions): SliderHandle {
|
||||
|
||||
const percent = getPercentFromPointerEvent(event, cachedRect, options.getOrientation(), cachedRTL);
|
||||
|
||||
state.patch({ pointing: true, pointerPercent: percent, dragPercent: percent });
|
||||
input.patch({ pointing: true, pointerPercent: percent, dragPercent: percent });
|
||||
options.onValueChange?.(percent);
|
||||
|
||||
// Focus the thumb for keyboard follow-up and screen reader tracking.
|
||||
@@ -197,12 +197,12 @@ export function createSlider(options: SliderOptions): SliderHandle {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const percent = getPercentFromPointerEvent(event, rect, options.getOrientation(), options.isRTL());
|
||||
|
||||
state.patch({ pointing: true, pointerPercent: percent });
|
||||
input.patch({ pointing: true, pointerPercent: percent });
|
||||
},
|
||||
|
||||
onPointerLeave() {
|
||||
if (isDragging) return;
|
||||
state.patch({ pointing: false, pointerPercent: 0 });
|
||||
input.patch({ pointing: false, pointerPercent: 0 });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -266,25 +266,25 @@ export function createSlider(options: SliderOptions): SliderHandle {
|
||||
if (newPercent !== null) {
|
||||
event.preventDefault();
|
||||
newPercent = clamp(newPercent, 0, 100);
|
||||
state.patch({ pointerPercent: newPercent, dragPercent: newPercent });
|
||||
input.patch({ pointerPercent: newPercent, dragPercent: newPercent });
|
||||
options.onValueChange?.(newPercent);
|
||||
options.onValueCommit?.(newPercent);
|
||||
}
|
||||
},
|
||||
|
||||
onFocus() {
|
||||
state.patch({ focused: true });
|
||||
input.patch({ focused: true });
|
||||
},
|
||||
|
||||
onBlur() {
|
||||
state.patch({ focused: false });
|
||||
input.patch({ focused: false });
|
||||
},
|
||||
};
|
||||
|
||||
listen(abort.signal, 'abort', cleanup, { once: true });
|
||||
|
||||
return {
|
||||
interaction: state,
|
||||
input,
|
||||
rootProps,
|
||||
thumbProps,
|
||||
destroy() {
|
||||
|
||||
@@ -95,12 +95,12 @@ describe('createSlider', () => {
|
||||
});
|
||||
|
||||
describe('shape', () => {
|
||||
it('returns interaction, rootProps, thumbProps, and destroy', () => {
|
||||
it('returns state, rootProps, thumbProps, and destroy', () => {
|
||||
const slider = createSlider(createOptions());
|
||||
|
||||
expect(slider.interaction).toBeDefined();
|
||||
expect(slider.interaction.current).toBeDefined();
|
||||
expect(slider.interaction.subscribe).toBeTypeOf('function');
|
||||
expect(slider.input).toBeDefined();
|
||||
expect(slider.input.current).toBeDefined();
|
||||
expect(slider.input.subscribe).toBeTypeOf('function');
|
||||
expect(slider.rootProps.onPointerDown).toBeTypeOf('function');
|
||||
expect(slider.rootProps.onPointerMove).toBeTypeOf('function');
|
||||
expect(slider.rootProps.onPointerLeave).toBeTypeOf('function');
|
||||
@@ -112,10 +112,10 @@ describe('createSlider', () => {
|
||||
slider.destroy();
|
||||
});
|
||||
|
||||
it('has correct initial interaction state', () => {
|
||||
it('has correct initial state', () => {
|
||||
const slider = createSlider(createOptions());
|
||||
|
||||
expect(slider.interaction.current).toEqual({
|
||||
expect(slider.input.current).toEqual({
|
||||
pointerPercent: 0,
|
||||
dragPercent: 0,
|
||||
dragging: false,
|
||||
@@ -135,8 +135,8 @@ describe('createSlider', () => {
|
||||
slider.rootProps.onPointerDown(pointerEvent({ clientX: 100 }));
|
||||
flush();
|
||||
|
||||
expect(slider.interaction.current.pointing).toBe(true);
|
||||
expect(slider.interaction.current.pointerPercent).toBe(50);
|
||||
expect(slider.input.current.pointing).toBe(true);
|
||||
expect(slider.input.current.pointerPercent).toBe(50);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
@@ -185,7 +185,7 @@ describe('createSlider', () => {
|
||||
flush();
|
||||
|
||||
expect(onValueChange).not.toHaveBeenCalled();
|
||||
expect(slider.interaction.current.pointing).toBe(false);
|
||||
expect(slider.input.current.pointing).toBe(false);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
@@ -202,13 +202,13 @@ describe('createSlider', () => {
|
||||
// First move — below threshold
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
flush();
|
||||
expect(slider.interaction.current.dragging).toBe(false);
|
||||
expect(slider.input.current.dragging).toBe(false);
|
||||
expect(onDragStart).not.toHaveBeenCalled();
|
||||
|
||||
// Second move — meets threshold
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
flush();
|
||||
expect(slider.interaction.current.dragging).toBe(true);
|
||||
expect(slider.input.current.dragging).toBe(true);
|
||||
expect(onDragStart).toHaveBeenCalledOnce();
|
||||
|
||||
slider.destroy();
|
||||
@@ -246,7 +246,7 @@ describe('createSlider', () => {
|
||||
fireDocumentPointerMove({ clientX: 100 });
|
||||
flush();
|
||||
|
||||
expect(slider.interaction.current.dragPercent).toBe(50);
|
||||
expect(slider.input.current.dragPercent).toBe(50);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
@@ -267,8 +267,8 @@ describe('createSlider', () => {
|
||||
|
||||
expect(onValueCommit).toHaveBeenCalledWith(50);
|
||||
expect(onDragEnd).toHaveBeenCalled();
|
||||
expect(slider.interaction.current.dragging).toBe(false);
|
||||
expect(slider.interaction.current.pointing).toBe(false);
|
||||
expect(slider.input.current.dragging).toBe(false);
|
||||
expect(slider.input.current.pointing).toBe(false);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
@@ -312,7 +312,7 @@ describe('createSlider', () => {
|
||||
flush();
|
||||
|
||||
expect(onDragEnd).toHaveBeenCalled();
|
||||
expect(slider.interaction.current.dragging).toBe(false);
|
||||
expect(slider.input.current.dragging).toBe(false);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
@@ -340,13 +340,13 @@ describe('createSlider', () => {
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
flush();
|
||||
expect(slider.interaction.current.dragging).toBe(true);
|
||||
expect(slider.input.current.dragging).toBe(true);
|
||||
|
||||
// Stale: buttons = 0, mouse pointer
|
||||
fireDocumentPointerMove({ clientX: 100, buttons: 0, pointerType: 'mouse' });
|
||||
flush();
|
||||
|
||||
expect(slider.interaction.current.dragging).toBe(false);
|
||||
expect(slider.input.current.dragging).toBe(false);
|
||||
expect(onDragEnd).toHaveBeenCalled();
|
||||
|
||||
slider.destroy();
|
||||
@@ -374,13 +374,13 @@ describe('createSlider', () => {
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
flush();
|
||||
expect(slider.interaction.current.dragging).toBe(true);
|
||||
expect(slider.input.current.dragging).toBe(true);
|
||||
|
||||
// Touch with buttons=0 should NOT trigger stale drag detection
|
||||
fireDocumentPointerMove({ clientX: 100, buttons: 0, pointerType: 'touch' });
|
||||
flush();
|
||||
|
||||
expect(slider.interaction.current.dragging).toBe(true);
|
||||
expect(slider.input.current.dragging).toBe(true);
|
||||
expect(onDragEnd).not.toHaveBeenCalled();
|
||||
|
||||
slider.destroy();
|
||||
@@ -395,8 +395,8 @@ describe('createSlider', () => {
|
||||
slider.rootProps.onPointerMove(pointerEvent({ clientX: 60 }));
|
||||
flush();
|
||||
|
||||
expect(slider.interaction.current.pointing).toBe(true);
|
||||
expect(slider.interaction.current.pointerPercent).toBe(30);
|
||||
expect(slider.input.current.pointing).toBe(true);
|
||||
expect(slider.input.current.pointerPercent).toBe(30);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
@@ -409,8 +409,8 @@ describe('createSlider', () => {
|
||||
slider.rootProps.onPointerLeave(pointerEvent());
|
||||
flush();
|
||||
|
||||
expect(slider.interaction.current.pointing).toBe(false);
|
||||
expect(slider.interaction.current.pointerPercent).toBe(0);
|
||||
expect(slider.input.current.pointing).toBe(false);
|
||||
expect(slider.input.current.pointerPercent).toBe(0);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
@@ -423,12 +423,12 @@ describe('createSlider', () => {
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
flush();
|
||||
expect(slider.interaction.current.dragging).toBe(true);
|
||||
expect(slider.input.current.dragging).toBe(true);
|
||||
|
||||
slider.rootProps.onPointerLeave(pointerEvent());
|
||||
flush();
|
||||
|
||||
expect(slider.interaction.current.pointing).toBe(true);
|
||||
expect(slider.input.current.pointing).toBe(true);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
@@ -761,7 +761,7 @@ describe('createSlider', () => {
|
||||
slider.thumbProps.onFocus();
|
||||
flush();
|
||||
|
||||
expect(slider.interaction.current.focused).toBe(true);
|
||||
expect(slider.input.current.focused).toBe(true);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
@@ -773,7 +773,7 @@ describe('createSlider', () => {
|
||||
slider.thumbProps.onBlur();
|
||||
flush();
|
||||
|
||||
expect(slider.interaction.current.focused).toBe(false);
|
||||
expect(slider.input.current.focused).toBe(false);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
@@ -788,7 +788,7 @@ describe('createSlider', () => {
|
||||
slider.rootProps.onPointerDown(pointerEvent({ clientY: 25 }));
|
||||
flush();
|
||||
|
||||
expect(slider.interaction.current.pointerPercent).toBe(75);
|
||||
expect(slider.input.current.pointerPercent).toBe(75);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
@@ -800,7 +800,7 @@ describe('createSlider', () => {
|
||||
slider.rootProps.onPointerDown(pointerEvent({ clientX: 50 }));
|
||||
flush();
|
||||
|
||||
expect(slider.interaction.current.pointerPercent).toBe(25);
|
||||
expect(slider.input.current.pointerPercent).toBe(25);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
@@ -817,7 +817,7 @@ describe('createSlider', () => {
|
||||
flush();
|
||||
|
||||
// Same result as vertical + LTR — RTL has no effect.
|
||||
expect(slider.interaction.current.pointerPercent).toBe(75);
|
||||
expect(slider.input.current.pointerPercent).toBe(75);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
@@ -833,7 +833,7 @@ describe('createSlider', () => {
|
||||
slider.rootProps.onPointerDown(pointerEvent({ clientX: 50 }));
|
||||
flush();
|
||||
|
||||
expect(slider.interaction.current.pointerPercent).toBe(75);
|
||||
expect(slider.input.current.pointerPercent).toBe(75);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createTransitionHandler } from '../transition';
|
||||
import { createTransition } from '../transition';
|
||||
|
||||
describe('createTransitionHandler', () => {
|
||||
describe('createTransition', () => {
|
||||
it('starts with idle state', () => {
|
||||
const handler = createTransitionHandler();
|
||||
const handler = createTransition();
|
||||
expect(handler.state.current).toEqual({ active: false, status: 'idle' });
|
||||
});
|
||||
|
||||
describe('open', () => {
|
||||
it('patches open and starting status synchronously', () => {
|
||||
const handler = createTransitionHandler();
|
||||
const handler = createTransition();
|
||||
|
||||
handler.open();
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('createTransitionHandler', () => {
|
||||
});
|
||||
|
||||
it('transitions to idle after one RAF', async () => {
|
||||
const handler = createTransitionHandler();
|
||||
const handler = createTransition();
|
||||
|
||||
const promise = handler.open();
|
||||
expect(handler.state.current.status).toBe('starting');
|
||||
@@ -33,7 +33,7 @@ describe('createTransitionHandler', () => {
|
||||
|
||||
describe('close', () => {
|
||||
it('patches ending status synchronously', () => {
|
||||
const handler = createTransitionHandler();
|
||||
const handler = createTransition();
|
||||
const el = document.createElement('div');
|
||||
|
||||
// Open first
|
||||
@@ -45,7 +45,7 @@ describe('createTransitionHandler', () => {
|
||||
});
|
||||
|
||||
it('keeps open true during close animation', () => {
|
||||
const handler = createTransitionHandler();
|
||||
const handler = createTransition();
|
||||
const el = document.createElement('div');
|
||||
|
||||
handler.open();
|
||||
@@ -56,7 +56,7 @@ describe('createTransitionHandler', () => {
|
||||
});
|
||||
|
||||
it('handles null element gracefully', async () => {
|
||||
const handler = createTransitionHandler();
|
||||
const handler = createTransition();
|
||||
|
||||
handler.open();
|
||||
const promise = handler.close(null);
|
||||
@@ -74,7 +74,7 @@ describe('createTransitionHandler', () => {
|
||||
|
||||
describe('cancel', () => {
|
||||
it('resets status to idle', () => {
|
||||
const handler = createTransitionHandler();
|
||||
const handler = createTransition();
|
||||
|
||||
handler.open();
|
||||
expect(handler.state.current.status).toBe('starting');
|
||||
@@ -84,7 +84,7 @@ describe('createTransitionHandler', () => {
|
||||
});
|
||||
|
||||
it('preserves open state', () => {
|
||||
const handler = createTransitionHandler();
|
||||
const handler = createTransition();
|
||||
|
||||
handler.open();
|
||||
handler.cancel();
|
||||
@@ -94,7 +94,7 @@ describe('createTransitionHandler', () => {
|
||||
});
|
||||
|
||||
it('is a no-op when already idle', () => {
|
||||
const handler = createTransitionHandler();
|
||||
const handler = createTransition();
|
||||
const callback = vi.fn();
|
||||
|
||||
handler.state.subscribe(callback);
|
||||
@@ -107,7 +107,7 @@ describe('createTransitionHandler', () => {
|
||||
|
||||
describe('destroy', () => {
|
||||
it('prevents further open calls from updating state', () => {
|
||||
const handler = createTransitionHandler();
|
||||
const handler = createTransition();
|
||||
|
||||
handler.destroy();
|
||||
handler.open();
|
||||
@@ -118,7 +118,7 @@ describe('createTransitionHandler', () => {
|
||||
});
|
||||
|
||||
it('is idempotent', () => {
|
||||
const handler = createTransitionHandler();
|
||||
const handler = createTransition();
|
||||
|
||||
handler.destroy();
|
||||
handler.destroy(); // should not throw
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface CreateThumbnailOptions {
|
||||
onStateChange: () => void;
|
||||
}
|
||||
|
||||
export interface ThumbnailHandle {
|
||||
export interface ThumbnailApi {
|
||||
readonly loading: boolean;
|
||||
readonly error: boolean;
|
||||
readonly naturalWidth: number;
|
||||
@@ -19,7 +19,7 @@ export interface ThumbnailHandle {
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export function createThumbnail(options: CreateThumbnailOptions): ThumbnailHandle {
|
||||
export function createThumbnail(options: CreateThumbnailOptions): ThumbnailApi {
|
||||
const { getContainer, getImg, onStateChange } = options;
|
||||
const core = new ThumbnailCore();
|
||||
const abort = new AbortController();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createState, type State } from '@videojs/store';
|
||||
import { noop } from '@videojs/utils/function';
|
||||
import type { TransitionState } from '../../core/ui/transition';
|
||||
|
||||
export interface TransitionHandler {
|
||||
export interface TransitionApi {
|
||||
state: State<TransitionState>;
|
||||
open(): Promise<void>;
|
||||
close(el: HTMLElement | null): Promise<void>;
|
||||
@@ -21,7 +21,7 @@ export interface TransitionHandler {
|
||||
* element stays mounted), then after a double-RAF waits for
|
||||
* `getAnimations()` to settle before patching `{ active: false, status: 'idle' }`.
|
||||
*/
|
||||
export function createTransitionHandler(): TransitionHandler {
|
||||
export function createTransition(): TransitionApi {
|
||||
const state = createState<TransitionState>({ active: false, status: 'idle' });
|
||||
|
||||
let destroyed = false;
|
||||
|
||||
@@ -10,7 +10,9 @@ import { isFunction, isUndefined } from '@videojs/utils/predicate';
|
||||
* - `undefined` removes the attribute
|
||||
* - Other props are set as string attributes
|
||||
*/
|
||||
export function applyElementProps(element: HTMLElement, props: object, signal?: AbortSignal): void {
|
||||
export function applyElementProps(element: HTMLElement, props: object, options?: { signal?: AbortSignal }): void {
|
||||
const signal = options?.signal;
|
||||
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
if (isFunction(value) && key.startsWith('on')) {
|
||||
const event = key.slice(2).toLowerCase();
|
||||
|
||||
Reference in New Issue
Block a user