Skip to content

Commit

Permalink
fix(player): improve volume slider popups
Browse files Browse the repository at this point in the history
  • Loading branch information
mihar-22 committed Mar 6, 2024
1 parent 0a96ec6 commit 07ebc12
Show file tree
Hide file tree
Showing 13 changed files with 161 additions and 159 deletions.
48 changes: 4 additions & 44 deletions packages/react/src/components/layouts/default/audio-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@ import { useSignal } from 'maverick.js/react';
import { listenEvent, toggleClass } from 'maverick.js/std';

import { useChapterTitle } from '../../../hooks/use-chapter-title';
import {
useActive,
useMouseEnter,
useRectCSSVars,
useResizeObserver,
useTransitionActive,
} from '../../../hooks/use-dom';
import { useResizeObserver, useTransitionActive } from '../../../hooks/use-dom';
import { useMediaContext } from '../../../hooks/use-media-context';
import { useMediaState } from '../../../hooks/use-media-state';
import { createComputed } from '../../../hooks/use-signals';
Expand All @@ -26,17 +20,12 @@ import {
type Slots,
} from './slots';
import { DefaultAnnouncer } from './ui/announcer';
import {
DefaultCaptionButton,
DefaultMuteButton,
DefaultPlayButton,
DefaultSeekButton,
} from './ui/buttons';
import { DefaultCaptionButton, DefaultPlayButton, DefaultSeekButton } from './ui/buttons';
import { DefaultCaptions } from './ui/captions';
import { DefaultControlsSpacer } from './ui/controls';
import { DefaultChaptersMenu } from './ui/menus/chapters-menu';
import { DefaultSettingsMenu } from './ui/menus/settings-menu';
import { DefaultTimeSlider, DefaultVolumeSlider } from './ui/sliders';
import { DefaultTimeSlider, DefaultVolumePopup } from './ui/sliders';
import { DefaultTimeInvert } from './ui/time';

/* -------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -121,7 +110,7 @@ function AudioLayout() {
<DefaultAudioTitle />
{slot(slots, 'timeSlider', <DefaultTimeSlider />)}
<DefaultTimeInvert />
<DefaultAudioVolume />
<DefaultVolumePopup orientation="vertical" slots={slots} />
{slot(slots, 'captionButton', <DefaultCaptionButton tooltip="top center" />)}
<DefaultAudioMenus slots={slots} />
</Controls.Group>
Expand Down Expand Up @@ -231,32 +220,3 @@ function AudioTitle({ title, chapterTitle }: { title: string; chapterTitle: stri
}

AudioTitle.displayName = 'AudioTitle';

/* -------------------------------------------------------------------------------------------------
* DefaultAudioVolume
* -----------------------------------------------------------------------------------------------*/

function DefaultAudioVolume() {
const $pointer = useMediaState('pointer'),
$muted = useMediaState('muted'),
[rootEl, setRootEl] = React.useState<HTMLElement | null>(null),
[triggerEl, setTriggerEl] = React.useState<HTMLElement | null>(null),
[popperEl, setPopperEl] = React.useState<HTMLDivElement | null>(null),
isRootActive = useActive(rootEl),
hasMouseEnteredTrigger = useMouseEnter(triggerEl),
slots = useDefaultAudioLayoutSlots();

useRectCSSVars(rootEl, hasMouseEnteredTrigger ? triggerEl : null, 'trigger');
useRectCSSVars(rootEl, hasMouseEnteredTrigger ? popperEl : null, 'popper');

return $pointer === 'coarse' && !$muted ? null : (
<div className="vds-volume" data-active={isRootActive ? '' : null} ref={setRootEl}>
{slot(slots, 'muteButton', <DefaultMuteButton tooltip="top center" ref={setTriggerEl} />)}
<div className="vds-volume-popup" ref={setPopperEl}>
{slot(slots, 'volumeSlider', <DefaultVolumeSlider orientation="vertical" />)}
</div>
</div>
);
}

DefaultAudioVolume.displayName = 'DefaultAudioVolume';
33 changes: 32 additions & 1 deletion packages/react/src/components/layouts/default/ui/sliders.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,46 @@
import * as React from 'react';

import { useSignal } from 'maverick.js/react';
import type { SliderOrientation } from 'vidstack';

import { useResizeObserver } from '../../../../hooks/use-dom';
import { useActive, useResizeObserver } from '../../../../hooks/use-dom';
import { useMediaState } from '../../../../hooks/use-media-state';
import { isRemotionSource } from '../../../../providers/remotion/type-check';
import type { TimeSliderInstance } from '../../../primitives/instances';
import * as TimeSlider from '../../../ui/sliders/time-slider';
import * as VolumeSlider from '../../../ui/sliders/volume-slider';
import { RemotionSliderThumbnail } from '../../remotion-ui';
import { useDefaultLayoutContext, useDefaultLayoutWord } from '../context';
import { slot, type DefaultLayoutSlots } from '../slots';
import { DefaultMuteButton } from './buttons';

