From 882019f458f1b0e005de3667539184e116833c23 Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Mon, 8 Sep 2025 16:33:48 -0700 Subject: [PATCH] refactor(react,html): implement hook-style component architecture for MuteButton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React changes: - Add muteButtonStateDef with keys, stateTransform, and requestMethods - Refactor useMuteButtonState to use shallowEqual optimization - Migrate from useMediaDispatch to useMediaStore pattern - Create request methods factory for consistent state management - Improve type safety and performance with optimized state selection HTML changes: - Rename MediaMuteButton class to MuteButtonBase for consistency - Restructure event handling logic to match PlayButton pattern - Add TODO comment for React vs. W.C. data-* attribute discrepancies - Update component factory usage to use renamed base class This completes the migration to hook-style architecture that can be shared between HTML and React implementations, matching the PlayButton pattern. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../html/src/components/media-mute-button.ts | 13 ++-- .../react/react/src/components/MuteButton.tsx | 67 ++++++++++++++----- 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/packages/html/html/src/components/media-mute-button.ts b/packages/html/html/src/components/media-mute-button.ts index caf0761d..35bbbb12 100644 --- a/packages/html/html/src/components/media-mute-button.ts +++ b/packages/html/html/src/components/media-mute-button.ts @@ -5,21 +5,21 @@ import { } from '../utils/component-factory'; import { MediaChromeButton } from './media-chrome-button'; -export class MediaMuteButton extends MediaChromeButton { +export class MuteButtonBase extends MediaChromeButton { _state: | { muted: boolean; volumeLevel: string; - requestUnmute: () => void; requestMute: () => void; + requestUnmute: () => void; } | undefined; handleEvent(event: Event) { const { type } = event; - if (type === 'click') { - const state = this._state; - if (state) { + const state = this._state; + if (state) { + if (type === 'click') { if (state.muted) { state.requestUnmute(); } else { @@ -39,6 +39,7 @@ export class MediaMuteButton extends MediaChromeButton { _update(props: any, state: any) { this._state = state; + /** @TODO Follow up with React vs. W.C. data-* attributes discrepancies (CJP) */ // Make generic this.toggleAttribute('data-muted', props['data-muted']); this.setAttribute('data-volume-level', props['data-volume-level']); @@ -102,7 +103,7 @@ export const useMuteButtonProps: PropsHook<{ * Equivalent to React's MuteButton = toConnectedComponent(...) */ export const MuteButton = toConnectedHTMLComponent( - MediaMuteButton, + MuteButtonBase, useMuteButtonState, useMuteButtonProps, 'MuteButton', diff --git a/packages/react/react/src/components/MuteButton.tsx b/packages/react/react/src/components/MuteButton.tsx index 07e92688..0db7a677 100644 --- a/packages/react/react/src/components/MuteButton.tsx +++ b/packages/react/react/src/components/MuteButton.tsx @@ -1,27 +1,58 @@ -import { useMediaDispatch, useMediaSelector } from '@vjs-10/react-media-store'; +import { + shallowEqual, + // useMediaDispatch, + useMediaSelector, + useMediaStore, +} 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) */ - const volumeLevel = useMediaSelector( - (state: any) => state.volumeLevel, - ) as string; - const muted = useMediaSelector((state: any) => state.muted) as boolean; +/** + * MuteButton state hook - equivalent to React's useMuteButtonState + * Handles media store state subscription and transformation + */ +export const muteButtonStateDef = { + keys: ['muted', 'volumeLevel'], + stateTransform: (rawState: any) => ({ + muted: rawState.muted ?? false, + volumeLevel: rawState.volumeLevel ?? 'off', + }), + /** @TODO Consider "promoting" this up to state-mediator defs + media store (CJP) */ + requestMethods: (mediaStore: ReturnType) => { + return { + requestMute() { + const type = 'muterequest'; + mediaStore.dispatch({ type }); + }, + requestUnmute() { + const type = 'unmuterequest'; + mediaStore.dispatch({ type }); + }, + }; + }, +} as const; - const dispatch = useMediaDispatch(); - const requestMute = React.useCallback(() => { - dispatch({ type: 'muterequest' }); - }, [dispatch]); - const requestUnmute = React.useCallback(() => { - dispatch({ type: 'unmuterequest' }); - }, [dispatch]); +export const useMuteButtonState = (_props: any) => { + const mediaStore = useMediaStore(); + /** @TODO Fix type issues with hooks (CJP) */ + const mediaState = useMediaSelector( + muteButtonStateDef.stateTransform, + shallowEqual, + ); + + const [methods, setMethods] = React.useState( + muteButtonStateDef.requestMethods(mediaStore), + ); + + React.useEffect(() => { + setMethods(muteButtonStateDef.requestMethods(mediaStore)); + }, [mediaStore]); return { - volumeLevel, - muted, - requestMute, - requestUnmute, + volumeLevel: mediaState.volumeLevel, + muted: mediaState.muted, + requestMute: methods.requestMute, + requestUnmute: methods.requestUnmute, } as const; };