mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(packages): add UI support for gestures and hotkeys (#1388)
Co-authored-by: Rahim <rahim.alwer@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Rahim
Claude Opus 4.6
parent
6c81f2d190
commit
0620814a67
@@ -37,7 +37,7 @@ export function Gesture({ type, action, value, pointer, region, disabled }: Gest
|
||||
resolver({ store, value, event });
|
||||
};
|
||||
|
||||
const options = { pointer, region, action };
|
||||
const options = { pointer, region, action, value };
|
||||
|
||||
if (type === 'doubletap') {
|
||||
return createDoubleTapGesture(container, onActivate, options);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './gesture';
|
||||
@@ -33,6 +33,7 @@ export function Hotkey({ keys, action, value, disabled, target }: HotkeyProps):
|
||||
return createHotkey(container, {
|
||||
keys,
|
||||
action,
|
||||
value,
|
||||
target,
|
||||
disabled,
|
||||
repeatable: !isHotkeyToggleAction(action),
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './hotkey';
|
||||
@@ -0,0 +1,136 @@
|
||||
import { cleanup, fireEvent, render } from '@testing-library/react';
|
||||
import type { SeekIndicatorCore, StatusIndicatorCore, VolumeIndicatorCore } from '@videojs/core';
|
||||
import type { ReactNode } from 'react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { PlayerContextProvider, type PlayerContextValue } from '../../../player/context';
|
||||
import { SeekIndicator } from '../../seek-indicator';
|
||||
import { SeekIndicatorProvider } from '../../seek-indicator/context';
|
||||
import { StatusAnnouncer } from '../../status-announcer/status-announcer';
|
||||
import { StatusIndicator } from '../../status-indicator';
|
||||
import { StatusIndicatorProvider } from '../../status-indicator/context';
|
||||
import { VolumeIndicator } from '../../volume-indicator';
|
||||
import { VolumeIndicatorProvider } from '../../volume-indicator/context';
|
||||
import { useIndicatorVisibility } from '../use-indicator-visibility';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe('input indicators', () => {
|
||||
it('renders status values from the nearest status item context', () => {
|
||||
const state: StatusIndicatorCore.State = {
|
||||
open: true,
|
||||
generation: 1,
|
||||
status: 'captions-on',
|
||||
label: 'Captions on',
|
||||
value: null,
|
||||
transitionStarting: false,
|
||||
transitionEnding: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<StatusIndicatorProvider value={{ state }}>
|
||||
<StatusIndicator.Value data-testid="value" />
|
||||
</StatusIndicatorProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId('value').textContent).toBe('Captions on');
|
||||
});
|
||||
|
||||
it('uses implicit StatusAnnouncer live-region semantics without rendering text content', () => {
|
||||
const { getByRole } = renderWithPlayer(<StatusAnnouncer />);
|
||||
|
||||
expect(getByRole('status').hasAttribute('aria-live')).toBe(false);
|
||||
expect(getByRole('status').textContent).toBe('');
|
||||
});
|
||||
|
||||
it('scopes the volume CSS variable to VolumeIndicator.Fill', () => {
|
||||
const state: VolumeIndicatorCore.State = {
|
||||
open: true,
|
||||
generation: 1,
|
||||
level: 'high',
|
||||
value: '60%',
|
||||
fill: '60%',
|
||||
min: false,
|
||||
max: false,
|
||||
transitionStarting: false,
|
||||
transitionEnding: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<VolumeIndicatorProvider value={{ state }}>
|
||||
<div data-testid="root">
|
||||
<VolumeIndicator.Fill data-testid="fill">
|
||||
<VolumeIndicator.Value data-testid="value" />
|
||||
</VolumeIndicator.Fill>
|
||||
</div>
|
||||
</VolumeIndicatorProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId('root').style.getPropertyValue('--media-volume-fill')).toBe('');
|
||||
expect(getByTestId('fill').style.getPropertyValue('--media-volume-fill')).toBe('60%');
|
||||
expect(getByTestId('value').textContent).toBe('60%');
|
||||
});
|
||||
|
||||
it('keeps seek value content populated while mounted', () => {
|
||||
const state: SeekIndicatorCore.State = {
|
||||
open: true,
|
||||
generation: 1,
|
||||
direction: 'forward',
|
||||
count: 1,
|
||||
seekTotal: 10,
|
||||
value: null,
|
||||
currentTime: '0:30',
|
||||
transitionStarting: false,
|
||||
transitionEnding: false,
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<SeekIndicatorProvider value={{ state }}>
|
||||
<SeekIndicator.Value data-testid="value" />
|
||||
</SeekIndicatorProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId('value').textContent).toBe('0:30');
|
||||
});
|
||||
|
||||
it('closes the previous visual indicator when a new one is shown', () => {
|
||||
const firstClose = vi.fn();
|
||||
const secondClose = vi.fn();
|
||||
const { getByTestId } = renderWithPlayer(
|
||||
<>
|
||||
<VisibilityProbe close={firstClose} id="first" />
|
||||
<VisibilityProbe close={secondClose} id="second" />
|
||||
</>
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('second'));
|
||||
|
||||
expect(firstClose).toHaveBeenCalledOnce();
|
||||
expect(secondClose).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
function VisibilityProbe({ close, id }: { close: () => void; id: string }) {
|
||||
const show = useIndicatorVisibility(close);
|
||||
return (
|
||||
<button data-testid={id} onClick={show} type="button">
|
||||
{id}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function renderWithPlayer(ui: ReactNode) {
|
||||
const container = document.createElement('div');
|
||||
const playerContextValue = {
|
||||
store: {
|
||||
state: {},
|
||||
subscribe: () => () => {},
|
||||
},
|
||||
media: null,
|
||||
setMedia: vi.fn(),
|
||||
container,
|
||||
setContainer: vi.fn(),
|
||||
} as unknown as PlayerContextValue;
|
||||
|
||||
return render(<PlayerContextProvider value={playerContextValue}>{ui}</PlayerContextProvider>);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import type { IndicatorVisibilityHandle } from '@videojs/core';
|
||||
import { getIndicatorVisibilityCoordinator } from '@videojs/core/dom';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useContainer } from '../../player/context';
|
||||
import { useLatestRef } from '../../utils/use-latest-ref';
|
||||
|
||||
export function useIndicatorVisibility(close: () => void): () => void {
|
||||
const container = useContainer();
|
||||
const closeRef = useLatestRef(close);
|
||||
const coordinatorRef = useRef<ReturnType<typeof getIndicatorVisibilityCoordinator> | null>(null);
|
||||
const [handle] = useState<IndicatorVisibilityHandle>(() => ({
|
||||
close: () => closeRef.current(),
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (!container) return;
|
||||
|
||||
const coordinator = getIndicatorVisibilityCoordinator(container);
|
||||
coordinatorRef.current = coordinator;
|
||||
|
||||
return coordinator.register(handle);
|
||||
}, [container, handle]);
|
||||
|
||||
return useCallback(() => {
|
||||
coordinatorRef.current?.show(handle);
|
||||
}, [handle]);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import type { InputActionEvent, MediaSnapshot } from '@videojs/core';
|
||||
import { getMediaSnapshot, subscribeToInputActions } from '@videojs/core/dom';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useContainer, usePlayer } from '../../player/context';
|
||||
import { useLatestRef } from '../../utils/use-latest-ref';
|
||||
|
||||
export function useInputActionSubscription(callback: (event: InputActionEvent, snapshot: MediaSnapshot) => void): void {
|
||||
const container = useContainer();
|
||||
const store = usePlayer();
|
||||
const callbackRef = useLatestRef(callback);
|
||||
const storeRef = useLatestRef(store);
|
||||
|
||||
useEffect(() => {
|
||||
if (!container) return;
|
||||
|
||||
return subscribeToInputActions(container, (event) => {
|
||||
callbackRef.current(event, getMediaSnapshot(storeRef.current));
|
||||
});
|
||||
}, [container]);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import type { IndicatorLifecycleState, InputActionEvent, MediaSnapshot } from '@videojs/core';
|
||||
import type { State as StoreState } from '@videojs/store';
|
||||
import { useState, useSyncExternalStore } from 'react';
|
||||
|
||||
import { useDestroy } from '../../utils/use-destroy';
|
||||
import { useIndicatorVisibility } from './use-indicator-visibility';
|
||||
import { useInputActionSubscription } from './use-input-action-subscription';
|
||||
import { useRenderedIndicatorState } from './use-rendered-indicator-state';
|
||||
|
||||
interface InputIndicatorRootCore<IndicatorState extends IndicatorLifecycleState, Props> {
|
||||
readonly state: StoreState<IndicatorState>;
|
||||
setProps(props: Props): void;
|
||||
destroy(): void;
|
||||
close(): void;
|
||||
processEvent(event: InputActionEvent, snapshot: MediaSnapshot): boolean;
|
||||
}
|
||||
|
||||
export function useInputIndicatorRoot<IndicatorState extends IndicatorLifecycleState, Props>(
|
||||
createCore: () => InputIndicatorRootCore<IndicatorState, Props>,
|
||||
props: Props
|
||||
) {
|
||||
const [core] = useState(createCore);
|
||||
useDestroy(core);
|
||||
core.setProps(props);
|
||||
const showIndicator = useIndicatorVisibility(() => core.close());
|
||||
|
||||
useInputActionSubscription((event, snapshot) => {
|
||||
if (core.processEvent(event, snapshot)) showIndicator();
|
||||
});
|
||||
|
||||
const currentState = useSyncExternalStore(
|
||||
(callback) => core.state.subscribe(callback),
|
||||
() => core.state.current,
|
||||
() => core.state.current
|
||||
);
|
||||
|
||||
return useRenderedIndicatorState(currentState);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { getRenderedIndicatorState, type IndicatorLifecycleState, isIndicatorPresent } from '@videojs/core';
|
||||
import { createTransition } from '@videojs/core/dom';
|
||||
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
||||
|
||||
import { useDestroy } from '../../utils/use-destroy';
|
||||
|
||||
export function useRenderedIndicatorState<State extends IndicatorLifecycleState>(currentState: State) {
|
||||
const elementRef = useRef<HTMLElement>(null);
|
||||
const currentStateRef = useRef(currentState);
|
||||
const snapshotRef = useRef(currentState);
|
||||
const [transition] = useState(() => createTransition());
|
||||
useDestroy(transition);
|
||||
currentStateRef.current = currentState;
|
||||
|
||||
const transitionState = useSyncExternalStore(
|
||||
(callback) => transition.state.subscribe(callback),
|
||||
() => transition.state.current,
|
||||
() => transition.state.current
|
||||
);
|
||||
|
||||
const { generation, open } = currentState;
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const nextState = currentStateRef.current;
|
||||
if (nextState.generation !== generation) return;
|
||||
|
||||
snapshotRef.current = nextState;
|
||||
void transition.open();
|
||||
return;
|
||||
}
|
||||
|
||||
const { active, status } = transition.state.current;
|
||||
if (active && status !== 'ending') {
|
||||
void transition.close(elementRef.current);
|
||||
}
|
||||
}, [generation, open, transition]);
|
||||
|
||||
return {
|
||||
elementRef,
|
||||
present: isIndicatorPresent(currentState, transitionState),
|
||||
state: getRenderedIndicatorState(currentState, snapshotRef.current, transitionState),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import type { SeekIndicatorCore } from '@videojs/core';
|
||||
import { createContext, type ProviderProps, useContext } from 'react';
|
||||
|
||||
export interface SeekIndicatorContextValue {
|
||||
state: SeekIndicatorCore.State;
|
||||
}
|
||||
|
||||
const SeekIndicatorContext = createContext<SeekIndicatorContextValue | null>(null);
|
||||
|
||||
export function SeekIndicatorProvider({ value, children }: ProviderProps<SeekIndicatorContextValue>) {
|
||||
return <SeekIndicatorContext.Provider value={value}>{children}</SeekIndicatorContext.Provider>;
|
||||
}
|
||||
|
||||
export function useSeekIndicatorContext(): SeekIndicatorContextValue {
|
||||
const ctx = useContext(SeekIndicatorContext);
|
||||
if (!ctx) throw new Error('SeekIndicator child compounds must be used within a SeekIndicator.Root');
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { SeekIndicatorRoot as Root, type SeekIndicatorRootProps as RootProps } from './seek-indicator-root';
|
||||
export { SeekIndicatorValue as Value, type SeekIndicatorValueProps as ValueProps } from './seek-indicator-value';
|
||||
@@ -0,0 +1 @@
|
||||
export * as SeekIndicator from './index.parts';
|
||||
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import { SeekIndicatorCore, SeekIndicatorDataAttrs } from '@videojs/core';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import type { UIComponentProps } from '../../utils/types';
|
||||
import { renderElement } from '../../utils/use-render';
|
||||
import { useInputIndicatorRoot } from '../input-indicators/use-input-indicator-root';
|
||||
import { SeekIndicatorProvider } from './context';
|
||||
|
||||
export interface SeekIndicatorRootProps
|
||||
extends UIComponentProps<'div', SeekIndicatorCore.State>,
|
||||
SeekIndicatorCore.Props {}
|
||||
|
||||
export const SeekIndicatorRoot = forwardRef(function SeekIndicatorRoot(
|
||||
componentProps: SeekIndicatorRootProps,
|
||||
forwardedRef: ForwardedRef<HTMLDivElement>
|
||||
) {
|
||||
const { render, className, style, closeDelay, ...elementProps } = componentProps;
|
||||
const { elementRef, present, state } = useInputIndicatorRoot(() => new SeekIndicatorCore(), { closeDelay });
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
return (
|
||||
<SeekIndicatorProvider value={{ state }}>
|
||||
{renderElement(
|
||||
'div',
|
||||
{ render, className, style },
|
||||
{
|
||||
state,
|
||||
stateAttrMap: SeekIndicatorDataAttrs,
|
||||
ref: [forwardedRef, elementRef],
|
||||
props: [elementProps],
|
||||
}
|
||||
)}
|
||||
</SeekIndicatorProvider>
|
||||
);
|
||||
});
|
||||
|
||||
export namespace SeekIndicatorRoot {
|
||||
export type Props = SeekIndicatorRootProps;
|
||||
export type State = SeekIndicatorCore.State;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { getSeekIndicatorDisplayValue, type SeekIndicatorCore } from '@videojs/core';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import type { UIComponentProps } from '../../utils/types';
|
||||
import { renderElement } from '../../utils/use-render';
|
||||
import { useSeekIndicatorContext } from './context';
|
||||
|
||||
export interface SeekIndicatorValueProps extends UIComponentProps<'div', SeekIndicatorCore.State> {}
|
||||
|
||||
export const SeekIndicatorValue = forwardRef(function SeekIndicatorValue(
|
||||
componentProps: SeekIndicatorValueProps,
|
||||
forwardedRef: ForwardedRef<HTMLDivElement>
|
||||
) {
|
||||
const { render, className, style, ...elementProps } = componentProps;
|
||||
const { state } = useSeekIndicatorContext();
|
||||
|
||||
return renderElement(
|
||||
'div',
|
||||
{ render, className, style },
|
||||
{
|
||||
state,
|
||||
ref: forwardedRef,
|
||||
props: [{ children: getSeekIndicatorDisplayValue(state) }, elementProps],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export namespace SeekIndicatorValue {
|
||||
export type Props = SeekIndicatorValueProps;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { SliderState, StateAttrMap } from '@videojs/core';
|
||||
import type { SliderThumbProps } from '@videojs/core/dom';
|
||||
import type { RefCallback } from 'react';
|
||||
import type { ProviderProps, RefCallback } from 'react';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export interface SliderContextValue {
|
||||
@@ -18,7 +18,9 @@ export interface SliderContextValue {
|
||||
|
||||
const SliderContext = createContext<SliderContextValue | null>(null);
|
||||
|
||||
export function SliderProvider({ value, children }: { value: SliderContextValue; children: React.ReactNode }) {
|
||||
type SliderProviderProps = ProviderProps<SliderContextValue>;
|
||||
|
||||
export function SliderProvider({ value, children }: SliderProviderProps) {
|
||||
return <SliderContext.Provider value={value}>{children}</SliderContext.Provider>;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { StatusAnnouncerCore } from '@videojs/core';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef, useState, useSyncExternalStore } from 'react';
|
||||
|
||||
import type { UIComponentProps } from '../../utils/types';
|
||||
import { useDestroy } from '../../utils/use-destroy';
|
||||
import { renderElement } from '../../utils/use-render';
|
||||
import { useInputActionSubscription } from '../input-indicators/use-input-action-subscription';
|
||||
|
||||
export interface StatusAnnouncerProps
|
||||
extends UIComponentProps<'div', StatusAnnouncerCore.State>,
|
||||
StatusAnnouncerCore.Props {}
|
||||
|
||||
export const StatusAnnouncer = forwardRef(function StatusAnnouncer(
|
||||
componentProps: StatusAnnouncerProps,
|
||||
forwardedRef: ForwardedRef<HTMLDivElement>
|
||||
) {
|
||||
const { render, className, style, closeDelay, labels, ...elementProps } = componentProps;
|
||||
const [core] = useState(() => new StatusAnnouncerCore());
|
||||
useDestroy(core);
|
||||
core.setProps({ closeDelay, labels });
|
||||
|
||||
useInputActionSubscription((event, snapshot) => {
|
||||
core.processEvent(event, snapshot);
|
||||
});
|
||||
|
||||
const state = useSyncExternalStore(
|
||||
(callback) => core.state.subscribe(callback),
|
||||
() => core.state.current,
|
||||
() => core.state.current
|
||||
);
|
||||
|
||||
return renderElement(
|
||||
'div',
|
||||
{ render, className, style },
|
||||
{
|
||||
state,
|
||||
ref: forwardedRef,
|
||||
props: [
|
||||
{
|
||||
role: 'status',
|
||||
'aria-label': state.label ?? undefined,
|
||||
},
|
||||
elementProps,
|
||||
],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export namespace StatusAnnouncer {
|
||||
export type Props = StatusAnnouncerProps;
|
||||
export type State = StatusAnnouncerCore.State;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import type { StatusIndicatorCore } from '@videojs/core';
|
||||
import { createContext, type ProviderProps, useContext } from 'react';
|
||||
|
||||
export interface StatusIndicatorContextValue {
|
||||
state: StatusIndicatorCore.State;
|
||||
}
|
||||
|
||||
const StatusIndicatorContext = createContext<StatusIndicatorContextValue | null>(null);
|
||||
|
||||
export function StatusIndicatorProvider({ value, children }: ProviderProps<StatusIndicatorContextValue>) {
|
||||
return <StatusIndicatorContext.Provider value={value}>{children}</StatusIndicatorContext.Provider>;
|
||||
}
|
||||
|
||||
export function useStatusIndicatorContext(): StatusIndicatorContextValue {
|
||||
const ctx = useContext(StatusIndicatorContext);
|
||||
if (!ctx) throw new Error('StatusIndicator child compounds must be used within a StatusIndicator.Root');
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
StatusIndicatorRoot as Root,
|
||||
type StatusIndicatorRootProps as RootProps,
|
||||
} from './status-indicator-root';
|
||||
export {
|
||||
StatusIndicatorValue as Value,
|
||||
type StatusIndicatorValueProps as ValueProps,
|
||||
} from './status-indicator-value';
|
||||
@@ -0,0 +1 @@
|
||||
export * as StatusIndicator from './index.parts';
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import { StatusIndicatorCore, StatusIndicatorDataAttrs } from '@videojs/core';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import type { UIComponentProps } from '../../utils/types';
|
||||
import { renderElement } from '../../utils/use-render';
|
||||
import { useInputIndicatorRoot } from '../input-indicators/use-input-indicator-root';
|
||||
import { StatusIndicatorProvider } from './context';
|
||||
|
||||
export interface StatusIndicatorRootProps
|
||||
extends UIComponentProps<'div', StatusIndicatorCore.State>,
|
||||
StatusIndicatorCore.Props {}
|
||||
|
||||
export const StatusIndicatorRoot = forwardRef(function StatusIndicatorRoot(
|
||||
componentProps: StatusIndicatorRootProps,
|
||||
forwardedRef: ForwardedRef<HTMLDivElement>
|
||||
) {
|
||||
const { render, className, style, actions, closeDelay, labels, ...elementProps } = componentProps;
|
||||
const { elementRef, present, state } = useInputIndicatorRoot(() => new StatusIndicatorCore(), {
|
||||
actions,
|
||||
closeDelay,
|
||||
labels,
|
||||
});
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
return (
|
||||
<StatusIndicatorProvider value={{ state }}>
|
||||
{renderElement(
|
||||
'div',
|
||||
{ render, className, style },
|
||||
{
|
||||
state,
|
||||
stateAttrMap: StatusIndicatorDataAttrs,
|
||||
ref: [forwardedRef, elementRef],
|
||||
props: [elementProps],
|
||||
}
|
||||
)}
|
||||
</StatusIndicatorProvider>
|
||||
);
|
||||
});
|
||||
|
||||
export namespace StatusIndicatorRoot {
|
||||
export type Props = StatusIndicatorRootProps;
|
||||
export type State = StatusIndicatorCore.State;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { getStatusIndicatorDisplayValue, type StatusIndicatorCore } from '@videojs/core';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import type { UIComponentProps } from '../../utils/types';
|
||||
import { renderElement } from '../../utils/use-render';
|
||||
import { useStatusIndicatorContext } from './context';
|
||||
|
||||
export interface StatusIndicatorValueProps extends UIComponentProps<'span', StatusIndicatorCore.State> {}
|
||||
|
||||
export const StatusIndicatorValue = forwardRef(function StatusIndicatorValue(
|
||||
componentProps: StatusIndicatorValueProps,
|
||||
forwardedRef: ForwardedRef<HTMLSpanElement>
|
||||
) {
|
||||
const { render, className, style, ...elementProps } = componentProps;
|
||||
const { state } = useStatusIndicatorContext();
|
||||
|
||||
return renderElement(
|
||||
'span',
|
||||
{ render, className, style },
|
||||
{
|
||||
state,
|
||||
ref: forwardedRef,
|
||||
props: [{ children: getStatusIndicatorDisplayValue(state) }, elementProps],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export namespace StatusIndicatorValue {
|
||||
export type Props = StatusIndicatorValueProps;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import type { VolumeIndicatorCore } from '@videojs/core';
|
||||
import { createContext, type ProviderProps, useContext } from 'react';
|
||||
|
||||
export interface VolumeIndicatorContextValue {
|
||||
state: VolumeIndicatorCore.State;
|
||||
}
|
||||
|
||||
const VolumeIndicatorContext = createContext<VolumeIndicatorContextValue | null>(null);
|
||||
|
||||
export function VolumeIndicatorProvider({ value, children }: ProviderProps<VolumeIndicatorContextValue>) {
|
||||
return <VolumeIndicatorContext.Provider value={value}>{children}</VolumeIndicatorContext.Provider>;
|
||||
}
|
||||
|
||||
export function useVolumeIndicatorContext(): VolumeIndicatorContextValue {
|
||||
const ctx = useContext(VolumeIndicatorContext);
|
||||
if (!ctx) throw new Error('VolumeIndicator child compounds must be used within a VolumeIndicator.Root');
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export { VolumeIndicatorFill as Fill, type VolumeIndicatorFillProps as FillProps } from './volume-indicator-fill';
|
||||
export { VolumeIndicatorRoot as Root, type VolumeIndicatorRootProps as RootProps } from './volume-indicator-root';
|
||||
export {
|
||||
VolumeIndicatorValue as Value,
|
||||
type VolumeIndicatorValueProps as ValueProps,
|
||||
} from './volume-indicator-value';
|
||||
@@ -0,0 +1 @@
|
||||
export * as VolumeIndicator from './index.parts';
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { type VolumeIndicatorCore, VolumeIndicatorCSSVars } from '@videojs/core';
|
||||
import { isFunction } from '@videojs/utils/predicate';
|
||||
import type { CSSProperties, ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import type { UIComponentProps } from '../../utils/types';
|
||||
import { renderElement } from '../../utils/use-render';
|
||||
import { useVolumeIndicatorContext } from './context';
|
||||
|
||||
export interface VolumeIndicatorFillProps extends UIComponentProps<'div', VolumeIndicatorCore.State> {}
|
||||
|
||||
export const VolumeIndicatorFill = forwardRef(function VolumeIndicatorFill(
|
||||
componentProps: VolumeIndicatorFillProps,
|
||||
forwardedRef: ForwardedRef<HTMLDivElement>
|
||||
) {
|
||||
const { render, className, style, ...elementProps } = componentProps;
|
||||
const { state } = useVolumeIndicatorContext();
|
||||
const fillStyle = getVolumeIndicatorFillStyle(state, style);
|
||||
|
||||
return renderElement(
|
||||
'div',
|
||||
{ render, className, style: fillStyle },
|
||||
{
|
||||
state,
|
||||
ref: forwardedRef,
|
||||
props: [elementProps],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export namespace VolumeIndicatorFill {
|
||||
export type Props = VolumeIndicatorFillProps;
|
||||
}
|
||||
|
||||
function getVolumeIndicatorFillStyle(
|
||||
state: VolumeIndicatorCore.State,
|
||||
style: VolumeIndicatorFillProps['style']
|
||||
): VolumeIndicatorFillProps['style'] {
|
||||
const vars = state.fill
|
||||
? ({
|
||||
[VolumeIndicatorCSSVars.fill]: state.fill,
|
||||
} as CSSProperties)
|
||||
: undefined;
|
||||
|
||||
if (!vars) return style;
|
||||
|
||||
if (isFunction(style)) {
|
||||
return (nextState) => ({
|
||||
...style(nextState),
|
||||
...vars,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...style,
|
||||
...vars,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import { VolumeIndicatorCore, VolumeIndicatorDataAttrs } from '@videojs/core';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import type { UIComponentProps } from '../../utils/types';
|
||||
import { renderElement } from '../../utils/use-render';
|
||||
import { useInputIndicatorRoot } from '../input-indicators/use-input-indicator-root';
|
||||
import { VolumeIndicatorProvider } from './context';
|
||||
|
||||
export interface VolumeIndicatorRootProps
|
||||
extends UIComponentProps<'div', VolumeIndicatorCore.State>,
|
||||
VolumeIndicatorCore.Props {}
|
||||
|
||||
export const VolumeIndicatorRoot = forwardRef(function VolumeIndicatorRoot(
|
||||
componentProps: VolumeIndicatorRootProps,
|
||||
forwardedRef: ForwardedRef<HTMLDivElement>
|
||||
) {
|
||||
const { render, className, style, closeDelay, ...elementProps } = componentProps;
|
||||
const { elementRef, present, state } = useInputIndicatorRoot(() => new VolumeIndicatorCore(), { closeDelay });
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
return (
|
||||
<VolumeIndicatorProvider value={{ state }}>
|
||||
{renderElement(
|
||||
'div',
|
||||
{ render, className, style },
|
||||
{
|
||||
state,
|
||||
stateAttrMap: VolumeIndicatorDataAttrs,
|
||||
ref: [forwardedRef, elementRef],
|
||||
props: [elementProps],
|
||||
}
|
||||
)}
|
||||
</VolumeIndicatorProvider>
|
||||
);
|
||||
});
|
||||
|
||||
export namespace VolumeIndicatorRoot {
|
||||
export type Props = VolumeIndicatorRootProps;
|
||||
export type State = VolumeIndicatorCore.State;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { getVolumeIndicatorDisplayValue, type VolumeIndicatorCore } from '@videojs/core';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import type { UIComponentProps } from '../../utils/types';
|
||||
import { renderElement } from '../../utils/use-render';
|
||||
import { useVolumeIndicatorContext } from './context';
|
||||
|
||||
export interface VolumeIndicatorValueProps extends UIComponentProps<'span', VolumeIndicatorCore.State> {}
|
||||
|
||||
export const VolumeIndicatorValue = forwardRef(function VolumeIndicatorValue(
|
||||
componentProps: VolumeIndicatorValueProps,
|
||||
forwardedRef: ForwardedRef<HTMLSpanElement>
|
||||
) {
|
||||
const { render, className, style, ...elementProps } = componentProps;
|
||||
const { state } = useVolumeIndicatorContext();
|
||||
|
||||
return renderElement(
|
||||
'span',
|
||||
{ render, className, style },
|
||||
{
|
||||
state,
|
||||
ref: forwardedRef,
|
||||
props: [{ children: getVolumeIndicatorDisplayValue(state) }, elementProps],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export namespace VolumeIndicatorValue {
|
||||
export type Props = VolumeIndicatorValueProps;
|
||||
}
|
||||
Reference in New Issue
Block a user