fix(skin): hide volume popover when volume control is unsupported (#1025)

This commit is contained in:
rahim
2026-03-19 01:14:57 -07:00
committed by GitHub
parent c07da27ead
commit c09dbdd121
11 changed files with 277 additions and 200 deletions
+5
View File
@@ -6,3 +6,8 @@ media-tooltip-group {
:host {
display: grid;
}
/* Hide volume popover when volume control is unsupported (e.g., iOS Safari). */
.media-popover--volume:has(media-volume-slider[data-availability="unsupported"]) {
display: none;
}
@@ -98,6 +98,38 @@ function PlayLabel(): ReactNode {
return paused ? <>Play</> : <>Pause</>;
}
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>
)}
/>
);
if (volumeUnsupported) return muteButton;
return (
<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.Track>
<VolumeSlider.Thumb render={(props) => <SliderThumb persistent {...props} />} />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
);
}
/* ------------------------------------------ Skin ------------------------------------------- */
export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNode {
@@ -196,33 +228,7 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
<Tooltip.Popup className={cn(popup.tooltip)}>Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
<Popover.Root openOnHover delay={200} closeDelay={100} side="left">
<Popover.Trigger
render={
<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>
)}
/>
}
/>
<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.Track>
<VolumeSlider.Thumb render={(props) => <SliderThumb persistent {...props} />} />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
<VolumePopover />
</div>
</Tooltip.Provider>
</div>
@@ -36,6 +36,38 @@ function PlayLabel(): ReactNode {
return paused ? <>Play</> : <>Pause</>;
}
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>
)}
/>
);
if (volumeUnsupported) return muteButton;
return (
<Popover.Root openOnHover delay={200} closeDelay={100} side="left">
<Popover.Trigger render={muteButton} />
<Popover.Popup className="media-popover media-popover--volume">
<VolumeSlider.Root className="media-slider" orientation="horizontal" thumbAlignment="edge">
<VolumeSlider.Track className="media-slider__track">
<VolumeSlider.Fill className="media-slider__fill" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
);
}
export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode {
const { children, className, ...rest } = props;
@@ -132,29 +164,7 @@ export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode {
<Tooltip.Popup className="media-tooltip">Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
<Popover.Root openOnHover delay={200} closeDelay={100} side="left">
<Popover.Trigger
render={
<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>
)}
/>
}
/>
<Popover.Popup className="media-popover media-popover--volume">
<VolumeSlider.Root className="media-slider" orientation="horizontal" thumbAlignment="edge">
<VolumeSlider.Track className="media-slider__track">
<VolumeSlider.Fill className="media-slider__fill" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
<VolumePopover />
</div>
</Tooltip.Provider>
</div>
@@ -97,6 +97,38 @@ function PlayLabel(): ReactNode {
return paused ? <>Play</> : <>Pause</>;
}
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>
)}
/>
);
if (volumeUnsupported) return muteButton;
return (
<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.Track>
<VolumeSlider.Thumb render={(props) => <SliderThumb persistent {...props} />} />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
);
}
/* ------------------------------------------ Skin ------------------------------------------- */
export function AudioSkinTailwind(props: AudioSkinProps): ReactNode {
@@ -188,33 +220,7 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode {
<Tooltip.Popup className={cn(popup.tooltip)}>Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
<Popover.Root openOnHover delay={200} closeDelay={100} side="top">
<Popover.Trigger
render={
<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>
)}
/>
}
/>
<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.Track>
<VolumeSlider.Thumb render={(props) => <SliderThumb persistent {...props} />} />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
<VolumePopover />
</Tooltip.Provider>
</div>
</Container>
+33 -23
View File
@@ -36,6 +36,38 @@ function PlayLabel(): ReactNode {
return paused ? <>Play</> : <>Pause</>;
}
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>
)}
/>
);
if (volumeUnsupported) return muteButton;
return (
<Popover.Root openOnHover delay={200} closeDelay={100} side="top">
<Popover.Trigger render={muteButton} />
<Popover.Popup className="media-surface media-popover media-popover--volume">
<VolumeSlider.Root className="media-slider" orientation="vertical" thumbAlignment="edge">
<VolumeSlider.Track className="media-slider__track">
<VolumeSlider.Fill className="media-slider__fill" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
);
}
export function AudioSkin(props: AudioSkinProps): ReactNode {
const { children, className, ...rest } = props;
@@ -125,29 +157,7 @@ export function AudioSkin(props: AudioSkinProps): ReactNode {
<Tooltip.Popup className="media-surface media-tooltip">Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
<Popover.Root openOnHover delay={200} closeDelay={100} side="top">
<Popover.Trigger
render={
<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>
)}
/>
}
/>
<Popover.Popup className="media-surface media-popover media-popover--volume">
<VolumeSlider.Root className="media-slider" orientation="vertical" thumbAlignment="edge">
<VolumeSlider.Track className="media-slider__track">
<VolumeSlider.Fill className="media-slider__fill" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
<VolumePopover />
</Tooltip.Provider>
</div>
</Container>
@@ -144,6 +144,38 @@ function FullscreenLabel(): ReactNode {
return fullscreen ? <>Exit fullscreen</> : <>Enter fullscreen</>;
}
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>
)}
/>
);
if (volumeUnsupported) return muteButton;
return (
<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.Track>
<VolumeSlider.Thumb render={(props) => <SliderThumb persistent {...props} />} />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
);
}
/* ------------------------------------------ Skin ------------------------------------------- */
export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNode {
@@ -270,33 +302,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<Tooltip.Popup className={cn(popup.tooltip)}>Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
<Popover.Root openOnHover delay={200} closeDelay={100} side="top">
<Popover.Trigger
render={
<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>
)}
/>
}
/>
<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.Track>
<VolumeSlider.Thumb render={(props) => <SliderThumb persistent {...props} />} />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
<VolumePopover />
<Tooltip.Root side="top">
<Tooltip.Trigger
@@ -78,6 +78,38 @@ function FullscreenLabel(): ReactNode {
return fullscreen ? <>Exit fullscreen</> : <>Enter fullscreen</>;
}
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>
)}
/>
);
if (volumeUnsupported) return muteButton;
return (
<Popover.Root openOnHover delay={200} closeDelay={100} side="top">
<Popover.Trigger render={muteButton} />
<Popover.Popup className="media-popover media-popover--volume">
<VolumeSlider.Root className="media-slider" orientation="vertical" thumbAlignment="edge">
<VolumeSlider.Track className="media-slider__track">
<VolumeSlider.Fill className="media-slider__fill" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
);
}
export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
const { children, className, poster, ...rest } = props;
@@ -196,29 +228,7 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
<Tooltip.Popup className="media-tooltip">Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
<Popover.Root openOnHover delay={200} closeDelay={100} side="top">
<Popover.Trigger
render={
<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>
)}
/>
}
/>
<Popover.Popup className="media-popover media-popover--volume">
<VolumeSlider.Root className="media-slider" orientation="vertical" thumbAlignment="edge">
<VolumeSlider.Track className="media-slider__track">
<VolumeSlider.Fill className="media-slider__fill" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
<VolumePopover />
<Tooltip.Root side="top">
<Tooltip.Trigger
@@ -144,6 +144,38 @@ function FullscreenLabel(): ReactNode {
return fullscreen ? <>Exit fullscreen</> : <>Enter fullscreen</>;
}
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>
)}
/>
);
if (volumeUnsupported) return muteButton;
return (
<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.Track>
<VolumeSlider.Thumb render={(props) => <SliderThumb persistent {...props} />} />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
);
}
/* ------------------------------------------ Skin ------------------------------------------- */
export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
@@ -263,33 +295,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<Tooltip.Popup className={cn(popup.tooltip)}>Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
<Popover.Root openOnHover delay={200} closeDelay={100} side="top">
<Popover.Trigger
render={
<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>
)}
/>
}
/>
<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.Track>
<VolumeSlider.Thumb render={(props) => <SliderThumb persistent {...props} />} />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
<VolumePopover />
<Tooltip.Root side="top">
<Tooltip.Trigger
+33 -23
View File
@@ -78,6 +78,38 @@ function FullscreenLabel(): ReactNode {
return fullscreen ? <>Exit fullscreen</> : <>Enter fullscreen</>;
}
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>
)}
/>
);
if (volumeUnsupported) return muteButton;
return (
<Popover.Root openOnHover delay={200} closeDelay={100} side="top">
<Popover.Trigger render={muteButton} />
<Popover.Popup className="media-surface media-popover media-popover--volume">
<VolumeSlider.Root className="media-slider" orientation="vertical" thumbAlignment="edge">
<VolumeSlider.Track className="media-slider__track">
<VolumeSlider.Fill className="media-slider__fill" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
);
}
export function VideoSkin(props: VideoSkinProps): ReactNode {
const { children, className, poster, ...rest } = props;
@@ -189,29 +221,7 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
<Tooltip.Popup className="media-surface media-tooltip">Toggle playback rate</Tooltip.Popup>
</Tooltip.Root>
<Popover.Root openOnHover delay={200} closeDelay={100} side="top">
<Popover.Trigger
render={
<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>
)}
/>
}
/>
<Popover.Popup className="media-surface media-popover media-popover--volume">
<VolumeSlider.Root className="media-slider" orientation="vertical" thumbAlignment="edge">
<VolumeSlider.Track className="media-slider__track">
<VolumeSlider.Fill className="media-slider__fill" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
</VolumeSlider.Root>
</Popover.Popup>
</Popover.Root>
<VolumePopover />
<Tooltip.Root side="top">
<Tooltip.Trigger
@@ -82,6 +82,10 @@
.media-default-skin .media-popover--volume {
padding: 0.625rem 0.25rem;
border-radius: calc(infinity * 1px);
&:has(media-volume-slider[data-availability="unsupported"]) {
display: none;
}
}
.media-default-skin .media-tooltip {
@@ -107,3 +107,7 @@
background-color: oklch(0 0 0 / 0.9);
}
}
.media-minimal-skin .media-popover--volume:has(media-volume-slider[data-availability="unsupported"]) {
display: none;
}