diff --git a/.changeset/bright-parts-compose.md b/.changeset/bright-parts-compose.md new file mode 100644 index 00000000..1662841a --- /dev/null +++ b/.changeset/bright-parts-compose.md @@ -0,0 +1,5 @@ +--- +"@bazza-ui/react": patch +--- + +Compose video player component part event handlers so consumers can opt out of internal behavior with preventBaseUIHandler. diff --git a/.gitignore b/.gitignore index 45e46cb9..5b9e78ba 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ tmp/ # Repomix repomix-output.txt .osgrep + +base-ui/ diff --git a/packages/react/src/video-player/components/captions-button/captions-button.tsx b/packages/react/src/video-player/components/captions-button/captions-button.tsx index 4afad4e2..72501b54 100644 --- a/packages/react/src/video-player/components/captions-button/captions-button.tsx +++ b/packages/react/src/video-player/components/captions-button/captions-button.tsx @@ -1,8 +1,10 @@ 'use client' import * as React from 'react' +import type { WithPreventableBaseHandlers } from '../../../utils/types.js' import { useVideoPlayerContext } from '../../contexts/video-player-context.js' import type { RenderProp } from '../../types.js' +import { mergeElementProps } from '../../utils/merge-element-props.js' import { CaptionsButtonDataAttributes } from './captions-button.data-attributes.js' // ============================================================================ @@ -10,13 +12,13 @@ import { CaptionsButtonDataAttributes } from './captions-button.data-attributes. // ============================================================================ export interface CaptionsButtonProps - extends React.ComponentPropsWithRef<'button'> { + extends WithPreventableBaseHandlers> { render?: RenderProp } export interface CaptionsButtonRenderProps { ref: React.Ref - type: 'button' + type: React.ButtonHTMLAttributes['type'] 'aria-label': string 'aria-pressed': boolean disabled: boolean @@ -39,7 +41,7 @@ export const CaptionsButton = React.forwardRef< HTMLButtonElement, CaptionsButtonProps >(function CaptionsButton(props, forwardedRef) { - const { render, onClick, ...buttonProps } = props + const { render, ...buttonProps } = props const context = useVideoPlayerContext('CaptionsButton') const available = context.registeredTracks.length > 0 @@ -51,10 +53,16 @@ export const CaptionsButton = React.forwardRef< trackCount: context.registeredTracks.length, } - const handleClick = React.useCallback( - (event: React.MouseEvent) => { - onClick?.(event) - if (!event.defaultPrevented) { + const renderProps = mergeElementProps( + { + ref: forwardedRef, + type: 'button', + 'aria-label': active ? 'Disable captions' : 'Enable captions', + 'aria-pressed': active, + disabled: !available, + [CaptionsButtonDataAttributes.active]: active || undefined, + [CaptionsButtonDataAttributes.available]: available || undefined, + onClick() { if (active) { // Turn off captions context.setTextTrack(null) @@ -65,22 +73,11 @@ export const CaptionsButton = React.forwardRef< context.setTextTrack(firstTrack.textTrack) } } - } + }, }, - [onClick, context, active], + buttonProps, ) - const renderProps: CaptionsButtonRenderProps = { - ref: forwardedRef, - type: 'button', - 'aria-label': active ? 'Disable captions' : 'Enable captions', - 'aria-pressed': active, - disabled: !available, - [CaptionsButtonDataAttributes.active]: active || undefined, - [CaptionsButtonDataAttributes.available]: available || undefined, - onClick: handleClick, - } - if (render) { return render(renderProps, state) } diff --git a/packages/react/src/video-player/components/captions-menu/captions-menu.tsx b/packages/react/src/video-player/components/captions-menu/captions-menu.tsx index 40f6fc1c..741bee74 100644 --- a/packages/react/src/video-player/components/captions-menu/captions-menu.tsx +++ b/packages/react/src/video-player/components/captions-menu/captions-menu.tsx @@ -1,8 +1,10 @@ 'use client' import * as React from 'react' +import type { WithPreventableBaseHandlers } from '../../../utils/types.js' import { useVideoPlayerContext } from '../../contexts/video-player-context.js' import type { RenderProp, TrackInfo } from '../../types.js' +import { mergeElementProps } from '../../utils/merge-element-props.js' import { CaptionsMenuDataAttributes, CaptionsMenuItemDataAttributes, @@ -77,7 +79,7 @@ export const CaptionsMenu = React.forwardRef( // ============================================================================ export interface CaptionsMenuItemProps - extends React.ComponentPropsWithRef<'button'> { + extends WithPreventableBaseHandlers> { track: TextTrack | null } @@ -85,34 +87,26 @@ export const CaptionsMenuItem = React.forwardRef< HTMLButtonElement, CaptionsMenuItemProps >(function CaptionsMenuItem(props, forwardedRef) { - const { track, onClick, children, ...buttonProps } = props + const { track, children, ...buttonProps } = props const context = useVideoPlayerContext('CaptionsMenuItem') const isActive = context.activeTextTrack === track - const handleClick = React.useCallback( - (event: React.MouseEvent) => { - onClick?.(event) - if (!event.defaultPrevented) { + const elementProps = mergeElementProps( + { + ref: forwardedRef, + type: 'button', + role: 'menuitemradio', + 'aria-checked': isActive, + [CaptionsMenuItemDataAttributes.active]: isActive || undefined, + onClick() { context.setTextTrack(track) - } + }, }, - [onClick, context, track], + buttonProps, ) - return ( - - ) + return }) // ============================================================================ diff --git a/packages/react/src/video-player/components/fullscreen-button/fullscreen-button.tsx b/packages/react/src/video-player/components/fullscreen-button/fullscreen-button.tsx index 9bfd1b0a..c6abf89b 100644 --- a/packages/react/src/video-player/components/fullscreen-button/fullscreen-button.tsx +++ b/packages/react/src/video-player/components/fullscreen-button/fullscreen-button.tsx @@ -1,8 +1,10 @@ 'use client' import * as React from 'react' +import type { WithPreventableBaseHandlers } from '../../../utils/types.js' import { useVideoPlayerContext } from '../../contexts/video-player-context.js' import type { RenderProp } from '../../types.js' +import { mergeElementProps } from '../../utils/merge-element-props.js' import { FullscreenButtonDataAttributes } from './fullscreen-button.data-attributes.js' // ============================================================================ @@ -10,14 +12,17 @@ import { FullscreenButtonDataAttributes } from './fullscreen-button.data-attribu // ============================================================================ export interface FullscreenButtonProps - extends Omit, 'children'> { + extends Omit< + WithPreventableBaseHandlers>, + 'children' + > { render?: RenderProp children?: React.ReactNode } export interface FullscreenButtonRenderProps { ref: React.Ref - type: 'button' + type: React.ButtonHTMLAttributes['type'] 'aria-label': string 'aria-pressed': boolean disabled: boolean @@ -39,7 +44,7 @@ export const FullscreenButton = React.forwardRef< HTMLButtonElement, FullscreenButtonProps >(function FullscreenButton(props, forwardedRef) { - const { render, onClick, children, ...buttonProps } = props + const { render, children, ...buttonProps } = props const context = useVideoPlayerContext('FullscreenButton') // Defer to after hydration to avoid mismatch @@ -48,32 +53,27 @@ export const FullscreenButton = React.forwardRef< setSupported('fullscreenEnabled' in document && document.fullscreenEnabled) }, []) - const handleClick = React.useCallback( - (event: React.MouseEvent) => { - onClick?.(event) - if (!event.defaultPrevented) { - context.toggleFullscreen() - } - }, - [onClick, context], - ) - const state: FullscreenButtonState = { fullscreen: context.fullscreen, supported, } - const renderProps: FullscreenButtonRenderProps = { - ref: forwardedRef, - type: 'button', - 'aria-label': context.fullscreen ? 'Exit fullscreen' : 'Enter fullscreen', - 'aria-pressed': context.fullscreen, - disabled: !supported, - [FullscreenButtonDataAttributes.fullscreen]: - context.fullscreen || undefined, - [FullscreenButtonDataAttributes.supported]: supported || undefined, - onClick: handleClick, - } + const renderProps = mergeElementProps( + { + ref: forwardedRef, + type: 'button', + 'aria-label': context.fullscreen ? 'Exit fullscreen' : 'Enter fullscreen', + 'aria-pressed': context.fullscreen, + disabled: !supported, + [FullscreenButtonDataAttributes.fullscreen]: + context.fullscreen || undefined, + [FullscreenButtonDataAttributes.supported]: supported || undefined, + onClick() { + context.toggleFullscreen() + }, + }, + buttonProps, + ) if (render) { return render(renderProps, state) diff --git a/packages/react/src/video-player/components/mute-button/mute-button.tsx b/packages/react/src/video-player/components/mute-button/mute-button.tsx index 917ef7bb..c87c8908 100644 --- a/packages/react/src/video-player/components/mute-button/mute-button.tsx +++ b/packages/react/src/video-player/components/mute-button/mute-button.tsx @@ -1,21 +1,24 @@ 'use client' import * as React from 'react' +import type { WithPreventableBaseHandlers } from '../../../utils/types.js' import { useVideoPlayerContext } from '../../contexts/video-player-context.js' import type { RenderProp } from '../../types.js' +import { mergeElementProps } from '../../utils/merge-element-props.js' import { MuteButtonDataAttributes } from './mute-button.data-attributes.js' // ============================================================================ // MuteButton Props // ============================================================================ -export interface MuteButtonProps extends React.ComponentPropsWithRef<'button'> { +export interface MuteButtonProps + extends WithPreventableBaseHandlers> { render?: RenderProp } export interface MuteButtonRenderProps { ref: React.Ref - type: 'button' + type: React.ButtonHTMLAttributes['type'] 'aria-label': string 'aria-pressed': boolean [MuteButtonDataAttributes.muted]?: boolean @@ -37,7 +40,7 @@ export interface MuteButtonState { export const MuteButton = React.forwardRef( function MuteButton(props, forwardedRef) { - const { render, onClick, ...buttonProps } = props + const { render, ...buttonProps } = props const context = useVideoPlayerContext('MuteButton') const state: MuteButtonState = { @@ -45,16 +48,6 @@ export const MuteButton = React.forwardRef( volume: context.volume, } - const handleClick = React.useCallback( - (event: React.MouseEvent) => { - onClick?.(event) - if (!event.defaultPrevented) { - context.toggleMute() - } - }, - [onClick, context], - ) - const volume = context.volume const muted = context.muted const volumeOff = volume === 0 || muted @@ -62,18 +55,23 @@ export const MuteButton = React.forwardRef( const volumeLow = !muted && volume > 0 && volume < 0.5 const volumeHigh = !muted && volume >= 0.5 - const renderProps: MuteButtonRenderProps = { - ref: forwardedRef, - type: 'button', - 'aria-label': context.muted ? 'Unmute' : 'Mute', - 'aria-pressed': context.muted, - [MuteButtonDataAttributes.muted]: muted || undefined, - [MuteButtonDataAttributes.volumeOff]: volumeOff || undefined, - [MuteButtonDataAttributes.volumeOn]: volumeOn || undefined, - [MuteButtonDataAttributes.volumeLow]: volumeLow || undefined, - [MuteButtonDataAttributes.volumeHigh]: volumeHigh || undefined, - onClick: handleClick, - } + const renderProps = mergeElementProps( + { + ref: forwardedRef, + type: 'button', + 'aria-label': context.muted ? 'Unmute' : 'Mute', + 'aria-pressed': context.muted, + [MuteButtonDataAttributes.muted]: muted || undefined, + [MuteButtonDataAttributes.volumeOff]: volumeOff || undefined, + [MuteButtonDataAttributes.volumeOn]: volumeOn || undefined, + [MuteButtonDataAttributes.volumeLow]: volumeLow || undefined, + [MuteButtonDataAttributes.volumeHigh]: volumeHigh || undefined, + onClick() { + context.toggleMute() + }, + }, + buttonProps, + ) if (render) { return render(renderProps, state) diff --git a/packages/react/src/video-player/components/overlay/overlay.tsx b/packages/react/src/video-player/components/overlay/overlay.tsx index c189731f..3d446672 100644 --- a/packages/react/src/video-player/components/overlay/overlay.tsx +++ b/packages/react/src/video-player/components/overlay/overlay.tsx @@ -1,9 +1,11 @@ 'use client' import * as React from 'react' +import type { WithPreventableBaseHandlers } from '../../../utils/types.js' import { useVideoPlayerContext } from '../../contexts/video-player-context.js' import { useTransitionStatus } from '../../hooks/use-transition-status.js' import type { RenderProp } from '../../types.js' +import { mergeElementProps } from '../../utils/merge-element-props.js' import { OverlayDataAttributes } from './overlay.data-attributes.js' // ============================================================================ @@ -11,7 +13,10 @@ import { OverlayDataAttributes } from './overlay.data-attributes.js' // ============================================================================ export interface OverlayProps - extends Omit, 'children'> { + extends Omit< + WithPreventableBaseHandlers>, + 'children' + > { /** * Delay before hiding after player becomes idle (ms). * Defaults to Root's idleTimeout. Set to 0 to disable auto-hide. @@ -68,7 +73,6 @@ export const Overlay = React.forwardRef( idleTimeout: idleTimeoutProp, keepMounted = false, render, - onClick, children, ...divProps } = props @@ -100,16 +104,6 @@ export const Overlay = React.forwardRef( [forwardedRef, elementRef], ) - const handleClick = React.useCallback( - (event: React.MouseEvent) => { - onClick?.(event) - if (!event.defaultPrevented) { - context.toggle() - } - }, - [onClick, context], - ) - const state: OverlayState = { playing: context.playing, paused: context.paused, @@ -121,23 +115,28 @@ export const Overlay = React.forwardRef( open, } - const renderProps: OverlayRenderProps = { - ref: composedRef, - onClick: handleClick, - [OverlayDataAttributes.playing]: context.playing || undefined, - [OverlayDataAttributes.paused]: context.paused || undefined, - [OverlayDataAttributes.ended]: context.ended || undefined, - [OverlayDataAttributes.waiting]: context.waiting || undefined, - [OverlayDataAttributes.seeking]: context.seeking || undefined, - [OverlayDataAttributes.fullscreen]: context.fullscreen || undefined, - [OverlayDataAttributes.pip]: context.pictureInPicture || undefined, - [OverlayDataAttributes.open]: open || undefined, - [OverlayDataAttributes.closed]: !open || undefined, - [OverlayDataAttributes.startingStyle]: - transitionStatus === 'starting' || undefined, - [OverlayDataAttributes.endingStyle]: - transitionStatus === 'ending' || undefined, - } + const renderProps = mergeElementProps( + { + ref: composedRef, + onClick() { + context.toggle() + }, + [OverlayDataAttributes.playing]: context.playing || undefined, + [OverlayDataAttributes.paused]: context.paused || undefined, + [OverlayDataAttributes.ended]: context.ended || undefined, + [OverlayDataAttributes.waiting]: context.waiting || undefined, + [OverlayDataAttributes.seeking]: context.seeking || undefined, + [OverlayDataAttributes.fullscreen]: context.fullscreen || undefined, + [OverlayDataAttributes.pip]: context.pictureInPicture || undefined, + [OverlayDataAttributes.open]: open || undefined, + [OverlayDataAttributes.closed]: !open || undefined, + [OverlayDataAttributes.startingStyle]: + transitionStatus === 'starting' || undefined, + [OverlayDataAttributes.endingStyle]: + transitionStatus === 'ending' || undefined, + }, + divProps, + ) if (!mounted) { return null diff --git a/packages/react/src/video-player/components/picture-in-picture-button/picture-in-picture-button.tsx b/packages/react/src/video-player/components/picture-in-picture-button/picture-in-picture-button.tsx index 15477f50..70269adb 100644 --- a/packages/react/src/video-player/components/picture-in-picture-button/picture-in-picture-button.tsx +++ b/packages/react/src/video-player/components/picture-in-picture-button/picture-in-picture-button.tsx @@ -1,8 +1,10 @@ 'use client' import * as React from 'react' +import type { WithPreventableBaseHandlers } from '../../../utils/types.js' import { useVideoPlayerContext } from '../../contexts/video-player-context.js' import type { RenderProp } from '../../types.js' +import { mergeElementProps } from '../../utils/merge-element-props.js' import { PictureInPictureButtonDataAttributes } from './picture-in-picture-button.data-attributes.js' // ============================================================================ @@ -10,7 +12,7 @@ import { PictureInPictureButtonDataAttributes } from './picture-in-picture-butto // ============================================================================ export interface PictureInPictureButtonProps - extends React.ComponentPropsWithRef<'button'> { + extends WithPreventableBaseHandlers> { render?: RenderProp< PictureInPictureButtonRenderProps, PictureInPictureButtonState @@ -19,7 +21,7 @@ export interface PictureInPictureButtonProps export interface PictureInPictureButtonRenderProps { ref: React.Ref - type: 'button' + type: React.ButtonHTMLAttributes['type'] 'aria-label': string 'aria-pressed': boolean disabled: boolean @@ -41,7 +43,7 @@ export const PictureInPictureButton = React.forwardRef< HTMLButtonElement, PictureInPictureButtonProps >(function PictureInPictureButton(props, forwardedRef) { - const { render, onClick, ...buttonProps } = props + const { render, ...buttonProps } = props const context = useVideoPlayerContext('PictureInPictureButton') // Defer to after hydration to avoid mismatch @@ -57,30 +59,25 @@ export const PictureInPictureButton = React.forwardRef< supported, } - const handleClick = React.useCallback( - (event: React.MouseEvent) => { - onClick?.(event) - if (!event.defaultPrevented) { + const renderProps = mergeElementProps( + { + ref: forwardedRef, + type: 'button', + 'aria-label': context.pictureInPicture + ? 'Exit picture-in-picture' + : 'Enter picture-in-picture', + 'aria-pressed': context.pictureInPicture, + disabled: !supported, + [PictureInPictureButtonDataAttributes.pip]: + context.pictureInPicture || undefined, + [PictureInPictureButtonDataAttributes.supported]: supported || undefined, + onClick() { context.togglePictureInPicture() - } + }, }, - [onClick, context], + buttonProps, ) - const renderProps: PictureInPictureButtonRenderProps = { - ref: forwardedRef, - type: 'button', - 'aria-label': context.pictureInPicture - ? 'Exit picture-in-picture' - : 'Enter picture-in-picture', - 'aria-pressed': context.pictureInPicture, - disabled: !supported, - [PictureInPictureButtonDataAttributes.pip]: - context.pictureInPicture || undefined, - [PictureInPictureButtonDataAttributes.supported]: supported || undefined, - onClick: handleClick, - } - if (render) { return render(renderProps, state) } diff --git a/packages/react/src/video-player/components/play-button/play-button.tsx b/packages/react/src/video-player/components/play-button/play-button.tsx index 6331d352..1fc0df2c 100644 --- a/packages/react/src/video-player/components/play-button/play-button.tsx +++ b/packages/react/src/video-player/components/play-button/play-button.tsx @@ -1,8 +1,10 @@ 'use client' import * as React from 'react' +import type { WithPreventableBaseHandlers } from '../../../utils/types.js' import { useVideoPlayerContext } from '../../contexts/video-player-context.js' import type { RenderProp } from '../../types.js' +import { mergeElementProps } from '../../utils/merge-element-props.js' import { PlayButtonDataAttributes } from './play-button.data-attributes.js' // ============================================================================ @@ -10,14 +12,17 @@ import { PlayButtonDataAttributes } from './play-button.data-attributes.js' // ============================================================================ export interface PlayButtonProps - extends Omit, 'children'> { + extends Omit< + WithPreventableBaseHandlers>, + 'children' + > { render?: RenderProp children?: React.ReactNode } export interface PlayButtonRenderProps { ref: React.Ref - type: 'button' + type: React.ButtonHTMLAttributes['type'] 'aria-label': string [PlayButtonDataAttributes.playing]?: boolean [PlayButtonDataAttributes.paused]?: boolean @@ -39,19 +44,9 @@ export interface PlayButtonState { export const PlayButton = React.forwardRef( function PlayButton(props, forwardedRef) { - const { render, onClick, children, ...buttonProps } = props + const { render, children, ...buttonProps } = props const context = useVideoPlayerContext('PlayButton') - const handleClick = React.useCallback( - (event: React.MouseEvent) => { - onClick?.(event) - if (!event.defaultPrevented) { - context.toggle() - } - }, - [onClick, context], - ) - const state: PlayButtonState = { playing: context.playing, paused: context.paused, @@ -59,16 +54,21 @@ export const PlayButton = React.forwardRef( waiting: context.waiting, } - const renderProps: PlayButtonRenderProps = { - ref: forwardedRef, - type: 'button', - 'aria-label': context.playing ? 'Pause' : 'Play', - [PlayButtonDataAttributes.playing]: context.playing || undefined, - [PlayButtonDataAttributes.paused]: context.paused || undefined, - [PlayButtonDataAttributes.ended]: context.ended || undefined, - [PlayButtonDataAttributes.waiting]: context.waiting || undefined, - onClick: handleClick, - } + const renderProps = mergeElementProps( + { + ref: forwardedRef, + type: 'button', + 'aria-label': context.playing ? 'Pause' : 'Play', + [PlayButtonDataAttributes.playing]: context.playing || undefined, + [PlayButtonDataAttributes.paused]: context.paused || undefined, + [PlayButtonDataAttributes.ended]: context.ended || undefined, + [PlayButtonDataAttributes.waiting]: context.waiting || undefined, + onClick() { + context.toggle() + }, + }, + buttonProps, + ) if (render) { return render(renderProps, state) diff --git a/packages/react/src/video-player/components/playback-rate-button/playback-rate-button.tsx b/packages/react/src/video-player/components/playback-rate-button/playback-rate-button.tsx index d4eb4e93..655a8532 100644 --- a/packages/react/src/video-player/components/playback-rate-button/playback-rate-button.tsx +++ b/packages/react/src/video-player/components/playback-rate-button/playback-rate-button.tsx @@ -1,8 +1,10 @@ 'use client' import * as React from 'react' +import type { WithPreventableBaseHandlers } from '../../../utils/types.js' import { useVideoPlayerContext } from '../../contexts/video-player-context.js' import type { RenderProp } from '../../types.js' +import { mergeElementProps } from '../../utils/merge-element-props.js' import { PlaybackRateButtonDataAttributes } from './playback-rate-button.data-attributes.js' // ============================================================================ @@ -14,7 +16,7 @@ export const DEFAULT_PLAYBACK_RATES = [ ] as const export interface PlaybackRateButtonProps - extends React.ComponentPropsWithRef<'button'> { + extends WithPreventableBaseHandlers> { /** * Available playback rates to cycle through. * @default [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] @@ -26,7 +28,7 @@ export interface PlaybackRateButtonProps export interface PlaybackRateButtonRenderProps { ref: React.Ref - type: 'button' + type: React.ButtonHTMLAttributes['type'] 'aria-label': string [PlaybackRateButtonDataAttributes.rate]: number onClick: (event: React.MouseEvent) => void @@ -48,7 +50,6 @@ export const PlaybackRateButton = React.forwardRef< const { rates = DEFAULT_PLAYBACK_RATES, render, - onClick, children, ...buttonProps } = props @@ -59,28 +60,24 @@ export const PlaybackRateButton = React.forwardRef< rates, } - const handleClick = React.useCallback( - (event: React.MouseEvent) => { - onClick?.(event) - if (!event.defaultPrevented) { - // Find next rate in the cycle + const renderProps = mergeElementProps( + { + ref: forwardedRef, + type: 'button', + 'aria-label': `Playback speed: ${context.playbackRate}x`, + [PlaybackRateButtonDataAttributes.rate]: context.playbackRate, + onClick() { const currentIndex = rates.indexOf(context.playbackRate) - const nextIndex = (currentIndex + 1) % rates.length - const nextRate = rates[nextIndex] ?? rates[0] ?? context.playbackRate + const nextRate = + rates[(currentIndex + 1) % rates.length] ?? + rates[0] ?? + context.playbackRate context.setPlaybackRate(nextRate) - } + }, }, - [onClick, context, rates], + buttonProps, ) - const renderProps: PlaybackRateButtonRenderProps = { - ref: forwardedRef, - type: 'button', - 'aria-label': `Playback speed: ${context.playbackRate}x`, - [PlaybackRateButtonDataAttributes.rate]: context.playbackRate, - onClick: handleClick, - } - if (render) { return render(renderProps, state) } diff --git a/packages/react/src/video-player/components/playback-rate-menu/playback-rate-menu.tsx b/packages/react/src/video-player/components/playback-rate-menu/playback-rate-menu.tsx index 7fb37fdd..a28afe43 100644 --- a/packages/react/src/video-player/components/playback-rate-menu/playback-rate-menu.tsx +++ b/packages/react/src/video-player/components/playback-rate-menu/playback-rate-menu.tsx @@ -1,8 +1,10 @@ 'use client' import * as React from 'react' +import type { WithPreventableBaseHandlers } from '../../../utils/types.js' import { useVideoPlayerContext } from '../../contexts/video-player-context.js' import type { RenderProp } from '../../types.js' +import { mergeElementProps } from '../../utils/merge-element-props.js' import { PlaybackRateMenuDataAttributes, PlaybackRateMenuItemDataAttributes, @@ -85,7 +87,7 @@ export const PlaybackRateMenu = React.forwardRef< // ============================================================================ export interface PlaybackRateMenuItemProps - extends React.ComponentPropsWithRef<'button'> { + extends WithPreventableBaseHandlers> { rate: number } @@ -93,36 +95,26 @@ export const PlaybackRateMenuItem = React.forwardRef< HTMLButtonElement, PlaybackRateMenuItemProps >(function PlaybackRateMenuItem(props, forwardedRef) { - const { rate, onClick, children, ...buttonProps } = props + const { rate, children, ...buttonProps } = props const context = useVideoPlayerContext('PlaybackRateMenuItem') const isActive = context.playbackRate === rate - const handleClick = React.useCallback( - (event: React.MouseEvent) => { - onClick?.(event) - if (!event.defaultPrevented) { + const elementProps = mergeElementProps( + { + ref: forwardedRef, + type: 'button', + role: 'menuitemradio', + 'aria-checked': isActive, + [PlaybackRateMenuItemDataAttributes.active]: isActive || undefined, + onClick() { context.setPlaybackRate(rate) - } + }, }, - [onClick, context, rate], + buttonProps, ) - return ( - - ) + return }) // ============================================================================ diff --git a/packages/react/src/video-player/components/quality-menu/quality-menu.tsx b/packages/react/src/video-player/components/quality-menu/quality-menu.tsx index 0c7b0d9f..716f4947 100644 --- a/packages/react/src/video-player/components/quality-menu/quality-menu.tsx +++ b/packages/react/src/video-player/components/quality-menu/quality-menu.tsx @@ -1,8 +1,10 @@ 'use client' import * as React from 'react' +import type { WithPreventableBaseHandlers } from '../../../utils/types.js' import { useVideoPlayerContext } from '../../contexts/video-player-context.js' import type { RenderProp, VideoQuality } from '../../types.js' +import { mergeElementProps } from '../../utils/merge-element-props.js' import { QualityMenuDataAttributes, QualityMenuItemDataAttributes, @@ -73,7 +75,7 @@ export const QualityMenu = React.forwardRef( // ============================================================================ export interface QualityMenuItemProps - extends React.ComponentPropsWithRef<'button'> { + extends WithPreventableBaseHandlers> { quality: VideoQuality } @@ -81,34 +83,26 @@ export const QualityMenuItem = React.forwardRef< HTMLButtonElement, QualityMenuItemProps >(function QualityMenuItem(props, forwardedRef) { - const { quality, onClick, children, ...buttonProps } = props + const { quality, children, ...buttonProps } = props const context = useVideoPlayerContext('QualityMenuItem') const isActive = context.activeQuality?.label === quality.label - const handleClick = React.useCallback( - (event: React.MouseEvent) => { - onClick?.(event) - if (!event.defaultPrevented) { + const elementProps = mergeElementProps( + { + ref: forwardedRef, + type: 'button', + role: 'menuitemradio', + 'aria-checked': isActive, + [QualityMenuItemDataAttributes.active]: isActive || undefined, + onClick() { context.setQuality(quality) - } + }, }, - [onClick, context, quality], + buttonProps, ) - return ( - - ) + return }) // ============================================================================ diff --git a/packages/react/src/video-player/components/root/root.tsx b/packages/react/src/video-player/components/root/root.tsx index 1f1fdcc2..517494dc 100644 --- a/packages/react/src/video-player/components/root/root.tsx +++ b/packages/react/src/video-player/components/root/root.tsx @@ -1,6 +1,5 @@ 'use client' -import { mergeProps } from '@base-ui/react/merge-props' import * as React from 'react' import { VideoPlayerContext } from '../../contexts/video-player-context.js' import { useKeyboardShortcuts } from '../../hooks/use-keyboard-shortcuts.js' @@ -13,6 +12,7 @@ import type { VideoPlayerRootProps, VideoQuality, } from '../../types.js' +import { mergeElementProps } from '../../utils/merge-element-props.js' import { RootDataAttributes } from './root.data-attributes.js' // ============================================================================ @@ -980,7 +980,7 @@ const VideoPlayerRootImpl = React.forwardRef< [context], ) - const rootProps = mergeProps<'div'>( + const rootProps = mergeElementProps( { onMouseMove: handleMouseMove, onMouseEnter: handleMouseEnter, diff --git a/packages/react/src/video-player/components/video/video.tsx b/packages/react/src/video-player/components/video/video.tsx index 8c2a1f57..7618aa04 100644 --- a/packages/react/src/video-player/components/video/video.tsx +++ b/packages/react/src/video-player/components/video/video.tsx @@ -1,7 +1,9 @@ 'use client' import * as React from 'react' +import type { WithPreventableBaseHandlers } from '../../../utils/types.js' import { useVideoPlayerContext } from '../../contexts/video-player-context.js' +import { mergeElementProps } from '../../utils/merge-element-props.js' import { VideoDataAttributes } from './video.data-attributes.js' // ============================================================================ @@ -10,7 +12,7 @@ import { VideoDataAttributes } from './video.data-attributes.js' export interface VideoProps extends Omit< - React.ComponentPropsWithRef<'video'>, + WithPreventableBaseHandlers>, | 'onTimeUpdate' | 'onDurationChange' | 'onProgress' @@ -45,7 +47,7 @@ export interface VideoState { export const Video = React.forwardRef( function Video(props, forwardedRef) { - const { toggleOnClick = true, onClick, ...videoProps } = props + const { toggleOnClick = true, ...videoProps } = props const context = useVideoPlayerContext('Video') const hasCheckedMetadata = React.useRef(false) @@ -82,17 +84,6 @@ export const Video = React.forwardRef( [forwardedRef, context.videoRef, context._handlers], ) - const handleClick = React.useCallback( - (event: React.MouseEvent) => { - onClick?.(event) - if (!event.defaultPrevented && toggleOnClick) { - context.toggle() - } - }, - [onClick, toggleOnClick, context], - ) - - // Data attributes const dataAttributes = { [VideoDataAttributes.playing]: context.playing || undefined, [VideoDataAttributes.paused]: context.paused || undefined, @@ -101,27 +92,32 @@ export const Video = React.forwardRef( [VideoDataAttributes.seeking]: context.seeking || undefined, } - return ( -