refactor(react): consolidate MuteButton components into unified implementation

- Merge three separate MuteButton variants into single composable component
- Move from connected-with-defaults/MuteButton.tsx to components/MuteButton.tsx
- Remove connected/ and ui/ MuteButton variants to reduce code duplication
- Update MediaSkinDefault to use consolidated MuteButton component
- Implement composable pattern with hooks for state, props, and rendering
- Add temporary @ts-ignore for type compatibility during refactoring
This commit is contained in:
Christian Pillsbury
2025-09-08 17:15:40 -07:00
committed by Christian Pillsbury
parent 3fd26eb654
commit ba8797cd21
5 changed files with 110 additions and 134 deletions
@@ -0,0 +1,103 @@
import { useMediaDispatch, useMediaSelector } from '@vjs-10/react-media-store';
import * as React from 'react';
import type { ElementType, PropsWithChildren } from 'react';
type DefaultMuteButtonState = { mediaVolumeLevel: string };
type DefaultMuteButtonEventCallbacks = {
onmediamuterequest: (event: Pick<CustomEvent, 'type'>) => void;
onmediaunmuterequest: (event: Pick<CustomEvent, 'type'>) => void;
};
type ComponentType = ElementType<
PropsWithChildren<
Partial<DefaultMuteButtonState & DefaultMuteButtonEventCallbacks>
>
>;
export const useMuteButtonState = (_props: any) => {
/** @TODO Fix type issues with hooks (CJP) */
const volumeLevel = useMediaSelector(
(state: any) => state.mediaVolumeLevel,
) as string;
const muted = useMediaSelector((state: any) => state.mediaMuted) as boolean;
const dispatch = useMediaDispatch();
const requestMute = React.useCallback(() => {
dispatch({ type: 'mediamuterequest' });
}, [dispatch]);
const requestUnmute = React.useCallback(() => {
dispatch({ type: 'mediaunmuterequest' });
}, [dispatch]);
return {
volumeLevel,
muted,
requestMute,
requestUnmute,
} as const;
};
export type useMuteButtonState = typeof useMuteButtonState;
export type MuteButtonState = ReturnType<useMuteButtonState>;
export const useMuteButtonProps = (
props: React.PropsWithChildren<{ [k: string]: any }>,
state: ReturnType<typeof useMuteButtonState>,
) => {
return {
...props,
['data-muted']: state.muted,
['data-volume-level']: state.volumeLevel,
};
};
export type useMuteButtonProps = typeof useMuteButtonProps;
type MuteButtonProps = ReturnType<useMuteButtonProps>;
export const renderMuteButton = (
props: MuteButtonProps,
state: MuteButtonState,
) => {
return (
<button
{...props}
onClick={() => {
if (state.muted) {
state.requestUnmute();
} else {
state.requestMute();
}
}}
>
{props.children}
</button>
);
};
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,
renderMuteButton,
'MuteButton',
);
export default MuteButton;
@@ -1,31 +0,0 @@
// NOTE: This is just a thin wrapper around the "skeletal" connected component that also applies a default (but overridable) definition of the
// "BaseComponent" that defines the actual UI.
// NOTE: Definitions like this should be able to be autogenerated via codegen, defined via a factory function (HoC or higher order component), or both.
import * as React from 'react';
// import { MediaMuteButton as BaseComponent } from 'media-chrome/react';
import BaseComponent from '../ui/MuteButton';
import ConnectedComponent from '../connected/MuteButton';
// NOTE: Assuming FC or "functional component" here for type expediency (though may be fine for our use cases).
type ConnectedComponentProps = Parameters<typeof ConnectedComponent>[0];
type ConnectedComponentReturnType = ReturnType<typeof ConnectedComponent>;
type DefaultedProps = 'component';
type ConnectedComponentWithDefaults = (
props: Omit<ConnectedComponentProps, DefaultedProps> &
Partial<Pick<ConnectedComponentProps, DefaultedProps>>
) => ConnectedComponentReturnType;
const Component: ConnectedComponentWithDefaults = ({
component = BaseComponent,
children,
...props
}) => {
return (
<ConnectedComponent {...props} component={component}>
{children}
</ConnectedComponent>
);
};
export default Component;
@@ -1,54 +0,0 @@
// NOTE: This is an example of a "skeletal" connected component definition of a Mute Button. It "knows about" A Media (UI) Store and expects
// to be provided a non-connected component
import * as React from 'react';
import { useMediaDispatch, useMediaSelector } from '@vjs-10/react-media-store';
import type { CSSProperties, ElementType, PropsWithChildren } from 'react';
/** @TODO Export more types. Define more contracts (CJP) */
type MediaCallbackType = ReturnType<typeof useMediaDispatch>;
type DefaultMuteButtonState = { mediaVolumeLevel: string };
/** @TODO Support camel case deeply for callbacks. Define these as consts and types in core lib with clear relationships to event types (CJP) */
type DefaultMuteButtonEventCallbacks = {
onmediamuterequest: MediaCallbackType;
onmediaunmuterequest: MediaCallbackType;
};
type ComponentType = ElementType<
PropsWithChildren<
Partial<
DefaultMuteButtonState &
DefaultMuteButtonEventCallbacks & {
className: string | undefined;
style: CSSProperties | undefined;
}
>
>
>;
const MuteButton = ({
component,
children,
...props
}: PropsWithChildren<{
component: ComponentType;
className?: string | undefined;
style?: CSSProperties | undefined;
}>) => {
const Component = component;
const dispatch = useMediaDispatch();
// @ts-ignore - State type issues
const mediaVolumeLevel = useMediaSelector((state) => state.mediaVolumeLevel);
console.log('mediaVolumeLevel', mediaVolumeLevel);
return (
<Component
{...props}
onmediamuterequest={dispatch}
onmediaunmuterequest={dispatch}
mediaVolumeLevel={mediaVolumeLevel}
>
{children}
</Component>
);
};
export default MuteButton;
@@ -1,47 +0,0 @@
import * as React from 'react';
import type { ElementType, PropsWithChildren } from 'react';
type DefaultMuteButtonState = { mediaVolumeLevel: string };
type DefaultMuteButtonEventCallbacks = {
onmediamuterequest: (event: Pick<CustomEvent, 'type'>) => void;
onmediaunmuterequest: (event: Pick<CustomEvent, 'type'>) => void;
};
type ComponentType = ElementType<
PropsWithChildren<
Partial<DefaultMuteButtonState & DefaultMuteButtonEventCallbacks>
>
>;
const MuteButton: ComponentType = ({
mediaVolumeLevel,
onmediamuterequest,
onmediaunmuterequest,
children,
...props
}) => {
const mediaMuted = mediaVolumeLevel === 'off';
console.log('mediaVolumeLevel', mediaVolumeLevel);
return (
<button
{...props}
data-muted={mediaMuted || undefined}
data-volume-level={mediaVolumeLevel}
onClick={() => {
const type = mediaMuted ? 'mediaunmuterequest' : 'mediamuterequest';
console.log('clicked', type);
/** @TODO Discuss tradeoffs of requiring passing in object with type, esp. since callback already identifies action type (CJP) */
mediaMuted
? onmediaunmuterequest?.({
type,
})
: onmediamuterequest?.({
type,
});
}}
>
{children}
</button>
);
};
export default MuteButton;
@@ -1,8 +1,12 @@
import * as React from 'react';
import { PauseIcon, PlayIcon } from '@vjs-10/react-icons';
import PlayButton from '../components/connected-with-defaults/PlayButton';
import MuteButton from '../components/connected-with-defaults/MuteButton';
import { VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@vjs-10/react-icons';
import MuteButton from '../components/MuteButton';
import {
VolumeHighIcon,
VolumeLowIcon,
VolumeOffIcon,
} from '@vjs-10/react-icons';
import styles from './styles.module.css';
export const MediaSkinDefault: React.FC<{ children: React.ReactNode }> = ({
@@ -18,6 +22,7 @@ export const MediaSkinDefault: React.FC<{ children: React.ReactNode }> = ({
<PlayIcon className={styles.PlayIcon}></PlayIcon>
<PauseIcon className={styles.PauseIcon}></PauseIcon>
</PlayButton>
{/* @ts-ignore */}
<MuteButton className={`${styles.Button} ${styles.MediaMuteButton}`}>
<VolumeHighIcon
className={`${styles.Icon} ${styles.VolumeHighIcon}`}