feat(core): support nested component parts

This commit is contained in:
Rahim
2026-06-24 13:07:47 -07:00
parent ddccbd89d4
commit 69d5afecdd
10 changed files with 132 additions and 20 deletions
@@ -29,10 +29,10 @@ describe('skins compiler config', () => {
expect(code).toContain('from "@/ui/play-button"');
expect(code).toContain('from "@/ui/poster"');
expect(code).toContain('from "@/ui/seek-indicator"');
expect(code).toContain('from "@/ui/slider"');
expect(code).toContain('from "@/ui/status-indicator"');
expect(code).toContain('from "@/ui/volume-indicator"');
expect(code).not.toContain('from "@/ui/text"');
expect(code).not.toContain('from "@/ui/slider"');
expect(code).toContain('from "@/utils/use-render"');
expect(code).toContain('from "@videojs/utils/predicate"');
expect(code).toContain('from "@/icons"');
@@ -123,6 +123,10 @@ describe('skins compiler config', () => {
expect(compactCode).toContain(
compact('<TimeSlider.Thumb className={cn(slider.thumb.base, slider.thumb.interactive)} />')
);
expect(compactCode).toContain(compact('<Slider.Thumbnail.Root className={thumbnail.root}>'));
expect(compactCode).toContain(compact('<Slider.Thumbnail.Image className={thumbnail.image} />'));
expect(compactCode).toContain(compact('<TimeSlider.Value type="pointer" className={thumbnail.time} />'));
expect(compactCode).toContain(compact('<SpinnerIcon className={cn(icon, thumbnail.spinner)} />'));
expect(compactCode).toContain(compact('<TimeSlider.Preview className={slider.preview}>'));
expect(compactCode).toContain(compact('<TimeSlider.Value type="pointer" className={slider.value} />'));
expect(compactCode).not.toContain('SliderTrack');
@@ -21,6 +21,7 @@ import {
poster as posterClass,
seek,
slider,
thumbnail,
time,
} from '@videojs/skins/default/tailwind/video.tailwind';
import { isString } from '@videojs/utils/predicate';
@@ -62,6 +63,7 @@ import { PlayButton } from '@/ui/play-button';
import { Poster } from '@/ui/poster';
import { SeekButton } from '@/ui/seek-button';
import { SeekIndicator } from '@/ui/seek-indicator';
import { Slider } from '@/ui/slider';
import { StatusAnnouncer } from '@/ui/status-announcer';
import { StatusIndicator } from '@/ui/status-indicator';
import { Time } from '@/ui/time';
@@ -167,6 +169,11 @@ export function DefaultVideoSkin({ className, children, poster, ...rest }: Defau
<TimeSlider.Buffer className={cn(slider.fill.base, slider.fill.buffer)} />
</TimeSlider.Track>
<TimeSlider.Thumb className={cn(slider.thumb.base, slider.thumb.interactive)} />
<Slider.Thumbnail.Root className={thumbnail.root}>
<Slider.Thumbnail.Image className={thumbnail.image} />
<TimeSlider.Value type="pointer" className={thumbnail.time} />
<SpinnerIcon className={cn(icon, thumbnail.spinner)} />
</Slider.Thumbnail.Root>
<TimeSlider.Preview className={slider.preview}>
<TimeSlider.Value type="pointer" className={slider.value} />
</TimeSlider.Preview>
@@ -0,0 +1,19 @@
'use client';
import type { SliderState } from '@videojs/core';
import type { UIComponentProps } from '../../utils/types';
import { createContextPart } from '../create-context-part';
import { useSliderContext } from './context';
export interface SliderThumbnailRootProps extends UIComponentProps<'div', SliderState> {}
export const SliderThumbnailRoot = createContextPart<SliderThumbnailRootProps, SliderState>({
displayName: 'SliderThumbnailRoot',
tag: 'div',
useContext: useSliderContext,
});
export namespace SliderThumbnailRoot {
export type Props = SliderThumbnailRootProps;
}
@@ -5,17 +5,25 @@ import { forwardRef } from 'react';
import { Thumbnail, type ThumbnailProps } from '../thumbnail/thumbnail';
import { useSliderContext } from './context';
import { SliderThumbnailRoot, type SliderThumbnailRootProps } from './slider-thumbnail-root';
export interface SliderThumbnailProps extends Omit<ThumbnailProps, 'time'> {}
export const SliderThumbnail = forwardRef<HTMLDivElement, SliderThumbnailProps>(
const SliderThumbnailImage = forwardRef<HTMLDivElement, SliderThumbnailProps>(
function SliderThumbnail(componentProps, forwardedRef) {
const { pointerValue } = useSliderContext();
return <Thumbnail ref={forwardedRef} {...componentProps} time={pointerValue} />;
}
);
export const SliderThumbnail = Object.assign(SliderThumbnailImage, {
Root: SliderThumbnailRoot,
Image: SliderThumbnailImage,
});
export namespace SliderThumbnail {
export type Props = SliderThumbnailProps;
export type RootProps = SliderThumbnailRootProps;
export type ImageProps = SliderThumbnailProps;
export type State = ThumbnailCore.State;
}
@@ -74,6 +74,19 @@ describe('SliderThumbnail', () => {
expect(container.querySelector('[data-testid="thumbnail"]')).toBeTruthy();
});
it('renders namespaced root and image parts', () => {
const { container } = render(
<SliderRoot>
<SliderThumbnail.Root data-testid="thumbnail-root">
<SliderThumbnail.Image data-testid="thumbnail-image" />
</SliderThumbnail.Root>
</SliderRoot>
);
expect(container.querySelector('[data-testid="thumbnail-root"]')).toBeTruthy();
expect(container.querySelector('[data-testid="thumbnail-image"]')).toBeTruthy();
});
it('throws outside of SliderRoot', () => {
expect(() => render(<SliderThumbnail />)).toThrow('Slider compound components must be used within a Slider.Root');
});