docs(site): add menu reference (#1673)

This commit is contained in:
Sam Potts
2026-07-07 13:06:19 -05:00
committed by GitHub
parent e060cf0d54
commit cc020b4b89
14 changed files with 1082 additions and 2 deletions
@@ -0,0 +1,9 @@
---
import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
import html from './BasicUsage.html?raw';
---
<HtmlDemo html={html} />
<script>
import "./BasicUsage.ts";
</script>
@@ -0,0 +1,169 @@
.video-player,
.video-player media-container {
position: relative;
display: block;
}
.video-media {
width: 100%;
}
.menu-bar {
position: absolute;
right: 10px;
bottom: 10px;
}
.settings-trigger {
padding: 6px 16px;
color: black;
cursor: pointer;
background: rgba(255, 255, 255, 0.75);
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
.menu {
--media-menu-side-offset: 8px;
--menu-transition-duration: 220ms;
position: relative;
box-sizing: border-box;
width: var(--media-menu-width);
min-width: 180px;
max-width: var(--media-menu-available-width, var(--media-popover-available-width, none));
height: var(--media-menu-height);
max-height: var(--media-menu-available-height, var(--media-popover-available-height, none));
padding: 6px;
overflow: hidden;
overscroll-behavior: none;
font-size: 14px;
color: white;
background: rgba(0, 0, 0, 0.88);
border-radius: 8px;
backdrop-filter: blur(10px);
transition-timing-function: ease-in-out;
transition-duration: var(--menu-transition-duration);
transition-property: width, height, opacity, filter;
}
.menu-panel {
position: absolute;
inset: 0;
display: grid;
gap: 2px;
padding: 6px;
overflow: auto;
overscroll-behavior: none;
outline: none;
translate: 0 0;
transition-timing-function: ease-in-out;
transition-duration: var(--menu-transition-duration);
transition-property: translate, filter;
will-change: translate;
}
.menu-panel[data-starting-style],
.menu-panel[data-ending-style] {
overflow: hidden;
}
.menu-panel[data-menu-root-view][data-menu-view-state="inactive"] {
filter: blur(8px);
translate: -100% 0;
}
.menu-panel[data-submenu] {
z-index: 1;
}
.menu-panel[data-submenu]:not([data-open], [data-ending-style]) {
translate: -100% 0;
transition-property: none;
}
.menu-panel[data-submenu][data-starting-style],
.menu-panel[data-submenu][data-ending-style] {
pointer-events: none;
filter: blur(8px);
}
.menu-panel[data-submenu][data-starting-style][data-direction="forward"],
.menu-panel[data-submenu][data-ending-style][data-direction="back"] {
translate: 100% 0;
}
.menu-panel[data-submenu][data-ending-style][data-direction="forward"],
.menu-panel[data-submenu][data-starting-style][data-direction="back"] {
translate: -100% 0;
}
.menu-item,
.menu-back {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 32px;
padding: 0 10px;
font: inherit;
color: inherit;
cursor: pointer;
background: none;
border: 0;
border-radius: 6px;
}
.menu-item[data-highlighted],
.menu-back:hover {
background: rgba(255, 255, 255, 0.16);
}
.menu-back {
gap: 8px;
justify-content: flex-start;
}
.menu-item[data-availability="unavailable"] {
display: none;
}
.menu-value {
display: inline-flex;
gap: 8px;
align-items: center;
color: rgba(255, 255, 255, 0.72);
}
.menu-value [aria-hidden] {
font-size: 18px;
line-height: 1;
}
.menu-back [aria-hidden] {
font-size: 18px;
line-height: 1;
color: rgba(255, 255, 255, 0.72);
}
.menu-group {
display: grid;
gap: 2px;
}
.menu-tier {
margin-left: 2px;
font-size: 10px;
}
.menu-badge {
margin-left: auto;
color: rgba(255, 255, 255, 0.72);
}
.menu-indicator {
opacity: 0;
}
media-menu-radio-item[aria-checked="true"] .menu-indicator {
opacity: 1;
}
@@ -0,0 +1,116 @@
<video-player class="video-player">
<media-container>
<hlsjs-video
class="video-media"
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4.m3u8"
autoplay
crossorigin="anonymous"
muted
playsinline
loop
>
<track kind="captions" src="/docs/demos/captions-button/captions.vtt" srclang="en" label="English" />
<track kind="subtitles" src="/docs/demos/captions-button/captions.vtt" srclang="es" label="Spanish" />
</hlsjs-video>
<div class="menu-bar">
<button type="button" commandfor="settings-menu" class="settings-trigger">Settings</button>
<media-menu id="settings-menu" side="top" align="end" class="menu">
<media-menu-view class="menu-panel">
<media-menu-item commandfor="quality-menu" type="quality" class="menu-item">
<span>Quality</span>
<span class="menu-value">
<media-menu-item-value></media-menu-item-value>
<span aria-hidden="true"></span>
</span>
</media-menu-item>
<media-menu-item commandfor="audio-menu" type="audio-track" class="menu-item">
<span>Audio</span>
<span class="menu-value">
<media-menu-item-value></media-menu-item-value>
<span aria-hidden="true"></span>
</span>
</media-menu-item>
<media-menu-item commandfor="speed-menu" type="playback-rate" class="menu-item">
<span>Speed</span>
<span class="menu-value">
<media-menu-item-value></media-menu-item-value>
<span aria-hidden="true"></span>
</span>
</media-menu-item>
<media-menu-item commandfor="captions-menu" type="captions" class="menu-item">
<span>Captions</span>
<span class="menu-value">
<media-menu-item-value></media-menu-item-value>
<span aria-hidden="true"></span>
</span>
</media-menu-item>
<media-menu-item class="menu-item">Copy link</media-menu-item>
</media-menu-view>
<media-menu id="quality-menu" class="menu-panel">
<media-menu-back class="menu-back">
<span aria-hidden="true"></span>
Quality
</media-menu-back>
<media-quality-radio-group class="menu-group">
<template>
<media-menu-radio-item class="menu-item">
<span>
<span data-part="label"></span>
<sup data-part="tier" class="menu-tier"></sup>
</span>
<span data-part="badge" class="menu-badge"></span>
<media-menu-item-indicator force-mount class="menu-indicator"></media-menu-item-indicator>
</media-menu-radio-item>
</template>
</media-quality-radio-group>
</media-menu>
<media-menu id="audio-menu" class="menu-panel">
<media-menu-back class="menu-back">
<span aria-hidden="true"></span>
Audio
</media-menu-back>
<media-audio-track-radio-group class="menu-group">
<template>
<media-menu-radio-item class="menu-item">
<span data-part="label"></span>
<media-menu-item-indicator force-mount class="menu-indicator"></media-menu-item-indicator>
</media-menu-radio-item>
</template>
</media-audio-track-radio-group>
</media-menu>
<media-menu id="speed-menu" class="menu-panel">
<media-menu-back class="menu-back">
<span aria-hidden="true"></span>
Speed
</media-menu-back>
<media-playback-rate-radio-group class="menu-group">
<template>
<media-menu-radio-item class="menu-item">
<span data-part="label"></span>
<media-menu-item-indicator force-mount class="menu-indicator"></media-menu-item-indicator>
</media-menu-radio-item>
</template>
</media-playback-rate-radio-group>
</media-menu>
<media-menu id="captions-menu" class="menu-panel">
<media-menu-back class="menu-back">
<span aria-hidden="true"></span>
Captions
</media-menu-back>
<media-captions-radio-group class="menu-group">
<template>
<media-menu-radio-item class="menu-item">
<span data-part="label"></span>
<media-menu-item-indicator force-mount class="menu-indicator"></media-menu-item-indicator>
</media-menu-radio-item>
</template>
</media-captions-radio-group>
</media-menu>
</media-menu>
</div>
</media-container>
</video-player>
@@ -0,0 +1,7 @@
import '@videojs/html/video/player';
import '@videojs/html/media/hlsjs-video';
import '@videojs/html/ui/menu';
import '@videojs/html/ui/quality-radio-group';
import '@videojs/html/ui/audio-track-radio-group';
import '@videojs/html/ui/playback-rate-radio-group';
import '@videojs/html/ui/captions-radio-group';
@@ -0,0 +1,162 @@
.media-container {
position: relative;
}
.media-container video {
width: 100%;
}
.menu-bar {
position: absolute;
right: 10px;
bottom: 10px;
}
.settings-trigger {
padding: 6px 16px;
color: black;
cursor: pointer;
background: rgba(255, 255, 255, 0.75);
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
.menu {
--media-menu-side-offset: 8px;
--menu-transition-duration: 220ms;
position: relative;
box-sizing: border-box;
width: var(--media-menu-width);
min-width: 180px;
max-width: var(--media-menu-available-width, var(--media-popover-available-width, none));
height: var(--media-menu-height);
max-height: var(--media-menu-available-height, var(--media-popover-available-height, none));
padding: 6px;
margin: 0;
overflow: hidden;
overscroll-behavior: none;
font-size: 14px;
color: white;
background: rgba(0, 0, 0, 0.88);
border: 0;
border-radius: 8px;
backdrop-filter: blur(10px);
transition-timing-function: ease-in-out;
transition-duration: var(--menu-transition-duration);
transition-property: width, height, opacity, filter;
}
.menu-panel {
position: absolute;
inset: 0;
display: grid;
gap: 2px;
padding: 6px;
overflow: auto;
overscroll-behavior: none;
outline: none;
translate: 0 0;
transition-timing-function: ease-in-out;
transition-duration: var(--menu-transition-duration);
transition-property: translate, filter;
will-change: translate;
}
.menu-panel[data-starting-style],
.menu-panel[data-ending-style] {
overflow: hidden;
}
.menu-panel[data-menu-root-view][data-menu-view-state="inactive"] {
filter: blur(8px);
translate: -100% 0;
}
.menu-panel[data-submenu] {
z-index: 1;
}
.menu-panel[data-submenu]:not([data-open], [data-ending-style]) {
translate: -100% 0;
transition-property: none;
}
.menu-panel[data-submenu][data-starting-style],
.menu-panel[data-submenu][data-ending-style] {
pointer-events: none;
filter: blur(8px);
}
.menu-panel[data-submenu][data-starting-style][data-direction="forward"],
.menu-panel[data-submenu][data-ending-style][data-direction="back"] {
translate: 100% 0;
}
.menu-panel[data-submenu][data-ending-style][data-direction="forward"],
.menu-panel[data-submenu][data-starting-style][data-direction="back"] {
translate: -100% 0;
}
.menu-item,
.menu-back {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 32px;
padding: 0 10px;
font: inherit;
color: inherit;
cursor: pointer;
background: none;
border: 0;
border-radius: 6px;
}
.menu-item[data-highlighted],
.menu-back:hover {
background: rgba(255, 255, 255, 0.16);
}
.menu-back {
gap: 8px;
justify-content: flex-start;
}
.menu-value {
display: inline-flex;
gap: 8px;
align-items: center;
color: rgba(255, 255, 255, 0.72);
}
.menu-group {
display: grid;
gap: 2px;
}
.menu-tier {
margin-left: 2px;
font-size: 10px;
}
.menu-badge {
margin-left: auto;
color: rgba(255, 255, 255, 0.72);
}
.menu-back [aria-hidden],
[role="menuitemradio"] [aria-hidden],
.menu-item [aria-hidden] {
font-size: 18px;
line-height: 1;
color: rgba(255, 255, 255, 0.72);
}
.menu-indicator {
opacity: 0;
}
[role="menuitemradio"][aria-checked="true"] .menu-indicator {
opacity: 1;
}
@@ -0,0 +1,254 @@
import {
createPlayer,
Menu,
useAudioTrackOptions,
useCaptionsOptions,
usePlaybackRateOptions,
useQualityOptions,
} from '@videojs/react';
import { HlsJsVideo } from '@videojs/react/media/hlsjs-video';
import { videoFeatures } from '@videojs/react/video';
import type { ReactNode } from 'react';
const Player = createPlayer({ features: videoFeatures });
const src = 'https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4.m3u8';
function SettingsMenu(): ReactNode {
const playbackRate = usePlaybackRateOptions();
const quality = useQualityOptions();
const audioTrack = useAudioTrackOptions();
const captions = useCaptionsOptions();
const hasPlaybackRate = playbackRate?.state.availability === 'available';
const hasQuality = quality?.state.availability === 'available';
const hasAudioTrack = audioTrack?.state.availability === 'available';
const hasCaptions = captions?.state.availability === 'available';
if (!hasPlaybackRate && !hasQuality && !hasAudioTrack && !hasCaptions) return null;
return (
<Menu.Root side="top" align="end">
<Menu.Trigger className="settings-trigger" aria-label="Settings" render={<button type="button" />}>
Settings
</Menu.Trigger>
<Menu.Content className="menu">
<Menu.View className="menu-panel">
{hasQuality ? (
<Menu.Root>
<Menu.Trigger
type="quality"
className="menu-item"
render={(props) => (
<div {...props}>
<span>Quality</span>
<span className="menu-value">
<Menu.ItemValue />
<span aria-hidden="true"></span>
</span>
</div>
)}
/>
<Menu.Content className="menu-panel">
<Menu.Back className="menu-back">
<span aria-hidden="true"></span>
Quality
</Menu.Back>
<Menu.RadioGroup
className="menu-group"
value={quality.value}
onValueChange={quality.setValue}
aria-label="Quality"
>
{quality.options.map((option) => (
<Menu.RadioItem
key={option.value}
value={option.value}
disabled={option.disabled}
className="menu-item"
>
<span>
{option.label}
{option.tier ? <sup className="menu-tier">{option.tier}</sup> : null}
</span>
{option.badge ? <span className="menu-badge">{option.badge}</span> : null}
<Menu.ItemIndicator
checked={option.value === quality.value}
forceMount
className="menu-indicator"
>
</Menu.ItemIndicator>
</Menu.RadioItem>
))}
</Menu.RadioGroup>
</Menu.Content>
</Menu.Root>
) : null}
{hasAudioTrack ? (
<Menu.Root>
<Menu.Trigger
type="audio-track"
className="menu-item"
render={(props) => (
<div {...props}>
<span>Audio</span>
<span className="menu-value">
<Menu.ItemValue />
<span aria-hidden="true"></span>
</span>
</div>
)}
/>
<Menu.Content className="menu-panel">
<Menu.Back className="menu-back">
<span aria-hidden="true"></span>
Audio
</Menu.Back>
<Menu.RadioGroup
className="menu-group"
value={audioTrack.value}
onValueChange={audioTrack.setValue}
aria-label="Audio tracks"
>
{audioTrack.options.map((option) => (
<Menu.RadioItem
key={option.value}
value={option.value}
disabled={option.disabled}
className="menu-item"
>
<span>{option.label}</span>
<Menu.ItemIndicator
checked={option.value === audioTrack.value}
forceMount
className="menu-indicator"
>
</Menu.ItemIndicator>
</Menu.RadioItem>
))}
</Menu.RadioGroup>
</Menu.Content>
</Menu.Root>
) : null}
{hasPlaybackRate ? (
<Menu.Root>
<Menu.Trigger
type="playback-rate"
className="menu-item"
render={(props) => (
<div {...props}>
<span>Speed</span>
<span className="menu-value">
<Menu.ItemValue />
<span aria-hidden="true"></span>
</span>
</div>
)}
/>
<Menu.Content className="menu-panel">
<Menu.Back className="menu-back">
<span aria-hidden="true"></span>
Speed
</Menu.Back>
<Menu.RadioGroup
className="menu-group"
value={playbackRate.value}
onValueChange={playbackRate.setValue}
aria-label="Playback rate"
>
{playbackRate.options.map((option) => (
<Menu.RadioItem
key={option.value}
value={option.value}
disabled={option.disabled}
className="menu-item"
>
<span>{option.label}</span>
<Menu.ItemIndicator
checked={option.value === playbackRate.value}
forceMount
className="menu-indicator"
>
</Menu.ItemIndicator>
</Menu.RadioItem>
))}
</Menu.RadioGroup>
</Menu.Content>
</Menu.Root>
) : null}
{hasCaptions ? (
<Menu.Root>
<Menu.Trigger
type="captions"
className="menu-item"
render={(props) => (
<div {...props}>
<span>Captions</span>
<span className="menu-value">
<Menu.ItemValue />
<span aria-hidden="true"></span>
</span>
</div>
)}
/>
<Menu.Content className="menu-panel">
<Menu.Back className="menu-back">
<span aria-hidden="true"></span>
Captions
</Menu.Back>
<Menu.RadioGroup
className="menu-group"
value={captions.value}
onValueChange={captions.setValue}
aria-label="Captions"
>
{captions.options.map((option) => (
<Menu.RadioItem
key={option.value}
value={option.value}
disabled={option.disabled}
className="menu-item"
>
<span>{option.label}</span>
<Menu.ItemIndicator
checked={option.value === captions.value}
forceMount
className="menu-indicator"
>
</Menu.ItemIndicator>
</Menu.RadioItem>
))}
</Menu.RadioGroup>
</Menu.Content>
</Menu.Root>
) : null}
<Menu.Item className="menu-item" onSelect={() => navigator.clipboard?.writeText(window.location.href)}>
Copy link
</Menu.Item>
</Menu.View>
</Menu.Content>
</Menu.Root>
);
}
export default function BasicUsage() {
return (
<Player.Provider>
<Player.Container className="media-container">
<HlsJsVideo src={src} autoPlay crossOrigin="anonymous" muted playsInline loop>
<track kind="captions" src="/docs/demos/captions-button/captions.vtt" srcLang="en" label="English" />
<track kind="subtitles" src="/docs/demos/captions-button/captions.vtt" srcLang="es" label="Spanish" />
</HlsJsVideo>
<div className="menu-bar">
<SettingsMenu />
</div>
</Player.Container>
</Player.Provider>
);
}