refactor(react): simplify skin render props with element form (#1068)

This commit is contained in:
rahim
2026-03-23 08:16:24 +11:00
committed by GitHub
parent e2d55de26b
commit 4b17ac9ce1
11 changed files with 357 additions and 496 deletions
@@ -65,6 +65,42 @@ import { Button } from '@lib/button';
---
## Unnecessary Render Function Wrappers
```tsx
// BAD: Function wrapper just passes props through
<PlayButton
render={(props) => (
<Button {...props} className="play-btn">
<PlayIcon />
</Button>
)}
/>
// BAD: Trivial passthrough
<TimeSlider.Root render={(props) => <SliderRoot {...props} />} />
// BAD: Passing component reference directly — breaks hooks reconciliation
<PlayButton render={Button} />
// GOOD: Element form — className and children on the headless component
<PlayButton className="play-btn" render={<Button />}>
<PlayIcon />
</PlayButton>
// GOOD: Element form for passthrough
<TimeSlider.Root render={<SliderRoot />} />
// GOOD: Element form with extra props on render target
<PlayButton className="play-btn" render={<Button variant="icon" />}>
<PlayIcon />
</PlayButton>
```
**Why it fails:** Function wrappers add indirection, noise, and implicit `any` on `props` parameter. Component references (`render={Component}`) bypass React's reconciliation — components are called as plain functions, breaking hooks. Use element form (`render={<Component />}`) or function form only when rendering a different element type or accessing component state.
---
## The `as` Prop
```tsx
@@ -273,6 +309,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(({ children, ...props
- [ ] State styled via data attributes, not inline
- [ ] No CSS shipped in component package
- [ ] Polymorphism via `render` or `asChild`, not `as`
- [ ] No unnecessary render function wrappers (`render={<Component />}` over `render={(props) => <Component {...props} />}`)
- [ ] Both controlled and uncontrolled modes
- [ ] Nested instances don't interfere
- [ ] Exit animations possible
@@ -17,22 +17,53 @@ Polymorphism allows users to customize which element a component renders as. Two
## `render` Pattern (Preferred)
Two forms: element and function.
### Element Form — Simple Cases
```tsx
// Renders MyButton with Dialog.Trigger behavior
<Dialog.Trigger render={<MyButton size="md" />}>Open dialog</Dialog.Trigger>
// Renders Button with PlayButton behavior — clones element, merges props
<PlayButton className="play-btn" render={<Button />}>
<PlayIcon />
</PlayButton>
// Pass props to the render target directly
<PlayButton className="play-btn" render={<Button variant="icon" />}>
<PlayIcon />
</PlayButton>
```
### Function Form — State Access
The headless component clones the element and merges its own props onto it.
### Function Form — State Access or Different Element
```tsx
// Access internal state for conditional rendering
<Switch.Thumb
render={(props, state) => <span {...props}>{state.checked ? <CheckedIcon /> : <UncheckedIcon />}</span>}
/>
// Render a fundamentally different element type
<BufferingIndicator
render={(props) => (
<div {...props} className="buffering">
<Spinner />
</div>
)}
/>
```
### When to Use Which
| Scenario | Form |
| --- | --- |
| Simple element swap | `render={<Component />}` |
| Render target needs its own props | `render={<Component prop="..." />}` |
| Need internal state access | `render={(props, state) => ...}` |
| Rendering a different element type | `render={(props) => <div {...props}>...}` |
**Do not** pass component references directly (`render={Component}`). React calls render functions as plain functions, which breaks hooks reconciliation. Always use element form (`render={<Component />}`) or function form.
---
## `asChild` Pattern
@@ -51,7 +82,7 @@ Polymorphism allows users to customize which element a component renders as. Two
| Concern | `render` | `asChild` |
| ------------------ | ------------------------------------- | ----------------------------------------- |
| **Prop flow** | Explicit — you spread props visibly | Hidden — `cloneElement` merges implicitly |
| **Prop flow** | Explicit — element or function forms | Hidden — `cloneElement` merges implicitly |
| **State access** | Function form exposes component state | No state access |
| **TypeScript** | Predictable inference | Can slow IDE autocomplete |
| **Debugging** | Traceable prop flow | Magic makes tracing difficult |
@@ -157,6 +157,7 @@ useImperativeHandle(actionsRef, () => ({ open, close }));
- Accept `ReactElement` or `(props, state) => ReactElement`
- Use `cloneElement` for element form
- Use `mergeProps` to combine internal + external props
- **Do not** accept component references (`render={Component}`) — calling components as plain functions breaks hooks reconciliation
**`mergeProps` behavior:**
@@ -78,6 +78,10 @@ const SliderFill = forwardRef<HTMLDivElement, ComponentProps<'div'> & { type?: '
);
});
const SliderBuffer = forwardRef<HTMLDivElement, ComponentProps<'div'>>(function SliderBuffer(props, ref) {
return <SliderFill type="buffer" ref={ref} {...props} />;
});
const SliderThumb = forwardRef<HTMLDivElement, ComponentProps<'div'> & { persistent?: boolean }>(function SliderThumb(
{ persistent, className, ...props },
ref
@@ -102,15 +106,11 @@ function VolumePopover(): ReactNode {
const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported');
const muteButton = (
<MuteButton
render={(props) => (
<Button variant="icon" {...props} className={iconState.mute.button}>
<VolumeOffIcon className={cn(icon, iconState.mute.volumeOff)} />
<VolumeLowIcon className={cn(icon, iconState.mute.volumeLow)} />
<VolumeHighIcon className={cn(icon, iconState.mute.volumeHigh)} />
</Button>
)}
/>
<MuteButton className={iconState.mute.button} render={<Button variant="icon" />}>
<VolumeOffIcon className={cn(icon, iconState.mute.volumeOff)} />
<VolumeLowIcon className={cn(icon, iconState.mute.volumeLow)} />
<VolumeHighIcon className={cn(icon, iconState.mute.volumeHigh)} />
</MuteButton>
);
if (volumeUnsupported) return muteButton;
@@ -119,9 +119,9 @@ function VolumePopover(): ReactNode {
<Popover.Root openOnHover delay={200} closeDelay={100} side="left">
<Popover.Trigger render={muteButton} />
<Popover.Popup className={cn(popup.volume)}>
<VolumeSlider.Root orientation="horizontal" thumbAlignment="edge" render={(props) => <SliderRoot {...props} />}>
<VolumeSlider.Track render={(props) => <SliderTrack {...props} />}>
<VolumeSlider.Fill render={(props) => <SliderFill {...props} />} />
<VolumeSlider.Root orientation="horizontal" thumbAlignment="edge" render={<SliderRoot />}>
<VolumeSlider.Track render={<SliderTrack />}>
<VolumeSlider.Fill render={<SliderFill />} />
</VolumeSlider.Track>
<VolumeSlider.Thumb render={(props) => <SliderThumb persistent {...props} />} />
</VolumeSlider.Root>
@@ -145,15 +145,11 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlayButton
render={(props) => (
<Button variant="icon" {...props} className={iconState.play.button}>
<RestartIcon className={cn(icon, iconState.play.restart)} />
<PlayIcon className={cn(icon, iconState.play.play)} />
<PauseIcon className={cn(icon, iconState.play.pause)} />
</Button>
)}
/>
<PlayButton className={iconState.play.button} render={<Button variant="icon" />}>
<RestartIcon className={cn(icon, iconState.play.restart)} />
<PlayIcon className={cn(icon, iconState.play.play)} />
<PauseIcon className={cn(icon, iconState.play.pause)} />
</PlayButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>
@@ -164,17 +160,12 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={-SEEK_TIME}
render={(props) => (
<Button variant="icon" {...props} className={seek.button}>
<span className={iconContainer}>
<SeekIcon className={cn(icon, iconFlipped)} />
<span className={cn(seek.label, seek.labelBackward)}>{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={-SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<span className={iconContainer}>
<SeekIcon className={cn(icon, iconFlipped)} />
<span className={cn(seek.label, seek.labelBackward)}>{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -183,17 +174,12 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={SEEK_TIME}
render={(props) => (
<Button variant="icon" {...props} className={seek.button}>
<span className={iconContainer}>
<SeekIcon className={icon} />
<span className={cn(seek.label, seek.labelForward)}>{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<span className={iconContainer}>
<SeekIcon className={icon} />
<span className={cn(seek.label, seek.labelForward)}>{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -207,23 +193,19 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
<Time.Value type="duration" className={time.duration} />
</Time.Group>
<TimeSlider.Root render={(props) => <SliderRoot {...props} />}>
<TimeSlider.Track render={(props) => <SliderTrack {...props} />}>
<TimeSlider.Fill render={(props) => <SliderFill {...props} />} />
<TimeSlider.Buffer render={(props) => <SliderFill type="buffer" {...props} />} />
<TimeSlider.Root render={<SliderRoot />}>
<TimeSlider.Track render={<SliderTrack />}>
<TimeSlider.Fill render={<SliderFill />} />
<TimeSlider.Buffer render={<SliderBuffer />} />
</TimeSlider.Track>
<TimeSlider.Thumb render={(props) => <SliderThumb {...props} />} />
<TimeSlider.Thumb render={<SliderThumb />} />
</TimeSlider.Root>
</div>
<div className={buttonGroup}>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlaybackRateButton
render={(props) => <Button variant="icon" {...props} className={playbackRate.button} />}
/>
}
render={<PlaybackRateButton className={playbackRate.button} render={<Button variant="icon" />} />}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
@@ -26,7 +26,7 @@ const SEEK_TIME = 10;
export type MinimalAudioSkinProps = BaseSkinProps;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return <button ref={ref} type="button" className={cn('media-button', className)} {...props} />;
return <button ref={ref} type="button" className={cn('media-button media-button--icon', className)} {...props} />;
});
function PlayLabel(): ReactNode {
@@ -40,15 +40,11 @@ function VolumePopover(): ReactNode {
const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported');
const muteButton = (
<MuteButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--mute">
<VolumeOffIcon className="media-icon media-icon--volume-off" />
<VolumeLowIcon className="media-icon media-icon--volume-low" />
<VolumeHighIcon className="media-icon media-icon--volume-high" />
</Button>
)}
/>
<MuteButton className="media-button--mute" render={<Button />}>
<VolumeOffIcon className="media-icon media-icon--volume-off" />
<VolumeLowIcon className="media-icon media-icon--volume-low" />
<VolumeHighIcon className="media-icon media-icon--volume-high" />
</MuteButton>
);
if (volumeUnsupported) return muteButton;
@@ -81,15 +77,11 @@ export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlayButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--play">
<RestartIcon className="media-icon media-icon--restart" />
<PlayIcon className="media-icon media-icon--play" />
<PauseIcon className="media-icon media-icon--pause" />
</Button>
)}
/>
<PlayButton className="media-button--play" render={<Button />}>
<RestartIcon className="media-icon media-icon--restart" />
<PlayIcon className="media-icon media-icon--play" />
<PauseIcon className="media-icon media-icon--pause" />
</PlayButton>
}
/>
<Tooltip.Popup className="media-tooltip">
@@ -100,17 +92,12 @@ export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={-SEEK_TIME}
render={(props) => (
<Button {...props} className="media-button--icon media-button--seek">
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek media-icon--flipped" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={-SEEK_TIME} className="media-button--seek" render={<Button />}>
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek media-icon--flipped" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className="media-tooltip">Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -119,17 +106,12 @@ export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={SEEK_TIME}
render={(props) => (
<Button {...props} className="media-button--icon media-button--seek">
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={SEEK_TIME} className="media-button--seek" render={<Button />}>
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className="media-tooltip">Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -155,11 +137,7 @@ export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode {
<div className="media-button-group">
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlaybackRateButton
render={(props) => <Button {...props} className="media-button--icon media-button--playback-rate" />}
/>
}
render={<PlaybackRateButton className="media-button--playback-rate" render={<Button />} />}
/>
<Tooltip.Popup className="media-tooltip">Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
@@ -77,6 +77,10 @@ const SliderFill = forwardRef<HTMLDivElement, ComponentProps<'div'> & { type?: '
);
});
const SliderBuffer = forwardRef<HTMLDivElement, ComponentProps<'div'>>(function SliderBuffer(props, ref) {
return <SliderFill type="buffer" ref={ref} {...props} />;
});
const SliderThumb = forwardRef<HTMLDivElement, ComponentProps<'div'> & { persistent?: boolean }>(function SliderThumb(
{ persistent, className, ...props },
ref
@@ -101,15 +105,11 @@ function VolumePopover(): ReactNode {
const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported');
const muteButton = (
<MuteButton
render={(props) => (
<Button variant="icon" {...props} className={iconState.mute.button}>
<VolumeOffIcon className={cn(icon, iconState.mute.volumeOff)} />
<VolumeLowIcon className={cn(icon, iconState.mute.volumeLow)} />
<VolumeHighIcon className={cn(icon, iconState.mute.volumeHigh)} />
</Button>
)}
/>
<MuteButton className={iconState.mute.button} render={<Button variant="icon" />}>
<VolumeOffIcon className={cn(icon, iconState.mute.volumeOff)} />
<VolumeLowIcon className={cn(icon, iconState.mute.volumeLow)} />
<VolumeHighIcon className={cn(icon, iconState.mute.volumeHigh)} />
</MuteButton>
);
if (volumeUnsupported) return muteButton;
@@ -118,9 +118,9 @@ function VolumePopover(): ReactNode {
<Popover.Root openOnHover delay={200} closeDelay={100} side="top">
<Popover.Trigger render={muteButton} />
<Popover.Popup className={cn(popup.popover, popup.volume)}>
<VolumeSlider.Root orientation="vertical" thumbAlignment="edge" render={(props) => <SliderRoot {...props} />}>
<VolumeSlider.Track render={(props) => <SliderTrack {...props} />}>
<VolumeSlider.Fill render={(props) => <SliderFill {...props} />} />
<VolumeSlider.Root orientation="vertical" thumbAlignment="edge" render={<SliderRoot />}>
<VolumeSlider.Track render={<SliderTrack />}>
<VolumeSlider.Fill render={<SliderFill />} />
</VolumeSlider.Track>
<VolumeSlider.Thumb render={(props) => <SliderThumb persistent {...props} />} />
</VolumeSlider.Root>
@@ -143,15 +143,11 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlayButton
render={(props) => (
<Button variant="icon" {...props} className={iconState.play.button}>
<RestartIcon className={cn(icon, iconState.play.restart)} />
<PlayIcon className={cn(icon, iconState.play.play)} />
<PauseIcon className={cn(icon, iconState.play.pause)} />
</Button>
)}
/>
<PlayButton className={iconState.play.button} render={<Button variant="icon" />}>
<RestartIcon className={cn(icon, iconState.play.restart)} />
<PlayIcon className={cn(icon, iconState.play.play)} />
<PauseIcon className={cn(icon, iconState.play.pause)} />
</PlayButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>
@@ -162,17 +158,12 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={-SEEK_TIME}
render={(props) => (
<Button variant="icon" {...props} className={seek.button}>
<span className={iconContainer}>
<SeekIcon className={cn(icon, iconFlipped)} />
<span className={cn(seek.label, seek.labelBackward)}>{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={-SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<span className={iconContainer}>
<SeekIcon className={cn(icon, iconFlipped)} />
<span className={cn(seek.label, seek.labelBackward)}>{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -181,17 +172,12 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={SEEK_TIME}
render={(props) => (
<Button variant="icon" {...props} className={seek.button}>
<span className={iconContainer}>
<SeekIcon className={icon} />
<span className={cn(seek.label, seek.labelForward)}>{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<span className={iconContainer}>
<SeekIcon className={icon} />
<span className={cn(seek.label, seek.labelForward)}>{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -199,23 +185,19 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode {
<Time.Group className={time.group}>
<Time.Value type="current" className={time.current} />
<TimeSlider.Root render={(props) => <SliderRoot {...props} />}>
<TimeSlider.Track render={(props) => <SliderTrack {...props} />}>
<TimeSlider.Fill render={(props) => <SliderFill {...props} />} />
<TimeSlider.Buffer render={(props) => <SliderFill type="buffer" {...props} />} />
<TimeSlider.Root render={<SliderRoot />}>
<TimeSlider.Track render={<SliderTrack />}>
<TimeSlider.Fill render={<SliderFill />} />
<TimeSlider.Buffer render={<SliderBuffer />} />
</TimeSlider.Track>
<TimeSlider.Thumb render={(props) => <SliderThumb {...props} />} />
<TimeSlider.Thumb render={<SliderThumb />} />
</TimeSlider.Root>
<Time.Value type="duration" className={time.duration} />
</Time.Group>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlaybackRateButton
render={(props) => <Button variant="icon" {...props} className={playbackRate.button} />}
/>
}
render={<PlaybackRateButton className={playbackRate.button} render={<Button variant="icon" />} />}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
+24 -46
View File
@@ -26,7 +26,7 @@ const SEEK_TIME = 10;
export type AudioSkinProps = BaseSkinProps;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return <button ref={ref} type="button" className={cn('media-button', className)} {...props} />;
return <button ref={ref} type="button" className={cn('media-button media-button--icon', className)} {...props} />;
});
function PlayLabel(): ReactNode {
@@ -40,15 +40,11 @@ function VolumePopover(): ReactNode {
const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported');
const muteButton = (
<MuteButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--mute">
<VolumeOffIcon className="media-icon media-icon--volume-off" />
<VolumeLowIcon className="media-icon media-icon--volume-low" />
<VolumeHighIcon className="media-icon media-icon--volume-high" />
</Button>
)}
/>
<MuteButton className="media-button--mute" render={<Button />}>
<VolumeOffIcon className="media-icon media-icon--volume-off" />
<VolumeLowIcon className="media-icon media-icon--volume-low" />
<VolumeHighIcon className="media-icon media-icon--volume-high" />
</MuteButton>
);
if (volumeUnsupported) return muteButton;
@@ -80,15 +76,11 @@ export function AudioSkin(props: AudioSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlayButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--play">
<RestartIcon className="media-icon media-icon--restart" />
<PlayIcon className="media-icon media-icon--play" />
<PauseIcon className="media-icon media-icon--pause" />
</Button>
)}
/>
<PlayButton className="media-button--play" render={<Button />}>
<RestartIcon className="media-icon media-icon--restart" />
<PlayIcon className="media-icon media-icon--play" />
<PauseIcon className="media-icon media-icon--pause" />
</PlayButton>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">
@@ -99,17 +91,12 @@ export function AudioSkin(props: AudioSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={-SEEK_TIME}
render={(props) => (
<Button {...props} className="media-button--icon media-button--seek">
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek media-icon--flipped" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={-SEEK_TIME} className="media-button--seek" render={<Button />}>
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek media-icon--flipped" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -118,17 +105,12 @@ export function AudioSkin(props: AudioSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={SEEK_TIME}
render={(props) => (
<Button {...props} className="media-button--icon media-button--seek">
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={SEEK_TIME} className="media-button--seek" render={<Button />}>
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -148,11 +130,7 @@ export function AudioSkin(props: AudioSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlaybackRateButton
render={(props) => <Button {...props} className="media-button--icon media-button--playback-rate" />}
/>
}
render={<PlaybackRateButton className="media-button--playback-rate" render={<Button />} />}
/>
<Tooltip.Popup className="media-surface media-tooltip">Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
@@ -100,6 +100,10 @@ const SliderFill = forwardRef<HTMLDivElement, ComponentProps<'div'> & { type?: '
);
});
const SliderBuffer = forwardRef<HTMLDivElement, ComponentProps<'div'>>(function SliderBuffer(props, ref) {
return <SliderFill type="buffer" ref={ref} {...props} />;
});
const SliderThumb = forwardRef<HTMLDivElement, ComponentProps<'div'> & { persistent?: boolean }>(function SliderThumb(
{ persistent, className, ...props },
ref
@@ -148,15 +152,11 @@ function VolumePopover(): ReactNode {
const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported');
const muteButton = (
<MuteButton
render={(props) => (
<Button variant="icon" {...props} className={iconState.mute.button}>
<VolumeOffIcon className={cn(icon, iconState.mute.volumeOff)} />
<VolumeLowIcon className={cn(icon, iconState.mute.volumeLow)} />
<VolumeHighIcon className={cn(icon, iconState.mute.volumeHigh)} />
</Button>
)}
/>
<MuteButton className={iconState.mute.button} render={<Button variant="icon" />}>
<VolumeOffIcon className={cn(icon, iconState.mute.volumeOff)} />
<VolumeLowIcon className={cn(icon, iconState.mute.volumeLow)} />
<VolumeHighIcon className={cn(icon, iconState.mute.volumeHigh)} />
</MuteButton>
);
if (volumeUnsupported) return muteButton;
@@ -165,9 +165,9 @@ function VolumePopover(): ReactNode {
<Popover.Root openOnHover delay={200} closeDelay={100} side="top">
<Popover.Trigger render={muteButton} />
<Popover.Popup className={cn(popup.volume)}>
<VolumeSlider.Root orientation="vertical" thumbAlignment="edge" render={(props) => <SliderRoot {...props} />}>
<VolumeSlider.Track render={(props) => <SliderTrack {...props} />}>
<VolumeSlider.Fill render={(props) => <SliderFill {...props} />} />
<VolumeSlider.Root orientation="vertical" thumbAlignment="edge" render={<SliderRoot />}>
<VolumeSlider.Track render={<SliderTrack />}>
<VolumeSlider.Fill render={<SliderFill />} />
</VolumeSlider.Track>
<VolumeSlider.Thumb render={(props) => <SliderThumb persistent {...props} />} />
</VolumeSlider.Root>
@@ -212,15 +212,11 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlayButton
render={(props) => (
<Button variant="icon" {...props} className={iconState.play.button}>
<RestartIcon className={cn(icon, iconState.play.restart)} />
<PlayIcon className={cn(icon, iconState.play.play)} />
<PauseIcon className={cn(icon, iconState.play.pause)} />
</Button>
)}
/>
<PlayButton className={iconState.play.button} render={<Button variant="icon" />}>
<RestartIcon className={cn(icon, iconState.play.restart)} />
<PlayIcon className={cn(icon, iconState.play.play)} />
<PauseIcon className={cn(icon, iconState.play.pause)} />
</PlayButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>
@@ -231,17 +227,12 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={-SEEK_TIME}
render={(props) => (
<Button variant="icon" {...props} className={seek.button}>
<span className={iconContainer}>
<SeekIcon className={cn(icon, iconFlipped)} />
<span className={cn(seek.label, seek.labelBackward)}>{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={-SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<span className={iconContainer}>
<SeekIcon className={cn(icon, iconFlipped)} />
<span className={cn(seek.label, seek.labelBackward)}>{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -250,17 +241,12 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={SEEK_TIME}
render={(props) => (
<Button variant="icon" {...props} className={seek.button}>
<span className={iconContainer}>
<SeekIcon className={icon} />
<span className={cn(seek.label, seek.labelForward)}>{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<span className={iconContainer}>
<SeekIcon className={icon} />
<span className={cn(seek.label, seek.labelForward)}>{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -274,12 +260,12 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<Time.Value type="duration" className={time.duration} />
</Time.Group>
<TimeSlider.Root render={(props) => <SliderRoot {...props} />}>
<TimeSlider.Track render={(props) => <SliderTrack {...props} />}>
<TimeSlider.Fill render={(props) => <SliderFill {...props} />} />
<TimeSlider.Buffer render={(props) => <SliderFill type="buffer" {...props} />} />
<TimeSlider.Root render={<SliderRoot />}>
<TimeSlider.Track render={<SliderTrack />}>
<TimeSlider.Fill render={<SliderFill />} />
<TimeSlider.Buffer render={<SliderBuffer />} />
</TimeSlider.Track>
<TimeSlider.Thumb render={(props) => <SliderThumb {...props} />} />
<TimeSlider.Thumb render={<SliderThumb />} />
<div className={preview.root}>
<div className={preview.thumbnailWrapper}>
<Slider.Thumbnail className={preview.thumbnail} />
@@ -293,11 +279,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<div className={buttonGroup}>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlaybackRateButton
render={(props) => <Button variant="icon" {...props} className={playbackRate.button} />}
/>
}
render={<PlaybackRateButton className={playbackRate.button} render={<Button variant="icon" />} />}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
@@ -307,14 +289,10 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<CaptionsButton
render={(props) => (
<Button variant="icon" {...props} className={iconState.captions.button}>
<CaptionsOffIcon className={cn(icon, iconState.captions.off)} />
<CaptionsOnIcon className={cn(icon, iconState.captions.on)} />
</Button>
)}
/>
<CaptionsButton className={iconState.captions.button} render={<Button variant="icon" />}>
<CaptionsOffIcon className={cn(icon, iconState.captions.off)} />
<CaptionsOnIcon className={cn(icon, iconState.captions.on)} />
</CaptionsButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>
@@ -325,14 +303,10 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PiPButton
render={(props) => (
<Button variant="icon" {...props} className={iconState.pip.button}>
<PipEnterIcon className={cn(icon, iconState.pip.off)} />
<PipExitIcon className={cn(icon, iconState.pip.on)} />
</Button>
)}
/>
<PiPButton className={iconState.pip.button} render={<Button variant="icon" />}>
<PipEnterIcon className={cn(icon, iconState.pip.off)} />
<PipExitIcon className={cn(icon, iconState.pip.on)} />
</PiPButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>
@@ -343,14 +317,10 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<FullscreenButton
render={(props) => (
<Button variant="icon" {...props} className={iconState.fullscreen.button}>
<FullscreenEnterIcon className={cn(icon, iconState.fullscreen.enter)} />
<FullscreenExitIcon className={cn(icon, iconState.fullscreen.exit)} />
</Button>
)}
/>
<FullscreenButton className={iconState.fullscreen.button} render={<Button variant="icon" />}>
<FullscreenEnterIcon className={cn(icon, iconState.fullscreen.enter)} />
<FullscreenExitIcon className={cn(icon, iconState.fullscreen.exit)} />
</FullscreenButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>
@@ -43,7 +43,7 @@ const SEEK_TIME = 10;
export type MinimalVideoSkinProps = BaseVideoSkinProps;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return <button ref={ref} type="button" className={cn('media-button', className)} {...props} />;
return <button ref={ref} type="button" className={cn('media-button media-button--icon', className)} {...props} />;
});
const errorClasses = {
@@ -82,15 +82,11 @@ function VolumePopover(): ReactNode {
const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported');
const muteButton = (
<MuteButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--mute">
<VolumeOffIcon className="media-icon media-icon--volume-off" />
<VolumeLowIcon className="media-icon media-icon--volume-low" />
<VolumeHighIcon className="media-icon media-icon--volume-high" />
</Button>
)}
/>
<MuteButton className="media-button--mute" render={<Button />}>
<VolumeOffIcon className="media-icon media-icon--volume-off" />
<VolumeLowIcon className="media-icon media-icon--volume-low" />
<VolumeHighIcon className="media-icon media-icon--volume-high" />
</MuteButton>
);
if (volumeUnsupported) return muteButton;
@@ -137,15 +133,11 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlayButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--play">
<RestartIcon className="media-icon media-icon--restart" />
<PlayIcon className="media-icon media-icon--play" />
<PauseIcon className="media-icon media-icon--pause" />
</Button>
)}
/>
<PlayButton className="media-button--play" render={<Button />}>
<RestartIcon className="media-icon media-icon--restart" />
<PlayIcon className="media-icon media-icon--play" />
<PauseIcon className="media-icon media-icon--pause" />
</PlayButton>
}
/>
<Tooltip.Popup className="media-tooltip">
@@ -156,17 +148,12 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={-SEEK_TIME}
render={(props) => (
<Button {...props} className="media-button--icon media-button--seek">
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek media-icon--flipped" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={-SEEK_TIME} className="media-button--seek" render={<Button />}>
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek media-icon--flipped" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className="media-tooltip">Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -175,17 +162,12 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={SEEK_TIME}
render={(props) => (
<Button {...props} className="media-button--icon media-button--seek">
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={SEEK_TIME} className="media-button--seek" render={<Button />}>
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className="media-tooltip">Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -219,11 +201,7 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
<div className="media-button-group">
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlaybackRateButton
render={(props) => <Button {...props} className="media-button--icon media-button--playback-rate" />}
/>
}
render={<PlaybackRateButton className="media-button--playback-rate" render={<Button />} />}
/>
<Tooltip.Popup className="media-tooltip">Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
@@ -233,14 +211,10 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<CaptionsButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--captions">
<CaptionsOffIcon className="media-icon media-icon--captions-off" />
<CaptionsOnIcon className="media-icon media-icon--captions-on" />
</Button>
)}
/>
<CaptionsButton className="media-button--captions" render={<Button />}>
<CaptionsOffIcon className="media-icon media-icon--captions-off" />
<CaptionsOnIcon className="media-icon media-icon--captions-on" />
</CaptionsButton>
}
/>
<Tooltip.Popup className="media-tooltip">
@@ -251,14 +225,10 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PiPButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--pip">
<PipEnterIcon className="media-icon media-icon--pip-enter" />
<PipExitIcon className="media-icon media-icon--pip-exit" />
</Button>
)}
/>
<PiPButton className="media-button--pip" render={<Button />}>
<PipEnterIcon className="media-icon media-icon--pip-enter" />
<PipExitIcon className="media-icon media-icon--pip-exit" />
</PiPButton>
}
/>
<Tooltip.Popup className="media-tooltip">
@@ -269,14 +239,10 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<FullscreenButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--fullscreen">
<FullscreenEnterIcon className="media-icon media-icon--fullscreen-enter" />
<FullscreenExitIcon className="media-icon media-icon--fullscreen-exit" />
</Button>
)}
/>
<FullscreenButton className="media-button--fullscreen" render={<Button />}>
<FullscreenEnterIcon className="media-icon media-icon--fullscreen-enter" />
<FullscreenExitIcon className="media-icon media-icon--fullscreen-exit" />
</FullscreenButton>
}
/>
<Tooltip.Popup className="media-tooltip">
@@ -99,6 +99,10 @@ const SliderFill = forwardRef<HTMLDivElement, ComponentProps<'div'> & { type?: '
);
});
const SliderBuffer = forwardRef<HTMLDivElement, ComponentProps<'div'>>(function SliderBuffer(props, ref) {
return <SliderFill type="buffer" ref={ref} {...props} />;
});
const SliderThumb = forwardRef<HTMLDivElement, ComponentProps<'div'> & { persistent?: boolean }>(function SliderThumb(
{ persistent, className, ...props },
ref
@@ -148,15 +152,11 @@ function VolumePopover(): ReactNode {
const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported');
const muteButton = (
<MuteButton
render={(props) => (
<Button variant="icon" {...props} className={iconState.mute.button}>
<VolumeOffIcon className={cn(icon, iconState.mute.volumeOff)} />
<VolumeLowIcon className={cn(icon, iconState.mute.volumeLow)} />
<VolumeHighIcon className={cn(icon, iconState.mute.volumeHigh)} />
</Button>
)}
/>
<MuteButton className={iconState.mute.button} render={<Button variant="icon" />}>
<VolumeOffIcon className={cn(icon, iconState.mute.volumeOff)} />
<VolumeLowIcon className={cn(icon, iconState.mute.volumeLow)} />
<VolumeHighIcon className={cn(icon, iconState.mute.volumeHigh)} />
</MuteButton>
);
if (volumeUnsupported) return muteButton;
@@ -165,9 +165,9 @@ function VolumePopover(): ReactNode {
<Popover.Root openOnHover delay={200} closeDelay={100} side="top">
<Popover.Trigger render={muteButton} />
<Popover.Popup className={cn(popup.popover, popup.volume)}>
<VolumeSlider.Root orientation="vertical" thumbAlignment="edge" render={(props) => <SliderRoot {...props} />}>
<VolumeSlider.Track render={(props) => <SliderTrack {...props} />}>
<VolumeSlider.Fill render={(props) => <SliderFill {...props} />} />
<VolumeSlider.Root orientation="vertical" thumbAlignment="edge" render={<SliderRoot />}>
<VolumeSlider.Track render={<SliderTrack />}>
<VolumeSlider.Fill render={<SliderFill />} />
</VolumeSlider.Track>
<VolumeSlider.Thumb render={(props) => <SliderThumb persistent {...props} />} />
</VolumeSlider.Root>
@@ -213,15 +213,11 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlayButton
render={(props) => (
<Button variant="icon" {...props} className={iconState.play.button}>
<RestartIcon className={cn(icon, iconState.play.restart)} />
<PlayIcon className={cn(icon, iconState.play.play)} />
<PauseIcon className={cn(icon, iconState.play.pause)} />
</Button>
)}
/>
<PlayButton className={iconState.play.button} render={<Button variant="icon" />}>
<RestartIcon className={cn(icon, iconState.play.restart)} />
<PlayIcon className={cn(icon, iconState.play.play)} />
<PauseIcon className={cn(icon, iconState.play.pause)} />
</PlayButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>
@@ -232,17 +228,12 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={-SEEK_TIME}
render={(props) => (
<Button variant="icon" {...props} className={seek.button}>
<span className={iconContainer}>
<SeekIcon className={cn(icon, iconFlipped)} />
<span className={cn(seek.label, seek.labelBackward)}>{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={-SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<span className={iconContainer}>
<SeekIcon className={cn(icon, iconFlipped)} />
<span className={cn(seek.label, seek.labelBackward)}>{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -251,17 +242,12 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={SEEK_TIME}
render={(props) => (
<Button variant="icon" {...props} className={seek.button}>
<span className={iconContainer}>
<SeekIcon className={icon} />
<span className={cn(seek.label, seek.labelForward)}>{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={SEEK_TIME} className={seek.button} render={<Button variant="icon" />}>
<span className={iconContainer}>
<SeekIcon className={icon} />
<span className={cn(seek.label, seek.labelForward)}>{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -269,12 +255,12 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Time.Group className={time.group}>
<Time.Value type="current" className={time.current} />
<TimeSlider.Root render={(props) => <SliderRoot {...props} />}>
<TimeSlider.Track render={(props) => <SliderTrack {...props} />}>
<TimeSlider.Fill render={(props) => <SliderFill {...props} />} />
<TimeSlider.Buffer render={(props) => <SliderFill type="buffer" {...props} />} />
<TimeSlider.Root render={<SliderRoot />}>
<TimeSlider.Track render={<SliderTrack />}>
<TimeSlider.Fill render={<SliderFill />} />
<TimeSlider.Buffer render={<SliderBuffer />} />
</TimeSlider.Track>
<TimeSlider.Thumb render={(props) => <SliderThumb {...props} />} />
<TimeSlider.Thumb render={<SliderThumb />} />
<div className={preview.root}>
<Slider.Thumbnail className={preview.thumbnail} />
<TimeSlider.Value type="pointer" className={preview.timestamp} />
@@ -286,11 +272,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlaybackRateButton
render={(props) => <Button variant="icon" {...props} className={playbackRate.button} />}
/>
}
render={<PlaybackRateButton className={playbackRate.button} render={<Button variant="icon" />} />}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
@@ -300,14 +282,10 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<CaptionsButton
render={(props) => (
<Button variant="icon" {...props} className={iconState.captions.button}>
<CaptionsOffIcon className={cn(icon, iconState.captions.off)} />
<CaptionsOnIcon className={cn(icon, iconState.captions.on)} />
</Button>
)}
/>
<CaptionsButton className={iconState.captions.button} render={<Button variant="icon" />}>
<CaptionsOffIcon className={cn(icon, iconState.captions.off)} />
<CaptionsOnIcon className={cn(icon, iconState.captions.on)} />
</CaptionsButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>
@@ -318,14 +296,10 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PiPButton
render={(props) => (
<Button variant="icon" {...props} className={iconState.pip.button}>
<PipEnterIcon className={cn(icon, iconState.pip.off)} />
<PipExitIcon className={cn(icon, iconState.pip.on)} />
</Button>
)}
/>
<PiPButton className={iconState.pip.button} render={<Button variant="icon" />}>
<PipEnterIcon className={cn(icon, iconState.pip.off)} />
<PipExitIcon className={cn(icon, iconState.pip.on)} />
</PiPButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>
@@ -336,14 +310,10 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<FullscreenButton
render={(props) => (
<Button variant="icon" {...props} className={iconState.fullscreen.button}>
<FullscreenEnterIcon className={cn(icon, iconState.fullscreen.enter)} />
<FullscreenExitIcon className={cn(icon, iconState.fullscreen.exit)} />
</Button>
)}
/>
<FullscreenButton className={iconState.fullscreen.button} render={<Button variant="icon" />}>
<FullscreenEnterIcon className={cn(icon, iconState.fullscreen.enter)} />
<FullscreenExitIcon className={cn(icon, iconState.fullscreen.exit)} />
</FullscreenButton>
}
/>
<Tooltip.Popup className={cn(popup.tooltip)}>
+36 -70
View File
@@ -43,7 +43,7 @@ const SEEK_TIME = 10;
export type VideoSkinProps = BaseVideoSkinProps;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return <button ref={ref} type="button" className={cn('media-button', className)} {...props} />;
return <button ref={ref} type="button" className={cn('media-button media-button--icon', className)} {...props} />;
});
const errorClasses = {
@@ -82,15 +82,11 @@ function VolumePopover(): ReactNode {
const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported');
const muteButton = (
<MuteButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--mute">
<VolumeOffIcon className="media-icon media-icon--volume-off" />
<VolumeLowIcon className="media-icon media-icon--volume-low" />
<VolumeHighIcon className="media-icon media-icon--volume-high" />
</Button>
)}
/>
<MuteButton className="media-button--mute" render={<Button />}>
<VolumeOffIcon className="media-icon media-icon--volume-off" />
<VolumeLowIcon className="media-icon media-icon--volume-low" />
<VolumeHighIcon className="media-icon media-icon--volume-high" />
</MuteButton>
);
if (volumeUnsupported) return muteButton;
@@ -138,15 +134,11 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlayButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--play">
<RestartIcon className="media-icon media-icon--restart" />
<PlayIcon className="media-icon media-icon--play" />
<PauseIcon className="media-icon media-icon--pause" />
</Button>
)}
/>
<PlayButton className="media-button--play" render={<Button />}>
<RestartIcon className="media-icon media-icon--restart" />
<PlayIcon className="media-icon media-icon--play" />
<PauseIcon className="media-icon media-icon--pause" />
</PlayButton>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">
@@ -157,17 +149,12 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={-SEEK_TIME}
render={(props) => (
<Button {...props} className="media-button--icon media-button--seek">
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek media-icon--flipped" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={-SEEK_TIME} className="media-button--seek" render={<Button />}>
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek media-icon--flipped" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -176,17 +163,12 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton
seconds={SEEK_TIME}
render={(props) => (
<Button {...props} className="media-button--icon media-button--seek">
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</Button>
)}
/>
<SeekButton seconds={SEEK_TIME} className="media-button--seek" render={<Button />}>
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek" />
<span className="media-icon__label">{SEEK_TIME}</span>
</span>
</SeekButton>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
@@ -212,11 +194,7 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlaybackRateButton
render={(props) => <Button {...props} className="media-button--icon media-button--playback-rate" />}
/>
}
render={<PlaybackRateButton className="media-button--playback-rate" render={<Button />} />}
/>
<Tooltip.Popup className="media-surface media-tooltip">Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
@@ -226,14 +204,10 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<CaptionsButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--captions">
<CaptionsOffIcon className="media-icon media-icon--captions-off" />
<CaptionsOnIcon className="media-icon media-icon--captions-on" />
</Button>
)}
/>
<CaptionsButton className="media-button--captions" render={<Button />}>
<CaptionsOffIcon className="media-icon media-icon--captions-off" />
<CaptionsOnIcon className="media-icon media-icon--captions-on" />
</CaptionsButton>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">
@@ -244,14 +218,10 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PiPButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--pip">
<PipEnterIcon className="media-icon media-icon--pip-enter" />
<PipExitIcon className="media-icon media-icon--pip-exit" />
</Button>
)}
/>
<PiPButton className="media-button--pip" render={<Button />}>
<PipEnterIcon className="media-icon media-icon--pip-enter" />
<PipExitIcon className="media-icon media-icon--pip-exit" />
</PiPButton>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">
@@ -262,14 +232,10 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<FullscreenButton
render={(props) => (
<Button {...props} className="media-button--icon media-button--fullscreen">
<FullscreenEnterIcon className="media-icon media-icon--fullscreen-enter" />
<FullscreenExitIcon className="media-icon media-icon--fullscreen-exit" />
</Button>
)}
/>
<FullscreenButton className="media-button--fullscreen" render={<Button />}>
<FullscreenEnterIcon className="media-icon media-icon--fullscreen-enter" />
<FullscreenExitIcon className="media-icon media-icon--fullscreen-exit" />
</FullscreenButton>
}
/>
<Tooltip.Popup className="media-surface media-tooltip">