diff --git a/packages/core/core/src/components/slider.ts b/packages/core/core/src/components/slider.ts index e7b8ec87..5b961cf7 100644 --- a/packages/core/core/src/components/slider.ts +++ b/packages/core/core/src/components/slider.ts @@ -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 }); diff --git a/packages/core/core/src/components/time-slider.ts b/packages/core/core/src/components/time-slider.ts index 8b82b7c8..5b5e1875 100644 --- a/packages/core/core/src/components/time-slider.ts +++ b/packages/core/core/src/components/time-slider.ts @@ -41,13 +41,14 @@ export class TimeSlider extends Slider { return { ...state, _fillWidth, _currentTimeText, _durationText }; } - setState(state: Partial): void { + setState(newState: Partial): 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 { diff --git a/packages/core/core/src/components/volume-slider.ts b/packages/core/core/src/components/volume-slider.ts index ebc1a764..e62307f7 100644 --- a/packages/core/core/src/components/volume-slider.ts +++ b/packages/core/core/src/components/volume-slider.ts @@ -33,13 +33,14 @@ export class VolumeSlider extends Slider { return { ...state, _fillWidth, _volumeText }; } - setState(state: Partial): void { + setState(newState: Partial): 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 { diff --git a/packages/html/html/src/media-container.ts b/packages/html/html/src/media-container.ts index dec0bd3e..47ee59e9 100644 --- a/packages/html/html/src/media-container.ts +++ b/packages/html/html/src/media-container.ts @@ -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); diff --git a/packages/html/html/src/skins/media-skin-default.ts b/packages/html/html/src/skins/media-skin-default.ts index 8d116fad..1b11052b 100644 --- a/packages/html/html/src/skins/media-skin-default.ts +++ b/packages/html/html/src/skins/media-skin-default.ts @@ -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() { -
-
-
- - - - - - - - - - - - Play - Pause - - - - - - - - - - - - - - - - - - - - - - - - - +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Enter Fullscreen - Exit Fullscreen - - - - -
+
+ + + + + + + + + + + + Play + Pause + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Enter Fullscreen + Exit Fullscreen + + + +
`; diff --git a/packages/react/react/src/components/MediaContainer.tsx b/packages/react/react/src/components/MediaContainer.tsx index 702ca310..5232ccc6 100644 --- a/packages/react/react/src/components/MediaContainer.tsx +++ b/packages/react/react/src/components/MediaContainer.tsx @@ -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
{children}
; * }; */ +// eslint-disable-next-line react-refresh/only-export-components export function useMediaContainerRef(): RefCallback { const mediaStore = useMediaStore(); @@ -56,8 +58,28 @@ export const MediaContainer: FC & { ({ 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) => { + if (!['video', 'audio'].includes((event.target as HTMLElement).localName || '')) return; + + if (mediaState.paused) { + methods.requestPlay(); + } else { + methods.requestPause(); + } + }, [mediaState.paused, methods]); + return ( -
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions +
{children} {/* @TODO We need to make sure this is non-brittle longer term (CJP) */}
diff --git a/packages/react/react/src/skins/minimal/styles.ts b/packages/react/react/src/skins/minimal/styles.ts index 9af5b4d2..11873b1b 100644 --- a/packages/react/react/src/skins/minimal/styles.ts +++ b/packages/react/react/src/skins/minimal/styles.ts @@ -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