mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(skin): improve buffering, overlays, and input feedback (#1547)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
iconFlipped,
|
||||
iconState,
|
||||
menu,
|
||||
playButton,
|
||||
playbackRate,
|
||||
popup,
|
||||
root,
|
||||
@@ -23,11 +24,13 @@ import {
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@/icons/minimal';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { Menu } from '@/ui/menu';
|
||||
import { MuteButton } from '@/ui/mute-button';
|
||||
@@ -179,21 +182,30 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
|
||||
<div className={controls}>
|
||||
<Tooltip.Provider>
|
||||
<div className={buttonGroup}>
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className={iconState.play.button} render={<Button />}>
|
||||
<RestartIcon className={cn(icon, iconState.play.restart)} />
|
||||
<PlayIcon className={cn(icon, iconState.play.play)} />
|
||||
<PauseIcon className={cn(icon, iconState.play.pause)} />
|
||||
</PlayButton>
|
||||
}
|
||||
<span className={playButton.wrapper}>
|
||||
<BufferingIndicator
|
||||
render={(props) => (
|
||||
<div {...props} className={cn(playButton.bufferingRoot, props.className)}>
|
||||
<SpinnerIcon className={icon} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Tooltip.Popup className={cn(popup.tooltip)}>
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className={popup.tooltipShortcut} />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className={cn(iconState.play.button, playButton.control)} render={<Button />}>
|
||||
<RestartIcon className={cn(icon, iconState.play.restart)} />
|
||||
<PlayIcon className={cn(icon, iconState.play.play)} />
|
||||
<PauseIcon className={cn(icon, iconState.play.pause)} />
|
||||
</PlayButton>
|
||||
}
|
||||
/>
|
||||
<Tooltip.Popup className={cn(popup.tooltip)}>
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className={popup.tooltipShortcut} />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
</span>
|
||||
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
|
||||
@@ -6,11 +6,13 @@ import {
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@/icons/minimal';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { Hotkey } from '@/ui/hotkey';
|
||||
import { Menu } from '@/ui/menu';
|
||||
@@ -126,21 +128,30 @@ export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode {
|
||||
<div className="media-controls">
|
||||
<Tooltip.Provider>
|
||||
<div className="media-button-group">
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className="media-button--play" render={<Button />}>
|
||||
<RestartIcon className="media-icon media-icon--restart" />
|
||||
<PlayIcon className="media-icon media-icon--play" />
|
||||
<PauseIcon className="media-icon media-icon--pause" />
|
||||
</PlayButton>
|
||||
}
|
||||
<span className="media-button--play__wrapper">
|
||||
<BufferingIndicator
|
||||
render={(props) => (
|
||||
<div {...props} className="media-buffering-indicator">
|
||||
<SpinnerIcon className="media-icon" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Tooltip.Popup className="media-tooltip">
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className="media-tooltip__kbd" />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className="media-button--play" render={<Button />}>
|
||||
<RestartIcon className="media-icon media-icon--restart" />
|
||||
<PlayIcon className="media-icon media-icon--play" />
|
||||
<PauseIcon className="media-icon media-icon--pause" />
|
||||
</PlayButton>
|
||||
}
|
||||
/>
|
||||
<Tooltip.Popup className="media-tooltip">
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className="media-tooltip__kbd" />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
</span>
|
||||
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
iconFlipped,
|
||||
iconState,
|
||||
menu,
|
||||
playButton,
|
||||
playbackRate,
|
||||
popup,
|
||||
root,
|
||||
@@ -23,11 +24,13 @@ import {
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@/icons';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { Hotkey } from '@/ui/hotkey';
|
||||
import { Menu } from '@/ui/menu';
|
||||
@@ -181,21 +184,30 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode {
|
||||
<div className={controls}>
|
||||
<Tooltip.Provider>
|
||||
<div className={buttonGroup}>
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className={iconState.play.button} render={<Button />}>
|
||||
<RestartIcon className={cn(icon, iconState.play.restart)} />
|
||||
<PlayIcon className={cn(icon, iconState.play.play)} />
|
||||
<PauseIcon className={cn(icon, iconState.play.pause)} />
|
||||
</PlayButton>
|
||||
}
|
||||
<span className={playButton.wrapper}>
|
||||
<BufferingIndicator
|
||||
render={(props) => (
|
||||
<div {...props} className={cn(playButton.bufferingRoot, props.className)}>
|
||||
<SpinnerIcon className={icon} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Tooltip.Popup className={cn(popup.tooltip)}>
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className={popup.tooltipShortcut} />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className={cn(iconState.play.button, playButton.control)} render={<Button />}>
|
||||
<RestartIcon className={cn(icon, iconState.play.restart)} />
|
||||
<PlayIcon className={cn(icon, iconState.play.play)} />
|
||||
<PauseIcon className={cn(icon, iconState.play.pause)} />
|
||||
</PlayButton>
|
||||
}
|
||||
/>
|
||||
<Tooltip.Popup className={cn(popup.tooltip)}>
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className={popup.tooltipShortcut} />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
</span>
|
||||
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
|
||||
@@ -6,11 +6,13 @@ import {
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@/icons';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { Hotkey } from '@/ui/hotkey';
|
||||
import { Menu } from '@/ui/menu';
|
||||
@@ -126,21 +128,30 @@ export function AudioSkin(props: AudioSkinProps): ReactNode {
|
||||
<div className="media-surface media-controls">
|
||||
<Tooltip.Provider>
|
||||
<div className="media-button-group">
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className="media-button--play" render={<Button />}>
|
||||
<RestartIcon className="media-icon media-icon--restart" />
|
||||
<PlayIcon className="media-icon media-icon--play" />
|
||||
<PauseIcon className="media-icon media-icon--pause" />
|
||||
</PlayButton>
|
||||
}
|
||||
<span className="media-button--play__wrapper">
|
||||
<BufferingIndicator
|
||||
render={(props) => (
|
||||
<div {...props} className="media-buffering-indicator">
|
||||
<SpinnerIcon className="media-icon" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Tooltip.Popup className="media-surface media-tooltip">
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className="media-tooltip__kbd" />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className="media-button--play" render={<Button />}>
|
||||
<RestartIcon className="media-icon media-icon--restart" />
|
||||
<PlayIcon className="media-icon media-icon--play" />
|
||||
<PauseIcon className="media-icon media-icon--pause" />
|
||||
</PlayButton>
|
||||
}
|
||||
/>
|
||||
<Tooltip.Popup className="media-surface media-tooltip">
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className="media-tooltip__kbd" />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
</span>
|
||||
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
|
||||
@@ -5,14 +5,24 @@ import {
|
||||
error,
|
||||
icon,
|
||||
iconState,
|
||||
playButton,
|
||||
popup,
|
||||
root,
|
||||
slider,
|
||||
} from '@videojs/skins/minimal/tailwind/audio.tailwind';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { PauseIcon, PlayIcon, RestartIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@/icons/minimal';
|
||||
import {
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@/icons/minimal';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { Hotkey } from '@/ui/hotkey';
|
||||
import { LiveButton } from '@/ui/live-button';
|
||||
@@ -119,21 +129,30 @@ export function MinimalLiveAudioSkinTailwind(props: MinimalLiveAudioSkinProps):
|
||||
<div className={controls}>
|
||||
<Tooltip.Provider>
|
||||
<div className={buttonGroup}>
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className={iconState.play.button} render={<Button />}>
|
||||
<RestartIcon className={cn(icon, iconState.play.restart)} />
|
||||
<PlayIcon className={cn(icon, iconState.play.play)} />
|
||||
<PauseIcon className={cn(icon, iconState.play.pause)} />
|
||||
</PlayButton>
|
||||
}
|
||||
<span className={playButton.wrapper}>
|
||||
<BufferingIndicator
|
||||
render={(props) => (
|
||||
<div {...props} className={cn(playButton.bufferingRoot, props.className)}>
|
||||
<SpinnerIcon className={icon} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Tooltip.Popup className={cn(popup.tooltip)}>
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className={popup.tooltipShortcut} />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className={cn(iconState.play.button, playButton.control)} render={<Button />}>
|
||||
<RestartIcon className={cn(icon, iconState.play.restart)} />
|
||||
<PlayIcon className={cn(icon, iconState.play.play)} />
|
||||
<PauseIcon className={cn(icon, iconState.play.pause)} />
|
||||
</PlayButton>
|
||||
}
|
||||
/>
|
||||
<Tooltip.Popup className={cn(popup.tooltip)}>
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className={popup.tooltipShortcut} />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
</span>
|
||||
|
||||
<LiveButton className={cn(button.base, button.subtle, button.live)} />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { PauseIcon, PlayIcon, RestartIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@/icons/minimal';
|
||||
import {
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@/icons/minimal';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { Hotkey } from '@/ui/hotkey';
|
||||
import { LiveButton } from '@/ui/live-button';
|
||||
@@ -85,21 +94,30 @@ export function MinimalLiveAudioSkin(props: MinimalLiveAudioSkinProps): ReactNod
|
||||
<div className="media-controls">
|
||||
<Tooltip.Provider>
|
||||
<div className="media-button-group">
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className="media-button--play" render={<Button />}>
|
||||
<RestartIcon className="media-icon media-icon--restart" />
|
||||
<PlayIcon className="media-icon media-icon--play" />
|
||||
<PauseIcon className="media-icon media-icon--pause" />
|
||||
</PlayButton>
|
||||
}
|
||||
<span className="media-button--play__wrapper">
|
||||
<BufferingIndicator
|
||||
render={(props) => (
|
||||
<div {...props} className="media-buffering-indicator">
|
||||
<SpinnerIcon className="media-icon" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Tooltip.Popup className="media-tooltip">
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className="media-tooltip__kbd" />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className="media-button--play" render={<Button />}>
|
||||
<RestartIcon className="media-icon media-icon--restart" />
|
||||
<PlayIcon className="media-icon media-icon--play" />
|
||||
<PauseIcon className="media-icon media-icon--pause" />
|
||||
</PlayButton>
|
||||
}
|
||||
/>
|
||||
<Tooltip.Popup className="media-tooltip">
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className="media-tooltip__kbd" />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
</span>
|
||||
|
||||
<LiveButton className="media-button media-button--subtle media-button--live" />
|
||||
</div>
|
||||
|
||||
@@ -5,14 +5,16 @@ import {
|
||||
error,
|
||||
icon,
|
||||
iconState,
|
||||
playButton,
|
||||
popup,
|
||||
root,
|
||||
slider,
|
||||
} from '@videojs/skins/default/tailwind/audio.tailwind';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { PauseIcon, PlayIcon, RestartIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@/icons';
|
||||
import { PauseIcon, PlayIcon, RestartIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@/icons';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { Hotkey } from '@/ui/hotkey';
|
||||
import { LiveButton } from '@/ui/live-button';
|
||||
@@ -119,21 +121,30 @@ export function LiveAudioSkinTailwind(props: LiveAudioSkinProps): ReactNode {
|
||||
<div className={controls}>
|
||||
<Tooltip.Provider>
|
||||
<div className={buttonGroup}>
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className={iconState.play.button} render={<Button />}>
|
||||
<RestartIcon className={cn(icon, iconState.play.restart)} />
|
||||
<PlayIcon className={cn(icon, iconState.play.play)} />
|
||||
<PauseIcon className={cn(icon, iconState.play.pause)} />
|
||||
</PlayButton>
|
||||
}
|
||||
<span className={playButton.wrapper}>
|
||||
<BufferingIndicator
|
||||
render={(props) => (
|
||||
<div {...props} className={cn(playButton.bufferingRoot, props.className)}>
|
||||
<SpinnerIcon className={icon} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Tooltip.Popup className={cn(popup.tooltip)}>
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className={popup.tooltipShortcut} />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className={cn(iconState.play.button, playButton.control)} render={<Button />}>
|
||||
<RestartIcon className={cn(icon, iconState.play.restart)} />
|
||||
<PlayIcon className={cn(icon, iconState.play.play)} />
|
||||
<PauseIcon className={cn(icon, iconState.play.pause)} />
|
||||
</PlayButton>
|
||||
}
|
||||
/>
|
||||
<Tooltip.Popup className={cn(popup.tooltip)}>
|
||||
<Tooltip.Label />
|
||||
<Tooltip.Shortcut className={popup.tooltipShortcut} />
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
</span>
|
||||
|
||||
<LiveButton className={cn(button.base, button.subtle, button.live)} />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { PauseIcon, PlayIcon, RestartIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@/icons';
|
||||
import { PauseIcon, PlayIcon, RestartIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@/icons';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { Hotkey } from '@/ui/hotkey';
|
||||
import { LiveButton } from '@/ui/live-button';
|
||||
@@ -93,18 +94,27 @@ export function LiveAudioSkin(props: LiveAudioSkinProps): ReactNode {
|
||||
<div className="media-surface media-controls">
|
||||
<Tooltip.Provider>
|
||||
<div className="media-button-group">
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className="media-button--play" render={<Button />}>
|
||||
<RestartIcon className="media-icon media-icon--restart" />
|
||||
<PlayIcon className="media-icon media-icon--play" />
|
||||
<PauseIcon className="media-icon media-icon--pause" />
|
||||
</PlayButton>
|
||||
}
|
||||
<span className="media-button--play__wrapper">
|
||||
<BufferingIndicator
|
||||
render={(props) => (
|
||||
<div {...props} className="media-buffering-indicator">
|
||||
<SpinnerIcon className="media-icon" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<TooltipPopup />
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root side="top" boundary="viewport">
|
||||
<Tooltip.Trigger
|
||||
render={
|
||||
<PlayButton className="media-button--play" render={<Button />}>
|
||||
<RestartIcon className="media-icon media-icon--restart" />
|
||||
<PlayIcon className="media-icon media-icon--play" />
|
||||
<PauseIcon className="media-icon media-icon--pause" />
|
||||
</PlayButton>
|
||||
}
|
||||
/>
|
||||
<TooltipPopup />
|
||||
</Tooltip.Root>
|
||||
</span>
|
||||
|
||||
<LiveButton className="media-button media-button--subtle media-button--live" />
|
||||
</div>
|
||||
|
||||
@@ -217,9 +217,7 @@ export function LiveVideoSkinTailwind(props: LiveVideoSkinProps): ReactNode {
|
||||
<BufferingIndicator
|
||||
render={(props) => (
|
||||
<div {...props} className={bufferingIndicator.root}>
|
||||
<div className={bufferingIndicator.container}>
|
||||
<SpinnerIcon className={icon} />
|
||||
</div>
|
||||
<SpinnerIcon className={icon} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -178,9 +178,7 @@ export function LiveVideoSkin(props: LiveVideoSkinProps): ReactNode {
|
||||
<BufferingIndicator
|
||||
render={(props) => (
|
||||
<div {...props} className="media-buffering-indicator">
|
||||
<div className="media-surface">
|
||||
<SpinnerIcon className="media-icon" />
|
||||
</div>
|
||||
<SpinnerIcon className="media-icon" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -425,9 +425,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
|
||||
<BufferingIndicator
|
||||
render={(props) => (
|
||||
<div {...props} className={bufferingIndicator.root}>
|
||||
<div className={bufferingIndicator.container}>
|
||||
<SpinnerIcon className={icon} />
|
||||
</div>
|
||||
<SpinnerIcon className={icon} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -360,9 +360,7 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
|
||||
<BufferingIndicator
|
||||
render={(props) => (
|
||||
<div {...props} className="media-buffering-indicator">
|
||||
<div className="media-surface">
|
||||
<SpinnerIcon className="media-icon" />
|
||||
</div>
|
||||
<SpinnerIcon className="media-icon" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import type { IndicatorLifecycleState, TransitionState } from '@videojs/core';
|
||||
import { createState } from '@videojs/store';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const transitionMock = vi.hoisted(() => ({
|
||||
createTransition: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@videojs/core/dom', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@videojs/core/dom')>()),
|
||||
createTransition: transitionMock.createTransition,
|
||||
}));
|
||||
|
||||
import { useRenderedIndicatorState } from '../use-rendered-indicator-state';
|
||||
|
||||
interface TestIndicatorState extends IndicatorLifecycleState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
function makeState(generation: number): TestIndicatorState {
|
||||
return {
|
||||
open: true,
|
||||
generation,
|
||||
value: String(generation),
|
||||
transitionStarting: false,
|
||||
transitionEnding: false,
|
||||
};
|
||||
}
|
||||
|
||||
function mockTransition() {
|
||||
const state = createState<TransitionState>({ active: false, status: 'idle' });
|
||||
const open = vi.fn(() => {
|
||||
state.patch({ active: true, status: 'starting' });
|
||||
return Promise.resolve();
|
||||
});
|
||||
const close = vi.fn(() => Promise.resolve());
|
||||
const cancel = vi.fn();
|
||||
const destroy = vi.fn();
|
||||
|
||||
transitionMock.createTransition.mockReturnValue({ state, open, close, cancel, destroy });
|
||||
|
||||
return { state, open, close, cancel, destroy };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
transitionMock.createTransition.mockReset();
|
||||
});
|
||||
|
||||
describe('useRenderedIndicatorState', () => {
|
||||
it('replays the open transition on updates by default', async () => {
|
||||
const transition = mockTransition();
|
||||
const { rerender } = renderHook(({ current }) => useRenderedIndicatorState(current), {
|
||||
initialProps: { current: makeState(1) },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(transition.open).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
rerender({ current: makeState(2) });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(transition.open).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('skips replaying the open transition on updates when disabled', async () => {
|
||||
const transition = mockTransition();
|
||||
const { rerender } = renderHook(({ current }) => useRenderedIndicatorState(current, { replayOnUpdate: false }), {
|
||||
initialProps: { current: makeState(1) },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(transition.open).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
rerender({ current: makeState(2) });
|
||||
|
||||
expect(transition.open).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('cancels an ending transition when replay is disabled and an update arrives', async () => {
|
||||
const transition = mockTransition();
|
||||
const { rerender } = renderHook(({ current }) => useRenderedIndicatorState(current, { replayOnUpdate: false }), {
|
||||
initialProps: { current: makeState(1) },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(transition.open).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
transition.state.patch({ active: true, status: 'ending' });
|
||||
rerender({ current: makeState(2) });
|
||||
|
||||
expect(transition.open).toHaveBeenCalledTimes(1);
|
||||
expect(transition.cancel).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import { useState, useSyncExternalStore } from 'react';
|
||||
import { useDestroy } from '../../utils/use-destroy';
|
||||
import { useIndicatorVisibility } from './use-indicator-visibility';
|
||||
import { useInputActionSubscription } from './use-input-action-subscription';
|
||||
import { useRenderedIndicatorState } from './use-rendered-indicator-state';
|
||||
import { type RenderedIndicatorOptions, useRenderedIndicatorState } from './use-rendered-indicator-state';
|
||||
|
||||
interface InputIndicatorRootCore<IndicatorState extends IndicatorLifecycleState, Props> {
|
||||
readonly state: StoreState<IndicatorState>;
|
||||
@@ -19,7 +19,8 @@ interface InputIndicatorRootCore<IndicatorState extends IndicatorLifecycleState,
|
||||
|
||||
export function useInputIndicatorRoot<IndicatorState extends IndicatorLifecycleState, Props>(
|
||||
createCore: () => InputIndicatorRootCore<IndicatorState, Props>,
|
||||
props: Props
|
||||
props: Props,
|
||||
options?: RenderedIndicatorOptions
|
||||
) {
|
||||
const [core] = useState(createCore);
|
||||
useDestroy(core);
|
||||
@@ -36,5 +37,5 @@ export function useInputIndicatorRoot<IndicatorState extends IndicatorLifecycleS
|
||||
() => core.state.current
|
||||
);
|
||||
|
||||
return useRenderedIndicatorState(currentState);
|
||||
return useRenderedIndicatorState(currentState, options);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,18 @@
|
||||
|
||||
import { getRenderedIndicatorState, type IndicatorLifecycleState, isIndicatorPresent } from '@videojs/core';
|
||||
import { createTransition } from '@videojs/core/dom';
|
||||
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
||||
import { useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
|
||||
|
||||
import { useDestroy } from '../../utils/use-destroy';
|
||||
|
||||
export function useRenderedIndicatorState<State extends IndicatorLifecycleState>(currentState: State) {
|
||||
export interface RenderedIndicatorOptions {
|
||||
replayOnUpdate?: boolean | undefined;
|
||||
}
|
||||
|
||||
export function useRenderedIndicatorState<State extends IndicatorLifecycleState>(
|
||||
currentState: State,
|
||||
options: RenderedIndicatorOptions = {}
|
||||
) {
|
||||
const elementRef = useRef<HTMLElement>(null);
|
||||
const currentStateRef = useRef(currentState);
|
||||
const snapshotRef = useRef(currentState);
|
||||
@@ -22,13 +29,18 @@ export function useRenderedIndicatorState<State extends IndicatorLifecycleState>
|
||||
|
||||
const { generation, open } = currentState;
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (open) {
|
||||
const nextState = currentStateRef.current;
|
||||
if (nextState.generation !== generation) return;
|
||||
|
||||
snapshotRef.current = nextState;
|
||||
void transition.open();
|
||||
const transitionState = transition.state.current;
|
||||
if (!transitionState.active || options.replayOnUpdate !== false) {
|
||||
void transition.open(elementRef.current);
|
||||
} else if (transitionState.status === 'ending') {
|
||||
transition.cancel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -36,7 +48,7 @@ export function useRenderedIndicatorState<State extends IndicatorLifecycleState>
|
||||
if (active && status !== 'ending') {
|
||||
void transition.close(elementRef.current);
|
||||
}
|
||||
}, [generation, open, transition]);
|
||||
}, [generation, open, options.replayOnUpdate, transition]);
|
||||
|
||||
return {
|
||||
elementRef,
|
||||
|
||||
@@ -18,7 +18,11 @@ export const VolumeIndicatorRoot = forwardRef(function VolumeIndicatorRoot(
|
||||
forwardedRef: ForwardedRef<HTMLDivElement>
|
||||
) {
|
||||
const { render, className, style, closeDelay, ...elementProps } = componentProps;
|
||||
const { elementRef, present, state } = useInputIndicatorRoot(() => new VolumeIndicatorCore(), { closeDelay });
|
||||
const { elementRef, present, state } = useInputIndicatorRoot(
|
||||
() => new VolumeIndicatorCore(),
|
||||
{ closeDelay },
|
||||
{ replayOnUpdate: false }
|
||||
);
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user