feat: add display click to play / pause (#117)

This commit is contained in:
Wesley Luyten
2025-10-23 12:24:34 -05:00
committed by GitHub
parent fcc3bf2d7d
commit 4f06ef6c76
7 changed files with 146 additions and 99 deletions
@@ -109,7 +109,6 @@ export class Slider {
}
#handlePointerDown(event: PointerEvent) {
event.preventDefault();
this.#element?.setPointerCapture(event.pointerId);
this.setState({ _pointerRatio: this.getPointerRatio(event), _dragging: true });
@@ -41,13 +41,14 @@ export class TimeSlider extends Slider {
return { ...state, _fillWidth, _currentTimeText, _durationText };
}
setState(state: Partial<TimeSliderState>): void {
setState(newState: Partial<TimeSliderState>): void {
const state = this.getState();
// When not dragging or keying, set pointer ratio to current time / duration.
if (!state._dragging && !state._keying && state.currentTime && state.duration) {
super.setState({ ...state, _pointerRatio: state.currentTime / state.duration });
if (!state._dragging && !state._keying && newState.currentTime && newState.duration) {
super.setState({ ...newState, _pointerRatio: newState.currentTime / newState.duration });
return;
}
super.setState(state);
super.setState(newState);
}
handleEvent(event: Event): void {
@@ -33,13 +33,14 @@ export class VolumeSlider extends Slider {
return { ...state, _fillWidth, _volumeText };
}
setState(state: Partial<VolumeSliderState>): void {
setState(newState: Partial<VolumeSliderState>): void {
const state = this.getState();
// When not dragging or keying, set pointer ratio to current volume.
if (!state._dragging && !state._keying && state.volume) {
super.setState({ ...state, _pointerRatio: state.volume });
if (!state._dragging && !state._keying && newState.volume) {
super.setState({ ...newState, _pointerRatio: newState.volume });
return;
}
super.setState(state);
super.setState(newState);
}
handleEvent(event: Event): void {
+26 -1
View File
@@ -21,11 +21,13 @@ export class MediaContainer extends CustomElementConsumer {
_mediaStore: any;
_mediaSlot: HTMLSlotElement;
_paused: boolean = true;
contexts = {
mediaStore: (mediaStore: any): void => {
this._mediaStore = mediaStore;
this._handleMediaSlotChange();
this._registerContainerStateOwner();
this._subscribeToPlayState();
},
};
@@ -39,6 +41,9 @@ export class MediaContainer extends CustomElementConsumer {
this._mediaSlot = this.shadowRoot!.querySelector('slot[name=media]') as HTMLSlotElement;
this._mediaSlot.addEventListener('slotchange', this._handleMediaSlotChange);
// Add click handler for play/pause functionality
this.addEventListener('click', this._handleClick);
}
connectedCallback(): void {
@@ -65,9 +70,29 @@ export class MediaContainer extends CustomElementConsumer {
const media = this._mediaSlot.assignedElements({ flatten: true })[0];
this._mediaStore.dispatch({ type: 'mediastateownerchangerequest', detail: media });
};
_handleClick = (event: Event): void => {
if (!this._mediaStore) return;
if (!['video', 'audio'].includes((event.target as HTMLElement).localName || '')) return;
if (this._paused) {
this._mediaStore.dispatch({ type: 'playrequest' });
} else {
this._mediaStore.dispatch({ type: 'pauserequest' });
}
};
_subscribeToPlayState = (): void => {
if (!this._mediaStore) return;
// Subscribe to paused state changes
this._mediaStore.subscribe((state: any) => {
this._paused = state.paused ?? true;
});
};
}
// Register the custom element
if (!globalThis.customElements.get('media-container')) {
// @ts-expect-error ts(2345)
globalThis.customElements.define('media-container', MediaContainer);
@@ -42,7 +42,7 @@ export function getTemplateHTML() {
display: flex;
flex-flow: column nowrap;
align-items: start;
/* pointer-events: none; */
pointer-events: none;
background: none;
}
@@ -101,6 +101,10 @@ export function getTemplateHTML() {
/* Media Control Bar UI/Styles */
.control-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgb(20 20 30 / .7);
display: flex;
align-items: center;
@@ -108,10 +112,6 @@ export function getTemplateHTML() {
width: 100%;
}
.spacer {
flex-grow: 1;
}
/* TimeSlider Component Styles */
media-time-slider-root {
display: flex;
@@ -298,87 +298,86 @@ export function getTemplateHTML() {
</style>
<media-container>
<slot name="media" slot="media"></slot>
<div class="overlay">
<div class="spacer"></div>
<div class="control-bar">
<!-- NOTE: We can decide if we further want to provide a further, "themed" media-play-button that comes with baked in default styles and icons. (CJP) -->
<media-tooltip-root delay="600" close-delay="0">
<media-tooltip-trigger>
<media-play-button class="button">
<media-play-icon class="icon play-icon"></media-play-icon>
<media-pause-icon class="icon pause-icon"></media-pause-icon>
</media-play-button>
</media-tooltip-trigger>
<media-tooltip-portal>
<media-tooltip-positioner side="top" side-offset="8" collision-padding="8">
<media-tooltip-popup>
<span class="tooltip play-tooltip">Play</span>
<span class="tooltip pause-tooltip">Pause</span>
</media-tooltip-popup>
</media-tooltip-positioner>
</media-tooltip-portal>
</media-tooltip-root>
<!-- Use the show-remaining attribute to show count down/remaining time -->
<media-current-time-display show-remaining></media-current-time-display>
<media-tooltip-root track-cursor-axis="x">
<media-tooltip-trigger>
<media-time-slider-root>
<media-time-slider-track>
<media-time-slider-progress></media-time-slider-progress>
<media-time-slider-pointer></media-time-slider-pointer>
</media-time-slider-track>
<media-time-slider-thumb></media-time-slider-thumb>
</media-time-slider-root>
</media-tooltip-trigger>
<media-tooltip-portal>
<media-tooltip-positioner side="top" side-offset="18" collision-padding="12">
<media-tooltip-popup>
<preview-time-display></preview-time-display>
</media-tooltip-popup>
</media-tooltip-positioner>
</media-tooltip-portal>
</media-tooltip-root>
<div class="overlay"></div>
<media-duration-display></media-duration-display>
<media-popover-root open-on-hover delay="200" close-delay="100">
<media-popover-trigger>
<media-mute-button class="button">
<media-volume-high-icon class="icon volume-high-icon"></media-volume-high-icon>
<media-volume-low-icon class="icon volume-low-icon"></media-volume-low-icon>
<media-volume-off-icon class="icon volume-off-icon"></media-volume-off-icon>
</media-mute-button>
</media-popover-trigger>
<media-popover-portal>
<media-popover-positioner side="top">
<media-popover-popup>
<media-volume-slider-root orientation="vertical">
<media-volume-slider-track>
<media-volume-slider-progress></media-volume-slider-progress>
</media-volume-slider-track>
<media-volume-slider-thumb></media-volume-slider-thumb>
</media-volume-slider-root>
</media-popover-popup>
</media-popover-positioner>
</media-popover-portal>
</media-popover-root>
<media-tooltip-root delay="600" close-delay="0">
<media-tooltip-trigger>
<media-fullscreen-button class="button">
<media-fullscreen-enter-icon class="icon fullscreen-enter-icon"></media-fullscreen-enter-icon>
<media-fullscreen-exit-icon class="icon fullscreen-exit-icon"></media-fullscreen-exit-icon>
</media-fullscreen-button>
</media-tooltip-trigger>
<media-tooltip-portal>
<media-tooltip-positioner side="top" side-offset="8" collision-padding="8">
<media-tooltip-popup>
<span class="tooltip fullscreen-enter-tooltip">Enter Fullscreen</span>
<span class="tooltip fullscreen-exit-tooltip">Exit Fullscreen</span>
</media-tooltip-popup>
</media-tooltip-positioner>
</media-tooltip-portal>
</media-tooltip-root>
</div>
<div class="control-bar">
<!-- NOTE: We can decide if we further want to provide a further, "themed" media-play-button that comes with baked in default styles and icons. (CJP) -->
<media-tooltip-root delay="600" close-delay="0">
<media-tooltip-trigger>
<media-play-button class="button">
<media-play-icon class="icon play-icon"></media-play-icon>
<media-pause-icon class="icon pause-icon"></media-pause-icon>
</media-play-button>
</media-tooltip-trigger>
<media-tooltip-portal>
<media-tooltip-positioner side="top" side-offset="8" collision-padding="8">
<media-tooltip-popup>
<span class="tooltip play-tooltip">Play</span>
<span class="tooltip pause-tooltip">Pause</span>
</media-tooltip-popup>
</media-tooltip-positioner>
</media-tooltip-portal>
</media-tooltip-root>
<!-- Use the show-remaining attribute to show count down/remaining time -->
<media-current-time-display show-remaining></media-current-time-display>
<media-tooltip-root track-cursor-axis="x">
<media-tooltip-trigger>
<media-time-slider-root>
<media-time-slider-track>
<media-time-slider-progress></media-time-slider-progress>
<media-time-slider-pointer></media-time-slider-pointer>
</media-time-slider-track>
<media-time-slider-thumb></media-time-slider-thumb>
</media-time-slider-root>
</media-tooltip-trigger>
<media-tooltip-portal>
<media-tooltip-positioner side="top" side-offset="18" collision-padding="12">
<media-tooltip-popup>
<preview-time-display></preview-time-display>
</media-tooltip-popup>
</media-tooltip-positioner>
</media-tooltip-portal>
</media-tooltip-root>
<media-duration-display></media-duration-display>
<media-popover-root open-on-hover delay="200" close-delay="100">
<media-popover-trigger>
<media-mute-button class="button">
<media-volume-high-icon class="icon volume-high-icon"></media-volume-high-icon>
<media-volume-low-icon class="icon volume-low-icon"></media-volume-low-icon>
<media-volume-off-icon class="icon volume-off-icon"></media-volume-off-icon>
</media-mute-button>
</media-popover-trigger>
<media-popover-portal>
<media-popover-positioner side="top">
<media-popover-popup>
<media-volume-slider-root orientation="vertical">
<media-volume-slider-track>
<media-volume-slider-progress></media-volume-slider-progress>
</media-volume-slider-track>
<media-volume-slider-thumb></media-volume-slider-thumb>
</media-volume-slider-root>
</media-popover-popup>
</media-popover-positioner>
</media-popover-portal>
</media-popover-root>
<media-tooltip-root delay="600" close-delay="0">
<media-tooltip-trigger>
<media-fullscreen-button class="button">
<media-fullscreen-enter-icon class="icon fullscreen-enter-icon"></media-fullscreen-enter-icon>
<media-fullscreen-exit-icon class="icon fullscreen-exit-icon"></media-fullscreen-exit-icon>
</media-fullscreen-button>
</media-tooltip-trigger>
<media-tooltip-portal>
<media-tooltip-positioner side="top" side-offset="8" collision-padding="8">
<media-tooltip-popup>
<span class="tooltip fullscreen-enter-tooltip">Enter Fullscreen</span>
<span class="tooltip fullscreen-exit-tooltip">Exit Fullscreen</span>
</media-tooltip-popup>
</media-tooltip-positioner>
</media-tooltip-portal>
</media-tooltip-root>
</div>
</media-container>
`;
@@ -1,9 +1,10 @@
import type { FC, HTMLProps, PropsWithChildren, RefCallback } from 'react';
import { useMediaStore } from '@vjs-10/react-media-store';
import { playButtonStateDefinition } from '@vjs-10/media-store';
import { forwardRef, useCallback } from 'react';
import { shallowEqual, useMediaSelector, useMediaStore } from '@vjs-10/react-media-store';
import { forwardRef, useCallback, useMemo } from 'react';
import { useComposedRefs } from '../utils/useComposedRefs';
/**
@@ -21,6 +22,7 @@ import { useComposedRefs } from '../utils/useComposedRefs';
* return <div ref={containerRef}>{children}</div>;
* };
*/
// eslint-disable-next-line react-refresh/only-export-components
export function useMediaContainerRef(): RefCallback<HTMLElement | null> {
const mediaStore = useMediaStore();
@@ -56,8 +58,28 @@ export const MediaContainer: FC<PropsWithChildren<HTMLProps<HTMLDivElement> & {
({ children, portalId = '@default_portal_id', ...props }, ref) => {
const containerRef = useMediaContainerRef();
const composedRef = useComposedRefs(ref, containerRef);
const mediaStore = useMediaStore();
const mediaState = useMediaSelector(playButtonStateDefinition.stateTransform, shallowEqual);
const methods = useMemo(() => playButtonStateDefinition.createRequestMethods(mediaStore.dispatch), [mediaStore]);
const handleClick = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
if (!['video', 'audio'].includes((event.target as HTMLElement).localName || '')) return;
if (mediaState.paused) {
methods.requestPlay();
} else {
methods.requestPause();
}
}, [mediaState.paused, methods]);
return (
<div ref={composedRef} {...props}>
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div
ref={composedRef}
onClick={handleClick}
{...props}
>
{children}
{/* @TODO We need to make sure this is non-brittle longer term (CJP) */}
<div id={portalId} style={{ position: 'absolute', zIndex: 10 }} />
@@ -23,7 +23,7 @@ const styles: MinimalSkinStyles = {
'vjs:[&_video]:rounded-[inherit] vjs:[&_video]:w-full vjs:[&_video]:h-auto',
),
Overlay: cn(
'vjs:absolute vjs:inset-0 vjs:rounded-[inherit]',
'vjs:absolute vjs:inset-0 vjs:rounded-[inherit] vjs:pointer-events-none',
'vjs:bg-gradient-to-t vjs:from-black/70 vjs:via-black/50 vjs:via-[120px] vjs:to-transparent',
'vjs:opacity-0 vjs:delay-500 vjs:duration-300',
// FIXME: Temporary hide/show logic