diff --git a/packages/react/react/src/components/MuteButton.tsx b/packages/react/react/src/components/MuteButton.tsx index 8d7e731c..ad6e72ab 100644 --- a/packages/react/react/src/components/MuteButton.tsx +++ b/packages/react/react/src/components/MuteButton.tsx @@ -1,5 +1,6 @@ import { useMediaDispatch, useMediaSelector } from '@vjs-10/react-media-store'; import * as React from 'react'; +import { toConnectedComponent } from '../utils/component-factory'; export const useMuteButtonState = (_props: any) => { /** @TODO Fix type issues with hooks (CJP) */ @@ -31,7 +32,7 @@ export const useMuteButtonProps = ( props: React.PropsWithChildren<{ [k: string]: any }>, state: ReturnType, ) => { - const baseProps = { + const baseProps: Record = { /** data attributes/props - non-boolean */ ['data-volume-level']: state.volumeLevel, /** @TODO Need another state provider in core for i18n (CJP) */ @@ -79,25 +80,6 @@ export const renderMuteButton = ( export type renderMuteButton = typeof renderMuteButton; -export const toConnectedComponent = ( - useStateHook: useMuteButtonState, - usePropsHook: useMuteButtonProps, - defaultRender: renderMuteButton, - displayName: string, -) => { - const ConnectedComponent = ({ - render = defaultRender, - ...props - }: MuteButtonProps & { render: renderMuteButton }) => { - const connectedState = useStateHook(props); - const connectedProps = usePropsHook(props, connectedState); - return render(connectedProps, connectedState); - }; - - ConnectedComponent.displayName = displayName; - return ConnectedComponent; -}; - export const MuteButton = toConnectedComponent( useMuteButtonState, useMuteButtonProps, diff --git a/packages/react/react/src/components/PlayButton.tsx b/packages/react/react/src/components/PlayButton.tsx index 7169713a..d7ae95b3 100644 --- a/packages/react/react/src/components/PlayButton.tsx +++ b/packages/react/react/src/components/PlayButton.tsx @@ -1,5 +1,6 @@ import { useMediaDispatch, useMediaSelector } from '@vjs-10/react-media-store'; import * as React from 'react'; +import { toConnectedComponent } from '../utils/component-factory'; export const usePlayButtonState = (_props: any) => { /** @TODO Fix type issues with hooks (CJP) */ @@ -29,7 +30,7 @@ export const usePlayButtonProps = ( props: React.PropsWithChildren<{ [k: string]: any }>, state: ReturnType, ) => { - const baseProps = { + const baseProps: Record = { /** @TODO Need another state provider in core for i18n (CJP) */ /** aria attributes/props */ role: 'button', @@ -75,25 +76,6 @@ export const renderPlayButton = ( export type renderPlayButton = typeof renderPlayButton; -export const toConnectedComponent = ( - useStateHook: usePlayButtonState, - usePropsHook: usePlayButtonProps, - defaultRender: renderPlayButton, - displayName: string, -) => { - const ConnectedComponent = ({ - render = defaultRender, - ...props - }: PlayButtonProps & { render?: renderPlayButton }) => { - const connectedState = useStateHook(props); - const connectedProps = usePropsHook(props, connectedState); - return render(connectedProps, connectedState); - }; - - ConnectedComponent.displayName = displayName; - return ConnectedComponent; -}; - export const PlayButton = toConnectedComponent( usePlayButtonState, usePlayButtonProps, diff --git a/packages/react/react/src/utils/component-factory.tsx b/packages/react/react/src/utils/component-factory.tsx new file mode 100644 index 00000000..1eab1b82 --- /dev/null +++ b/packages/react/react/src/utils/component-factory.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; + +/** + * Generic types for the component factory pattern + */ +export type StateHookFn = (props: TProps) => TState; + +export type PropsHookFn = ( + props: TProps, + state: TState, +) => TResultProps; + +export type RenderFn = ( + props: TProps, + state: TState, +) => React.ReactElement; + +/** + * Generic factory function to create connected components following the hooks pattern + * inspired by Adobe React Spectrum and Base UI architectures. + * + * @param useStateHook - Hook that provides component state + * @param usePropsHook - Hook that enhances props with state-derived values + * @param defaultRender - Default render function for the component + * @param displayName - Display name for React DevTools + * @returns Connected component with customizable render prop + */ +export const toConnectedComponent = < + TProps extends Record, + TState, + TResultProps extends Record, + TRenderFn extends RenderFn +>( + useStateHook: StateHookFn, + usePropsHook: PropsHookFn, + defaultRender: TRenderFn, + displayName: string, +) => { + const ConnectedComponent = ({ + render = defaultRender, + ...props + }: TProps & { render?: TRenderFn }) => { + const connectedState = useStateHook(props as TProps); + const connectedProps = usePropsHook(props as TProps, connectedState); + return render(connectedProps, connectedState); + }; + + ConnectedComponent.displayName = displayName; + return ConnectedComponent; +}; + +/** + * Type helper to infer the component type from the factory + */ +export type ConnectedComponent< + TProps extends Record, + TRenderFn extends RenderFn +> = React.FC; \ No newline at end of file