diff --git a/packages/core/src/core/ui/captions-button/captions-button-core.ts b/packages/core/src/core/ui/captions-button/captions-button-core.ts index 707b9396..e09fae75 100644 --- a/packages/core/src/core/ui/captions-button/captions-button-core.ts +++ b/packages/core/src/core/ui/captions-button/captions-button-core.ts @@ -1,8 +1,10 @@ +import { createState } from '@videojs/store'; import { defaults } from '@videojs/utils/object'; import { isFunction } from '@videojs/utils/predicate'; import type { NonNullableObject } from '@videojs/utils/types'; import type { MediaTextTrackState } from '../../media/state'; +import type { ButtonState } from '../types'; export interface CaptionsButtonProps { /** Custom label for the button. */ @@ -11,7 +13,7 @@ export interface CaptionsButtonProps { disabled?: boolean | undefined; } -export interface CaptionsButtonState extends Pick { +export interface CaptionsButtonState extends Pick, ButtonState { availability: 'available' | 'unavailable'; } @@ -21,6 +23,12 @@ export class CaptionsButtonCore { disabled: false, }; + readonly state = createState({ + subtitlesShowing: false, + availability: 'unavailable', + label: '', + }); + #props = { ...CaptionsButtonCore.defaultProps }; #media: MediaTextTrackState | null = null; @@ -58,12 +66,16 @@ export class CaptionsButtonCore { getState(): CaptionsButtonState { const media = this.#media!; - return { - subtitlesShowing: media.subtitlesShowing, - availability: media.textTrackList.some((t) => t.kind === 'captions' || t.kind === 'subtitles') - ? 'available' - : 'unavailable', - }; + const availability: CaptionsButtonState['availability'] = media.textTrackList.some( + (t) => t.kind === 'captions' || t.kind === 'subtitles' + ) + ? 'available' + : 'unavailable'; + + this.state.patch({ subtitlesShowing: media.subtitlesShowing, availability }); + this.state.patch({ label: this.getLabel(this.state.current) }); + + return this.state.current; } toggle(media: MediaTextTrackState): void { diff --git a/packages/core/src/core/ui/captions-button/tests/captions-button-core.test.ts b/packages/core/src/core/ui/captions-button/tests/captions-button-core.test.ts index 9fc72f82..146b1bcc 100644 --- a/packages/core/src/core/ui/captions-button/tests/captions-button-core.test.ts +++ b/packages/core/src/core/ui/captions-button/tests/captions-button-core.test.ts @@ -20,6 +20,7 @@ function createState(overrides: Partial = {}): CaptionsButt return { subtitlesShowing: false, availability: 'available', + label: '', ...overrides, }; } diff --git a/packages/core/src/core/ui/fullscreen-button/fullscreen-button-core.ts b/packages/core/src/core/ui/fullscreen-button/fullscreen-button-core.ts index 9a2161ef..216e9ec2 100644 --- a/packages/core/src/core/ui/fullscreen-button/fullscreen-button-core.ts +++ b/packages/core/src/core/ui/fullscreen-button/fullscreen-button-core.ts @@ -1,8 +1,10 @@ +import { createState } from '@videojs/store'; import { defaults } from '@videojs/utils/object'; import { isFunction } from '@videojs/utils/predicate'; import type { NonNullableObject } from '@videojs/utils/types'; import type { MediaFullscreenState } from '../../media/state'; +import type { ButtonState } from '../types'; export interface FullscreenButtonProps { /** Custom label for the button. */ @@ -11,7 +13,7 @@ export interface FullscreenButtonProps { disabled?: boolean | undefined; } -export interface FullscreenButtonState extends Pick { +export interface FullscreenButtonState extends Pick, ButtonState { /** Whether fullscreen can be requested on this platform. */ availability: MediaFullscreenState['fullscreenAvailability']; } @@ -22,6 +24,12 @@ export class FullscreenButtonCore { disabled: false, }; + readonly state = createState({ + fullscreen: false, + availability: 'available', + label: '', + }); + #props = { ...FullscreenButtonCore.defaultProps }; #media: MediaFullscreenState | null = null; @@ -59,10 +67,10 @@ export class FullscreenButtonCore { getState(): FullscreenButtonState { const media = this.#media!; - return { - fullscreen: media.fullscreen, - availability: media.fullscreenAvailability, - }; + this.state.patch({ fullscreen: media.fullscreen, availability: media.fullscreenAvailability }); + this.state.patch({ label: this.getLabel(this.state.current) }); + + return this.state.current; } async toggle(media: MediaFullscreenState): Promise { diff --git a/packages/core/src/core/ui/fullscreen-button/tests/fullscreen-button-core.test.ts b/packages/core/src/core/ui/fullscreen-button/tests/fullscreen-button-core.test.ts index b9446ac1..a3fcdf1c 100644 --- a/packages/core/src/core/ui/fullscreen-button/tests/fullscreen-button-core.test.ts +++ b/packages/core/src/core/ui/fullscreen-button/tests/fullscreen-button-core.test.ts @@ -18,6 +18,7 @@ function createState(overrides: Partial = {}): Fullscreen return { fullscreen: false, availability: 'available', + label: '', ...overrides, }; } diff --git a/packages/core/src/core/ui/mute-button/mute-button-core.ts b/packages/core/src/core/ui/mute-button/mute-button-core.ts index 92d39e33..769314a6 100644 --- a/packages/core/src/core/ui/mute-button/mute-button-core.ts +++ b/packages/core/src/core/ui/mute-button/mute-button-core.ts @@ -1,8 +1,10 @@ +import { createState } from '@videojs/store'; import { defaults } from '@videojs/utils/object'; import { isFunction } from '@videojs/utils/predicate'; import type { NonNullableObject } from '@videojs/utils/types'; import type { MediaVolumeState } from '../../media/state'; +import type { ButtonState } from '../types'; export type VolumeLevel = 'off' | 'low' | 'medium' | 'high'; @@ -13,7 +15,7 @@ export interface MuteButtonProps { disabled?: boolean | undefined; } -export interface MuteButtonState extends Pick { +export interface MuteButtonState extends Pick, ButtonState { /** * Derived volume level: * - `off`: muted or volume is 0 @@ -30,6 +32,12 @@ export class MuteButtonCore { disabled: false, }; + readonly state = createState({ + muted: false, + volumeLevel: 'off', + label: '', + }); + #props = { ...MuteButtonCore.defaultProps }; #media: MediaVolumeState | null = null; @@ -67,10 +75,10 @@ export class MuteButtonCore { getState(): MuteButtonState { const media = this.#media!; - return { - muted: media.muted || media.volume === 0, - volumeLevel: getVolumeLevel(media), - }; + this.state.patch({ muted: media.muted || media.volume === 0, volumeLevel: getVolumeLevel(media) }); + this.state.patch({ label: this.getLabel(this.state.current) }); + + return this.state.current; } toggle(media: MediaVolumeState): void { diff --git a/packages/core/src/core/ui/mute-button/tests/mute-button-core.test.ts b/packages/core/src/core/ui/mute-button/tests/mute-button-core.test.ts index 630155c4..d1005565 100644 --- a/packages/core/src/core/ui/mute-button/tests/mute-button-core.test.ts +++ b/packages/core/src/core/ui/mute-button/tests/mute-button-core.test.ts @@ -19,6 +19,7 @@ function createState(overrides: Partial = {}): MuteButtonState return { muted: false, volumeLevel: 'high', + label: '', ...overrides, }; } diff --git a/packages/core/src/core/ui/pip-button/pip-button-core.ts b/packages/core/src/core/ui/pip-button/pip-button-core.ts index a3c9381e..837bd396 100644 --- a/packages/core/src/core/ui/pip-button/pip-button-core.ts +++ b/packages/core/src/core/ui/pip-button/pip-button-core.ts @@ -1,8 +1,10 @@ +import { createState } from '@videojs/store'; import { defaults } from '@videojs/utils/object'; import { isFunction } from '@videojs/utils/predicate'; import type { NonNullableObject } from '@videojs/utils/types'; import type { MediaPictureInPictureState } from '../../media/state'; +import type { ButtonState } from '../types'; export interface PiPButtonProps { /** Custom label for the button. */ @@ -11,7 +13,7 @@ export interface PiPButtonProps { disabled?: boolean | undefined; } -export interface PiPButtonState extends Pick { +export interface PiPButtonState extends Pick, ButtonState { /** Whether picture-in-picture can be requested on this platform. */ availability: MediaPictureInPictureState['pipAvailability']; } @@ -22,6 +24,12 @@ export class PiPButtonCore { disabled: false, }; + readonly state = createState({ + pip: false, + availability: 'available', + label: '', + }); + #props = { ...PiPButtonCore.defaultProps }; #media: MediaPictureInPictureState | null = null; @@ -59,10 +67,10 @@ export class PiPButtonCore { getState(): PiPButtonState { const media = this.#media!; - return { - pip: media.pip, - availability: media.pipAvailability, - }; + this.state.patch({ pip: media.pip, availability: media.pipAvailability }); + this.state.patch({ label: this.getLabel(this.state.current) }); + + return this.state.current; } async toggle(media: MediaPictureInPictureState): Promise { diff --git a/packages/core/src/core/ui/pip-button/tests/pip-button-core.test.ts b/packages/core/src/core/ui/pip-button/tests/pip-button-core.test.ts index 070e317d..082aac7f 100644 --- a/packages/core/src/core/ui/pip-button/tests/pip-button-core.test.ts +++ b/packages/core/src/core/ui/pip-button/tests/pip-button-core.test.ts @@ -18,6 +18,7 @@ function createState(overrides: Partial = {}): PiPButtonState { return { pip: false, availability: 'available', + label: '', ...overrides, }; } diff --git a/packages/core/src/core/ui/play-button/play-button-core.ts b/packages/core/src/core/ui/play-button/play-button-core.ts index 5dae0279..c091644a 100644 --- a/packages/core/src/core/ui/play-button/play-button-core.ts +++ b/packages/core/src/core/ui/play-button/play-button-core.ts @@ -1,8 +1,10 @@ +import { createState } from '@videojs/store'; import { defaults } from '@videojs/utils/object'; import { isFunction } from '@videojs/utils/predicate'; import type { NonNullableObject } from '@videojs/utils/types'; import type { MediaPlaybackState } from '../../media/state'; +import type { ButtonState } from '../types'; export interface PlayButtonProps { /** Custom label for the button. */ @@ -11,7 +13,7 @@ export interface PlayButtonProps { disabled?: boolean | undefined; } -export interface PlayButtonState extends Pick {} +export interface PlayButtonState extends Pick, ButtonState {} export class PlayButtonCore { static readonly defaultProps: NonNullableObject = { @@ -19,6 +21,13 @@ export class PlayButtonCore { disabled: false, }; + readonly state = createState({ + paused: true, + ended: false, + started: false, + label: '', + }); + #props = { ...PlayButtonCore.defaultProps }; #media: MediaPlaybackState | null = null; @@ -57,11 +66,11 @@ export class PlayButtonCore { getState(): PlayButtonState { const media = this.#media!; - return { - paused: media.paused, - ended: media.ended, - started: media.started, - }; + + this.state.patch({ paused: media.paused, ended: media.ended, started: media.started }); + this.state.patch({ label: this.getLabel(this.state.current) }); + + return this.state.current; } async toggle(media: MediaPlaybackState): Promise { diff --git a/packages/core/src/core/ui/play-button/tests/play-button-core.test.ts b/packages/core/src/core/ui/play-button/tests/play-button-core.test.ts index 2efb0779..b998fe1c 100644 --- a/packages/core/src/core/ui/play-button/tests/play-button-core.test.ts +++ b/packages/core/src/core/ui/play-button/tests/play-button-core.test.ts @@ -21,6 +21,7 @@ function createState(overrides: Partial = {}): PlayButtonState paused: true, ended: false, started: false, + label: '', ...overrides, }; } diff --git a/packages/core/src/core/ui/playback-rate-button/playback-rate-button-core.ts b/packages/core/src/core/ui/playback-rate-button/playback-rate-button-core.ts index 4f166ff0..15d32226 100644 --- a/packages/core/src/core/ui/playback-rate-button/playback-rate-button-core.ts +++ b/packages/core/src/core/ui/playback-rate-button/playback-rate-button-core.ts @@ -1,8 +1,10 @@ +import { createState } from '@videojs/store'; import { defaults } from '@videojs/utils/object'; import { isFunction } from '@videojs/utils/predicate'; import type { NonNullableObject } from '@videojs/utils/types'; import type { MediaPlaybackRateState } from '../../media/state'; +import type { ButtonState } from '../types'; export interface PlaybackRateButtonProps { /** Custom label for the button. */ @@ -11,7 +13,7 @@ export interface PlaybackRateButtonProps { disabled?: boolean | undefined; } -export interface PlaybackRateButtonState { +export interface PlaybackRateButtonState extends ButtonState { rate: number; } @@ -21,6 +23,11 @@ export class PlaybackRateButtonCore { disabled: false, }; + readonly state = createState({ + rate: 1, + label: '', + }); + #props = { ...PlaybackRateButtonCore.defaultProps }; #media: MediaPlaybackRateState | null = null; @@ -58,9 +65,10 @@ export class PlaybackRateButtonCore { getState(): PlaybackRateButtonState { const media = this.#media!; - return { - rate: media.playbackRate, - }; + this.state.patch({ rate: media.playbackRate }); + this.state.patch({ label: this.getLabel(this.state.current) }); + + return this.state.current; } cycle(media: MediaPlaybackRateState): void { diff --git a/packages/core/src/core/ui/playback-rate-button/tests/playback-rate-button-core.test.ts b/packages/core/src/core/ui/playback-rate-button/tests/playback-rate-button-core.test.ts index 0c9d6a82..9532a409 100644 --- a/packages/core/src/core/ui/playback-rate-button/tests/playback-rate-button-core.test.ts +++ b/packages/core/src/core/ui/playback-rate-button/tests/playback-rate-button-core.test.ts @@ -16,6 +16,7 @@ function createMediaState(overrides: Partial = {}): Medi function createState(overrides: Partial = {}): PlaybackRateButtonState { return { rate: 1, + label: '', ...overrides, }; } diff --git a/packages/core/src/core/ui/seek-button/seek-button-core.ts b/packages/core/src/core/ui/seek-button/seek-button-core.ts index 7e8ec9d6..c4062016 100644 --- a/packages/core/src/core/ui/seek-button/seek-button-core.ts +++ b/packages/core/src/core/ui/seek-button/seek-button-core.ts @@ -1,8 +1,10 @@ +import { createState } from '@videojs/store'; import { defaults } from '@videojs/utils/object'; import { isFunction } from '@videojs/utils/predicate'; import type { NonNullableObject } from '@videojs/utils/types'; import type { MediaTimeState } from '../../media/state'; +import type { ButtonState } from '../types'; export interface SeekButtonProps { /** Seconds to seek. Positive = forward, negative = backward. Default `30`. */ @@ -15,7 +17,7 @@ export interface SeekButtonProps { export type SeekButtonDirection = 'forward' | 'backward'; -export interface SeekButtonState { +export interface SeekButtonState extends ButtonState { /** Whether a seek is in progress. */ seeking: boolean; /** Whether the button seeks forward or backward. */ @@ -29,6 +31,12 @@ export class SeekButtonCore { disabled: false, }; + readonly state = createState({ + seeking: false, + direction: 'forward', + label: '', + }); + #props = { ...SeekButtonCore.defaultProps }; #media: MediaTimeState | null = null; @@ -67,10 +75,12 @@ export class SeekButtonCore { getState(): SeekButtonState { const media = this.#media!; - return { - seeking: media.seeking, - direction: this.#props.seconds < 0 ? 'backward' : 'forward', - }; + const direction: SeekButtonDirection = this.#props.seconds < 0 ? 'backward' : 'forward'; + + this.state.patch({ seeking: media.seeking, direction }); + this.state.patch({ label: this.getLabel(this.state.current) }); + + return this.state.current; } async seek(media: MediaTimeState): Promise { diff --git a/packages/core/src/core/ui/seek-button/tests/seek-button-core.test.ts b/packages/core/src/core/ui/seek-button/tests/seek-button-core.test.ts index 1829fd1a..334a0f6d 100644 --- a/packages/core/src/core/ui/seek-button/tests/seek-button-core.test.ts +++ b/packages/core/src/core/ui/seek-button/tests/seek-button-core.test.ts @@ -18,6 +18,7 @@ function createState(overrides: Partial = {}): SeekButtonState return { seeking: false, direction: 'forward', + label: '', ...overrides, }; } diff --git a/packages/core/src/core/ui/tooltip/tests/tooltip-core.test.ts b/packages/core/src/core/ui/tooltip/tests/tooltip-core.test.ts index a04ad362..af7f1001 100644 --- a/packages/core/src/core/ui/tooltip/tests/tooltip-core.test.ts +++ b/packages/core/src/core/ui/tooltip/tests/tooltip-core.test.ts @@ -44,39 +44,13 @@ describe('TooltipCore', () => { expect(state.align).toBe('center'); }); - describe('getTriggerAttrs', () => { - it('returns undefined aria-describedby when closed', () => { - const core = new TooltipCore(); - core.setInput(CLOSED); - const attrs = core.getTriggerAttrs(core.getState(), 'tooltip-1'); - - expect(attrs['aria-describedby']).toBeUndefined(); - }); - - it('returns aria-describedby with popupId when open', () => { - const core = new TooltipCore(); - core.setInput(OPEN); - const attrs = core.getTriggerAttrs(core.getState(), 'tooltip-1'); - - expect(attrs['aria-describedby']).toBe('tooltip-1'); - }); - - it('returns undefined aria-describedby when no popupId', () => { - const core = new TooltipCore(); - core.setInput(OPEN); - const attrs = core.getTriggerAttrs(core.getState()); - - expect(attrs['aria-describedby']).toBeUndefined(); - }); - }); - describe('getPopupAttrs', () => { - it('returns tooltip role', () => { + it('returns presentation role', () => { const core = new TooltipCore(); core.setInput(OPEN); const attrs = core.getPopupAttrs(core.getState()); - expect(attrs.role).toBe('tooltip'); + expect(attrs.role).toBe('presentation'); }); it('returns popover manual attribute', () => { diff --git a/packages/core/src/core/ui/tooltip/tooltip-core.ts b/packages/core/src/core/ui/tooltip/tooltip-core.ts index f564cabd..ad0d1eaa 100644 --- a/packages/core/src/core/ui/tooltip/tooltip-core.ts +++ b/packages/core/src/core/ui/tooltip/tooltip-core.ts @@ -76,16 +76,10 @@ export class TooltipCore { }; } - getTriggerAttrs(state: TooltipState, popupId?: string) { - return { - 'aria-describedby': state.open ? popupId : undefined, - }; - } - getPopupAttrs(_state: TooltipState) { return { popover: 'manual' as const, - role: 'tooltip', + role: 'presentation' as const, }; } } diff --git a/packages/core/src/core/ui/types.ts b/packages/core/src/core/ui/types.ts index 9d07a3d4..308bcefb 100644 --- a/packages/core/src/core/ui/types.ts +++ b/packages/core/src/core/ui/types.ts @@ -1,3 +1,5 @@ +import type { State } from '@videojs/store'; + export type StateAttrMap = { [Key in keyof State]?: string; }; @@ -14,6 +16,17 @@ export interface MediaUIComponent setMedia(media: object): void; } +export interface ButtonState { + label: string; +} + +/** Constraint for media button cores that provide a label derived from state. */ +export interface MediaButtonComponent + extends MediaUIComponent { + readonly state: State; + getLabel(state: ComponentState): string; +} + /** Extracts the media state parameter type from a core's `setMedia` method. */ export type InferMediaState = Parameters[0]; diff --git a/packages/html/src/define/audio/minimal-skin.tailwind.ts b/packages/html/src/define/audio/minimal-skin.tailwind.ts index 6fd15cc2..8e969a27 100644 --- a/packages/html/src/define/audio/minimal-skin.tailwind.ts +++ b/packages/html/src/define/audio/minimal-skin.tailwind.ts @@ -14,7 +14,6 @@ import { seek, slider, time, - tooltipState, } from '@videojs/skins/minimal/tailwind/audio.tailwind'; import { createTemplate } from '@videojs/utils/dom'; import { cn } from '@videojs/utils/style'; @@ -59,18 +58,12 @@ function getTemplateHTML() {
- ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} ${renderIcon('play', { class: cn(icon, iconState.play.play) })} ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} - - Replay - Play - Pause - - + diff --git a/packages/html/src/define/audio/minimal-skin.ts b/packages/html/src/define/audio/minimal-skin.ts index 24540a9c..7a8f97e3 100644 --- a/packages/html/src/define/audio/minimal-skin.ts +++ b/packages/html/src/define/audio/minimal-skin.ts @@ -47,11 +47,7 @@ function getTemplateHTML() { ${renderIcon('play', { class: 'media-icon media-icon--play' })} ${renderIcon('pause', { class: 'media-icon media-icon--pause' })} - - Replay - Play - Pause - + diff --git a/packages/html/src/define/audio/skin.tailwind.ts b/packages/html/src/define/audio/skin.tailwind.ts index 177c8fb3..0bc6a859 100644 --- a/packages/html/src/define/audio/skin.tailwind.ts +++ b/packages/html/src/define/audio/skin.tailwind.ts @@ -14,7 +14,6 @@ import { seek, slider, time, - tooltipState, } from '@videojs/skins/default/tailwind/audio.tailwind'; import { createTemplate } from '@videojs/utils/dom'; import { cn } from '@videojs/utils/style'; @@ -59,18 +58,12 @@ function getTemplateHTML() {
- ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} ${renderIcon('play', { class: cn(icon, iconState.play.play) })} ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} - - Replay - Play - Pause - - + diff --git a/packages/html/src/define/audio/skin.ts b/packages/html/src/define/audio/skin.ts index 0de80f85..b6bb18b7 100644 --- a/packages/html/src/define/audio/skin.ts +++ b/packages/html/src/define/audio/skin.ts @@ -47,11 +47,7 @@ function getTemplateHTML() { ${renderIcon('play', { class: 'media-icon media-icon--play' })} ${renderIcon('pause', { class: 'media-icon media-icon--pause' })} - - Replay - Play - Pause - + diff --git a/packages/html/src/define/video/minimal-skin.tailwind.ts b/packages/html/src/define/video/minimal-skin.tailwind.ts index 57bfa2f0..faff15ac 100644 --- a/packages/html/src/define/video/minimal-skin.tailwind.ts +++ b/packages/html/src/define/video/minimal-skin.tailwind.ts @@ -19,7 +19,6 @@ import { seek, slider, time, - tooltipState, } from '@videojs/skins/minimal/tailwind/video.tailwind'; import { createTemplate } from '@videojs/utils/dom'; import { cn } from '@videojs/utils/style'; @@ -78,18 +77,12 @@ function getTemplateHTML() {
- ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} ${renderIcon('play', { class: cn(icon, iconState.play.play) })} ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} - - Replay - Play - Pause - - + @@ -157,39 +150,21 @@ function getTemplateHTML() { - - ${renderIcon('captions-off', { class: cn(icon, iconState.captions.off) })} ${renderIcon('captions-on', { class: cn(icon, iconState.captions.on) })} - - Enable captions - Disable captions - - - - + ${renderIcon('pip-enter', { class: cn(icon, iconState.pip.off) })} ${renderIcon('pip-exit', { class: cn(icon, iconState.pip.on) })} - - Enter picture-in-picture - Exit picture-in-picture - - - - + ${renderIcon('fullscreen-enter', { class: cn(icon, iconState.fullscreen.enter) })} ${renderIcon('fullscreen-exit', { class: cn(icon, iconState.fullscreen.exit) })} - - Enter fullscreen - Exit fullscreen - - +
diff --git a/packages/html/src/define/video/minimal-skin.ts b/packages/html/src/define/video/minimal-skin.ts index a3a820ee..cb1d44d0 100644 --- a/packages/html/src/define/video/minimal-skin.ts +++ b/packages/html/src/define/video/minimal-skin.ts @@ -60,11 +60,7 @@ function getTemplateHTML() { ${renderIcon('play', { class: 'media-icon media-icon--play' })} ${renderIcon('pause', { class: 'media-icon media-icon--pause' })} - - Replay - Play - Pause - + @@ -144,19 +140,13 @@ function getTemplateHTML() { ${renderIcon('pip-enter', { class: 'media-icon media-icon--pip-enter' })} ${renderIcon('pip-exit', { class: 'media-icon media-icon--pip-exit' })} - - Enter picture-in-picture - Exit picture-in-picture - + ${renderIcon('fullscreen-enter', { class: 'media-icon media-icon--fullscreen-enter' })} ${renderIcon('fullscreen-exit', { class: 'media-icon media-icon--fullscreen-exit' })} - - Enter fullscreen - Exit fullscreen - +
diff --git a/packages/html/src/define/video/skin.tailwind.ts b/packages/html/src/define/video/skin.tailwind.ts index 2d20b391..6882dbe9 100644 --- a/packages/html/src/define/video/skin.tailwind.ts +++ b/packages/html/src/define/video/skin.tailwind.ts @@ -19,7 +19,6 @@ import { seek, slider, time, - tooltipState, } from '@videojs/skins/default/tailwind/video.tailwind'; import { createTemplate } from '@videojs/utils/dom'; import { cn } from '@videojs/utils/style'; @@ -80,18 +79,12 @@ function getTemplateHTML() {
- ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} ${renderIcon('play', { class: cn(icon, iconState.play.play) })} ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} - - Replay - Play - Pause - - + @@ -152,39 +145,21 @@ function getTemplateHTML() { - - ${renderIcon('captions-off', { class: cn(icon, iconState.captions.off) })} ${renderIcon('captions-on', { class: cn(icon, iconState.captions.on) })} - - Enable captions - Disable captions - - - - + ${renderIcon('pip-enter', { class: cn(icon, iconState.pip.off) })} ${renderIcon('pip-exit', { class: cn(icon, iconState.pip.on) })} - - Enter picture-in-picture - Exit picture-in-picture - - - - + ${renderIcon('fullscreen-enter', { class: cn(icon, iconState.fullscreen.enter) })} ${renderIcon('fullscreen-exit', { class: cn(icon, iconState.fullscreen.exit) })} - - Enter fullscreen - Exit fullscreen - - +
diff --git a/packages/html/src/define/video/skin.ts b/packages/html/src/define/video/skin.ts index 6f397c82..df4649a2 100644 --- a/packages/html/src/define/video/skin.ts +++ b/packages/html/src/define/video/skin.ts @@ -63,11 +63,7 @@ function getTemplateHTML() { ${renderIcon('play', { class: 'media-icon media-icon--play' })} ${renderIcon('pause', { class: 'media-icon media-icon--pause' })} - - Replay - Play - Pause - + @@ -133,28 +129,19 @@ function getTemplateHTML() { ${renderIcon('captions-off', { class: 'media-icon media-icon--captions-off' })} ${renderIcon('captions-on', { class: 'media-icon media-icon--captions-on' })} - - Enable captions - Disable captions - + ${renderIcon('pip-enter', { class: 'media-icon media-icon--pip-enter' })} ${renderIcon('pip-exit', { class: 'media-icon media-icon--pip-exit' })} - - Enter picture-in-picture - Exit picture-in-picture - + ${renderIcon('fullscreen-enter', { class: 'media-icon media-icon--fullscreen-enter' })} ${renderIcon('fullscreen-exit', { class: 'media-icon media-icon--fullscreen-exit' })} - - Enter fullscreen - Exit fullscreen - +
diff --git a/packages/html/src/ui/media-button-element.ts b/packages/html/src/ui/media-button-element.ts index c87054ef..ee74b075 100644 --- a/packages/html/src/ui/media-button-element.ts +++ b/packages/html/src/ui/media-button-element.ts @@ -1,12 +1,19 @@ -import type { InferComponentState, InferMediaState, MediaUIComponent, StateAttrMap } from '@videojs/core'; +import type { + ButtonState, + InferComponentState, + InferMediaState, + MediaButtonComponent, + StateAttrMap, +} from '@videojs/core'; import { applyElementProps, applyStateDataAttrs, createButton, logMissingFeature } from '@videojs/core/dom'; import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element'; +import type { State } from '@videojs/store'; import type { PlayerController } from '../player/player-controller'; import { MediaElement } from './media-element'; /** Abstract base for HTML custom elements that render a media-control button. */ -export abstract class MediaButtonElement extends MediaElement { +export abstract class MediaButtonElement extends MediaElement { static override properties: PropertyDeclarationMap = { label: { type: String }, disabled: { type: Boolean }, @@ -21,6 +28,10 @@ export abstract class MediaButtonElement extends protected abstract activate(state: InferMediaState): void; + get $state(): State { + return this.core.state; + } + #disconnect: AbortController | null = null; override connectedCallback(): void { @@ -46,6 +57,11 @@ export abstract class MediaButtonElement extends this.#disconnect = null; } + /** Returns the button's current label derived from media state. */ + getLabel(): string | undefined { + return this.core.state.current.label || undefined; + } + protected override willUpdate(changed: PropertyValues): void { super.willUpdate(changed); this.core.setProps?.(this); diff --git a/packages/html/src/ui/tooltip/tooltip-element.ts b/packages/html/src/ui/tooltip/tooltip-element.ts index a6661f49..0c484a02 100644 --- a/packages/html/src/ui/tooltip/tooltip-element.ts +++ b/packages/html/src/ui/tooltip/tooltip-element.ts @@ -1,4 +1,4 @@ -import { TooltipCore, TooltipCSSVars, TooltipDataAttrs, type TooltipInput } from '@videojs/core'; +import { type ButtonState, TooltipCore, TooltipCSSVars, TooltipDataAttrs, type TooltipInput } from '@videojs/core'; import { applyElementProps, applyStateDataAttrs, @@ -13,12 +13,22 @@ import { } from '@videojs/core/dom'; import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element'; import { ContextConsumer } from '@videojs/element/context'; +import type { State } from '@videojs/store'; import { SnapshotController } from '@videojs/store/html'; import { applyStyles, supportsAnchorPositioning, tryHidePopover, tryShowPopover } from '@videojs/utils/dom'; import { MediaElement } from '../media-element'; import { tooltipGroupContext } from './context'; +type TriggerElement = HTMLElement & { + getLabel(): string | undefined; + $state: State; +}; + +function isLabelTrigger(el: HTMLElement): el is TriggerElement { + return '$state' in el; +} + export class TooltipElement extends MediaElement { static readonly tagName = 'media-tooltip'; @@ -150,9 +160,8 @@ export class TooltipElement extends MediaElement { tryHidePopover(this); } - // Apply trigger ARIA and anchor-name to the discovered trigger. + // Apply anchor-name to the discovered trigger for CSS positioning. if (this.#currentTrigger) { - applyElementProps(this.#currentTrigger, this.#core.getTriggerAttrs(state, this.id)); applyStyles(this.#currentTrigger, getAnchorNameStyle(this.id)); } @@ -205,15 +214,22 @@ export class TooltipElement extends MediaElement { if (triggerEl && this.#tooltip) { this.#triggerAbort = new AbortController(); applyElementProps(triggerEl, this.#tooltip.triggerProps, { signal: this.#triggerAbort.signal }); + + if (isLabelTrigger(triggerEl)) { + this.#syncContent(triggerEl); + triggerEl.$state.subscribe(() => this.#syncContent(triggerEl), { + signal: this.#triggerAbort.signal, + }); + } } } + #syncContent(triggerEl: TriggerElement): void { + this.textContent = triggerEl.getLabel() ?? ''; + } + #cleanupTrigger(): void { if (this.#currentTrigger) { - // Remove ARIA attributes and anchor-name style from the old trigger. - applyElementProps(this.#currentTrigger, { - 'aria-describedby': undefined, - }); this.#currentTrigger.style.removeProperty('anchor-name'); } diff --git a/packages/react/src/presets/audio/minimal-skin.tailwind.tsx b/packages/react/src/presets/audio/minimal-skin.tailwind.tsx index 56134e59..ba58e14f 100644 --- a/packages/react/src/presets/audio/minimal-skin.tailwind.tsx +++ b/packages/react/src/presets/audio/minimal-skin.tailwind.tsx @@ -89,13 +89,6 @@ const SliderThumb = forwardRef & { persist ); }); -function PlayLabel(): string { - const paused = usePlayer((s) => Boolean(s.paused)); - const ended = usePlayer((s) => Boolean(s.ended)); - if (ended) return 'Replay'; - return paused ? 'Play' : 'Pause'; -} - function VolumePopover(): ReactNode { const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported'); @@ -160,9 +153,7 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod } /> - - - + diff --git a/packages/react/src/presets/audio/minimal-skin.tsx b/packages/react/src/presets/audio/minimal-skin.tsx index cbbfe079..08099f7b 100644 --- a/packages/react/src/presets/audio/minimal-skin.tsx +++ b/packages/react/src/presets/audio/minimal-skin.tsx @@ -37,13 +37,6 @@ const Button = forwardRef>(function ); }); -function PlayLabel(): string { - const paused = usePlayer((s) => Boolean(s.paused)); - const ended = usePlayer((s) => Boolean(s.ended)); - if (ended) return 'Replay'; - return paused ? 'Play' : 'Pause'; -} - function VolumePopover(): ReactNode { const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported'); @@ -106,9 +99,7 @@ export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode { } /> - - - + diff --git a/packages/react/src/presets/audio/skin.tailwind.tsx b/packages/react/src/presets/audio/skin.tailwind.tsx index 130f0e77..df9f8b8f 100644 --- a/packages/react/src/presets/audio/skin.tailwind.tsx +++ b/packages/react/src/presets/audio/skin.tailwind.tsx @@ -89,13 +89,6 @@ const SliderThumb = forwardRef & { persist ); }); -function PlayLabel(): string { - const paused = usePlayer((s) => Boolean(s.paused)); - const ended = usePlayer((s) => Boolean(s.ended)); - if (ended) return 'Replay'; - return paused ? 'Play' : 'Pause'; -} - function VolumePopover(): ReactNode { const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported'); @@ -160,9 +153,7 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode { } /> - - - + diff --git a/packages/react/src/presets/audio/skin.tsx b/packages/react/src/presets/audio/skin.tsx index 2bf21af7..f15f5748 100644 --- a/packages/react/src/presets/audio/skin.tsx +++ b/packages/react/src/presets/audio/skin.tsx @@ -37,13 +37,6 @@ const Button = forwardRef>(function ); }); -function PlayLabel(): string { - const paused = usePlayer((s) => Boolean(s.paused)); - const ended = usePlayer((s) => Boolean(s.ended)); - if (ended) return 'Replay'; - return paused ? 'Play' : 'Pause'; -} - function VolumePopover(): ReactNode { const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported'); @@ -106,9 +99,7 @@ export function AudioSkin(props: AudioSkinProps): ReactNode { } /> - - - + diff --git a/packages/react/src/presets/video/minimal-skin.tailwind.tsx b/packages/react/src/presets/video/minimal-skin.tailwind.tsx index 6ca086b4..3c7f978e 100644 --- a/packages/react/src/presets/video/minimal-skin.tailwind.tsx +++ b/packages/react/src/presets/video/minimal-skin.tailwind.tsx @@ -110,28 +110,6 @@ const SliderThumb = forwardRef & { persist ); }); -function PlayLabel(): string { - const paused = usePlayer((s) => Boolean(s.paused)); - const ended = usePlayer((s) => Boolean(s.ended)); - if (ended) return 'Replay'; - return paused ? 'Play' : 'Pause'; -} - -function CaptionsLabel(): string { - const active = usePlayer((s) => Boolean(s.subtitlesShowing)); - return active ? 'Disable captions' : 'Enable captions'; -} - -function PiPLabel(): string { - const pip = usePlayer((s) => Boolean(s.pip)); - return pip ? 'Exit picture-in-picture' : 'Enter picture-in-picture'; -} - -function FullscreenLabel(): string { - const fullscreen = usePlayer((s) => Boolean(s.fullscreen)); - return fullscreen ? 'Exit fullscreen' : 'Enter fullscreen'; -} - function VolumePopover(): ReactNode { const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported'); @@ -215,9 +193,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod } /> - - - + @@ -289,9 +265,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod } /> - - - + @@ -303,9 +277,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod } /> - - - + @@ -317,9 +289,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod } /> - - - +
diff --git a/packages/react/src/presets/video/minimal-skin.tsx b/packages/react/src/presets/video/minimal-skin.tsx index 386b2e51..d0157b24 100644 --- a/packages/react/src/presets/video/minimal-skin.tsx +++ b/packages/react/src/presets/video/minimal-skin.tsx @@ -53,28 +53,6 @@ const Button = forwardRef>(function ); }); -function PlayLabel(): string { - const paused = usePlayer((s) => Boolean(s.paused)); - const ended = usePlayer((s) => Boolean(s.ended)); - if (ended) return 'Replay'; - return paused ? 'Play' : 'Pause'; -} - -function CaptionsLabel(): string { - const active = usePlayer((s) => Boolean(s.subtitlesShowing)); - return active ? 'Disable captions' : 'Enable captions'; -} - -function PiPLabel(): string { - const pip = usePlayer((s) => Boolean(s.pip)); - return pip ? 'Exit picture-in-picture' : 'Enter picture-in-picture'; -} - -function FullscreenLabel(): string { - const fullscreen = usePlayer((s) => Boolean(s.fullscreen)); - return fullscreen ? 'Exit fullscreen' : 'Enter fullscreen'; -} - function VolumePopover(): ReactNode { const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported'); @@ -149,9 +127,7 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode { } /> - - - + @@ -226,9 +202,7 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode { } /> - - - + @@ -240,9 +214,7 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode { } /> - - - + @@ -254,9 +226,7 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode { } /> - - - +
diff --git a/packages/react/src/presets/video/skin.tailwind.tsx b/packages/react/src/presets/video/skin.tailwind.tsx index 5376e36b..5c433b24 100644 --- a/packages/react/src/presets/video/skin.tailwind.tsx +++ b/packages/react/src/presets/video/skin.tailwind.tsx @@ -110,28 +110,6 @@ const SliderThumb = forwardRef & { persist ); }); -function PlayLabel(): string { - const paused = usePlayer((s) => Boolean(s.paused)); - const ended = usePlayer((s) => Boolean(s.ended)); - if (ended) return 'Replay'; - return paused ? 'Play' : 'Pause'; -} - -function CaptionsLabel(): string { - const active = usePlayer((s) => Boolean(s.subtitlesShowing)); - return active ? 'Disable captions' : 'Enable captions'; -} - -function PiPLabel(): string { - const pip = usePlayer((s) => Boolean(s.pip)); - return pip ? 'Exit picture-in-picture' : 'Enter picture-in-picture'; -} - -function FullscreenLabel(): string { - const fullscreen = usePlayer((s) => Boolean(s.fullscreen)); - return fullscreen ? 'Exit fullscreen' : 'Enter fullscreen'; -} - function VolumePopover(): ReactNode { const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported'); @@ -217,9 +195,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode { } /> - - - + @@ -285,9 +261,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode { } /> - - - + @@ -299,9 +273,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode { } /> - - - + @@ -313,9 +285,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode { } /> - - - + diff --git a/packages/react/src/presets/video/skin.tsx b/packages/react/src/presets/video/skin.tsx index dc517548..78698215 100644 --- a/packages/react/src/presets/video/skin.tsx +++ b/packages/react/src/presets/video/skin.tsx @@ -53,28 +53,6 @@ const Button = forwardRef>(function ); }); -function PlayLabel(): string { - const paused = usePlayer((s) => Boolean(s.paused)); - const ended = usePlayer((s) => Boolean(s.ended)); - if (ended) return 'Replay'; - return paused ? 'Play' : 'Pause'; -} - -function CaptionsLabel(): string { - const active = usePlayer((s) => Boolean(s.subtitlesShowing)); - return active ? 'Disable captions' : 'Enable captions'; -} - -function PiPLabel(): string { - const pip = usePlayer((s) => Boolean(s.pip)); - return pip ? 'Exit picture-in-picture' : 'Enter picture-in-picture'; -} - -function FullscreenLabel(): string { - const fullscreen = usePlayer((s) => Boolean(s.fullscreen)); - return fullscreen ? 'Exit fullscreen' : 'Enter fullscreen'; -} - function VolumePopover(): ReactNode { const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported'); @@ -151,9 +129,7 @@ export function VideoSkin(props: VideoSkinProps): ReactNode { } /> - - - + @@ -222,9 +198,7 @@ export function VideoSkin(props: VideoSkinProps): ReactNode { } /> - - - + @@ -236,9 +210,7 @@ export function VideoSkin(props: VideoSkinProps): ReactNode { } /> - - - + @@ -250,9 +222,7 @@ export function VideoSkin(props: VideoSkinProps): ReactNode { } /> - - - + diff --git a/packages/react/src/ui/create-media-button.tsx b/packages/react/src/ui/create-media-button.tsx index 0f54554f..0f1ea83b 100644 --- a/packages/react/src/ui/create-media-button.tsx +++ b/packages/react/src/ui/create-media-button.tsx @@ -1,17 +1,18 @@ 'use client'; -import type { InferComponentState, InferMediaState, MediaUIComponent, StateAttrMap } from '@videojs/core'; +import type { InferComponentState, InferMediaState, MediaButtonComponent, StateAttrMap } from '@videojs/core'; import { logMissingFeature } from '@videojs/core/dom'; import type { Selector } from '@videojs/store'; import type { ForwardedRef, ForwardRefExoticComponent, RefAttributes } from 'react'; -import { forwardRef, useState } from 'react'; +import { forwardRef, useLayoutEffect, useState } from 'react'; import { usePlayer } from '../player/context'; import type { renderElement as renderElementFn } from '../utils/use-render'; import { renderElement } from '../utils/use-render'; import { useButton } from './hooks/use-button'; +import { useOptionalTooltipContext } from './tooltip/context'; -interface MediaButtonConfig> { +interface MediaButtonConfig> { displayName: string; core: { new (): Core; defaultProps: Record }; stateAttrMap: StateAttrMap>; @@ -20,7 +21,7 @@ interface MediaButtonConfig> { } /** Creates a media button React component from a core class and config. */ -export function createMediaButton, Props extends object>( +export function createMediaButton, Props extends object>( config: MediaButtonConfig ): ForwardRefExoticComponent> { const { displayName, core: CoreClass, stateAttrMap, selector, action } = config; @@ -45,6 +46,7 @@ export function createMediaButton, Props } } + const tooltipCtx = useOptionalTooltipContext(); const feature = usePlayer(selector); const [core] = useState(() => new CoreClass()); @@ -56,15 +58,26 @@ export function createMediaButton, Props isDisabled: () => !!coreProps.disabled || !feature, }); - if (!feature) { + // Derive state and label before the hooks boundary so the + // useLayoutEffect below (called unconditionally) can reference them. + type State = InferComponentState; + if (feature) core.setMedia(feature); + const state = feature ? (core.getState() as State) : null; + const label = state ? core.getLabel(state) : undefined; + + // Forward label to tooltip popup content when inside a Tooltip.Root. + useLayoutEffect(() => { + if (!tooltipCtx) return; + tooltipCtx.setContent(label); + return () => tooltipCtx.setContent(undefined); + }, [tooltipCtx, label]); + + if (!feature || !state) { if (__DEV__) logMissingFeature(displayName, selector.displayName ?? displayName); return null; } - type State = InferComponentState; - - core.setMedia(feature); - const state = core.getState() as State; + const attrs = core.getAttrs(state); return renderElement( 'button', @@ -73,7 +86,7 @@ export function createMediaButton, Props state, stateAttrMap, ref: [forwardedRef, buttonRef], - props: [core.getAttrs(state), elementProps, getButtonProps()], + props: [attrs, elementProps, getButtonProps()], } ); }); diff --git a/packages/react/src/ui/tooltip/context.tsx b/packages/react/src/ui/tooltip/context.tsx index 9ba09d2a..3c177fec 100644 --- a/packages/react/src/ui/tooltip/context.tsx +++ b/packages/react/src/ui/tooltip/context.tsx @@ -11,6 +11,8 @@ export interface TooltipContextValue { stateAttrMap: StateAttrMap; anchorName: string; popupId: string; + content: string | undefined; + setContent: (content: string | undefined) => void; } const TooltipContext = createContext(null); @@ -22,3 +24,7 @@ export function useTooltipContext(): TooltipContextValue { if (!ctx) throw new Error('Tooltip compound components must be used within a Tooltip.Root'); return ctx; } + +export function useOptionalTooltipContext(): TooltipContextValue | null { + return useContext(TooltipContext); +} diff --git a/packages/react/src/ui/tooltip/index.ts b/packages/react/src/ui/tooltip/index.ts index 03cdbff3..0cae6447 100644 --- a/packages/react/src/ui/tooltip/index.ts +++ b/packages/react/src/ui/tooltip/index.ts @@ -1,2 +1,2 @@ -export { type TooltipContextValue, useTooltipContext } from './context'; +export { type TooltipContextValue, useOptionalTooltipContext, useTooltipContext } from './context'; export * as Tooltip from './index.parts'; diff --git a/packages/react/src/ui/tooltip/tooltip-popup.tsx b/packages/react/src/ui/tooltip/tooltip-popup.tsx index 200142e5..2373ac20 100644 --- a/packages/react/src/ui/tooltip/tooltip-popup.tsx +++ b/packages/react/src/ui/tooltip/tooltip-popup.tsx @@ -21,7 +21,7 @@ export const TooltipPopup = forwardRef(functi { render, className, style, ...elementProps }, forwardedRef ) { - const { core, tooltip, state, stateAttrMap, anchorName, popupId } = useTooltipContext(); + const { core, tooltip, state, stateAttrMap, anchorName, popupId, content } = useTooltipContext(); const internalRef = useRef(null); const popupRef = useCallback( @@ -156,6 +156,8 @@ export const TooltipPopup = forwardRef(functi style: positioningStyle, ...core.getPopupAttrs(state), }, + // Forwarded content as default children — explicit children override. + { children: content }, { ...restPopupProps, onBlur: onFocusOut }, elementProps, ], diff --git a/packages/react/src/ui/tooltip/tooltip-root.tsx b/packages/react/src/ui/tooltip/tooltip-root.tsx index 061bf8e4..c0255aa7 100644 --- a/packages/react/src/ui/tooltip/tooltip-root.tsx +++ b/packages/react/src/ui/tooltip/tooltip-root.tsx @@ -73,6 +73,8 @@ export function TooltipRoot({ return instance; }); + const [content, setContent] = useState(); + const anchorName = useSafeId(); const popupId = useSafeId('tooltip'); @@ -97,7 +99,9 @@ export function TooltipRoot({ const state = core.getState(); return ( - + {children} ); diff --git a/packages/react/src/ui/tooltip/tooltip-trigger.tsx b/packages/react/src/ui/tooltip/tooltip-trigger.tsx index aae68e89..7c3bf1a5 100644 --- a/packages/react/src/ui/tooltip/tooltip-trigger.tsx +++ b/packages/react/src/ui/tooltip/tooltip-trigger.tsx @@ -15,7 +15,7 @@ export const TooltipTrigger = forwardRef { render, className, style, ...elementProps }, forwardedRef ) { - const { core, tooltip, state, stateAttrMap, anchorName, popupId } = useTooltipContext(); + const { tooltip, state, stateAttrMap, anchorName } = useTooltipContext(); const triggerRef = useCallback( (el: HTMLButtonElement | null) => { @@ -42,7 +42,6 @@ export const TooltipTrigger = forwardRef props: [ { type: 'button' as const, - ...core.getTriggerAttrs(state, popupId), }, { ...restTriggerProps, onFocus: onFocusIn, onBlur: onFocusOut }, elementProps, diff --git a/packages/skins/src/default/css/audio.css b/packages/skins/src/default/css/audio.css index dd30b596..7057f508 100644 --- a/packages/skins/src/default/css/audio.css +++ b/packages/skins/src/default/css/audio.css @@ -11,7 +11,6 @@ @import "./components/slider.css"; @import "./components/popup.css"; @import "../../shared/css/audio/icon-state.css"; -@import "../../shared/css/audio/tooltip-state.css"; /* ========================================================================== Root diff --git a/packages/skins/src/default/css/video.css b/packages/skins/src/default/css/video.css index 7a1e7893..165b538e 100644 --- a/packages/skins/src/default/css/video.css +++ b/packages/skins/src/default/css/video.css @@ -16,7 +16,6 @@ @import "./components/popup.css"; @import "./components/captions.css"; @import "../../shared/css/video/icon-state.css"; -@import "../../shared/css/video/tooltip-state.css"; /* ========================================================================== Root diff --git a/packages/skins/src/default/tailwind/audio.tailwind.ts b/packages/skins/src/default/tailwind/audio.tailwind.ts index ae577b04..938600f4 100644 --- a/packages/skins/src/default/tailwind/audio.tailwind.ts +++ b/packages/skins/src/default/tailwind/audio.tailwind.ts @@ -101,7 +101,6 @@ export const error = { ========================================================================== */ export { iconState } from '../../shared/tailwind/icon-state'; -export { tooltipState } from '../../shared/tailwind/tooltip-state'; export { button } from './components/button'; export { buttonGroup } from './components/button-group'; export { icon, iconContainer, iconFlipped, iconHidden } from './components/icon'; diff --git a/packages/skins/src/default/tailwind/video.tailwind.ts b/packages/skins/src/default/tailwind/video.tailwind.ts index 7f357da6..e54ee648 100644 --- a/packages/skins/src/default/tailwind/video.tailwind.ts +++ b/packages/skins/src/default/tailwind/video.tailwind.ts @@ -193,7 +193,6 @@ export const error = { ========================================================================== */ export { iconState } from '../../shared/tailwind/icon-state'; -export { tooltipState } from '../../shared/tailwind/tooltip-state'; export { button } from './components/button'; export { buttonGroup } from './components/button-group'; export { icon, iconContainer, iconFlipped, iconHidden } from './components/icon'; diff --git a/packages/skins/src/minimal/css/audio.css b/packages/skins/src/minimal/css/audio.css index 04ab4d93..36c30128 100644 --- a/packages/skins/src/minimal/css/audio.css +++ b/packages/skins/src/minimal/css/audio.css @@ -10,7 +10,6 @@ @import "./components/slider.css"; @import "./components/popup.css"; @import "../../shared/css/audio/icon-state.css"; -@import "../../shared/css/audio/tooltip-state.css"; /* ========================================================================== Root diff --git a/packages/skins/src/minimal/css/video.css b/packages/skins/src/minimal/css/video.css index 909a1339..6b144b5d 100644 --- a/packages/skins/src/minimal/css/video.css +++ b/packages/skins/src/minimal/css/video.css @@ -15,7 +15,6 @@ @import "./components/popup.css"; @import "./components/captions.css"; @import "../../shared/css/video/icon-state.css"; -@import "../../shared/css/video/tooltip-state.css"; /* ========================================================================== Root diff --git a/packages/skins/src/minimal/tailwind/audio.tailwind.ts b/packages/skins/src/minimal/tailwind/audio.tailwind.ts index d57bdd94..90f70e29 100644 --- a/packages/skins/src/minimal/tailwind/audio.tailwind.ts +++ b/packages/skins/src/minimal/tailwind/audio.tailwind.ts @@ -89,7 +89,6 @@ export const error = { ========================================================================== */ export { iconState } from '../../shared/tailwind/icon-state'; -export { tooltipState } from '../../shared/tailwind/tooltip-state'; export { bufferingIndicator } from './components/buffering'; export { button } from './components/button'; export { buttonGroup } from './components/button-group'; diff --git a/packages/skins/src/minimal/tailwind/video.tailwind.ts b/packages/skins/src/minimal/tailwind/video.tailwind.ts index 727cfc0b..e2a7eb48 100644 --- a/packages/skins/src/minimal/tailwind/video.tailwind.ts +++ b/packages/skins/src/minimal/tailwind/video.tailwind.ts @@ -188,7 +188,6 @@ export const popup = { ========================================================================== */ export { iconState } from '../../shared/tailwind/icon-state'; -export { tooltipState } from '../../shared/tailwind/tooltip-state'; export { bufferingIndicator } from './components/buffering'; export { button } from './components/button'; export { buttonGroup } from './components/button-group'; diff --git a/packages/skins/src/shared/css/audio/tooltip-state.css b/packages/skins/src/shared/css/audio/tooltip-state.css deleted file mode 100644 index 1cf52308..00000000 --- a/packages/skins/src/shared/css/audio/tooltip-state.css +++ /dev/null @@ -1,25 +0,0 @@ -/* ========================================================================== - Tooltip Label State Visibility for Audio Skins - - Data-attribute-driven visibility rules for multi-state tooltip labels. - Uses adjacent sibling selectors to match button state → tooltip content. - ========================================================================== */ - -/* --- All multi-state labels hidden by default --- */ - -.media-tooltip-label { - display: none; -} - -/* --- Active label per state --- */ - -/* Play: ended → replay */ -.media-button--play[data-ended] + .media-tooltip .media-tooltip-label--replay, -/* Play: paused (not ended) → play */ - .media-button--play:not([data-ended])[data-paused] + .media-tooltip - .media-tooltip-label--play, -/* Play: playing (not paused, not ended) → pause */ - .media-button--play:not([data-paused]):not([data-ended]) + .media-tooltip - .media-tooltip-label--pause { - display: block; -} diff --git a/packages/skins/src/shared/css/video/tooltip-state.css b/packages/skins/src/shared/css/video/tooltip-state.css deleted file mode 100644 index 91c7720e..00000000 --- a/packages/skins/src/shared/css/video/tooltip-state.css +++ /dev/null @@ -1,43 +0,0 @@ -/* ========================================================================== - Tooltip Label State Visibility for Video Skins - - Data-attribute-driven visibility rules for multi-state tooltip labels. - Uses adjacent sibling selectors to match button state → tooltip content. - ========================================================================== */ - -/* --- All multi-state labels hidden by default --- */ - -.media-tooltip-label { - display: none; -} - -/* --- Active label per state --- */ - -/* Play: ended → replay */ -.media-button--play[data-ended] + .media-tooltip .media-tooltip-label--replay, -/* Play: paused (not ended) → play */ - .media-button--play:not([data-ended])[data-paused] + .media-tooltip - .media-tooltip-label--play, -/* Play: playing (not paused, not ended) → pause */ - .media-button--play:not([data-paused]):not([data-ended]) + .media-tooltip - .media-tooltip-label--pause, -/* Fullscreen: not fullscreen → enter */ - .media-button--fullscreen:not([data-fullscreen]) + .media-tooltip - .media-tooltip-label--enter-fullscreen, -/* Fullscreen: fullscreen → exit */ - .media-button--fullscreen[data-fullscreen] + .media-tooltip - .media-tooltip-label--exit-fullscreen, -/* Captions: not active → enable */ - .media-button--captions:not([data-active]) + .media-tooltip - .media-tooltip-label--enable-captions, -/* Captions: active → disable */ - .media-button--captions[data-active] + .media-tooltip - .media-tooltip-label--disable-captions, -/* PiP: not in pip → enter */ - .media-button--pip:not([data-pip]) + .media-tooltip - .media-tooltip-label--enter-pip, -/* PiP: in pip → exit */ - .media-button--pip[data-pip] + .media-tooltip - .media-tooltip-label--exit-pip { - display: block; -} diff --git a/packages/skins/src/shared/tailwind/tooltip-state.ts b/packages/skins/src/shared/tailwind/tooltip-state.ts deleted file mode 100644 index a0399f4e..00000000 --- a/packages/skins/src/shared/tailwind/tooltip-state.ts +++ /dev/null @@ -1,23 +0,0 @@ -export const tooltipState = { - play: { - wrapper: 'contents group/play-tip', - replay: 'hidden group-has-data-ended/play-tip:block', - play: 'hidden group-[:has([data-paused]):not(:has([data-ended]))]/play-tip:block', - pause: 'hidden group-[:not(:has([data-paused])):not(:has([data-ended]))]/play-tip:block', - }, - fullscreen: { - wrapper: 'contents group/fullscreen-tip', - enter: 'hidden group-[:not(:has([data-fullscreen]))]/fullscreen-tip:block', - exit: 'hidden group-has-data-fullscreen/fullscreen-tip:block', - }, - captions: { - wrapper: 'contents group/captions-tip', - enable: 'hidden group-[:not(:has([data-active]))]/captions-tip:block', - disable: 'hidden group-has-data-active/captions-tip:block', - }, - pip: { - wrapper: 'contents group/pip-tip', - enter: 'hidden group-[:not(:has([data-pip]))]/pip-tip:block', - exit: 'hidden group-has-data-pip/pip-tip:block', - }, -};