/* -------------------------------------------------------------------------------------------------
* DefaultVolumePopup
* -----------------------------------------------------------------------------------------------*/

export interface DefaultVolumePopupProps {
slots?: DefaultLayoutSlots;
orientation: SliderOrientation;
}

function DefaultVolumePopup({ orientation, slots }: DefaultVolumePopupProps) {
const $pointer = useMediaState('pointer'),
$muted = useMediaState('muted'),
[rootEl, setRootEl] = React.useState<HTMLElement | null>(null),
isRootActive = useActive(rootEl);

return $pointer === 'coarse' && !$muted ? null : (
<div className="vds-volume" data-active={isRootActive ? '' : null} ref={setRootEl}>
{slot(slots, 'muteButton', <DefaultMuteButton tooltip="top center" />)}
<div className="vds-volume-popup">
{slot(slots, 'volumeSlider', <DefaultVolumeSlider orientation={orientation} />)}
</div>
</div>
);
}

DefaultVolumePopup.displayName = 'DefaultVolumePopup';
export { DefaultVolumePopup };

/* -------------------------------------------------------------------------------------------------
* DefaultVolumeSlider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { DefaultControlsSpacer } from './ui/controls';
import { DefaultKeyboardDisplay } from './ui/keyboard-display';
import { DefaultChaptersMenu } from './ui/menus/chapters-menu';
import { DefaultSettingsMenu } from './ui/menus/settings-menu';
import { DefaultTimeSlider, DefaultVolumeSlider } from './ui/sliders';
import { DefaultTimeSlider, DefaultVolumePopup } from './ui/sliders';
import { DefaultTimeInfo } from './ui/time';
import { DefaultChapterTitle } from './ui/title';

Expand Down Expand Up @@ -126,8 +126,7 @@ function DefaultVideoLargeLayout() {

<Controls.Group className="vds-controls-group">
{slot(slots, 'playButton', <DefaultPlayButton tooltip="top start" />)}
{slot(slots, 'muteButton', <DefaultMuteButton tooltip="top" />)}
{slot(slots, 'volumeSlider', <DefaultVolumeSlider />)}
<DefaultVolumePopup orientation="horizontal" slots={slots} />
<DefaultTimeInfo slots={slots} />
{slot(slots, 'chapterTitle', <DefaultChapterTitle />)}
{slot(slots, 'captionButton', <DefaultCaptionButton tooltip="top" />)}
Expand Down
18 changes: 11 additions & 7 deletions packages/react/src/hooks/use-dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,22 @@ export function useTransitionActive(el: Element | null) {
}

export function useMouseEnter(el: Element | null) {
const [hasEntered, setHasEntered] = React.useState(false);
const [isMouseEnter, setIsMouseEnter] = React.useState(false);

React.useEffect(() => {
if (!el) return;

const disposal = createDisposalBin();

disposal.add(
listenEvent(el, 'mouseenter', () => setHasEntered(true)),
listenEvent(el, 'mouseleave', () => setHasEntered(false)),
listenEvent(el, 'mouseenter', () => setIsMouseEnter(true)),
listenEvent(el, 'mouseleave', () => setIsMouseEnter(false)),
);

return () => disposal.empty();
}, [el]);

return hasEntered;
return isMouseEnter;
}

export function useFocusIn(el: Element | null) {
Expand All @@ -86,10 +86,14 @@ export function useFocusIn(el: Element | null) {
}

export function useActive(el: Element | null) {
const hasMouseEntered = useMouseEnter(el),
isFocusIn = useFocusIn(el);
const isMouseEnter = useMouseEnter(el),
isFocusIn = useFocusIn(el),
prevMouseEnter = React.useRef(false);

return hasMouseEntered || isFocusIn;
if (prevMouseEnter.current && !isMouseEnter) return false;

prevMouseEnter.current = isMouseEnter;
return isMouseEnter || isFocusIn;
}

export function useRectCSSVars(root: Element | null, el: Element | null, prefix: string) {
Expand Down
33 changes: 14 additions & 19 deletions packages/vidstack/player/styles/default/layouts/audio.css
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@
:where(.vds-audio-layout .vds-volume) {
--media-slider-height: var(--audio-volume-height, 96px);
--media-slider-preview-offset: 6px;
--gap: var(--audio-volume-gap, 16px);
position: relative;
display: flex;
align-items: center;
Expand All @@ -310,7 +311,7 @@

:where(.vds-audio-layout .vds-volume-popup) {
position: absolute;
bottom: calc(100% + 16px);
bottom: calc(100% + var(--gap));
left: 50%;
opacity: 0;
transform: translateX(-50%);
Expand All @@ -324,29 +325,23 @@
visibility: hidden;
}

@media (prefers-color-scheme: light) {
:where(.vds-audio-layout .vds-volume-popup) {
border: var(--audio-volume-border, 1px solid rgb(10 10 10 / 0.1));
background-color: var(--audio-volume-bg, var(--media-menu-bg, rgb(250 250 250)));
}
}

/* safe triangle. */
.vds-audio-layout .vds-volume::after {
/* safe area. */
.vds-audio-layout .vds-volume-popup::after {
content: '';
position: fixed;
top: 0;
bottom: calc(-1 * var(--gap));
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: var(--gap);
z-index: 1;
pointer-events: auto;
clip-path: polygon(
var(--trigger-left, 0) var(--trigger-top, 0),
var(--popper-left, 0) var(--popper-bottom, 0),
var(--popper-right, 0) var(--popper-bottom, 0),
var(--trigger-right, 0) var(--trigger-top, 0)
);
}

@media (prefers-color-scheme: light) {
:where(.vds-audio-layout .vds-volume-popup) {
border: var(--audio-volume-border, 1px solid rgb(10 10 10 / 0.1));
background-color: var(--audio-volume-bg, var(--media-menu-bg, rgb(250 250 250)));
}
}

:where(.vds-audio-layout .vds-volume[data-active] .vds-volume-popup),
Expand Down
50 changes: 31 additions & 19 deletions packages/vidstack/player/styles/default/layouts/video.css
Original file line number Diff line number Diff line change
Expand Up @@ -139,34 +139,46 @@
text-shadow: unset;
}

