From cc020b4b89c7fd130efb99f3dc3cd73fd19c4e3c Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 8 Jul 2026 04:06:19 +1000 Subject: [PATCH] docs(site): add menu reference (#1673) --- packages/react/src/ui/menu/menu-item.tsx | 2 +- packages/react/src/ui/menu/menu-trigger.tsx | 2 +- .../docs/demos/menu/html/css/BasicUsage.astro | 9 + .../docs/demos/menu/html/css/BasicUsage.css | 169 ++++++++++++ .../docs/demos/menu/html/css/BasicUsage.html | 116 ++++++++ .../docs/demos/menu/html/css/BasicUsage.ts | 7 + .../docs/demos/menu/react/css/BasicUsage.css | 162 +++++++++++ .../docs/demos/menu/react/css/BasicUsage.tsx | 254 ++++++++++++++++++ .../reference/audio-track-radio-group.mdx | 79 ++++++ .../docs/reference/captions-button.mdx | 4 + site/src/content/docs/reference/menu.mdx | 192 +++++++++++++ .../docs/reference/playback-rate-button.mdx | 4 + .../docs/reference/quality-radio-group.mdx | 81 ++++++ site/src/docs.config.ts | 3 + 14 files changed, 1082 insertions(+), 2 deletions(-) create mode 100644 site/src/components/docs/demos/menu/html/css/BasicUsage.astro create mode 100644 site/src/components/docs/demos/menu/html/css/BasicUsage.css create mode 100644 site/src/components/docs/demos/menu/html/css/BasicUsage.html create mode 100644 site/src/components/docs/demos/menu/html/css/BasicUsage.ts create mode 100644 site/src/components/docs/demos/menu/react/css/BasicUsage.css create mode 100644 site/src/components/docs/demos/menu/react/css/BasicUsage.tsx create mode 100644 site/src/content/docs/reference/audio-track-radio-group.mdx create mode 100644 site/src/content/docs/reference/menu.mdx create mode 100644 site/src/content/docs/reference/quality-radio-group.mdx diff --git a/packages/react/src/ui/menu/menu-item.tsx b/packages/react/src/ui/menu/menu-item.tsx index 5e8a3a3d..79ebc1ee 100644 --- a/packages/react/src/ui/menu/menu-item.tsx +++ b/packages/react/src/ui/menu/menu-item.tsx @@ -15,7 +15,7 @@ export interface MenuItemProps extends UIComponentProps<'div', MenuState> { onSelect?: () => void; /** Whether the item is disabled. */ disabled?: boolean; - /** Setting kind for submenu triggers (`playback-rate` or `captions`). */ + /** Setting kind for submenu triggers (`playback-rate`, `quality`, `audio-track`, or `captions`). */ type?: MenuItemSettingType | undefined; } diff --git a/packages/react/src/ui/menu/menu-trigger.tsx b/packages/react/src/ui/menu/menu-trigger.tsx index e70287f4..03f95e08 100644 --- a/packages/react/src/ui/menu/menu-trigger.tsx +++ b/packages/react/src/ui/menu/menu-trigger.tsx @@ -20,7 +20,7 @@ import type { MenuItemSettingType } from './menu-item-type'; export interface MenuTriggerProps extends Omit, 'type'> { /** Disables the trigger. */ disabled?: boolean; - /** Setting kind for submenu triggers (`playback-rate` or `captions`). */ + /** Setting kind for submenu triggers (`playback-rate`, `quality`, `audio-track`, or `captions`). */ type?: MenuItemSettingType | undefined; } diff --git a/site/src/components/docs/demos/menu/html/css/BasicUsage.astro b/site/src/components/docs/demos/menu/html/css/BasicUsage.astro new file mode 100644 index 00000000..c515bacd --- /dev/null +++ b/site/src/components/docs/demos/menu/html/css/BasicUsage.astro @@ -0,0 +1,9 @@ +--- +import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro'; +import html from './BasicUsage.html?raw'; +--- + + + diff --git a/site/src/components/docs/demos/menu/html/css/BasicUsage.css b/site/src/components/docs/demos/menu/html/css/BasicUsage.css new file mode 100644 index 00000000..adc962e3 --- /dev/null +++ b/site/src/components/docs/demos/menu/html/css/BasicUsage.css @@ -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; +} diff --git a/site/src/components/docs/demos/menu/html/css/BasicUsage.html b/site/src/components/docs/demos/menu/html/css/BasicUsage.html new file mode 100644 index 00000000..5cf0955d --- /dev/null +++ b/site/src/components/docs/demos/menu/html/css/BasicUsage.html @@ -0,0 +1,116 @@ + + + + + + + + + diff --git a/site/src/components/docs/demos/menu/html/css/BasicUsage.ts b/site/src/components/docs/demos/menu/html/css/BasicUsage.ts new file mode 100644 index 00000000..089682f1 --- /dev/null +++ b/site/src/components/docs/demos/menu/html/css/BasicUsage.ts @@ -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'; diff --git a/site/src/components/docs/demos/menu/react/css/BasicUsage.css b/site/src/components/docs/demos/menu/react/css/BasicUsage.css new file mode 100644 index 00000000..2c940c5a --- /dev/null +++ b/site/src/components/docs/demos/menu/react/css/BasicUsage.css @@ -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; +} diff --git a/site/src/components/docs/demos/menu/react/css/BasicUsage.tsx b/site/src/components/docs/demos/menu/react/css/BasicUsage.tsx new file mode 100644 index 00000000..a56165e3 --- /dev/null +++ b/site/src/components/docs/demos/menu/react/css/BasicUsage.tsx @@ -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 ( + + }> + Settings + + + + {hasQuality ? ( + + ( +
+ Quality + + + + +
+ )} + /> + + + + Quality + + + {quality.options.map((option) => ( + + + {option.label} + {option.tier ? {option.tier} : null} + + {option.badge ? {option.badge} : null} + + ✓ + + + ))} + + +
+ ) : null} + + {hasAudioTrack ? ( + + ( +
+ Audio + + + + +
+ )} + /> + + + + Audio + + + {audioTrack.options.map((option) => ( + + {option.label} + + ✓ + + + ))} + + +
+ ) : null} + + {hasPlaybackRate ? ( + + ( +
+ Speed + + + + +
+ )} + /> + + + + Speed + + + {playbackRate.options.map((option) => ( + + {option.label} + + ✓ + + + ))} + + +
+ ) : null} + + {hasCaptions ? ( + + ( +
+ Captions + + + + +
+ )} + /> + + + + Captions + + + {captions.options.map((option) => ( + + {option.label} + + ✓ + + + ))} + + +
+ ) : null} + + navigator.clipboard?.writeText(window.location.href)}> + Copy link + +
+
+
+ ); +} + +export default function BasicUsage() { + return ( + + + + + + +
+ +
+
+
+ ); +} diff --git a/site/src/content/docs/reference/audio-track-radio-group.mdx b/site/src/content/docs/reference/audio-track-radio-group.mdx new file mode 100644 index 00000000..749ce4a3 --- /dev/null +++ b/site/src/content/docs/reference/audio-track-radio-group.mdx @@ -0,0 +1,79 @@ +--- +title: AudioTrackRadioGroup +frameworkTitle: + html: media-audio-track-radio-group +description: A menu radio group for selecting an audio track +--- + +import ComponentReference from "@/components/docs/api-reference/ComponentReference.astro"; +import DocsLink from "@/components/docs/DocsLink.astro"; +import FrameworkCase from "@/components/docs/FrameworkCase.astro"; + +Creates radio items from the player audio track state and selects the enabled track. + +## Anatomy + + + ```tsx + const audioTrack = useAudioTrackOptions(); + + + Audio + {audioTrack.options.map((option) => ( + + {option.label} + + + ))} + + ``` + + + + ```html + + + + ``` + + +## Behavior + +The group is available when the configured media exposes more than one audio track. Labels use the track label, then language, then kind. Pass `formatTrack` to customize the visible labels. + + +`useAudioTrackOptions` returns `null` when the audio track feature is not configured. Use the returned options with `Menu.RadioGroup`. + + + +`` generates `` children. Add an optional `