mirror of
https://github.com/zoriya/v10.git
synced 2026-08-14 18:04:49 +00:00
feat: add display click to play / pause (#117)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user