mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(react): add video component and utility hooks (#293)
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import type { MutableRefObject, RefObject } from 'react';
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { composeRefs, useComposedRefs } from '../use-composed-refs';
|
||||
|
||||
// Helper to create a mutable ref object for testing (without using deprecated createRef)
|
||||
function createMutableRef<T>(initialValue: T | null = null): MutableRefObject<T | null> {
|
||||
return { current: initialValue };
|
||||
}
|
||||
|
||||
describe('composeRefs', () => {
|
||||
it('sets value on callback ref', () => {
|
||||
const callbackRef = vi.fn();
|
||||
const composed = composeRefs(callbackRef);
|
||||
|
||||
composed('test-value');
|
||||
|
||||
expect(callbackRef).toHaveBeenCalledWith('test-value');
|
||||
});
|
||||
|
||||
it('sets value on RefObject', () => {
|
||||
const refObject = createMutableRef<string>();
|
||||
const composed = composeRefs(refObject);
|
||||
|
||||
composed('test-value');
|
||||
|
||||
expect(refObject.current).toBe('test-value');
|
||||
});
|
||||
|
||||
it('sets value on multiple refs', () => {
|
||||
const callbackRef = vi.fn();
|
||||
const refObject = createMutableRef<string>();
|
||||
const composed = composeRefs(callbackRef, refObject);
|
||||
|
||||
composed('test-value');
|
||||
|
||||
expect(callbackRef).toHaveBeenCalledWith('test-value');
|
||||
expect(refObject.current).toBe('test-value');
|
||||
});
|
||||
|
||||
it('handles undefined refs', () => {
|
||||
const callbackRef = vi.fn();
|
||||
const composed = composeRefs(undefined, callbackRef, undefined);
|
||||
|
||||
composed('test-value');
|
||||
|
||||
expect(callbackRef).toHaveBeenCalledWith('test-value');
|
||||
});
|
||||
|
||||
it('returns cleanup function when callback ref returns one', () => {
|
||||
const cleanup = vi.fn();
|
||||
const callbackRef = vi.fn().mockReturnValue(cleanup);
|
||||
const composed = composeRefs(callbackRef);
|
||||
|
||||
const returnedCleanup = composed('test-value') as (() => void) | void;
|
||||
|
||||
expect(returnedCleanup).toBeTypeOf('function');
|
||||
if (typeof returnedCleanup === 'function') {
|
||||
returnedCleanup();
|
||||
}
|
||||
expect(cleanup).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clears RefObject on cleanup', () => {
|
||||
const cleanup = vi.fn();
|
||||
const callbackRef = vi.fn().mockReturnValue(cleanup);
|
||||
const refObject = createMutableRef<string>();
|
||||
const composed = composeRefs(callbackRef, refObject);
|
||||
|
||||
composed('test-value');
|
||||
expect(refObject.current).toBe('test-value');
|
||||
|
||||
const returnedCleanup = composed('test-value') as (() => void) | void;
|
||||
if (typeof returnedCleanup === 'function') {
|
||||
returnedCleanup();
|
||||
}
|
||||
|
||||
expect(refObject.current).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useComposedRefs', () => {
|
||||
it('returns a stable callback ref', () => {
|
||||
let composedRef1: ((value: HTMLDivElement | null) => void) | null = null;
|
||||
let composedRef2: ((value: HTMLDivElement | null) => void) | null = null;
|
||||
|
||||
function TestComponent() {
|
||||
const ref1 = useRef<HTMLDivElement>(null);
|
||||
const ref2 = useRef<HTMLDivElement>(null);
|
||||
const composed = useComposedRefs(ref1, ref2);
|
||||
|
||||
if (!composedRef1) {
|
||||
composedRef1 = composed;
|
||||
} else {
|
||||
composedRef2 = composed;
|
||||
}
|
||||
|
||||
return <div ref={composed}>Test</div>;
|
||||
}
|
||||
|
||||
const { rerender } = render(<TestComponent />);
|
||||
rerender(<TestComponent />);
|
||||
|
||||
// Same refs should produce same composed ref
|
||||
expect(composedRef1).toBe(composedRef2);
|
||||
});
|
||||
|
||||
it('works with forwardRef pattern', () => {
|
||||
const externalRef = createMutableRef<HTMLDivElement>();
|
||||
|
||||
function TestComponent({ forwardedRef }: { forwardedRef: RefObject<HTMLDivElement | null> }) {
|
||||
const internalRef = useRef<HTMLDivElement>(null);
|
||||
const composedRef = useComposedRefs(forwardedRef, internalRef);
|
||||
|
||||
return <div ref={composedRef}>Test</div>;
|
||||
}
|
||||
|
||||
render(<TestComponent forwardedRef={externalRef} />);
|
||||
|
||||
expect(externalRef.current).toBeInstanceOf(HTMLDivElement);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import type { Ref, RefCallback } from 'react';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
type PossibleRef<T> = Ref<T> | undefined;
|
||||
|
||||
/**
|
||||
* Set a given ref to a given value.
|
||||
*
|
||||
* Handles both callback refs and RefObject(s).
|
||||
*
|
||||
* @returns Cleanup function if the ref callback returned one (React 19+)
|
||||
*/
|
||||
function setRef<T>(ref: PossibleRef<T>, value: T): (() => void) | void | undefined {
|
||||
if (typeof ref === 'function') {
|
||||
return ref(value);
|
||||
} else if (ref !== null && ref !== undefined) {
|
||||
ref.current = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose multiple refs into a single callback ref.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const composedRef = composeRefs(ref1, ref2, ref3);
|
||||
* return <div ref={composedRef} />;
|
||||
* ```
|
||||
*/
|
||||
export function composeRefs<T>(...refs: PossibleRef<T>[]): RefCallback<T> {
|
||||
return (node): (() => void) | void => {
|
||||
const cleanups = refs.map(ref => setRef(ref, node));
|
||||
|
||||
return () => {
|
||||
for (let i = 0; i < cleanups.length; i++) {
|
||||
const cleanup = cleanups[i];
|
||||
if (typeof cleanup === 'function') {
|
||||
cleanup();
|
||||
} else {
|
||||
setRef(refs[i], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that composes multiple refs into a single callback ref.
|
||||
*
|
||||
* Memoized for stable reference.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const composedRef = useComposedRefs(forwardedRef, localRef);
|
||||
* return <div ref={composedRef} />;
|
||||
* ```
|
||||
*/
|
||||
export function useComposedRefs<T>(...refs: PossibleRef<T>[]): RefCallback<T> {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return useCallback(composeRefs(...refs), refs);
|
||||
}
|
||||
Reference in New Issue
Block a user