refactor(react): replace prototype-walking and inferred class props (#1376)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
rahim
2026-04-20 09:20:46 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 5e9a02c851
commit 2f7212028c
21 changed files with 256 additions and 176 deletions
+2 -4
View File
@@ -67,13 +67,11 @@ export { Time } from './ui/time';
export { TimeSlider } from './ui/time-slider';
export { Tooltip, type TooltipContextValue, useTooltipContext } from './ui/tooltip';
export { VolumeSlider } from './ui/volume-slider';
// Media utilities
export { attachMediaElement } from './utils/attach-media-element';
export { mediaProps } from './utils/media-props';
// Utilities
export { mergeProps } from './utils/merge-props';
export type { HTMLProps, RenderFunction, RenderProp, UIComponentProps } from './utils/types';
// Media utilities
export { useAttachMedia } from './utils/use-attach-media';
export { composeRefs, useComposedRefs } from './utils/use-composed-refs';
export { useDestroy } from './utils/use-destroy';
export { useLatestRef } from './utils/use-latest-ref';
+15 -11
View File
@@ -1,24 +1,28 @@
'use client';
import { DashMedia } from '@videojs/core/dom/media/dash';
import type { InferClassProps } from '@videojs/utils/types';
import type { PropsWithChildren, VideoHTMLAttributes } from 'react';
import type { DashMediaProps } from '@videojs/core/dom/media/dash';
import { DashMedia, dashMediaDefaultProps } from '@videojs/core/dom/media/dash';
import type { ReactNode, VideoHTMLAttributes } from 'react';
import { forwardRef } from 'react';
import { attachMediaElement } from '../../utils/attach-media-element';
import { mediaProps } from '../../utils/media-props';
import { useAttachMedia } from '../../utils/use-attach-media';
import { useComposedRefs } from '../../utils/use-composed-refs';
import { useMediaInstance } from '../../utils/use-media-instance';
import { useSyncProps } from '../../utils/use-sync-props';
export type DashVideoProps = PropsWithChildren<VideoHTMLAttributes<HTMLVideoElement>> &
InferClassProps<typeof DashMedia>;
export interface DashVideoProps
extends Omit<VideoHTMLAttributes<HTMLVideoElement>, keyof DashMediaProps>,
Partial<DashMediaProps> {
children?: ReactNode;
}
export const DashVideo = forwardRef<HTMLVideoElement, DashVideoProps>(function DashVideo({ children, ...props }, ref) {
const mediaApi = useMediaInstance(DashMedia);
const composedRef = useComposedRefs(attachMediaElement(mediaApi), ref);
const media = useMediaInstance(DashMedia);
const attachRef = useAttachMedia(media);
const composedRef = useComposedRefs(attachRef, ref);
const htmlProps = useSyncProps(media, props, dashMediaDefaultProps);
return (
<video ref={composedRef} {...mediaProps(mediaApi, DashMedia, props)}>
<video ref={composedRef} {...htmlProps}>
{children}
</video>
);
+15 -10
View File
@@ -1,23 +1,28 @@
'use client';
import { HlsMedia } from '@videojs/core/dom/media/hls';
import type { InferClassProps } from '@videojs/utils/types';
import type { PropsWithChildren, VideoHTMLAttributes } from 'react';
import type { HlsMediaProps } from '@videojs/core/dom/media/hls';
import { HlsMedia, hlsMediaDefaultProps } from '@videojs/core/dom/media/hls';
import type { ReactNode, VideoHTMLAttributes } from 'react';
import { forwardRef } from 'react';
import { attachMediaElement } from '../../utils/attach-media-element';
import { mediaProps } from '../../utils/media-props';
import { useAttachMedia } from '../../utils/use-attach-media';
import { useComposedRefs } from '../../utils/use-composed-refs';
import { useMediaInstance } from '../../utils/use-media-instance';
import { useSyncProps } from '../../utils/use-sync-props';
export type HlsVideoProps = PropsWithChildren<VideoHTMLAttributes<HTMLVideoElement>> & InferClassProps<typeof HlsMedia>;
export interface HlsVideoProps
extends Omit<VideoHTMLAttributes<HTMLVideoElement>, keyof HlsMediaProps>,
Partial<HlsMediaProps> {
children?: ReactNode;
}
export const HlsVideo = forwardRef<HTMLVideoElement, HlsVideoProps>(function HlsVideo({ children, ...props }, ref) {
const mediaApi = useMediaInstance(HlsMedia);
const composedRef = useComposedRefs(attachMediaElement(mediaApi), ref);
const media = useMediaInstance(HlsMedia);
const attachRef = useAttachMedia(media);
const composedRef = useComposedRefs(attachRef, ref);
const htmlProps = useSyncProps(media, props, hlsMediaDefaultProps);
return (
<video ref={composedRef} {...mediaProps(mediaApi, HlsMedia, props)}>
<video ref={composedRef} {...htmlProps}>
{children}
</video>
);
+21 -13
View File
@@ -1,25 +1,33 @@
import { MuxAudioMedia } from '@videojs/core/dom/media/mux';
import type { InferClassProps } from '@videojs/utils/types';
import type { AudioHTMLAttributes, PropsWithChildren } from 'react';
'use client';
import type { MuxMediaProps } from '@videojs/core/dom/media/mux';
import { MuxAudioMedia, muxMediaDefaultProps } from '@videojs/core/dom/media/mux';
import type { AudioHTMLAttributes, ReactNode } from 'react';
import { forwardRef } from 'react';
import { attachMediaElement } from '../../utils/attach-media-element';
import { mediaProps } from '../../utils/media-props';
import { useAttachMedia } from '../../utils/use-attach-media';
import { useComposedRefs } from '../../utils/use-composed-refs';
import { useMediaInstance } from '../../utils/use-media-instance';
import { useSyncProps } from '../../utils/use-sync-props';
export type MuxAudioProps = PropsWithChildren<AudioHTMLAttributes<HTMLAudioElement>> &
InferClassProps<typeof MuxAudioMedia>;
export interface MuxAudioProps
extends Omit<AudioHTMLAttributes<HTMLAudioElement>, keyof MuxMediaProps>,
Partial<MuxMediaProps> {
children?: ReactNode;
}
export const MuxAudio = forwardRef<HTMLAudioElement, MuxAudioProps>(({ children, ...props }, ref) => {
const mediaApi = useMediaInstance(MuxAudioMedia);
const composedRef = useComposedRefs(attachMediaElement(mediaApi), ref);
export const MuxAudio = forwardRef<HTMLAudioElement, MuxAudioProps>(function MuxAudio({ children, ...props }, ref) {
const media = useMediaInstance(MuxAudioMedia);
const attachRef = useAttachMedia(media);
const composedRef = useComposedRefs(attachRef, ref);
const htmlProps = useSyncProps(media, props, muxMediaDefaultProps);
return (
<audio ref={composedRef} {...mediaProps(mediaApi, MuxAudioMedia, props)}>
<audio ref={composedRef} {...htmlProps}>
{children}
</audio>
);
});
export default MuxAudio;
export namespace MuxAudio {
export type Props = MuxAudioProps;
}
+15 -11
View File
@@ -1,24 +1,28 @@
'use client';
import { MuxVideoMedia } from '@videojs/core/dom/media/mux';
import type { InferClassProps } from '@videojs/utils/types';
import type { PropsWithChildren, VideoHTMLAttributes } from 'react';
import type { MuxMediaProps } from '@videojs/core/dom/media/mux';
import { MuxVideoMedia, muxMediaDefaultProps } from '@videojs/core/dom/media/mux';
import type { ReactNode, VideoHTMLAttributes } from 'react';
import { forwardRef } from 'react';
import { attachMediaElement } from '../../utils/attach-media-element';
import { mediaProps } from '../../utils/media-props';
import { useAttachMedia } from '../../utils/use-attach-media';
import { useComposedRefs } from '../../utils/use-composed-refs';
import { useMediaInstance } from '../../utils/use-media-instance';
import { useSyncProps } from '../../utils/use-sync-props';
export type MuxVideoProps = PropsWithChildren<VideoHTMLAttributes<HTMLVideoElement>> &
InferClassProps<typeof MuxVideoMedia>;
export interface MuxVideoProps
extends Omit<VideoHTMLAttributes<HTMLVideoElement>, keyof MuxMediaProps>,
Partial<MuxMediaProps> {
children?: ReactNode;
}
export const MuxVideo = forwardRef<HTMLVideoElement, MuxVideoProps>(function MuxVideo({ children, ...props }, ref) {
const mediaApi = useMediaInstance(MuxVideoMedia);
const composedRef = useComposedRefs(attachMediaElement(mediaApi), ref);
const media = useMediaInstance(MuxVideoMedia);
const attachRef = useAttachMedia(media);
const composedRef = useComposedRefs(attachRef, ref);
const htmlProps = useSyncProps(media, props, muxMediaDefaultProps);
return (
<video ref={composedRef} {...mediaProps(mediaApi, MuxVideoMedia, props)}>
<video ref={composedRef} {...htmlProps}>
{children}
</video>
);
@@ -1,27 +1,31 @@
'use client';
import { NativeHlsMedia } from '@videojs/core/dom/media/native-hls';
import type { InferClassProps } from '@videojs/utils/types';
import type { PropsWithChildren, VideoHTMLAttributes } from 'react';
import type { NativeHlsMediaProps } from '@videojs/core/dom/media/native-hls';
import { NativeHlsMedia, nativeHlsMediaDefaultProps } from '@videojs/core/dom/media/native-hls';
import type { ReactNode, VideoHTMLAttributes } from 'react';
import { forwardRef } from 'react';
import { attachMediaElement } from '../../utils/attach-media-element';
import { mediaProps } from '../../utils/media-props';
import { useAttachMedia } from '../../utils/use-attach-media';
import { useComposedRefs } from '../../utils/use-composed-refs';
import { useMediaInstance } from '../../utils/use-media-instance';
import { useSyncProps } from '../../utils/use-sync-props';
export type NativeHlsVideoProps = PropsWithChildren<VideoHTMLAttributes<HTMLVideoElement>> &
InferClassProps<typeof NativeHlsMedia>;
export interface NativeHlsVideoProps
extends Omit<VideoHTMLAttributes<HTMLVideoElement>, keyof NativeHlsMediaProps>,
Partial<NativeHlsMediaProps> {
children?: ReactNode;
}
export const NativeHlsVideo = forwardRef<HTMLVideoElement, NativeHlsVideoProps>(function NativeHlsVideo(
{ children, ...props },
ref
) {
const mediaApi = useMediaInstance(NativeHlsMedia);
const composedRef = useComposedRefs(attachMediaElement(mediaApi), ref);
const media = useMediaInstance(NativeHlsMedia);
const attachRef = useAttachMedia(media);
const composedRef = useComposedRefs(attachRef, ref);
const htmlProps = useSyncProps(media, props, nativeHlsMediaDefaultProps);
return (
<video ref={composedRef} {...mediaProps(mediaApi, NativeHlsMedia, props)}>
<video ref={composedRef} {...htmlProps}>
{children}
</video>
);
@@ -1,25 +1,32 @@
'use client';
import { SimpleHlsMedia } from '@videojs/core/dom/media/simple-hls';
import type { PropsWithChildren, VideoHTMLAttributes } from 'react';
import type { SpfMediaProps } from '@videojs/spf/dom';
import { spfMediaDefaultProps } from '@videojs/spf/dom';
import type { ReactNode, VideoHTMLAttributes } from 'react';
import { forwardRef } from 'react';
import { attachMediaElement } from '../../utils/attach-media-element';
import { mediaProps } from '../../utils/media-props';
import { useAttachMedia } from '../../utils/use-attach-media';
import { useComposedRefs } from '../../utils/use-composed-refs';
import { useMediaInstance } from '../../utils/use-media-instance';
import { useSyncProps } from '../../utils/use-sync-props';
export type SimpleHlsVideoProps = PropsWithChildren<VideoHTMLAttributes<HTMLVideoElement>>;
export interface SimpleHlsVideoProps
extends Omit<VideoHTMLAttributes<HTMLVideoElement>, keyof SpfMediaProps>,
Partial<SpfMediaProps> {
children?: ReactNode;
}
export const SimpleHlsVideo = forwardRef<HTMLVideoElement, SimpleHlsVideoProps>(function SimpleHlsVideo(
{ children, ...props },
ref
) {
const mediaApi = useMediaInstance(SimpleHlsMedia);
const composedRef = useComposedRefs(attachMediaElement(mediaApi), ref);
const media = useMediaInstance(SimpleHlsMedia);
const attachRef = useAttachMedia(media);
const composedRef = useComposedRefs(attachRef, ref);
const htmlProps = useSyncProps(media, props, spfMediaDefaultProps);
return (
<video ref={composedRef} {...mediaProps(mediaApi, SimpleHlsMedia, props)}>
<video ref={composedRef} {...htmlProps}>
{children}
</video>
);
@@ -1,16 +0,0 @@
interface Attachable {
attach(target: EventTarget): void;
detach(): void;
}
export function attachMediaElement<T extends HTMLVideoElement>(media: Attachable): (element: T | null) => void {
return (element: T | null) => {
if (element) {
media.attach(element);
} else {
media.detach();
}
// React 19+ accepts a cleanup function as the return value
return () => media.detach();
};
}
-32
View File
@@ -1,32 +0,0 @@
import type { Media } from '@videojs/core';
type AnyClass = abstract new (...args: any[]) => any;
function getSettableProps(DelegateClass: AnyClass): Set<string> {
const props = new Set<string>();
for (let proto = DelegateClass.prototype; proto && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) {
for (const key of Object.getOwnPropertyNames(proto)) {
const desc = Object.getOwnPropertyDescriptor(proto, key);
if (desc?.set) props.add(key);
}
}
return props;
}
export function mediaProps(media: Media, DelegateClass: AnyClass, props: Record<string, any>) {
const delegateKeys = getSettableProps(DelegateClass);
const rest: Record<string, any> = {};
for (const key of Object.keys(props)) {
if (delegateKeys.has(key)) {
const value = props[key];
if ((media as any)[key] !== value) {
(media as any)[key] = value;
}
} else {
rest[key] = props[key];
}
}
return rest;
}
@@ -0,0 +1,16 @@
'use client';
import type { MediaEngineHost } from '@videojs/core';
import type { RefCallback } from 'react';
import { useCallback } from 'react';
export function useAttachMedia<T extends HTMLMediaElement>(media: MediaEngineHost): RefCallback<T> {
return useCallback(
(element: T | null) => {
if (element) media.attach?.(element);
else media.detach?.();
return () => media.detach?.();
},
[media]
);
}
@@ -0,0 +1,20 @@
import { isUndefined } from '@videojs/utils/predicate';
export function useSyncProps<Props extends object, Rest extends Record<string, unknown>>(
target: Props,
props: Partial<Props> & Rest,
defaults: Props
): Omit<Rest, keyof Props> {
const rest: Record<string, unknown> = {};
for (const key in props) {
if (key in defaults) {
const value = isUndefined(props[key]) ? (defaults as Record<string, unknown>)[key] : props[key];
if (target[key as keyof typeof target] !== value) target[key as keyof typeof target] = value as any;
} else {
rest[key] = props[key];
}
}
return rest as Omit<Rest, keyof Props>;
}