refactor(react): restructure VolumeRange to use render function pattern

Refactor VolumeRange component to follow the same architectural pattern as
PlayButton and MuteButton components for consistency and maintainability.

Changes:
- Split component into useVolumeRangeState, useVolumeRangeProps, renderVolumeRange
- Use toConnectedComponent factory for consistent component creation
- Separate state management, props transformation, and rendering concerns
- Add proper TypeScript exports for all hooks and types
- Maintain existing functionality while improving code organization

This ensures architectural consistency across all React media components
and makes the codebase easier to maintain and extend.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2025-09-09 07:59:56 -07:00
committed by Christian Pillsbury
co-authored by Claude
parent 2282f4799b
commit 75c6cb6bc3
@@ -1,24 +1,15 @@
import React from 'react';
import {
shallowEqual,
useMediaSelector,
useMediaStore,
} from '@vjs-10/react-media-store';
import * as React from 'react';
import { toConnectedComponent } from '../utils/component-factory';
import { volumeRangeStateDefinition } from '@vjs-10/media-store';
interface VolumeRangeProps {
className?: string;
style?: React.CSSProperties;
}
export const VolumeRange: React.FC<VolumeRangeProps> = ({
className,
style,
...props
}) => {
export const useVolumeRangeState = (_props: any) => {
const mediaStore = useMediaStore();
// Use useMediaSelector to properly subscribe to state changes
/** @TODO Fix type issues with hooks (CJP) */
const mediaState = useMediaSelector(
volumeRangeStateDefinition.stateTransform,
shallowEqual,
@@ -29,27 +20,69 @@ export const VolumeRange: React.FC<VolumeRangeProps> = ({
[mediaStore],
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
methods.requestVolumeChange(parseFloat(e.target.value));
return {
volume: mediaState.volume,
muted: mediaState.muted,
volumeLevel: mediaState.volumeLevel,
requestVolumeChange: methods.requestVolumeChange,
} as const;
};
export type useVolumeRangeState = typeof useVolumeRangeState;
export type VolumeRangeState = ReturnType<useVolumeRangeState>;
export const useVolumeRangeProps = (
props: React.PropsWithChildren<{ [k: string]: any }>,
state: ReturnType<typeof useVolumeRangeState>,
) => {
const displayValue = state.muted ? 0 : state.volume;
const baseProps: Record<string, any> = {
/** @TODO These should probably be defined in the render function (CJP) */
/** input properties */
type: 'range',
min: '0',
max: '1',
step: '0.01',
value: displayValue,
/** aria attributes/props */
'aria-label': 'Volume',
'aria-valuetext': `${Math.round(displayValue * 100)}%`,
/** data attributes */
'data-muted': state.muted,
'data-volume-level': state.volumeLevel,
/** external props spread last to allow for overriding */
...props,
};
const displayValue = mediaState.muted ? 0 : mediaState.volume;
return baseProps;
};
export type useVolumeRangeProps = typeof useVolumeRangeProps;
type VolumeRangeProps = ReturnType<useVolumeRangeProps>;
export const renderVolumeRange = (
props: VolumeRangeProps,
state: VolumeRangeState,
) => {
return (
<input
type="range"
min="0"
max="1"
step="0.01"
value={displayValue}
onChange={handleChange}
aria-label="Volume"
aria-valuetext={`${Math.round(displayValue * 100)}%`}
data-muted={mediaState.muted}
data-volume-level={mediaState.volumeLevel}
className={className}
style={style}
{...props}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
/** @ts-ignore */
if (props.disabled) return;
state.requestVolumeChange(parseFloat(e.target.value));
}}
/>
);
};
export type renderVolumeRange = typeof renderVolumeRange;
export const VolumeRange = toConnectedComponent(
useVolumeRangeState,
useVolumeRangeProps,
renderVolumeRange,
'VolumeRange',
);
export default VolumeRange;