mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(core): add toggleControls to controls feature (#1280)
This commit is contained in:
@@ -168,6 +168,8 @@ export interface MediaControlsState {
|
||||
userActive: boolean;
|
||||
/** Whether controls should be visible (userActive || paused). */
|
||||
controlsVisible: boolean;
|
||||
/** Toggle controls visibility. Returns the new `controlsVisible` value. */
|
||||
toggleControls(): boolean;
|
||||
}
|
||||
|
||||
export interface MediaPlaybackRateState {
|
||||
|
||||
@@ -73,6 +73,7 @@ function createControlsState(overrides: Partial<MediaControlsState> = {}): Media
|
||||
return {
|
||||
userActive: true,
|
||||
controlsVisible: true,
|
||||
toggleControls: () => true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,9 +9,15 @@ const TAP_THRESHOLD = 250;
|
||||
|
||||
export const controlsFeature = definePlayerFeature({
|
||||
name: 'controls',
|
||||
state: (): MediaControlsState => ({
|
||||
state: ({ get, set }): MediaControlsState => ({
|
||||
userActive: true,
|
||||
controlsVisible: true,
|
||||
toggleControls() {
|
||||
// Fallback before attach — no idle timer, just flip state.
|
||||
const next = !get().userActive;
|
||||
set({ userActive: next, controlsVisible: next });
|
||||
return next as boolean;
|
||||
},
|
||||
}),
|
||||
|
||||
attach({ target, signal, get, set }) {
|
||||
@@ -53,6 +59,18 @@ export const controlsFeature = definePlayerFeature({
|
||||
set({ userActive: false, controlsVisible: computeVisible(false) });
|
||||
}
|
||||
|
||||
// Expose toggleControls with access to idle timer.
|
||||
set({
|
||||
toggleControls() {
|
||||
if (get().controlsVisible) {
|
||||
setInactive();
|
||||
} else {
|
||||
setActive();
|
||||
}
|
||||
return get().controlsVisible;
|
||||
},
|
||||
});
|
||||
|
||||
// Touch tap-to-toggle
|
||||
let pointerDownTime = 0;
|
||||
|
||||
|
||||
@@ -287,6 +287,73 @@ describe('controlsFeature', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleControls', () => {
|
||||
it('hides controls when visible and playing', () => {
|
||||
const video = createMockVideo({ paused: false });
|
||||
const { store } = createPlayerStore(video);
|
||||
|
||||
const result = store.state.toggleControls();
|
||||
flush();
|
||||
|
||||
expect(store.state.userActive).toBe(false);
|
||||
expect(store.state.controlsVisible).toBe(false);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('shows controls when hidden', () => {
|
||||
const video = createMockVideo({ paused: false });
|
||||
const { store } = createPlayerStore(video);
|
||||
|
||||
// First toggle to hide
|
||||
store.state.toggleControls();
|
||||
flush();
|
||||
|
||||
expect(store.state.controlsVisible).toBe(false);
|
||||
|
||||
// Second toggle to show
|
||||
const result = store.state.toggleControls();
|
||||
flush();
|
||||
|
||||
expect(store.state.userActive).toBe(true);
|
||||
expect(store.state.controlsVisible).toBe(true);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('reschedules idle timer when showing controls', () => {
|
||||
const video = createMockVideo({ paused: false });
|
||||
const { store } = createPlayerStore(video);
|
||||
|
||||
// Hide controls
|
||||
store.state.toggleControls();
|
||||
flush();
|
||||
|
||||
// Show controls
|
||||
store.state.toggleControls();
|
||||
flush();
|
||||
|
||||
expect(store.state.controlsVisible).toBe(true);
|
||||
|
||||
// Should hide again after idle delay
|
||||
vi.advanceTimersByTime(IDLE_DELAY);
|
||||
flush();
|
||||
|
||||
expect(store.state.userActive).toBe(false);
|
||||
expect(store.state.controlsVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps controlsVisible true when toggling off while paused', () => {
|
||||
const video = createMockVideo({ paused: true });
|
||||
const { store } = createPlayerStore(video);
|
||||
|
||||
const result = store.state.toggleControls();
|
||||
flush();
|
||||
|
||||
expect(store.state.userActive).toBe(false);
|
||||
expect(store.state.controlsVisible).toBe(true);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('null container', () => {
|
||||
it('does not track activity without container', () => {
|
||||
const video = createMockVideo({ paused: false });
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { AnySlice, InferSliceState, StateContext } from './slice';
|
||||
const stateContext: StateContext<unknown> = {
|
||||
target: throwNoTargetError,
|
||||
signals: new AbortControllerRegistry(),
|
||||
get: throwNoTargetError,
|
||||
set: throwNoTargetError,
|
||||
};
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ export interface StateContext<Target> {
|
||||
* (e.g., loading a new source cancels pending seeks).
|
||||
*/
|
||||
signals: AbortControllerRegistry;
|
||||
/** Read current slice state. Safe to use inside action closures (not during `state()` init). */
|
||||
get: () => Readonly<Record<string, unknown>>;
|
||||
/** Patch the slice state. Safe to use inside action closures (not during `state()` init). */
|
||||
set: (partial: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ export function createStore<Target = unknown>(): <State>(
|
||||
return target!;
|
||||
},
|
||||
signals,
|
||||
get: () => state.current as Readonly<Record<string, unknown>>,
|
||||
set: (partial) => state.patch(partial as Partial<State>),
|
||||
} satisfies StateContext<Target>);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user