mirror of
https://github.com/zoriya/v10.git
synced 2026-08-13 01:19:44 +00:00
feat(html): add slider preview element (#733)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { SliderElement } from '../../ui/slider/slider-element';
|
||||
import { SliderFillElement } from '../../ui/slider/slider-fill-element';
|
||||
import { SliderPreviewElement } from '../../ui/slider/slider-preview-element';
|
||||
import { SliderThumbElement } from '../../ui/slider/slider-thumb-element';
|
||||
import { SliderTrackElement } from '../../ui/slider/slider-track-element';
|
||||
import { SliderValueElement } from '../../ui/slider/slider-value-element';
|
||||
@@ -8,6 +9,7 @@ import { safeDefine } from '../safe-define';
|
||||
// Parent slider first — sub-elements consume its context.
|
||||
safeDefine(SliderElement);
|
||||
safeDefine(SliderFillElement);
|
||||
safeDefine(SliderPreviewElement);
|
||||
safeDefine(SliderThumbElement);
|
||||
safeDefine(SliderTrackElement);
|
||||
safeDefine(SliderValueElement);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SliderBufferElement } from '../../ui/slider/slider-buffer-element';
|
||||
import { SliderFillElement } from '../../ui/slider/slider-fill-element';
|
||||
import { SliderPreviewElement } from '../../ui/slider/slider-preview-element';
|
||||
import { SliderThumbElement } from '../../ui/slider/slider-thumb-element';
|
||||
import { SliderThumbnailElement } from '../../ui/slider/slider-thumbnail-element';
|
||||
import { SliderTrackElement } from '../../ui/slider/slider-track-element';
|
||||
@@ -11,6 +12,7 @@ import { safeDefine } from '../safe-define';
|
||||
safeDefine(TimeSliderElement);
|
||||
safeDefine(SliderBufferElement);
|
||||
safeDefine(SliderFillElement);
|
||||
safeDefine(SliderPreviewElement);
|
||||
safeDefine(SliderThumbElement);
|
||||
safeDefine(SliderThumbnailElement);
|
||||
safeDefine(SliderTrackElement);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SliderFillElement } from '../../ui/slider/slider-fill-element';
|
||||
import { SliderPreviewElement } from '../../ui/slider/slider-preview-element';
|
||||
import { SliderThumbElement } from '../../ui/slider/slider-thumb-element';
|
||||
import { SliderTrackElement } from '../../ui/slider/slider-track-element';
|
||||
import { SliderValueElement } from '../../ui/slider/slider-value-element';
|
||||
@@ -8,6 +9,7 @@ import { safeDefine } from '../safe-define';
|
||||
// Parent slider first — sub-elements consume its context.
|
||||
safeDefine(VolumeSliderElement);
|
||||
safeDefine(SliderFillElement);
|
||||
safeDefine(SliderPreviewElement);
|
||||
safeDefine(SliderThumbElement);
|
||||
safeDefine(SliderTrackElement);
|
||||
safeDefine(SliderValueElement);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { SliderPreviewOverflow } from '@videojs/core/dom';
|
||||
import { applyStateDataAttrs, getSliderPreviewStyle } from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
||||
import { ContextConsumer } from '@videojs/element/context';
|
||||
import { applyStyles } from '@videojs/utils/dom';
|
||||
|
||||
import { MediaElement } from '../media-element';
|
||||
import { sliderContext } from './context';
|
||||
|
||||
export class SliderPreviewElement extends MediaElement {
|
||||
static readonly tagName = 'media-slider-preview';
|
||||
|
||||
static override properties = {
|
||||
overflow: { type: String },
|
||||
} satisfies PropertyDeclarationMap<'overflow'>;
|
||||
|
||||
overflow: SliderPreviewOverflow = 'clamp';
|
||||
|
||||
readonly #ctx = new ContextConsumer(this, {
|
||||
context: sliderContext,
|
||||
subscribe: true,
|
||||
});
|
||||
|
||||
#resizeObserver: ResizeObserver | null = null;
|
||||
#width = 0;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.#resizeObserver = new ResizeObserver(([entry]) => {
|
||||
this.#width = entry!.contentRect.width;
|
||||
this.#applyPosition();
|
||||
});
|
||||
|
||||
this.#resizeObserver.observe(this);
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.#resizeObserver?.disconnect();
|
||||
this.#resizeObserver = null;
|
||||
}
|
||||
|
||||
#applyPosition(): void {
|
||||
applyStyles(this, getSliderPreviewStyle(this.#width, this.overflow));
|
||||
}
|
||||
|
||||
protected override update(_changed: PropertyValues): void {
|
||||
super.update(_changed);
|
||||
|
||||
const ctx = this.#ctx.value;
|
||||
if (ctx) applyStateDataAttrs(this, ctx.state, ctx.stateAttrMap);
|
||||
|
||||
this.#applyPosition();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { SliderElement } from '../slider-element';
|
||||
import { SliderPreviewElement } from '../slider-preview-element';
|
||||
|
||||
// jsdom doesn't provide ResizeObserver.
|
||||
beforeAll(() => {
|
||||
globalThis.ResizeObserver = class ResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
} as unknown as typeof globalThis.ResizeObserver;
|
||||
});
|
||||
|
||||
let tagCounter = 0;
|
||||
|
||||
function uniqueTag(base: string): string {
|
||||
return `${base}-${tagCounter++}`;
|
||||
}
|
||||
|
||||
function createElement<Element extends HTMLElement>(Base: abstract new () => Element): Element {
|
||||
const tag = uniqueTag('test-slp');
|
||||
customElements.define(tag, class extends (Base as unknown as typeof HTMLElement) {});
|
||||
return document.createElement(tag) as Element;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
describe('SliderPreviewElement', () => {
|
||||
it('has the correct tag name', () => {
|
||||
expect(SliderPreviewElement.tagName).toBe('media-slider-preview');
|
||||
});
|
||||
|
||||
it('defaults overflow to clamp', () => {
|
||||
const el = createElement(SliderPreviewElement);
|
||||
expect(el.overflow).toBe('clamp');
|
||||
});
|
||||
|
||||
it('sets structural positioning styles after connect', async () => {
|
||||
const slider = createElement(SliderElement);
|
||||
const preview = createElement(SliderPreviewElement);
|
||||
|
||||
slider.appendChild(preview);
|
||||
document.body.appendChild(slider);
|
||||
|
||||
await slider.updateComplete;
|
||||
await preview.updateComplete;
|
||||
|
||||
expect(preview.style.position).toBe('absolute');
|
||||
expect(preview.style.pointerEvents).toBe('none');
|
||||
expect(preview.style.width).toBe('max-content');
|
||||
});
|
||||
|
||||
it('applies clamped left style by default', async () => {
|
||||
const slider = createElement(SliderElement);
|
||||
const preview = createElement(SliderPreviewElement);
|
||||
|
||||
// jsdom rejects CSS min()/calc() — spy on setProperty to capture values.
|
||||
const spy = vi.spyOn(preview.style, 'setProperty');
|
||||
|
||||
slider.appendChild(preview);
|
||||
document.body.appendChild(slider);
|
||||
|
||||
await slider.updateComplete;
|
||||
await preview.updateComplete;
|
||||
|
||||
const leftCall = spy.mock.calls.find(([key]) => key === 'left');
|
||||
expect(leftCall).toBeTruthy();
|
||||
expect(leftCall![1]).toContain('min(');
|
||||
expect(leftCall![1]).toContain('max(');
|
||||
});
|
||||
|
||||
it('applies unclamped left style when overflow is visible', async () => {
|
||||
const slider = createElement(SliderElement);
|
||||
const preview = createElement(SliderPreviewElement);
|
||||
preview.overflow = 'visible';
|
||||
|
||||
const spy = vi.spyOn(preview.style, 'setProperty');
|
||||
|
||||
slider.appendChild(preview);
|
||||
document.body.appendChild(slider);
|
||||
|
||||
await slider.updateComplete;
|
||||
await preview.updateComplete;
|
||||
|
||||
const leftCall = spy.mock.calls.find(([key]) => key === 'left');
|
||||
expect(leftCall).toBeTruthy();
|
||||
expect(leftCall![1]).toContain('calc(var(--media-slider-pointer)');
|
||||
expect(leftCall![1]).not.toContain('min(');
|
||||
});
|
||||
|
||||
it('propagates data attributes from slider state', async () => {
|
||||
const slider = createElement(SliderElement);
|
||||
const preview = createElement(SliderPreviewElement);
|
||||
|
||||
slider.appendChild(preview);
|
||||
document.body.appendChild(slider);
|
||||
|
||||
await slider.updateComplete;
|
||||
await preview.updateComplete;
|
||||
|
||||
expect(preview.getAttribute('data-orientation')).toBe('horizontal');
|
||||
});
|
||||
|
||||
it('cleans up ResizeObserver on disconnect', async () => {
|
||||
const slider = createElement(SliderElement);
|
||||
const preview = createElement(SliderPreviewElement);
|
||||
|
||||
slider.appendChild(preview);
|
||||
document.body.appendChild(slider);
|
||||
|
||||
await slider.updateComplete;
|
||||
await preview.updateComplete;
|
||||
|
||||
// Should not throw when removed.
|
||||
slider.removeChild(preview);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user