mirror of
https://github.com/zoriya/v10.git
synced 2026-08-15 10:23:32 +00:00
feat(react): add hotkeys (#1241)
This commit is contained in:
@@ -41,6 +41,9 @@ export { ErrorDialog, type ErrorDialogContextValue, useErrorDialogContext } from
|
||||
export { FullscreenButton, type FullscreenButtonProps } from './ui/fullscreen-button/fullscreen-button';
|
||||
export { useButton } from './ui/hooks/use-button';
|
||||
export { useSlider } from './ui/hooks/use-slider';
|
||||
export { MediaHotkey, type MediaHotkeyProps } from './ui/hotkey/media-hotkey';
|
||||
export { useAriaKeyShortcuts } from './ui/hotkey/use-aria-key-shortcuts';
|
||||
export { type UseHotkeyOptions, useHotkey } from './ui/hotkey/use-hotkey';
|
||||
export { MuteButton, type MuteButtonProps } from './ui/mute-button/mute-button';
|
||||
export { PiPButton, type PiPButtonProps } from './ui/pip-button/pip-button';
|
||||
export { PlayButton, type PlayButtonProps } from './ui/play-button/play-button';
|
||||
|
||||
@@ -17,6 +17,7 @@ export const CaptionsButton = createMediaButton<CaptionsButtonCore, CaptionsButt
|
||||
stateAttrMap: CaptionsButtonDataAttrs,
|
||||
selector: selectTextTrack,
|
||||
action: (core, state) => core.toggle(state),
|
||||
hotkeyAction: 'toggleSubtitles',
|
||||
});
|
||||
|
||||
export namespace CaptionsButton {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { usePlayer } from '../player/context';
|
||||
import type { renderElement as renderElementFn } from '../utils/use-render';
|
||||
import { renderElement } from '../utils/use-render';
|
||||
import { useButton } from './hooks/use-button';
|
||||
import { useAriaKeyShortcuts } from './hotkey/use-aria-key-shortcuts';
|
||||
import { useOptionalTooltipContext } from './tooltip/context';
|
||||
|
||||
interface MediaButtonConfig<Core extends Required<MediaButtonComponent>> {
|
||||
@@ -18,13 +19,14 @@ interface MediaButtonConfig<Core extends Required<MediaButtonComponent>> {
|
||||
stateAttrMap: StateAttrMap<InferComponentState<Core>>;
|
||||
selector: Selector<object, InferMediaState<Core> | undefined>;
|
||||
action: (core: Core, state: InferMediaState<Core>) => void;
|
||||
hotkeyAction?: string;
|
||||
}
|
||||
|
||||
/** Creates a media button React component from a core class and config. */
|
||||
export function createMediaButton<Core extends Required<MediaButtonComponent>, Props extends object>(
|
||||
config: MediaButtonConfig<Core>
|
||||
): ForwardRefExoticComponent<Props & RefAttributes<HTMLButtonElement>> {
|
||||
const { displayName, core: CoreClass, stateAttrMap, selector, action } = config;
|
||||
const { displayName, core: CoreClass, stateAttrMap, selector, action, hotkeyAction } = config;
|
||||
|
||||
// Props that exist in the core's defaultProps are routed to setProps; the rest go to the DOM element.
|
||||
const corePropKeys = new Set(Object.keys(CoreClass.defaultProps));
|
||||
@@ -48,6 +50,7 @@ export function createMediaButton<Core extends Required<MediaButtonComponent>, P
|
||||
|
||||
const tooltipCtx = useOptionalTooltipContext();
|
||||
const feature = usePlayer(selector);
|
||||
const shortcuts = useAriaKeyShortcuts(hotkeyAction);
|
||||
|
||||
const [core] = useState(() => new CoreClass());
|
||||
core.setProps(coreProps);
|
||||
@@ -77,7 +80,7 @@ export function createMediaButton<Core extends Required<MediaButtonComponent>, P
|
||||
return null;
|
||||
}
|
||||
|
||||
const attrs = core.getAttrs(state);
|
||||
const attrs = { ...core.getAttrs(state), 'aria-keyshortcuts': shortcuts };
|
||||
|
||||
return renderElement(
|
||||
'button',
|
||||
|
||||
@@ -17,6 +17,7 @@ export const FullscreenButton = createMediaButton<FullscreenButtonCore, Fullscre
|
||||
stateAttrMap: FullscreenButtonDataAttrs,
|
||||
selector: selectFullscreen,
|
||||
action: (core, state) => core.toggle(state),
|
||||
hotkeyAction: 'toggleFullscreen',
|
||||
});
|
||||
|
||||
export namespace FullscreenButton {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
type AnyPlayerStore,
|
||||
createHotkey,
|
||||
type HotkeyActionName,
|
||||
isHotkeyToggleAction,
|
||||
resolveHotkeyAction,
|
||||
} from '@videojs/core/dom';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useContainer, usePlayer } from '../../player/context';
|
||||
|
||||
export interface MediaHotkeyProps {
|
||||
keys: string;
|
||||
action: HotkeyActionName | (string & {});
|
||||
value?: number;
|
||||
disabled?: boolean;
|
||||
target?: 'player' | 'document';
|
||||
}
|
||||
|
||||
export function MediaHotkey({ keys, action, value, disabled, target }: MediaHotkeyProps): ReactNode {
|
||||
const store = usePlayer() as AnyPlayerStore;
|
||||
const container = useContainer();
|
||||
|
||||
useEffect(() => {
|
||||
if (!container || !keys || !action || disabled) return;
|
||||
|
||||
const resolver = resolveHotkeyAction(action);
|
||||
if (!resolver) return;
|
||||
|
||||
return createHotkey(container, {
|
||||
keys,
|
||||
action,
|
||||
target,
|
||||
disabled,
|
||||
repeatable: !isHotkeyToggleAction(action),
|
||||
onActivate: (_event, key) => {
|
||||
resolver({ store, key, value });
|
||||
},
|
||||
});
|
||||
}, [container, store, keys, action, value, disabled, target]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { findHotkeyCoordinator } from '@videojs/core/dom';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useContainer } from '../../player/context';
|
||||
|
||||
export function useAriaKeyShortcuts(action: string | undefined): string | undefined {
|
||||
const container = useContainer();
|
||||
return useMemo(() => {
|
||||
if (!container || !action) return undefined;
|
||||
return findHotkeyCoordinator(container)?.getAriaKeys(action);
|
||||
}, [container, action]);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { createHotkey } from '@videojs/core/dom';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useContainer } from '../../player/context';
|
||||
import { useLatestRef } from '../../utils/use-latest-ref';
|
||||
|
||||
export interface UseHotkeyOptions {
|
||||
keys: string;
|
||||
onActivate: (event: KeyboardEvent, key: string) => void;
|
||||
target?: 'player' | 'document';
|
||||
repeatable?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function useHotkey(options: UseHotkeyOptions): void {
|
||||
const { keys, target = 'player', repeatable = true, disabled = false } = options;
|
||||
const container = useContainer();
|
||||
const onActivateRef = useLatestRef(options.onActivate);
|
||||
|
||||
useEffect(() => {
|
||||
if (!container || !keys || disabled) return;
|
||||
|
||||
return createHotkey(container, {
|
||||
keys,
|
||||
target,
|
||||
repeatable,
|
||||
disabled,
|
||||
onActivate: (event, key) => onActivateRef.current(event, key),
|
||||
});
|
||||
}, [container, keys, target, repeatable, disabled, onActivateRef]);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export const MuteButton = createMediaButton<MuteButtonCore, MuteButtonProps>({
|
||||
stateAttrMap: MuteButtonDataAttrs,
|
||||
selector: selectVolume,
|
||||
action: (core, state) => core.toggle(state),
|
||||
hotkeyAction: 'toggleMuted',
|
||||
});
|
||||
|
||||
export namespace MuteButton {
|
||||
|
||||
@@ -15,6 +15,7 @@ export const PiPButton = createMediaButton<PiPButtonCore, PiPButtonProps>({
|
||||
stateAttrMap: PiPButtonDataAttrs,
|
||||
selector: selectPiP,
|
||||
action: (core, state) => core.toggle(state),
|
||||
hotkeyAction: 'togglePiP',
|
||||
});
|
||||
|
||||
export namespace PiPButton {
|
||||
|
||||
@@ -30,6 +30,7 @@ export const PlayButton = createMediaButton<PlayButtonCore, PlayButtonProps>({
|
||||
stateAttrMap: PlayButtonDataAttrs,
|
||||
selector: selectPlayback,
|
||||
action: (core, state) => core.toggle(state),
|
||||
hotkeyAction: 'togglePaused',
|
||||
});
|
||||
|
||||
export namespace PlayButton {
|
||||
|
||||
Reference in New Issue
Block a user