feat(core): add time display component (#460)

This commit is contained in:
rahim
2026-02-06 14:31:33 +11:00
committed by GitHub
parent d5e5cec6ab
commit 7b8bc11f9f
20 changed files with 811 additions and 0 deletions
@@ -0,0 +1,3 @@
export { Group, type GroupProps } from './time-group';
export { Separator, type SeparatorProps } from './time-separator';
export { Value, type ValueProps } from './time-value';
+1
View File
@@ -0,0 +1 @@
export * as Time from './index.parts';
+50
View File
@@ -0,0 +1,50 @@
'use client';
import type { ForwardedRef, ReactNode } from 'react';
import { forwardRef } from 'react';
import type { UIComponentProps } from '../../utils/types';
import { renderElement } from '../../utils/use-render';
// Empty state for Group (no dynamic state)
type GroupState = Record<string, never>;
export interface GroupProps extends UIComponentProps<'span', GroupState> {
/** Time value components to render inside the group. */
children?: ReactNode | undefined;
}
/**
* Container for composed time displays. Renders a `<span>` element.
*
* @example
* ```tsx
* <Time.Group>
* <Time.Value type="current" />
* <Time.Separator />
* <Time.Value type="duration" />
* </Time.Group>
* ```
*/
export const Group = forwardRef(function Group(
componentProps: GroupProps,
forwardedRef: ForwardedRef<HTMLSpanElement>
) {
const { render, className, style, children, ...elementProps } = componentProps;
const state: GroupState = {};
return renderElement(
'span',
{ render, className, style },
{
state,
ref: [forwardedRef],
props: [{ children }, elementProps],
}
);
});
export namespace Group {
export type Props = GroupProps;
}
@@ -0,0 +1,47 @@
'use client';
import type { ForwardedRef, ReactNode } from 'react';
import { forwardRef } from 'react';
import type { UIComponentProps } from '../../utils/types';
import { renderElement } from '../../utils/use-render';
// Empty state for Separator (no dynamic state)
type SeparatorState = Record<string, never>;
export interface SeparatorProps extends UIComponentProps<'span', SeparatorState> {
/** Separator content. Defaults to "/". */
children?: ReactNode | undefined;
}
/**
* Divider between time values. Hidden from screen readers.
*
* @example
* ```tsx
* <Time.Separator />
* <Time.Separator> of </Time.Separator>
* ```
*/
export const Separator = forwardRef(function Separator(
componentProps: SeparatorProps,
forwardedRef: ForwardedRef<HTMLSpanElement>
) {
const { render, className, style, children = '/', ...elementProps } = componentProps;
const state: SeparatorState = {};
return renderElement(
'span',
{ render, className, style },
{
state,
ref: [forwardedRef],
props: [{ 'aria-hidden': 'true', children }, elementProps],
}
);
});
export namespace Separator {
export type Props = SeparatorProps;
}
+74
View File
@@ -0,0 +1,74 @@
'use client';
import { TimeCore } from '@videojs/core';
import { logMissingFeature, selectTime } from '@videojs/core/dom';
import type { ForwardedRef } from 'react';
import { forwardRef, useState } from 'react';
import { usePlayer } from '../../player/context';
import type { UIComponentProps } from '../../utils/types';
import { renderElement } from '../../utils/use-render';
export interface ValueProps extends Omit<UIComponentProps<'time', TimeCore.State>, 'children'>, TimeCore.Props {}
/**
* Displays a formatted time value (current, duration, or remaining).
*
* @example
* ```tsx
* <Time.Value />
* <Time.Value type="duration" />
* <Time.Value type="remaining" negativeSign="" />
* ```
*/
export const Value = forwardRef(function Value(
componentProps: ValueProps,
forwardedRef: ForwardedRef<HTMLTimeElement>
) {
const { render, className, style, type, negativeSign, label, ...elementProps } = componentProps;
const time = usePlayer(selectTime);
const [core] = useState(() => new TimeCore());
core.setProps({ type, negativeSign, label });
if (!time) {
logMissingFeature('Time.Value', 'time');
return null;
}
const state = core.getState(time);
// Render negative sign as aria-hidden span for remaining time
const content =
state.type === 'remaining' && state.seconds < 0 ? (
<>
<span aria-hidden="true">{negativeSign ?? '-'}</span>
{state.text.replace(/^-/, '')}
</>
) : (
state.text
);
return renderElement(
'time',
{ render, className, style },
{
state,
ref: [forwardedRef],
props: [
{
datetime: state.datetime,
children: content,
...core.getAttrs(time),
},
elementProps,
],
}
);
});
export namespace Value {
export type Props = ValueProps;
export type State = TimeCore.State;
}