mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(react): add slider thumbnail component (#722)
This commit is contained in:
@@ -47,6 +47,7 @@ export type { SliderBufferProps } from './ui/slider/slider-buffer';
|
||||
export type { SliderFillProps } from './ui/slider/slider-fill';
|
||||
export type { SliderRootProps } from './ui/slider/slider-root';
|
||||
export type { SliderThumbProps } from './ui/slider/slider-thumb';
|
||||
export type { SliderThumbnailProps } from './ui/slider/slider-thumbnail';
|
||||
export type { SliderTrackProps } from './ui/slider/slider-track';
|
||||
export type { SliderValueProps } from './ui/slider/slider-value';
|
||||
export { Thumbnail, type ThumbnailProps } from './ui/thumbnail/thumbnail';
|
||||
|
||||
@@ -2,5 +2,6 @@ export { SliderBuffer as Buffer, type SliderBufferProps as BufferProps } from '.
|
||||
export { SliderFill as Fill, type SliderFillProps as FillProps } from './slider-fill';
|
||||
export { SliderRoot as Root, type SliderRootProps as RootProps } from './slider-root';
|
||||
export { SliderThumb as Thumb, type SliderThumbProps as ThumbProps } from './slider-thumb';
|
||||
export { SliderThumbnail as Thumbnail, type SliderThumbnailProps as ThumbnailProps } from './slider-thumbnail';
|
||||
export { SliderTrack as Track, type SliderTrackProps as TrackProps } from './slider-track';
|
||||
export { SliderValue as Value, type SliderValueProps as ValueProps } from './slider-value';
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import type { ThumbnailCore } from '@videojs/core';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { Thumbnail, type ThumbnailProps } from '../thumbnail/thumbnail';
|
||||
import { useSliderContext } from './context';
|
||||
|
||||
export interface SliderThumbnailProps extends Omit<ThumbnailProps, 'time'> {}
|
||||
|
||||
export const SliderThumbnail = forwardRef<HTMLDivElement, SliderThumbnailProps>(
|
||||
function SliderThumbnail(componentProps, forwardedRef) {
|
||||
const { pointerValue } = useSliderContext();
|
||||
return <Thumbnail ref={forwardedRef} {...componentProps} time={pointerValue} />;
|
||||
}
|
||||
);
|
||||
|
||||
export namespace SliderThumbnail {
|
||||
export type Props = SliderThumbnailProps;
|
||||
export type State = ThumbnailCore.State;
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { cleanup, render } from '@testing-library/react';
|
||||
import { createRef } from 'react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { SliderRoot } from '../slider-root';
|
||||
import { SliderThumbnail } from '../slider-thumbnail';
|
||||
|
||||
const { mockSliderApi, mockThumbnailApi } = vi.hoisted(() => ({
|
||||
mockSliderApi: () => ({
|
||||
input: {
|
||||
current: {
|
||||
pointerPercent: 50,
|
||||
dragPercent: 0,
|
||||
dragging: false,
|
||||
pointing: true,
|
||||
focused: false,
|
||||
},
|
||||
subscribe: vi.fn(() => vi.fn()),
|
||||
},
|
||||
rootProps: {
|
||||
onPointerDown: vi.fn(),
|
||||
onPointerMove: vi.fn(),
|
||||
onPointerLeave: vi.fn(),
|
||||
},
|
||||
thumbProps: {
|
||||
onKeyDown: vi.fn(),
|
||||
onFocus: vi.fn(),
|
||||
onBlur: vi.fn(),
|
||||
},
|
||||
destroy: vi.fn(),
|
||||
}),
|
||||
mockThumbnailApi: () => ({
|
||||
loading: false,
|
||||
error: false,
|
||||
naturalWidth: 0,
|
||||
naturalHeight: 0,
|
||||
readConstraints: vi.fn(() => ({
|
||||
minWidth: 0,
|
||||
maxWidth: Infinity,
|
||||
minHeight: 0,
|
||||
maxHeight: Infinity,
|
||||
})),
|
||||
updateSrc: vi.fn(),
|
||||
connect: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@videojs/core/dom', async (importOriginal) => {
|
||||
const orig: Record<string, unknown> = await importOriginal();
|
||||
return {
|
||||
...orig,
|
||||
createSlider: vi.fn(mockSliderApi),
|
||||
createThumbnail: vi.fn(mockThumbnailApi),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@videojs/store/react', () => ({
|
||||
useSnapshot: vi.fn((state: { current: unknown }) => state.current),
|
||||
useStore: vi.fn(),
|
||||
}));
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe('SliderThumbnail', () => {
|
||||
it('renders inside SliderRoot context', () => {
|
||||
const { container } = render(
|
||||
<SliderRoot>
|
||||
<SliderThumbnail data-testid="thumbnail" />
|
||||
</SliderRoot>
|
||||
);
|
||||
|
||||
expect(container.querySelector('[data-testid="thumbnail"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('throws outside of SliderRoot', () => {
|
||||
expect(() => render(<SliderThumbnail />)).toThrow('Slider compound components must be used within a Slider.Root');
|
||||
});
|
||||
|
||||
it('forwards ref', () => {
|
||||
const ref = createRef<HTMLDivElement>();
|
||||
render(
|
||||
<SliderRoot>
|
||||
<SliderThumbnail ref={ref} />
|
||||
</SliderRoot>
|
||||
);
|
||||
|
||||
expect(ref.current).toBeInstanceOf(HTMLDivElement);
|
||||
});
|
||||
|
||||
it('renders a div with thumbnail ARIA attributes', () => {
|
||||
const { container } = render(
|
||||
<SliderRoot>
|
||||
<SliderThumbnail data-testid="thumbnail" />
|
||||
</SliderRoot>
|
||||
);
|
||||
|
||||
const el = container.querySelector('[data-testid="thumbnail"]');
|
||||
expect(el?.tagName).toBe('DIV');
|
||||
expect(el?.getAttribute('role')).toBe('img');
|
||||
expect(el?.getAttribute('aria-hidden')).toBe('true');
|
||||
});
|
||||
|
||||
it('applies data-hidden when no thumbnails are available', () => {
|
||||
const { container } = render(
|
||||
<SliderRoot>
|
||||
<SliderThumbnail data-testid="thumbnail" />
|
||||
</SliderRoot>
|
||||
);
|
||||
|
||||
const el = container.querySelector('[data-testid="thumbnail"]');
|
||||
expect(el?.hasAttribute('data-hidden')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders an img child element', () => {
|
||||
const { container } = render(
|
||||
<SliderRoot>
|
||||
<SliderThumbnail data-testid="thumbnail" />
|
||||
</SliderRoot>
|
||||
);
|
||||
|
||||
const el = container.querySelector('[data-testid="thumbnail"]');
|
||||
const img = el?.querySelector('img');
|
||||
expect(img).toBeTruthy();
|
||||
expect(img?.getAttribute('aria-hidden')).toBe('true');
|
||||
expect(img?.getAttribute('decoding')).toBe('async');
|
||||
});
|
||||
|
||||
it('accepts thumbnails prop', () => {
|
||||
const thumbnails = [
|
||||
{ url: 'thumb-0.jpg', startTime: 0 },
|
||||
{ url: 'thumb-5.jpg', startTime: 5 },
|
||||
];
|
||||
|
||||
const { container } = render(
|
||||
<SliderRoot>
|
||||
<SliderThumbnail data-testid="thumbnail" thumbnails={thumbnails} />
|
||||
</SliderRoot>
|
||||
);
|
||||
|
||||
const el = container.querySelector('[data-testid="thumbnail"]');
|
||||
const img = el?.querySelector('img');
|
||||
|
||||
// pointerPercent is 50 → pointerValue = 50 (generic slider, min=0, max=100).
|
||||
// findActiveThumbnail(thumbnails, 50) → 'thumb-5.jpg' (startTime 5 ≤ 50).
|
||||
expect(img?.getAttribute('src')).toBe('thumb-5.jpg');
|
||||
});
|
||||
|
||||
it('forwards crossOrigin to inner img', () => {
|
||||
const { container } = render(
|
||||
<SliderRoot>
|
||||
<SliderThumbnail
|
||||
data-testid="thumbnail"
|
||||
crossOrigin="anonymous"
|
||||
thumbnails={[{ url: 'thumb.jpg', startTime: 0 }]}
|
||||
/>
|
||||
</SliderRoot>
|
||||
);
|
||||
|
||||
const img = container.querySelector('[data-testid="thumbnail"] img');
|
||||
expect(img?.getAttribute('crossorigin')).toBe('anonymous');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user