chore(root): prepare workspace for alpha (#276)

This commit is contained in:
rahim
2025-12-21 20:26:13 +11:00
committed by GitHub
parent 5ddf02278d
commit b89fef56dd
247 changed files with 1197 additions and 446 deletions
@@ -0,0 +1,76 @@
import { namedNodeMapToObject } from '@videojs/utils-preview/dom';
export function getTemplateHTML(
this: typeof ButtonElement,
_attrs: Record<string, string>,
_props: Record<string, any> = {},
): string {
return /* html */ `
<style>
/*
NOTE: Even though primitives should aim to be "unstyled" in their core definitions, we should
still add pointer-events, as this defines functionality. (CJP)
*/
:host {
pointer-events: auto;
}
</style>
<slot>
</slot>
`;
}
export class ButtonElement extends HTMLElement {
static shadowRootOptions = {
mode: 'open' as ShadowRootMode,
};
static getTemplateHTML: typeof getTemplateHTML = getTemplateHTML;
static observedAttributes: string[] = ['commandfor'];
constructor() {
super();
if (!this.shadowRoot) {
// Set up the Shadow DOM if not using Declarative Shadow DOM.
this.attachShadow((this.constructor as typeof ButtonElement).shadowRootOptions);
const attrs = namedNodeMapToObject(this.attributes);
const html = (this.constructor as typeof ButtonElement).getTemplateHTML(attrs);
// From MDN: setHTMLUnsafe should be used instead of ShadowRoot.innerHTML
// when a string of HTML may contain declarative shadow roots.
const shadowRoot = this.shadowRoot as unknown as ShadowRoot;
shadowRoot.setHTMLUnsafe ? shadowRoot.setHTMLUnsafe(html) : (shadowRoot.innerHTML = html);
}
this.addEventListener('click', this);
this.addEventListener('keydown', this);
}
attributeChangedCallback(name: string, _oldValue: string, newValue: string): void {
if (name === 'commandfor') {
this.style.setProperty('anchor-name', `--${newValue}`);
}
}
handleEvent(event: Event): void {
const { type } = event;
if (type === 'keydown') {
this.#handleKeyDown(event as KeyboardEvent);
}
}
#handleKeyDown = (event: KeyboardEvent): void => {
const { metaKey, altKey, key } = event;
if (metaKey || altKey || !['Enter', ' '].includes(key)) {
this.removeEventListener('keyup', this.#handleKeyUp);
return;
}
this.addEventListener('keyup', this.#handleKeyUp, { once: true });
};
#handleKeyUp = (_event: KeyboardEvent): void => {
this.handleEvent({ type: 'click' } as Event);
};
}
@@ -0,0 +1,56 @@
import type { CurrentTimeDisplayState } from '@videojs/core-preview/store';
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
import { currentTimeDisplayStateDefinition } from '@videojs/core-preview/store';
import { formatDisplayTime } from '@videojs/utils-preview';
import { toConnectedHTMLComponent } from '../utils/component-factory';
export const getCurrentTimeDisplayState: StateHook<CurrentTimeDisplay, CurrentTimeDisplayState> = (_element, mediaStore) => {
return {
...currentTimeDisplayStateDefinition.stateTransform(mediaStore.getState()),
// Current time display is read-only, so no request methods needed
};
};
export const getCurrentTimeDisplayProps: PropsHook<CurrentTimeDisplay, CurrentTimeDisplayState> = (_element, _state) => {
return {};
};
export class CurrentTimeDisplay extends HTMLElement {
static shadowRootOptions = {
mode: 'open' as ShadowRootMode,
};
static observedAttributes: string[] = ['show-remaining'];
constructor() {
super();
if (!this.shadowRoot) {
this.attachShadow((this.constructor as typeof CurrentTimeDisplay).shadowRootOptions);
}
}
get showRemaining(): boolean {
return this.hasAttribute('show-remaining');
}
_update(_props: any, state: CurrentTimeDisplayState): void {
/** @TODO Should this live here or elsewhere? (CJP) */
const timeLabel
= this.showRemaining && state.duration != null && state.currentTime != null
? formatDisplayTime(-(state.duration - state.currentTime))
: formatDisplayTime(state.currentTime);
if (this.shadowRoot) {
this.shadowRoot.textContent = timeLabel;
}
}
}
export const CurrentTimeDisplayElement: ConnectedComponentConstructor<CurrentTimeDisplay, CurrentTimeDisplayState> = toConnectedHTMLComponent(
CurrentTimeDisplay,
getCurrentTimeDisplayState,
getCurrentTimeDisplayProps,
'CurrentTimeDisplay',
);
@@ -0,0 +1,65 @@
import type { DurationDisplayState } from '@videojs/core-preview/store';
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
import { durationDisplayStateDefinition } from '@videojs/core-preview/store';
import { formatDisplayTime } from '@videojs/utils-preview';
import { namedNodeMapToObject } from '@videojs/utils-preview/dom';
import { toConnectedHTMLComponent } from '../utils/component-factory';
export function getTemplateHTML(
this: typeof DurationDisplay,
_attrs: Record<string, string>,
_props: Record<string, any> = {},
) {
return /* html */ `
<span></span>
`;
}
export class DurationDisplay extends HTMLElement {
static shadowRootOptions = {
mode: 'open' as ShadowRootMode,
};
static getTemplateHTML: typeof getTemplateHTML = getTemplateHTML;
constructor() {
super();
if (!this.shadowRoot) {
this.attachShadow((this.constructor as typeof DurationDisplay).shadowRootOptions);
const attrs = namedNodeMapToObject(this.attributes);
const html = (this.constructor as typeof DurationDisplay).getTemplateHTML(attrs);
const shadowRoot = this.shadowRoot as unknown as ShadowRoot;
shadowRoot.setHTMLUnsafe ? shadowRoot.setHTMLUnsafe(html) : (shadowRoot.innerHTML = html);
}
}
_update(_props: any, state: DurationDisplayState): void {
// Update the span content with formatted duration
const spanElement = this.shadowRoot?.querySelector('span') as HTMLElement;
if (spanElement) {
spanElement.textContent = formatDisplayTime(state.duration);
}
}
}
export const getDurationDisplayState: StateHook<DurationDisplay, DurationDisplayState> = (_element, mediaStore) => {
return {
...durationDisplayStateDefinition.stateTransform(mediaStore.getState()),
// Duration display is read-only, so no request methods needed
};
};
export const getDurationDisplayProps: PropsHook<DurationDisplay, DurationDisplayState> = (_element, _state) => {
return {};
};
export const DurationDisplayElement: ConnectedComponentConstructor<DurationDisplay, DurationDisplayState> = toConnectedHTMLComponent(
DurationDisplay,
getDurationDisplayState,
getDurationDisplayProps,
'DurationDisplay',
);
@@ -0,0 +1,67 @@
import type { FullscreenButtonState } from '@videojs/core-preview/store';
import type { Prettify } from '../types';
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
import { fullscreenButtonStateDefinition } from '@videojs/core-preview/store';
import { memoize } from '@videojs/utils-preview';
import { toConnectedHTMLComponent } from '../utils/component-factory';
import { ButtonElement } from './button';
type FullscreenButtonStateWithMethods = Prettify<FullscreenButtonState & ReturnType<typeof fullscreenButtonStateDefinition.createRequestMethods>>;
const fullscreenButtonCreateRequestMethods = memoize(fullscreenButtonStateDefinition.createRequestMethods);
/**
* FullscreenButton state hook - equivalent to React's useFullscreenButtonState
* Handles media store state subscription and transformation
*/
export const getFullscreenButtonState: StateHook<FullscreenButton, FullscreenButtonStateWithMethods> = (_element, mediaStore) => {
return {
...fullscreenButtonStateDefinition.stateTransform(mediaStore.getState()),
...fullscreenButtonCreateRequestMethods(mediaStore.dispatch),
};
};
export const getFullscreenButtonProps: PropsHook<FullscreenButton, FullscreenButtonStateWithMethods> = (_element, state) => {
const baseProps: Record<string, any> = {
/** data attributes/props */
'data-fullscreen': state.fullscreen,
/** @TODO Need another state provider in core for i18n (CJP) */
/** aria attributes/props */
role: 'button',
tabindex: '0',
'aria-label': state.fullscreen ? 'exit fullscreen' : 'enter fullscreen',
/** tooltip */
'data-tooltip': state.fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen',
/** @TODO Figure out how we want to handle attr overrides (e.g. aria-label) (CJP) */
/** external props spread last to allow for overriding */
// ...props,
};
return baseProps;
};
export class FullscreenButton extends ButtonElement {
_state: FullscreenButtonStateWithMethods | undefined;
handleEvent(event: Event): void {
super.handleEvent(event);
const { type } = event;
const state = this._state;
if (state && type === 'click') {
if (state.fullscreen) {
state.requestExitFullscreen();
} else {
state.requestEnterFullscreen();
}
}
}
}
export const FullscreenButtonElement: ConnectedComponentConstructor<FullscreenButton, FullscreenButtonStateWithMethods> = toConnectedHTMLComponent(
FullscreenButton,
getFullscreenButtonState,
getFullscreenButtonProps,
'FullscreenButton',
);
@@ -0,0 +1,67 @@
import type { MuteButtonState } from '@videojs/core-preview/store';
import type { Prettify } from '../types';
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
import { muteButtonStateDefinition } from '@videojs/core-preview/store';
import { memoize } from '@videojs/utils-preview';
import { toConnectedHTMLComponent } from '../utils/component-factory';
import { ButtonElement } from './button';
type MuteButtonStateWithMethods = Prettify<MuteButtonState & ReturnType<typeof muteButtonStateDefinition.createRequestMethods>>;
const muteButtonCreateRequestMethods = memoize(muteButtonStateDefinition.createRequestMethods);
export const getMuteButtonState: StateHook<MuteButton, MuteButtonStateWithMethods> = (_element, mediaStore) => {
return {
...muteButtonStateDefinition.stateTransform(mediaStore.getState()),
...muteButtonCreateRequestMethods(mediaStore.dispatch),
};
};
export const getMuteButtonProps: PropsHook<MuteButton, MuteButtonStateWithMethods> = (_element, state) => {
const baseProps: Record<string, any> = {
/** data attributes/props */
'data-muted': state.muted,
'data-volume-level': state.volumeLevel,
/** @TODO Need another state provider in core for i18n (CJP) */
/** aria attributes/props */
role: 'button',
tabindex: '0',
'aria-label': state.muted ? 'unmute' : 'mute',
/** tooltip */
'data-tooltip': state.muted ? 'Unmute' : 'Mute',
/** @TODO Figure out how we want to handle attr overrides (e.g. aria-label) (CJP) */
/** external props spread last to allow for overriding */
// ...props,
};
return baseProps;
};
export class MuteButton extends ButtonElement {
_state: MuteButtonStateWithMethods | undefined;
handleEvent(event: Event): void {
super.handleEvent(event);
const { type } = event;
const state = this._state;
if (state) {
if (type === 'click') {
if (state.volumeLevel === 'off') {
state.requestUnmute();
} else {
state.requestMute();
}
}
}
}
}
export const MuteButtonElement: ConnectedComponentConstructor<MuteButton, MuteButtonStateWithMethods> = toConnectedHTMLComponent(
MuteButton,
getMuteButtonState,
getMuteButtonProps,
'MuteButton',
);
@@ -0,0 +1,67 @@
import type { PlayButtonState } from '@videojs/core-preview/store';
import type { Prettify } from '../types';
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
import { playButtonStateDefinition } from '@videojs/core-preview/store';
import { memoize } from '@videojs/utils-preview';
import { toConnectedHTMLComponent } from '../utils/component-factory';
import { ButtonElement } from './button';
type PlayButtonStateWithMethods = Prettify<PlayButtonState & ReturnType<typeof playButtonStateDefinition.createRequestMethods>>;
const playButtonCreateRequestMethods = memoize(playButtonStateDefinition.createRequestMethods);
/**
* PlayButton state hook - equivalent to React's usePlayButtonState
* Handles media store state subscription and transformation
*/
export const getPlayButtonState: StateHook<PlayButton, PlayButtonStateWithMethods> = (_element, mediaStore) => {
return {
...playButtonStateDefinition.stateTransform(mediaStore.getState()),
...playButtonCreateRequestMethods(mediaStore.dispatch),
};
};
export const getPlayButtonProps: PropsHook<PlayButton, PlayButtonStateWithMethods> = (_element, state) => {
const baseProps: Record<string, any> = {
/** data attributes/props */
'data-paused': state.paused,
/** @TODO Need another state provider in core for i18n (CJP) */
/** aria attributes/props */
role: 'button',
tabindex: '0',
'aria-label': state.paused ? 'play' : 'pause',
/** tooltip */
'data-tooltip': state.paused ? 'Play' : 'Pause',
/** @TODO Figure out how we want to handle attr overrides (e.g. aria-label) (CJP) */
/** external props spread last to allow for overriding */
// ...props,
};
return baseProps;
};
export class PlayButton extends ButtonElement {
_state: PlayButtonStateWithMethods | undefined;
handleEvent(event: Event): void {
super.handleEvent(event);
const { type } = event;
const state = this._state;
if (state && type === 'click') {
if (state.paused) {
state.requestPlay();
} else {
state.requestPause();
}
}
}
}
export const PlayButtonElement: ConnectedComponentConstructor<PlayButton, PlayButtonStateWithMethods> = toConnectedHTMLComponent(
PlayButton,
getPlayButtonState,
getPlayButtonProps,
'PlayButton',
);
@@ -0,0 +1,100 @@
import type { PopoverState as CorePopoverState } from '@videojs/core-preview';
import type { Prettify } from '../types';
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
import { Popover as CorePopover } from '@videojs/core-preview';
import { getDocumentOrShadowRoot } from '@videojs/utils-preview/dom';
import { getCoreState, getPropsFromAttrs, toConnectedHTMLComponent } from '../utils/component-factory';
type Placement = CorePopoverState['placement'];
export type PopoverState = Prettify<ReturnType<CorePopover['getState']>>;
export const getPopoverState: StateHook<Popover, PopoverState> = (element, _mediaStore) => {
const coreState = getCoreState(CorePopover, getPropsFromAttrs(element));
return {
...coreState,
};
};
export const getPopoverProps: PropsHook<Popover, PopoverState> = (element, state) => {
if (state._popoverElement !== element) {
state._setPopoverElement(element);
}
const triggerElement = getDocumentOrShadowRoot(element)?.querySelector(
`[commandfor="${element.id}"]`,
) as HTMLElement | null;
if (state._triggerElement !== triggerElement) {
state._setTriggerElement(triggerElement);
}
const mediaContainer = element.closest(element.collisionBoundary
? `#${element.collisionBoundary}`
: 'media-container') as HTMLElement | null;
if (state._collisionBoundaryElement !== mediaContainer) {
state._setCollisionBoundaryElement(mediaContainer);
}
return {
'data-side': state.placement,
'data-starting-style': state._transitionStatus === 'initial',
'data-open': state._transitionStatus === 'initial' || state._transitionStatus === 'open',
'data-ending-style': state._transitionStatus === 'close' || state._transitionStatus === 'unmounted',
'data-closed': state._transitionStatus === 'close' || state._transitionStatus === 'unmounted',
style: {
...state._popoverStyle,
},
};
};
export class Popover extends HTMLElement {
static get observedAttributes(): string[] {
return [
'id',
'open-on-hover',
'delay',
'close-delay',
'side',
'side-offset',
'collision-padding',
'collision-boundary',
];
}
get openOnHover(): boolean {
return this.hasAttribute('open-on-hover');
}
get delay(): number {
return Number.parseInt(this.getAttribute('delay') ?? '0', 10);
}
get closeDelay(): number {
return Number.parseInt(this.getAttribute('close-delay') ?? '0', 10);
}
get side(): Placement {
return this.getAttribute('side') as Placement;
}
get sideOffset(): number {
return Number.parseInt(this.getAttribute('side-offset') ?? '0', 10);
}
get collisionPadding(): number {
return Number.parseInt(this.getAttribute('collision-padding') ?? '0', 10);
}
get collisionBoundary(): string | null {
return this.getAttribute('collision-boundary');
}
}
export const PopoverElement: ConnectedComponentConstructor<Popover, PopoverState> = toConnectedHTMLComponent(
Popover,
getPopoverState,
getPopoverProps,
'Popover',
);
@@ -0,0 +1,54 @@
import type { PreviewTimeDisplayState } from '@videojs/core-preview/store';
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
import { previewTimeDisplayStateDefinition } from '@videojs/core-preview/store';
import { formatDisplayTime } from '@videojs/utils-preview';
import { toConnectedHTMLComponent } from '../utils/component-factory';
export const getPreviewTimeDisplayState: StateHook<PreviewTimeDisplay, PreviewTimeDisplayState> = (_element, mediaStore) => {
return {
...previewTimeDisplayStateDefinition.stateTransform(mediaStore.getState()),
// Preview time display is read-only, so no request methods needed
};
};
export const getPreviewTimeDisplayProps: PropsHook<PreviewTimeDisplay, PreviewTimeDisplayState> = (_element, _state) => {
return {};
};
export class PreviewTimeDisplay extends HTMLElement {
static shadowRootOptions = {
mode: 'open' as ShadowRootMode,
};
static observedAttributes: string[] = ['show-remaining'];
constructor() {
super();
if (!this.shadowRoot) {
this.attachShadow((this.constructor as typeof PreviewTimeDisplay).shadowRootOptions);
}
}
get showRemaining(): boolean {
return this.hasAttribute('show-remaining');
}
_update(_props: any, state: PreviewTimeDisplayState): void {
/** @TODO Should this live here or elsewhere? (CJP) */
const timeLabel = formatDisplayTime(state.previewTime);
if (this.shadowRoot) {
this.shadowRoot.textContent = timeLabel;
}
}
}
export const PreviewTimeDisplayElement: ConnectedComponentConstructor<PreviewTimeDisplay, PreviewTimeDisplayState> = toConnectedHTMLComponent(
PreviewTimeDisplay,
getPreviewTimeDisplayState,
getPreviewTimeDisplayProps,
'PreviewTimeDisplay',
);
@@ -0,0 +1,242 @@
import type { Prettify } from '../types';
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
import { TimeSlider as CoreTimeSlider } from '@videojs/core-preview';
import { timeSliderStateDefinition } from '@videojs/core-preview/store';
import { memoize } from '@videojs/utils-preview';
import { getCoreState, getPropsFromAttrs, toConnectedHTMLComponent } from '../utils/component-factory';
// ============================================================================
// ROOT COMPONENT
// ============================================================================
type TimeSliderState = Prettify<ReturnType<CoreTimeSlider['getState']>>;
type TimeSliderStateWithMethods = Prettify<TimeSliderState & ReturnType<typeof timeSliderStateDefinition.createRequestMethods>>;
const timeSliderCreateRequestMethods = memoize(timeSliderStateDefinition.createRequestMethods);
export const getTimeSliderRootState: StateHook<TimeSliderRoot, TimeSliderStateWithMethods> = (element, mediaStore) => {
const mediaState = timeSliderStateDefinition.stateTransform(mediaStore.getState());
const mediaMethods = timeSliderCreateRequestMethods(mediaStore.dispatch);
const coreState = getCoreState(CoreTimeSlider, {
...getPropsFromAttrs(element),
...mediaState,
...mediaMethods,
});
return {
...coreState,
};
};
/**
* TimeSlider Root props hook - equivalent to React's useTimeSliderRootProps
* Handles element attributes and properties based on state
*/
export const getTimeSliderRootProps: PropsHook<TimeSliderRoot, TimeSliderStateWithMethods> = (element, state) => {
if (state._rootElement !== element) {
state._setRootElement(element);
}
return {
role: 'slider',
tabindex: element.getAttribute('tabindex') ?? '0',
'data-current-time': state.currentTime.toString(),
'data-duration': state.duration.toString(),
'data-orientation': element.orientation || 'horizontal',
'aria-label': 'Seek',
'aria-valuemin': '0',
'aria-valuemax': Math.round(state.duration).toString(),
'aria-valuenow': Math.round(state.currentTime).toString(),
'aria-valuetext': `${state._currentTimeText} of ${state._durationText}`,
'aria-orientation': element.orientation || 'horizontal',
style: {
...(element.hasAttribute('commandfor') ? { anchorName: `--${element.getAttribute('commandfor')}` } : {}),
'--slider-fill': `${state._fillWidth.toFixed(3)}%`,
'--slider-pointer': `${(state._pointerWidth * 100).toFixed(3)}%`,
},
};
};
export class TimeSliderRoot extends HTMLElement {
static readonly observedAttributes: readonly string[] = ['commandfor', 'orientation'];
_state: TimeSliderState | undefined;
get orientation(): 'horizontal' | 'vertical' {
return (this.getAttribute('orientation') as 'horizontal' | 'vertical') || 'horizontal';
}
}
export const TimeSliderRootElement: ConnectedComponentConstructor<TimeSliderRoot, TimeSliderStateWithMethods> = toConnectedHTMLComponent(
TimeSliderRoot,
getTimeSliderRootState,
getTimeSliderRootProps,
'TimeSliderRoot',
);
// ============================================================================
// TRACK COMPONENT
// ============================================================================
export const getTimeSliderTrackProps: PropsHook<TimeSliderTrack, undefined> = (element, _state) => {
const rootElement = element.closest('media-time-slider') as TimeSliderRoot;
if (rootElement._state?._trackElement !== element) {
rootElement._state?._setTrackElement?.(element);
}
const orientation = rootElement?.orientation || 'horizontal';
return {
'data-orientation': orientation,
style: {
width: orientation === 'horizontal' ? '100%' : undefined,
height: orientation !== 'horizontal' ? '100%' : undefined,
},
};
};
export class TimeSliderTrack extends HTMLElement {}
export const TimeSliderTrackElement: ConnectedComponentConstructor<TimeSliderTrack, undefined> = toConnectedHTMLComponent(
TimeSliderTrack,
undefined,
getTimeSliderTrackProps,
'TimeSliderTrack',
);
// ============================================================================
// PROGRESS COMPONENT
// ============================================================================
export const getTimeSliderProgressProps: PropsHook<TimeSliderProgress, undefined> = (element, _state) => {
const rootElement = element.closest('media-time-slider') as TimeSliderRoot;
const orientation = rootElement?.orientation || 'horizontal';
const style = orientation === 'horizontal'
? {
position: 'absolute',
width: 'var(--slider-fill, 0%)',
height: '100%',
top: '0',
bottom: undefined,
}
: {
position: 'absolute',
height: 'var(--slider-fill, 0%)',
width: '100%',
bottom: '0',
top: undefined,
};
return {
'data-orientation': orientation,
style,
};
};
export class TimeSliderProgress extends HTMLElement {}
export const TimeSliderProgressElement: ConnectedComponentConstructor<TimeSliderProgress, undefined> = toConnectedHTMLComponent(
TimeSliderProgress,
undefined,
getTimeSliderProgressProps,
'TimeSliderProgress',
);
// ============================================================================
// POINTER COMPONENT
// ============================================================================
export const getTimeSliderPointerProps: PropsHook<TimeSliderPointer, undefined> = (element, _state) => {
const rootElement = element.closest('media-time-slider') as TimeSliderRoot;
const orientation = rootElement?.orientation || 'horizontal';
const style = orientation === 'horizontal'
? {
position: 'absolute',
width: 'var(--slider-pointer, 0%)',
height: '100%',
top: '0',
bottom: undefined,
}
: {
position: 'absolute',
height: 'var(--slider-pointer, 0%)',
width: '100%',
bottom: '0',
top: undefined,
};
return {
'data-orientation': orientation,
style,
};
};
export class TimeSliderPointer extends HTMLElement {}
export const TimeSliderPointerElement: ConnectedComponentConstructor<TimeSliderPointer, undefined> = toConnectedHTMLComponent(
TimeSliderPointer,
undefined,
getTimeSliderPointerProps,
'TimeSliderPointer',
);
// ============================================================================
// THUMB COMPONENT
// ============================================================================
export const getTimeSliderThumbProps: PropsHook<TimeSliderThumb, undefined> = (element, _state) => {
const rootElement = element.closest('media-time-slider') as TimeSliderRoot;
const orientation = rootElement?.orientation || 'horizontal';
const style = orientation === 'horizontal'
? {
position: 'absolute',
left: 'var(--slider-fill, 0%)',
top: '50%',
bottom: undefined,
translate: '-50% -50%',
}
: {
position: 'absolute',
bottom: 'var(--slider-fill, 0%)',
left: '50%',
top: undefined,
translate: '-50% 50%',
};
return {
'data-orientation': orientation,
style,
};
};
export class TimeSliderThumb extends HTMLElement {}
export const TimeSliderThumbElement: ConnectedComponentConstructor<TimeSliderThumb, undefined> = toConnectedHTMLComponent(
TimeSliderThumb,
undefined,
getTimeSliderThumbProps,
'TimeSliderThumb',
);
// ============================================================================
// EXPORTS
// ============================================================================
export const TimeSliderElement = Object.assign(
{},
{
Root: TimeSliderRootElement,
Track: TimeSliderTrackElement,
Progress: TimeSliderProgressElement,
Pointer: TimeSliderPointerElement,
Thumb: TimeSliderThumbElement,
},
) as {
Root: typeof TimeSliderRootElement;
Track: typeof TimeSliderTrackElement;
Progress: typeof TimeSliderProgressElement;
Pointer: typeof TimeSliderPointerElement;
Thumb: typeof TimeSliderThumbElement;
};
@@ -0,0 +1,37 @@
import type { Prettify } from '../types';
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
import { Tooltip as CoreTooltip } from '@videojs/core-preview';
import { getCoreState, getPropsFromAttrs, toConnectedHTMLComponent } from '../utils/component-factory';
import { getPopoverProps, Popover } from './popover';
export type TooltipState = Prettify<ReturnType<CoreTooltip['getState']>>;
export const getTooltipState: StateHook<Tooltip, TooltipState> = (element, _mediaStore) => {
const coreState = getCoreState(CoreTooltip, getPropsFromAttrs(element));
return {
...coreState,
};
};
export const getTooltipProps: PropsHook<Tooltip, TooltipState> = getPopoverProps;
export class Tooltip extends Popover {
static get observedAttributes(): string[] {
return [
...super.observedAttributes,
'track-cursor-axis',
];
}
get trackCursorAxis(): 'x' | null {
const value = this.getAttribute('track-cursor-axis');
return value === 'x' ? value : null;
}
}
export const TooltipElement: ConnectedComponentConstructor<Tooltip, TooltipState> = toConnectedHTMLComponent(
Tooltip,
getTooltipState,
getTooltipProps,
'Tooltip',
);
@@ -0,0 +1,203 @@
import type { Prettify } from '../types';
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
import { VolumeSlider as CoreVolumeSlider } from '@videojs/core-preview';
import { volumeSliderStateDefinition } from '@videojs/core-preview/store';
import { memoize } from '@videojs/utils-preview';
import { getCoreState, getPropsFromAttrs, toConnectedHTMLComponent } from '../utils/component-factory';
// ============================================================================
// ROOT COMPONENT
// ============================================================================
type VolumeSliderState = Prettify<ReturnType<CoreVolumeSlider['getState']>>;
type VolumeSliderStateWithMethods = Prettify<VolumeSliderState & ReturnType<typeof volumeSliderStateDefinition.createRequestMethods>>;
const volumeSliderCreateRequestMethods = memoize(volumeSliderStateDefinition.createRequestMethods);
export const getVolumeSliderRootState: StateHook<VolumeSliderRoot, VolumeSliderStateWithMethods> = (element, mediaStore) => {
const mediaState = volumeSliderStateDefinition.stateTransform(mediaStore.getState());
const mediaMethods = volumeSliderCreateRequestMethods(mediaStore.dispatch);
const coreState = getCoreState(CoreVolumeSlider, {
...getPropsFromAttrs(element),
...mediaState,
...mediaMethods,
});
return {
...coreState,
};
};
/**
* VolumeSlider Root props hook - equivalent to React's useVolumeSliderRootProps
* Handles element attributes and properties based on state
*/
export const getVolumeSliderRootProps: PropsHook<VolumeSliderRoot, VolumeSliderStateWithMethods> = (element, state) => {
if (state._rootElement !== element) {
state._setRootElement(element);
}
const volumeText = `${Math.round(state.muted ? 0 : state.volume * 100)}%`;
return {
role: 'slider',
tabindex: element.getAttribute('tabindex') ?? '0',
'data-muted': state.muted.toString(),
'data-volume-level': state.volumeLevel,
'data-orientation': element.orientation || 'horizontal',
'aria-label': 'Volume',
'aria-valuemin': '0',
'aria-valuemax': '100',
'aria-valuenow': Math.round(state.muted ? 0 : state.volume * 100).toString(),
'aria-valuetext': volumeText,
'aria-orientation': element.orientation || 'horizontal',
style: {
...(element.hasAttribute('commandfor') ? { anchorName: `--${element.getAttribute('commandfor')}` } : {}),
'--slider-fill': `${state._fillWidth.toFixed(3)}%`,
},
};
};
export class VolumeSliderRoot extends HTMLElement {
static readonly observedAttributes: readonly string[] = ['commandfor', 'orientation'];
_state: VolumeSliderState | undefined;
get orientation(): 'horizontal' | 'vertical' {
return (this.getAttribute('orientation') as 'horizontal' | 'vertical') || 'horizontal';
}
}
export const VolumeSliderRootElement: ConnectedComponentConstructor<VolumeSliderRoot, VolumeSliderStateWithMethods> = toConnectedHTMLComponent(
VolumeSliderRoot,
getVolumeSliderRootState,
getVolumeSliderRootProps,
'VolumeSliderRoot',
);
// ============================================================================
// TRACK COMPONENT
// ============================================================================
export const getVolumeSliderTrackProps: PropsHook<VolumeSliderTrack, undefined> = (element, _state) => {
const rootElement = element.closest('media-volume-slider') as VolumeSliderRoot;
if (rootElement._state?._trackElement !== element) {
rootElement._state?._setTrackElement?.(element);
}
const orientation = rootElement?.orientation || 'horizontal';
return {
'data-orientation': orientation,
style: {
width: orientation === 'horizontal' ? '100%' : undefined,
height: orientation !== 'horizontal' ? '100%' : undefined,
},
};
};
export class VolumeSliderTrack extends HTMLElement {}
export const VolumeSliderTrackElement: ConnectedComponentConstructor<VolumeSliderTrack, undefined> = toConnectedHTMLComponent(
VolumeSliderTrack,
undefined,
getVolumeSliderTrackProps,
'VolumeSliderTrack',
);
// ============================================================================
// INDICATOR COMPONENT
// ============================================================================
export const getVolumeSliderIndicatorProps: PropsHook<VolumeSliderIndicator, undefined> = (element, _state) => {
const rootElement = element.closest('media-volume-slider') as VolumeSliderRoot;
const orientation = rootElement?.orientation || 'horizontal';
const style = orientation === 'horizontal'
? {
position: 'absolute',
width: 'var(--slider-fill, 0%)',
height: '100%',
top: '0',
bottom: undefined,
}
: {
position: 'absolute',
height: 'var(--slider-fill, 0%)',
width: '100%',
bottom: '0',
top: undefined,
};
return {
'data-orientation': orientation,
style,
};
};
export class VolumeSliderIndicator extends HTMLElement {}
export const VolumeSliderIndicatorElement: ConnectedComponentConstructor<VolumeSliderIndicator, undefined> = toConnectedHTMLComponent(
VolumeSliderIndicator,
undefined,
getVolumeSliderIndicatorProps,
'VolumeSliderIndicator',
);
// ============================================================================
// THUMB COMPONENT
// ============================================================================
export const getVolumeSliderThumbProps: PropsHook<VolumeSliderThumb, undefined> = (element, _state) => {
const rootElement = element.closest('media-volume-slider') as VolumeSliderRoot;
const orientation = rootElement?.orientation || 'horizontal';
const style = orientation === 'horizontal'
? {
position: 'absolute',
left: 'var(--slider-fill, 0%)',
top: '50%',
bottom: undefined,
translate: '-50% -50%',
}
: {
position: 'absolute',
bottom: 'var(--slider-fill, 0%)',
left: '50%',
top: undefined,
translate: '-50% 50%',
};
return {
'data-orientation': orientation,
style,
};
};
export class VolumeSliderThumb extends HTMLElement {}
export const VolumeSliderThumbElement: ConnectedComponentConstructor<VolumeSliderThumb, undefined> = toConnectedHTMLComponent(
VolumeSliderThumb,
undefined,
getVolumeSliderThumbProps,
'VolumeSliderThumb',
);
// ============================================================================
// EXPORTS
// ============================================================================
export const VolumeSliderElement = Object.assign(
{},
{
Root: VolumeSliderRootElement,
Track: VolumeSliderTrackElement,
Indicator: VolumeSliderIndicatorElement,
Thumb: VolumeSliderThumbElement,
},
) as {
Root: typeof VolumeSliderRootElement;
Track: typeof VolumeSliderTrackElement;
Indicator: typeof VolumeSliderIndicatorElement;
Thumb: typeof VolumeSliderThumbElement;
};