feat(packages): add poster component to video skins (#994)

This commit is contained in:
Sam Potts
2026-03-18 17:57:10 +11:00
committed by GitHub
parent b9bada9567
commit 59bbf6c209
45 changed files with 445 additions and 171 deletions
+6
View File
@@ -1,4 +1,6 @@
import type { CSSProperties, PropsWithChildren } from 'react';
import type { Poster } from '@/ui/poster';
import type { RenderProp } from '@/utils/types';
export type BaseSkinProps<T = unknown> = PropsWithChildren<
T & {
@@ -6,3 +8,7 @@ export type BaseSkinProps<T = unknown> = PropsWithChildren<
className?: string;
}
>;
export type BaseVideoSkinProps<T = unknown> = BaseSkinProps<T> & {
poster?: string | RenderProp<Poster.State> | undefined;
};
@@ -26,12 +26,14 @@ import {
iconState,
overlay,
popup,
poster,
preview,
root,
seek,
slider,
time,
} from '@videojs/skins/minimal/tailwind/video.tailwind';
import { isString } from '@videojs/utils/predicate';
import { cn } from '@videojs/utils/style';
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
import { Container, usePlayer } from '@/player/context';
@@ -44,12 +46,14 @@ import { PiPButton } from '@/ui/pip-button';
import { PlayButton } from '@/ui/play-button';
import { PlaybackRateButton } from '@/ui/playback-rate-button';
import { Popover } from '@/ui/popover';
import { Poster } from '@/ui/poster';
import { SeekButton } from '@/ui/seek-button';
import { Slider } from '@/ui/slider';
import { Time } from '@/ui/time';
import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
import { VolumeSlider } from '@/ui/volume-slider';
import { isRenderProp } from '@/utils/use-render';
import { ErrorDialog } from './error-dialog';
import type { MinimalVideoSkinProps } from './minimal-skin';
@@ -142,12 +146,20 @@ function FullscreenLabel(): ReactNode {
/* ------------------------------------------ Skin ------------------------------------------- */
export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNode {
const { children, className, ...rest } = props;
const { children, className, poster: posterProp, ...rest } = props;
return (
<Container className={cn(root(false), className)} {...rest}>
{children}
{posterProp && (
<Poster
src={isString(posterProp) ? posterProp : undefined}
render={isRenderProp(posterProp) ? posterProp : undefined}
className={poster(false)}
/>
)}
<BufferingIndicator
render={(props) => (
<div {...props} className={bufferingIndicator}>
@@ -13,6 +13,7 @@ import {
VolumeLowIcon,
VolumeOffIcon,
} from '@videojs/icons/react/minimal';
import { isString } from '@videojs/utils/predicate';
import { cn } from '@videojs/utils/style';
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
import { Container, usePlayer } from '@/player/context';
@@ -25,18 +26,20 @@ import { PiPButton } from '@/ui/pip-button';
import { PlayButton } from '@/ui/play-button';
import { PlaybackRateButton } from '@/ui/playback-rate-button';
import { Popover } from '@/ui/popover';
import { Poster } from '@/ui/poster';
import { SeekButton } from '@/ui/seek-button';
import { Slider } from '@/ui/slider';
import { Time } from '@/ui/time';
import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
import { VolumeSlider } from '@/ui/volume-slider';
import type { BaseSkinProps } from '../types';
import { isRenderProp } from '@/utils/use-render';
import type { BaseVideoSkinProps } from '../types';
import { ErrorDialog } from './error-dialog';
const SEEK_TIME = 10;
export type MinimalVideoSkinProps = BaseSkinProps;
export type MinimalVideoSkinProps = BaseVideoSkinProps;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return <button ref={ref} type="button" className={cn('media-button', className)} {...props} />;
@@ -75,12 +78,16 @@ function FullscreenLabel(): ReactNode {
}
export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
const { children, className, ...rest } = props;
const { children, className, poster, ...rest } = props;
return (
<Container className={cn('media-minimal-skin media-minimal-skin--video', className)} {...rest}>
{children}
{poster && (
<Poster src={isString(poster) ? poster : undefined} render={isRenderProp(poster) ? poster : undefined} />
)}
<BufferingIndicator
render={(props) => (
<div {...props} className="media-buffering-indicator">
@@ -25,12 +25,14 @@ import {
overlay,
playbackRate,
popup,
poster,
preview,
root,
seek,
slider,
time,
} from '@videojs/skins/default/tailwind/video.tailwind';
import { isString } from '@videojs/utils/predicate';
import { cn } from '@videojs/utils/style';
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
import { Container, usePlayer } from '@/player/context';
@@ -43,12 +45,14 @@ import { PiPButton } from '@/ui/pip-button';
import { PlayButton } from '@/ui/play-button';
import { PlaybackRateButton } from '@/ui/playback-rate-button';
import { Popover } from '@/ui/popover';
import { Poster } from '@/ui/poster';
import { SeekButton } from '@/ui/seek-button';
import { Slider } from '@/ui/slider';
import { Time } from '@/ui/time';
import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
import { VolumeSlider } from '@/ui/volume-slider';
import { isRenderProp } from '@/utils/use-render';
import { ErrorDialog } from './error-dialog';
import type { VideoSkinProps } from './skin';
@@ -142,12 +146,20 @@ function FullscreenLabel(): ReactNode {
/* ------------------------------------------ Skin ------------------------------------------- */
export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
const { children, className, ...rest } = props;
const { children, className, poster: posterProp, ...rest } = props;
return (
<Container className={cn(root(false), className)} {...rest}>
{children}
{posterProp && (
<Poster
src={isString(posterProp) ? posterProp : undefined}
render={isRenderProp(posterProp) ? posterProp : undefined}
className={poster(false)}
/>
)}
<BufferingIndicator
render={(props) => (
<div {...props} className={bufferingIndicator.root}>
+10 -3
View File
@@ -13,6 +13,7 @@ import {
VolumeLowIcon,
VolumeOffIcon,
} from '@videojs/icons/react';
import { isString } from '@videojs/utils/predicate';
import { cn } from '@videojs/utils/style';
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
import { Container, usePlayer } from '@/player/context';
@@ -25,18 +26,20 @@ import { PiPButton } from '@/ui/pip-button';
import { PlayButton } from '@/ui/play-button';
import { PlaybackRateButton } from '@/ui/playback-rate-button';
import { Popover } from '@/ui/popover';
import { Poster } from '@/ui/poster';
import { SeekButton } from '@/ui/seek-button';
import { Slider } from '@/ui/slider';
import { Time } from '@/ui/time';
import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
import { VolumeSlider } from '@/ui/volume-slider';
import type { BaseSkinProps } from '../types';
import { isRenderProp } from '@/utils/use-render';
import type { BaseVideoSkinProps } from '../types';
import { ErrorDialog } from './error-dialog';
const SEEK_TIME = 10;
export type VideoSkinProps = BaseSkinProps;
export type VideoSkinProps = BaseVideoSkinProps;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return <button ref={ref} type="button" className={cn('media-button', className)} {...props} />;
@@ -75,12 +78,16 @@ function FullscreenLabel(): ReactNode {
}
export function VideoSkin(props: VideoSkinProps): ReactNode {
const { children, className, ...rest } = props;
const { children, className, poster, ...rest } = props;
return (
<Container className={cn('media-default-skin media-default-skin--video', className)} {...rest}>
{children}
{poster && (
<Poster src={isString(poster) ? poster : undefined} render={isRenderProp(poster) ? poster : undefined} />
)}
<BufferingIndicator
render={(props) => (
<div {...props} className="media-buffering-indicator">
+5
View File
@@ -8,6 +8,11 @@ import { mergeProps } from './merge-props';
import type { HTMLProps, RenderProp } from './types';
import { composeRefs } from './use-composed-refs';
/** Check if a value is a render prop (function or React element). */
export function isRenderProp(value: unknown): value is RenderProp<any> {
return isFunction(value) || isValidElement(value);
}
type IntrinsicTagName = keyof React.JSX.IntrinsicElements;
export interface UseRenderComponentProps<State> {