/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Volume Slider
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

:where(.vds-video-layout .vds-volume) {
--gap: var(--video-volume-gap, 10px);
display: contents;
}

:where(.vds-video-layout .vds-volume-popup) {
display: contents;
}

:where(.vds-video-layout .vds-volume-slider) {
width: 100%;
margin: 0;
max-width: 0;
transition: all 0.15s ease;
transform: translateX(-2px);
}

:where(.vds-video-layout [data-media-mute-button][data-hocus] + .vds-volume-slider),
:where(
.vds-video-layout
[data-media-mute-button][data-hocus]
+ .vds-tooltip-content
+ .vds-volume-slider
),
:where(.vds-video-layout [data-media-mute-tooltip][data-hocus] + .vds-volume-slider),
:where(.vds-video-layout .vds-volume-slider[data-active]) {
margin-left: 9px;
:where(.vds-video-layout .vds-volume[data-active] .vds-volume-slider),
:where(.vds-video-layout .vds-volume:has([data-active]) .vds-volume-slider) {
margin-left: var(--gap);
opacity: 1;
visibility: visible;
max-width: var(--video-volume-slider-max-width, 72px);
}

:where(.vds-video-layout .vds-volume-slider .vds-slider-value) {
bottom: 70px;
}

@media (orientation: landscape) and (pointer: coarse) {
:where(.vds-video-layout .vds-volume-slider) {
display: none;
}
/* safe area. */
.vds-video-layout .vds-volume-slider::after {
content: '';
position: fixed;
top: 0;
left: calc(-1 * var(--gap));
width: var(--gap);
height: 100%;
z-index: 1;
pointer-events: auto;
}

/*
Expand Down
10 changes: 8 additions & 2 deletions packages/vidstack/src/components/ui/menu/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ import {
import { useMediaContext, type MediaContext } from '../../../core/api/media-context';
import type { MediaRequestEvents } from '../../../core/api/media-request-events';
import { $ariaBool } from '../../../utils/aria';
import { isElementParent, isEventInside, onPress, setAttributeIfEmpty } from '../../../utils/dom';
import {
isElementParent,
isElementVisible,
isEventInside,
onPress,
setAttributeIfEmpty,
} from '../../../utils/dom';
import { Popper } from '../popper/popper';
import { sliderObserverContext } from '../sliders/slider/slider-context';
import type { MenuButton } from './menu-button';
Expand Down Expand Up @@ -515,8 +521,8 @@ export class Menu extends Component<MenuProps, {}, MenuEvents> {
} else if (child.nodeType === 3) {
height += parseFloat(getComputedStyle(child).fontSize);
} else if (child instanceof HTMLElement) {
if (!isElementVisible(child)) continue;
const style = getComputedStyle(child);
if (style.display === 'none') continue;
height +=
child.offsetHeight +
(parseFloat(style.marginTop) || 0) +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ export class SliderPreview extends Component<SliderPreviewProps> {

if (_disabled()) return;

const el = this.el!,
const el = this.el,
{ offset, noClamp } = this.$props;

if (!el) return;

updateSliderPreviewPlacement(el, {
clamp: !noClamp(),
offset: offset(),
Expand Down
Loading

0 comments on commit 07ebc12

Please sign in to comment.