mirror of
https://github.com/zoriya/v10.git
synced 2026-08-12 00:49:29 +00:00
feat(packages): add hotkey bindings to preset skins (#1264)
This commit is contained in:
@@ -94,6 +94,22 @@ function getTemplateHTML() {
|
||||
</div>
|
||||
</media-tooltip-group>
|
||||
</div>
|
||||
|
||||
<!-- Hotkeys -->
|
||||
<media-hotkey keys="Space" action="togglePaused"></media-hotkey>
|
||||
<media-hotkey keys="k" action="togglePaused"></media-hotkey>
|
||||
<media-hotkey keys="m" action="toggleMuted"></media-hotkey>
|
||||
<media-hotkey keys="ArrowRight" action="seekStep" value="5"></media-hotkey>
|
||||
<media-hotkey keys="ArrowLeft" action="seekStep" value="-5"></media-hotkey>
|
||||
<media-hotkey keys="l" action="seekStep" value="10"></media-hotkey>
|
||||
<media-hotkey keys="j" action="seekStep" value="-10"></media-hotkey>
|
||||
<media-hotkey keys="ArrowUp" action="volumeStep" value="0.05"></media-hotkey>
|
||||
<media-hotkey keys="ArrowDown" action="volumeStep" value="-0.05"></media-hotkey>
|
||||
<media-hotkey keys="0-9" action="seekToPercent"></media-hotkey>
|
||||
<media-hotkey keys="Home" action="seekToPercent" value="0"></media-hotkey>
|
||||
<media-hotkey keys="End" action="seekToPercent" value="100"></media-hotkey>
|
||||
<media-hotkey keys="Shift+>" action="speedUp"></media-hotkey>
|
||||
<media-hotkey keys="Shift+<" action="speedDown"></media-hotkey>
|
||||
</media-container>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import './player';
|
||||
|
||||
import '../ui/error-dialog';
|
||||
import '../ui/hotkey';
|
||||
import '../ui/mute-button';
|
||||
import '../ui/play-button';
|
||||
import '../ui/playback-rate-button';
|
||||
|
||||
@@ -130,6 +130,25 @@ function getTemplateHTML() {
|
||||
</media-controls>
|
||||
|
||||
<div class="media-overlay"></div>
|
||||
|
||||
<!-- Hotkeys -->
|
||||
<media-hotkey keys="Space" action="togglePaused"></media-hotkey>
|
||||
<media-hotkey keys="k" action="togglePaused"></media-hotkey>
|
||||
<media-hotkey keys="m" action="toggleMuted"></media-hotkey>
|
||||
<media-hotkey keys="f" action="toggleFullscreen"></media-hotkey>
|
||||
<media-hotkey keys="c" action="toggleSubtitles"></media-hotkey>
|
||||
<media-hotkey keys="i" action="togglePiP"></media-hotkey>
|
||||
<media-hotkey keys="ArrowRight" action="seekStep" value="5"></media-hotkey>
|
||||
<media-hotkey keys="ArrowLeft" action="seekStep" value="-5"></media-hotkey>
|
||||
<media-hotkey keys="l" action="seekStep" value="10"></media-hotkey>
|
||||
<media-hotkey keys="j" action="seekStep" value="-10"></media-hotkey>
|
||||
<media-hotkey keys="ArrowUp" action="volumeStep" value="0.05"></media-hotkey>
|
||||
<media-hotkey keys="ArrowDown" action="volumeStep" value="-0.05"></media-hotkey>
|
||||
<media-hotkey keys="0-9" action="seekToPercent"></media-hotkey>
|
||||
<media-hotkey keys="Home" action="seekToPercent" value="0"></media-hotkey>
|
||||
<media-hotkey keys="End" action="seekToPercent" value="100"></media-hotkey>
|
||||
<media-hotkey keys="Shift+>" action="speedUp"></media-hotkey>
|
||||
<media-hotkey keys="Shift+<" action="speedDown"></media-hotkey>
|
||||
</media-container>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../ui/captions-button';
|
||||
import '../ui/controls';
|
||||
import '../ui/error-dialog';
|
||||
import '../ui/fullscreen-button';
|
||||
import '../ui/hotkey';
|
||||
import '../ui/mute-button';
|
||||
import '../ui/pip-button';
|
||||
import '../ui/play-button';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { listen } from '@videojs/utils/dom';
|
||||
|
||||
import { containerContext, playerContext } from '../player/context';
|
||||
import { createContainerMixin } from '../store/container-mixin';
|
||||
import { MediaElement } from '../ui/media-element';
|
||||
@@ -6,4 +8,32 @@ const ContainerMixin = createContainerMixin({ playerContext, containerContext })
|
||||
|
||||
export class MediaContainerElement extends ContainerMixin(MediaElement) {
|
||||
static readonly tagName = 'media-container';
|
||||
|
||||
#disconnect: AbortController | null = null;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
// Make focusable so keyboard events reach hotkey listeners.
|
||||
if (!this.hasAttribute('tabindex')) {
|
||||
this.setAttribute('tabindex', '0');
|
||||
}
|
||||
|
||||
this.#disconnect = new AbortController();
|
||||
listen(this, 'pointerup', this.#onPointerUp, { signal: this.#disconnect.signal });
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.#disconnect?.abort();
|
||||
this.#disconnect = null;
|
||||
}
|
||||
|
||||
#onPointerUp = (): void => {
|
||||
// If nothing inside the container has focus, grab it so keyboard
|
||||
// events reach the hotkey coordinator's listener.
|
||||
if (!this.contains(document.activeElement) || document.activeElement === document.body) {
|
||||
this.focus({ preventScroll: true });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,11 @@ export class HotkeyElement extends MediaElement {
|
||||
target: 'player' | 'document' = 'player';
|
||||
|
||||
readonly #player = new PlayerController(this, playerContext);
|
||||
readonly #container = new ContextConsumer(this, { context: containerContext, subscribe: true });
|
||||
readonly #container = new ContextConsumer(this, {
|
||||
context: containerContext,
|
||||
callback: () => this.requestUpdate(),
|
||||
subscribe: true,
|
||||
});
|
||||
#cleanup: (() => void) | null = null;
|
||||
|
||||
override connectedCallback(): void {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type { Media, MediaContainer } from '@videojs/core/dom';
|
||||
import type { UnknownState, UnknownStore } from '@videojs/store';
|
||||
import { useStore } from '@videojs/store/react';
|
||||
import type { Dispatch, HTMLAttributes, ReactNode, SetStateAction } from 'react';
|
||||
import type { Dispatch, HTMLAttributes, ReactNode, PointerEvent as ReactPointerEvent, SetStateAction } from 'react';
|
||||
import { createContext, forwardRef, useContext, useEffect, useRef } from 'react';
|
||||
|
||||
import { useComposedRefs } from '../utils/use-composed-refs';
|
||||
@@ -103,7 +103,10 @@ export interface ContainerProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const Container = forwardRef<HTMLDivElement, ContainerProps>(function Container({ children, ...props }, ref) {
|
||||
export const Container = forwardRef<HTMLDivElement, ContainerProps>(function Container(
|
||||
{ children, tabIndex = 0, ...props },
|
||||
ref
|
||||
) {
|
||||
const setContainer = useContainerAttach();
|
||||
const internalRef = useRef<HTMLDivElement>(null);
|
||||
const composedRef = useComposedRefs(ref, internalRef);
|
||||
@@ -113,8 +116,18 @@ export const Container = forwardRef<HTMLDivElement, ContainerProps>(function Con
|
||||
return () => setContainer?.(null);
|
||||
}, [setContainer]);
|
||||
|
||||
const handlePointerUp = (event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
props.onPointerUp?.(event);
|
||||
const el = internalRef.current;
|
||||
if (!el) return;
|
||||
// If nothing inside has focus, grab it so keyboard events reach hotkey listeners.
|
||||
if (!el.contains(document.activeElement) || document.activeElement === document.body) {
|
||||
el.focus({ preventScroll: true });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={composedRef} {...props}>
|
||||
<div ref={composedRef} tabIndex={tabIndex} {...props} onPointerUp={handlePointerUp}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { MediaHotkey } from '@/ui/hotkey/media-hotkey';
|
||||
import { MuteButton } from '@/ui/mute-button';
|
||||
import { PlayButton } from '@/ui/play-button';
|
||||
import { PlaybackRateButton } from '@/ui/playback-rate-button';
|
||||
@@ -155,6 +156,22 @@ export function AudioSkin(props: AudioSkinProps): ReactNode {
|
||||
</div>
|
||||
</Tooltip.Provider>
|
||||
</div>
|
||||
|
||||
{/* Hotkeys */}
|
||||
<MediaHotkey keys="Space" action="togglePaused" />
|
||||
<MediaHotkey keys="k" action="togglePaused" />
|
||||
<MediaHotkey keys="m" action="toggleMuted" />
|
||||
<MediaHotkey keys="ArrowRight" action="seekStep" value={5} />
|
||||
<MediaHotkey keys="ArrowLeft" action="seekStep" value={-5} />
|
||||
<MediaHotkey keys="l" action="seekStep" value={10} />
|
||||
<MediaHotkey keys="j" action="seekStep" value={-10} />
|
||||
<MediaHotkey keys="ArrowUp" action="volumeStep" value={0.05} />
|
||||
<MediaHotkey keys="ArrowDown" action="volumeStep" value={-0.05} />
|
||||
<MediaHotkey keys="0-9" action="seekToPercent" />
|
||||
<MediaHotkey keys="Home" action="seekToPercent" value={0} />
|
||||
<MediaHotkey keys="End" action="seekToPercent" value={100} />
|
||||
<MediaHotkey keys="Shift+>" action="speedUp" />
|
||||
<MediaHotkey keys="Shift+<" action="speedDown" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { CaptionsButton } from '@/ui/captions-button';
|
||||
import { Controls } from '@/ui/controls';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { FullscreenButton } from '@/ui/fullscreen-button';
|
||||
import { MediaHotkey } from '@/ui/hotkey/media-hotkey';
|
||||
import { MuteButton } from '@/ui/mute-button';
|
||||
import { PiPButton } from '@/ui/pip-button';
|
||||
import { PlayButton } from '@/ui/play-button';
|
||||
@@ -229,6 +230,25 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
|
||||
</Controls.Root>
|
||||
|
||||
<div className="media-overlay" />
|
||||
|
||||
{/* Hotkeys */}
|
||||
<MediaHotkey keys="Space" action="togglePaused" />
|
||||
<MediaHotkey keys="k" action="togglePaused" />
|
||||
<MediaHotkey keys="m" action="toggleMuted" />
|
||||
<MediaHotkey keys="f" action="toggleFullscreen" />
|
||||
<MediaHotkey keys="c" action="toggleSubtitles" />
|
||||
<MediaHotkey keys="i" action="togglePiP" />
|
||||
<MediaHotkey keys="ArrowRight" action="seekStep" value={5} />
|
||||
<MediaHotkey keys="ArrowLeft" action="seekStep" value={-5} />
|
||||
<MediaHotkey keys="l" action="seekStep" value={10} />
|
||||
<MediaHotkey keys="j" action="seekStep" value={-10} />
|
||||
<MediaHotkey keys="ArrowUp" action="volumeStep" value={0.05} />
|
||||
<MediaHotkey keys="ArrowDown" action="volumeStep" value={-0.05} />
|
||||
<MediaHotkey keys="0-9" action="seekToPercent" />
|
||||
<MediaHotkey keys="Home" action="seekToPercent" value={0} />
|
||||
<MediaHotkey keys="End" action="seekToPercent" value={100} />
|
||||
<MediaHotkey keys="Shift+>" action="speedUp" />
|
||||
<MediaHotkey keys="Shift+<" action="speedDown" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: var(--media-border-radius, 2rem);
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
|
||||
&:focus-visible {
|
||||
outline-color: currentColor;
|
||||
}
|
||||
font-family:
|
||||
Inter Variable,
|
||||
Inter,
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: var(--media-border-radius, 0.75rem);
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
|
||||
&:focus-visible {
|
||||
outline-color: currentColor;
|
||||
}
|
||||
font-family:
|
||||
Inter Variable,
|
||||
Inter,
|
||||
|
||||
Reference in New Issue
Block a user