'use client'; import { getStateDataAttrs, type StateAttrMap } from '@videojs/core/dom'; import { isFunction } from '@videojs/utils/predicate'; import type { CSSProperties, ReactElement, Ref } from 'react'; import { cloneElement, createElement, isValidElement } from 'react'; import { mergeProps } from './merge-props'; import type { HTMLProps, RenderProp } from './types'; import { composeRefs } from './use-composed-refs'; type IntrinsicTagName = keyof React.JSX.IntrinsicElements; export interface UseRenderComponentProps { className?: string | ((state: State) => string | undefined) | undefined; style?: CSSProperties | ((state: State) => CSSProperties | undefined) | undefined; render?: RenderProp | undefined; } export interface UseRenderParameters { state: State; ref?: Ref | Ref[] | undefined; props?: object | object[] | undefined; stateAttrMap?: StateAttrMap | undefined; } function resolveClassName( className: string | ((state: State) => string | undefined) | undefined, state: State ): string | undefined { return isFunction(className) ? className(state) : className; } function resolveStyle( style: CSSProperties | ((state: State) => CSSProperties | undefined) | undefined, state: State ): CSSProperties | undefined { return isFunction(style) ? style(state) : style; } function getElementRef(element: ReactElement): Ref | undefined { // React 19+ uses element.props.ref, older versions use element.ref // eslint-disable-next-line @typescript-eslint/no-explicit-any const elementAny = element as any; return elementAny.ref ?? elementAny.props?.ref; } /** * Render a UI component element. * * Handles: * - Default tag rendering * - Render prop (element or function) * - Props merging (event handlers chained, className concatenated, style merged) * - Ref composition * - className/style as functions of state * * @public * @example * ```tsx * return renderElement('button', componentProps, { * state, * ref: [forwardedRef, buttonRef], * props: [{ type: 'button' }, elementProps, getButtonProps], * }); * ``` */ export function renderElement< State extends object, RenderedElementType extends Element, TagName extends IntrinsicTagName, >( element: TagName, componentProps: UseRenderComponentProps, params: UseRenderParameters ): ReactElement | null { const { className: classNameProp, style: styleProp, render } = componentProps; const { state, ref, props, stateAttrMap } = params; // Resolve className and style if they're functions const className = resolveClassName(classNameProp, state); const style = resolveStyle(styleProp, state); // Generate data attributes from state const stateDataAttrs = getStateDataAttrs(state, stateAttrMap); // Merge: state data attrs first, then props (so props can override) const propsArray = Array.isArray(props) ? props : props ? [props] : []; const mergedProps = mergeProps(stateDataAttrs, ...(propsArray as Record[])); if (className !== undefined) { // Add resolved className and style mergedProps.className = mergedProps.className ? `${mergedProps.className} ${className}` : className; } if (style !== undefined) { mergedProps.style = mergedProps.style ? { ...(mergedProps.style as CSSProperties), ...style } : style; } if (isFunction(render)) { // Render function: call with props and state const mergedRef = composeRefs(ref, mergedProps.ref); return render({ ...mergedProps, ref: mergedRef } as HTMLProps, state); } if (isValidElement(render)) { const elementRef = getElementRef(render); const mergedRef = composeRefs(ref, mergedProps.ref, elementRef); const elementProps = mergeProps(mergedProps, render.props as Record); elementProps.ref = mergedRef; return cloneElement(render, elementProps); } // Default tag const mergedRef = composeRefs(ref, mergedProps.ref); mergedProps.ref = mergedRef; return createElement(element, mergedProps); } export namespace renderElement { export type ComponentProps = UseRenderComponentProps; export type Parameters = UseRenderParameters; }