diff --git a/packages/html/src/define/audio/player.ts b/packages/html/src/define/audio/player.ts index dc9ce71a..3364225b 100644 --- a/packages/html/src/define/audio/player.ts +++ b/packages/html/src/define/audio/player.ts @@ -1,7 +1,8 @@ import { audioFeatures } from '@videojs/core/dom'; +import { MediaContainerElement } from '../../media/container-element'; import { createPlayer } from '../../player/create-player'; import { MediaElement } from '../../ui/media-element'; -import '../media/container'; +import { safeDefine } from '../safe-define'; const { ProviderMixin } = createPlayer({ features: audioFeatures, @@ -11,7 +12,9 @@ export class AudioPlayerElement extends ProviderMixin(MediaElement) { static readonly tagName = 'audio-player'; } -customElements.define(AudioPlayerElement.tagName, AudioPlayerElement); +// Provider must be defined before consumer for context handshake during upgrade. +safeDefine(AudioPlayerElement); +safeDefine(MediaContainerElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/background/player.ts b/packages/html/src/define/background/player.ts index 329bb015..03caaa2b 100644 --- a/packages/html/src/define/background/player.ts +++ b/packages/html/src/define/background/player.ts @@ -1,7 +1,8 @@ import { backgroundFeatures } from '@videojs/core/dom'; +import { MediaContainerElement } from '../../media/container-element'; import { createPlayer } from '../../player/create-player'; import { MediaElement } from '../../ui/media-element'; -import '../media/container'; +import { safeDefine } from '../safe-define'; const { ProviderMixin } = createPlayer({ features: backgroundFeatures, @@ -11,7 +12,9 @@ export class BackgroundVideoPlayerElement extends ProviderMixin(MediaElement) { static readonly tagName = 'background-video-player'; } -customElements.define(BackgroundVideoPlayerElement.tagName, BackgroundVideoPlayerElement); +// Provider must be defined before consumer for context handshake during upgrade. +safeDefine(BackgroundVideoPlayerElement); +safeDefine(MediaContainerElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/background/skin.ts b/packages/html/src/define/background/skin.ts index 2e88a663..4f51e0e4 100644 --- a/packages/html/src/define/background/skin.ts +++ b/packages/html/src/define/background/skin.ts @@ -1,5 +1,6 @@ import { ReactiveElement } from '@videojs/element'; import { namedNodeMapToObject } from '@videojs/utils/dom'; +import { safeDefine } from '../safe-define'; function getTemplateHTML(_attrs: Record) { return /*html*/ ` @@ -24,7 +25,7 @@ export class BackgroundVideoSkinElement extends ReactiveElement { } } -customElements.define(BackgroundVideoSkinElement.tagName, BackgroundVideoSkinElement); +safeDefine(BackgroundVideoSkinElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/media/background-video.ts b/packages/html/src/define/media/background-video.ts index 01ee8581..c58b6520 100644 --- a/packages/html/src/define/media/background-video.ts +++ b/packages/html/src/define/media/background-video.ts @@ -1,10 +1,11 @@ import { BackgroundVideo } from '../../media/background-video'; +import { safeDefine } from '../safe-define'; export class BackgroundVideoElement extends BackgroundVideo { static readonly tagName = 'background-video'; } -customElements.define(BackgroundVideoElement.tagName, BackgroundVideoElement); +safeDefine(BackgroundVideoElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/media/container.ts b/packages/html/src/define/media/container.ts index b02495e3..485da22b 100644 --- a/packages/html/src/define/media/container.ts +++ b/packages/html/src/define/media/container.ts @@ -1,14 +1,7 @@ -import { playerContext } from '../../player/context'; -import { createContainerMixin } from '../../store/container-mixin'; -import { MediaElement } from '../../ui/media-element'; +import { MediaContainerElement } from '../../media/container-element'; +import { safeDefine } from '../safe-define'; -const ContainerMixin = createContainerMixin(playerContext); - -export class MediaContainerElement extends ContainerMixin(MediaElement) { - static readonly tagName = 'media-container'; -} - -customElements.define(MediaContainerElement.tagName, MediaContainerElement); +safeDefine(MediaContainerElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/media/hls-video.ts b/packages/html/src/define/media/hls-video.ts index 2b8e3780..bc51a0a3 100644 --- a/packages/html/src/define/media/hls-video.ts +++ b/packages/html/src/define/media/hls-video.ts @@ -1,10 +1,11 @@ import { HlsVideo } from '../../media/hls-video'; +import { safeDefine } from '../safe-define'; export class HlsVideoElement extends HlsVideo { static readonly tagName = 'hls-video'; } -customElements.define(HlsVideoElement.tagName, HlsVideoElement); +safeDefine(HlsVideoElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/safe-define.ts b/packages/html/src/define/safe-define.ts new file mode 100644 index 00000000..f5ff2f9b --- /dev/null +++ b/packages/html/src/define/safe-define.ts @@ -0,0 +1,8 @@ +type DefinableElement = CustomElementConstructor & { tagName: string }; + +/** Define a custom element only if not already registered. */ +export function safeDefine(element: DefinableElement): void { + if (!customElements.get(element.tagName)) { + customElements.define(element.tagName, element); + } +} diff --git a/packages/html/src/define/tests/registration.test.ts b/packages/html/src/define/tests/registration.test.ts new file mode 100644 index 00000000..d0b7e314 --- /dev/null +++ b/packages/html/src/define/tests/registration.test.ts @@ -0,0 +1,171 @@ +import { afterAll, beforeAll, describe, expect, it, type MockInstance, vi } from 'vitest'; + +/** + * Tests that composite define files register all expected custom elements + * and that provider/parent elements are defined before consumer/child elements. + * + * Tests run sequentially. Each dynamically imports a composite define file and + * checks the batch of `customElements.define()` calls that resulted. Because + * modules are cached within a test file, shared sub-elements (e.g. slider parts) + * are only registered by the first composite that imports them — subsequent + * composites skip them via `safeDefine`. This is intentional and tested. + */ +describe('composite define registration', () => { + let spy: MockInstance; + + /** Tag names registered since `offset` (the call count before an import). */ + function batchSince(offset: number): string[] { + return spy.mock.calls.slice(offset).map((call) => call[0] as string); + } + + beforeAll(() => { + spy = vi.spyOn(customElements, 'define'); + }); + + afterAll(() => { + spy.mockRestore(); + }); + + // ── Player composites ──────────────────────────────────────────────── + + describe('video/player', () => { + it('registers video-player before media-container', async () => { + const before = spy.mock.calls.length; + await import('../video/player'); + const batch = batchSince(before); + + expect(batch).toContain('video-player'); + expect(batch).toContain('media-container'); + expect(batch.indexOf('video-player')).toBeLessThan(batch.indexOf('media-container')); + }); + }); + + describe('audio/player', () => { + it('registers audio-player', async () => { + const before = spy.mock.calls.length; + await import('../audio/player'); + const batch = batchSince(before); + + expect(batch).toContain('audio-player'); + // media-container already registered by video/player — safeDefine skips it + expect(batch).not.toContain('media-container'); + }); + }); + + describe('background/player', () => { + it('registers background-video-player', async () => { + const before = spy.mock.calls.length; + await import('../background/player'); + const batch = batchSince(before); + + expect(batch).toContain('background-video-player'); + expect(batch).not.toContain('media-container'); + }); + }); + + // ── Slider composites ──────────────────────────────────────────────── + + describe('ui/time-slider', () => { + it('registers media-time-slider before sub-elements', async () => { + const before = spy.mock.calls.length; + await import('../ui/time-slider'); + const batch = batchSince(before); + + // Parent slider must be first (provider for sliderContext) + expect(batch[0]).toBe('media-time-slider'); + + // All sub-elements registered (first composite to claim them) + expect(batch).toContain('media-slider-buffer'); + expect(batch).toContain('media-slider-fill'); + expect(batch).toContain('media-slider-thumb'); + expect(batch).toContain('media-slider-track'); + expect(batch).toContain('media-slider-value'); + }); + }); + + describe('ui/volume-slider', () => { + it('registers media-volume-slider and skips already-defined sub-elements', async () => { + const before = spy.mock.calls.length; + await import('../ui/volume-slider'); + const batch = batchSince(before); + + expect(batch).toContain('media-volume-slider'); + + // Sub-elements already registered by time-slider — safeDefine skips them + expect(batch).not.toContain('media-slider-fill'); + expect(batch).not.toContain('media-slider-thumb'); + expect(batch).not.toContain('media-slider-track'); + expect(batch).not.toContain('media-slider-value'); + }); + }); + + describe('ui/slider', () => { + it('registers media-slider', async () => { + const before = spy.mock.calls.length; + await import('../ui/slider'); + const batch = batchSince(before); + + expect(batch).toContain('media-slider'); + }); + }); + + // ── Other composites ───────────────────────────────────────────────── + + describe('ui/time', () => { + it('registers media-time before sub-elements', async () => { + const before = spy.mock.calls.length; + await import('../ui/time'); + const batch = batchSince(before); + + expect(batch[0]).toBe('media-time'); + expect(batch).toContain('media-time-group'); + expect(batch).toContain('media-time-separator'); + }); + }); + + describe('ui/controls', () => { + it('registers media-controls before sub-elements', async () => { + const before = spy.mock.calls.length; + await import('../ui/controls'); + const batch = batchSince(before); + + expect(batch[0]).toBe('media-controls'); + expect(batch).toContain('media-controls-group'); + }); + }); + + // ── Final state ────────────────────────────────────────────────────── + + describe('registry completeness', () => { + it('all expected elements are registered after importing all composites', () => { + const expected = [ + // Players + container + 'video-player', + 'audio-player', + 'background-video-player', + 'media-container', + // Sliders + 'media-slider', + 'media-time-slider', + 'media-volume-slider', + // Slider sub-elements + 'media-slider-buffer', + 'media-slider-fill', + 'media-slider-thumb', + 'media-slider-track', + 'media-slider-value', + // Time + 'media-time', + 'media-time-group', + 'media-time-separator', + // Controls + 'media-controls', + 'media-controls-group', + ]; + + for (const tagName of expected) { + expect(customElements.get(tagName), `${tagName} should be registered`).toBeDefined(); + } + }); + }); +}); diff --git a/packages/html/src/define/tests/safe-define.test.ts b/packages/html/src/define/tests/safe-define.test.ts new file mode 100644 index 00000000..646f3f66 --- /dev/null +++ b/packages/html/src/define/tests/safe-define.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; +import { safeDefine } from '../safe-define'; + +describe('safeDefine', () => { + it('registers a custom element', () => { + class TestElement extends HTMLElement { + static tagName = 'test-sd-register'; + } + + expect(customElements.get('test-sd-register')).toBeUndefined(); + safeDefine(TestElement); + expect(customElements.get('test-sd-register')).toBe(TestElement); + }); + + it('does not throw when element is already registered', () => { + class TestElement extends HTMLElement { + static tagName = 'test-sd-no-throw'; + } + + customElements.define('test-sd-no-throw', TestElement); + expect(() => safeDefine(TestElement)).not.toThrow(); + }); + + it('does not replace an existing registration', () => { + class Original extends HTMLElement { + static tagName = 'test-sd-no-replace'; + } + class Replacement extends HTMLElement { + static tagName = 'test-sd-no-replace'; + } + + safeDefine(Original); + safeDefine(Replacement); + expect(customElements.get('test-sd-no-replace')).toBe(Original); + }); +}); diff --git a/packages/html/src/define/ui/buffering-indicator.ts b/packages/html/src/define/ui/buffering-indicator.ts index f0e0faee..0143e430 100644 --- a/packages/html/src/define/ui/buffering-indicator.ts +++ b/packages/html/src/define/ui/buffering-indicator.ts @@ -1,6 +1,7 @@ import { BufferingIndicatorElement } from '../../ui/buffering-indicator/buffering-indicator-element'; +import { safeDefine } from '../safe-define'; -customElements.define(BufferingIndicatorElement.tagName, BufferingIndicatorElement); +safeDefine(BufferingIndicatorElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/controls-group.ts b/packages/html/src/define/ui/controls-group.ts index 81d762e8..f176a5b8 100644 --- a/packages/html/src/define/ui/controls-group.ts +++ b/packages/html/src/define/ui/controls-group.ts @@ -1,6 +1,7 @@ import { ControlsGroupElement } from '../../ui/controls/controls-group-element'; +import { safeDefine } from '../safe-define'; -customElements.define(ControlsGroupElement.tagName, ControlsGroupElement); +safeDefine(ControlsGroupElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/controls.ts b/packages/html/src/define/ui/controls.ts index 9f8bfad6..d5044495 100644 --- a/packages/html/src/define/ui/controls.ts +++ b/packages/html/src/define/ui/controls.ts @@ -1,8 +1,9 @@ import { ControlsElement } from '../../ui/controls/controls-element'; +import { ControlsGroupElement } from '../../ui/controls/controls-group-element'; +import { safeDefine } from '../safe-define'; -import './controls-group'; - -customElements.define(ControlsElement.tagName, ControlsElement); +safeDefine(ControlsElement); +safeDefine(ControlsGroupElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/fullscreen-button.ts b/packages/html/src/define/ui/fullscreen-button.ts index 5f497a62..ed7abb50 100644 --- a/packages/html/src/define/ui/fullscreen-button.ts +++ b/packages/html/src/define/ui/fullscreen-button.ts @@ -1,6 +1,7 @@ import { FullscreenButtonElement } from '../../ui/fullscreen-button/fullscreen-button-element'; +import { safeDefine } from '../safe-define'; -customElements.define(FullscreenButtonElement.tagName, FullscreenButtonElement); +safeDefine(FullscreenButtonElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/mute-button.ts b/packages/html/src/define/ui/mute-button.ts index 6089abbf..08844269 100644 --- a/packages/html/src/define/ui/mute-button.ts +++ b/packages/html/src/define/ui/mute-button.ts @@ -1,6 +1,7 @@ import { MuteButtonElement } from '../../ui/mute-button/mute-button-element'; +import { safeDefine } from '../safe-define'; -customElements.define(MuteButtonElement.tagName, MuteButtonElement); +safeDefine(MuteButtonElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/pip-button.ts b/packages/html/src/define/ui/pip-button.ts index 11a84795..52901446 100644 --- a/packages/html/src/define/ui/pip-button.ts +++ b/packages/html/src/define/ui/pip-button.ts @@ -1,6 +1,7 @@ import { PiPButtonElement } from '../../ui/pip-button/pip-button-element'; +import { safeDefine } from '../safe-define'; -customElements.define(PiPButtonElement.tagName, PiPButtonElement); +safeDefine(PiPButtonElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/play-button.ts b/packages/html/src/define/ui/play-button.ts index 2600dea5..91890aa3 100644 --- a/packages/html/src/define/ui/play-button.ts +++ b/packages/html/src/define/ui/play-button.ts @@ -1,6 +1,7 @@ import { PlayButtonElement } from '../../ui/play-button/play-button-element'; +import { safeDefine } from '../safe-define'; -customElements.define(PlayButtonElement.tagName, PlayButtonElement); +safeDefine(PlayButtonElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/playback-rate-button.ts b/packages/html/src/define/ui/playback-rate-button.ts index 48f68c3e..2d5818c7 100644 --- a/packages/html/src/define/ui/playback-rate-button.ts +++ b/packages/html/src/define/ui/playback-rate-button.ts @@ -1,6 +1,7 @@ import { PlaybackRateButtonElement } from '../../ui/playback-rate-button/playback-rate-button-element'; +import { safeDefine } from '../safe-define'; -customElements.define(PlaybackRateButtonElement.tagName, PlaybackRateButtonElement); +safeDefine(PlaybackRateButtonElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/popover.ts b/packages/html/src/define/ui/popover.ts index 9bdb9cdb..e03e2234 100644 --- a/packages/html/src/define/ui/popover.ts +++ b/packages/html/src/define/ui/popover.ts @@ -1,6 +1,7 @@ import { PopoverElement } from '../../ui/popover/popover-element'; +import { safeDefine } from '../safe-define'; -customElements.define(PopoverElement.tagName, PopoverElement); +safeDefine(PopoverElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/poster.ts b/packages/html/src/define/ui/poster.ts index 8844f8a0..408bce46 100644 --- a/packages/html/src/define/ui/poster.ts +++ b/packages/html/src/define/ui/poster.ts @@ -1,6 +1,7 @@ import { PosterElement } from '../../ui/poster/poster-element'; +import { safeDefine } from '../safe-define'; -customElements.define(PosterElement.tagName, PosterElement); +safeDefine(PosterElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/seek-button.ts b/packages/html/src/define/ui/seek-button.ts index f4f11fe4..be228168 100644 --- a/packages/html/src/define/ui/seek-button.ts +++ b/packages/html/src/define/ui/seek-button.ts @@ -1,6 +1,7 @@ import { SeekButtonElement } from '../../ui/seek-button/seek-button-element'; +import { safeDefine } from '../safe-define'; -customElements.define(SeekButtonElement.tagName, SeekButtonElement); +safeDefine(SeekButtonElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/slider-buffer.ts b/packages/html/src/define/ui/slider-buffer.ts index 5050ce30..510f9142 100644 --- a/packages/html/src/define/ui/slider-buffer.ts +++ b/packages/html/src/define/ui/slider-buffer.ts @@ -1,6 +1,7 @@ import { SliderBufferElement } from '../../ui/slider/slider-buffer-element'; +import { safeDefine } from '../safe-define'; -customElements.define(SliderBufferElement.tagName, SliderBufferElement); +safeDefine(SliderBufferElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/slider-fill.ts b/packages/html/src/define/ui/slider-fill.ts index f2af0593..131070e3 100644 --- a/packages/html/src/define/ui/slider-fill.ts +++ b/packages/html/src/define/ui/slider-fill.ts @@ -1,6 +1,7 @@ import { SliderFillElement } from '../../ui/slider/slider-fill-element'; +import { safeDefine } from '../safe-define'; -customElements.define(SliderFillElement.tagName, SliderFillElement); +safeDefine(SliderFillElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/slider-thumb.ts b/packages/html/src/define/ui/slider-thumb.ts index e7e18832..752f8cd5 100644 --- a/packages/html/src/define/ui/slider-thumb.ts +++ b/packages/html/src/define/ui/slider-thumb.ts @@ -1,6 +1,7 @@ import { SliderThumbElement } from '../../ui/slider/slider-thumb-element'; +import { safeDefine } from '../safe-define'; -customElements.define(SliderThumbElement.tagName, SliderThumbElement); +safeDefine(SliderThumbElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/slider-track.ts b/packages/html/src/define/ui/slider-track.ts index b736122c..7364a639 100644 --- a/packages/html/src/define/ui/slider-track.ts +++ b/packages/html/src/define/ui/slider-track.ts @@ -1,6 +1,7 @@ import { SliderTrackElement } from '../../ui/slider/slider-track-element'; +import { safeDefine } from '../safe-define'; -customElements.define(SliderTrackElement.tagName, SliderTrackElement); +safeDefine(SliderTrackElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/slider-value.ts b/packages/html/src/define/ui/slider-value.ts index b3e0229d..78e62cf4 100644 --- a/packages/html/src/define/ui/slider-value.ts +++ b/packages/html/src/define/ui/slider-value.ts @@ -1,6 +1,7 @@ import { SliderValueElement } from '../../ui/slider/slider-value-element'; +import { safeDefine } from '../safe-define'; -customElements.define(SliderValueElement.tagName, SliderValueElement); +safeDefine(SliderValueElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/slider.ts b/packages/html/src/define/ui/slider.ts index c49c6efc..d65ebe40 100644 --- a/packages/html/src/define/ui/slider.ts +++ b/packages/html/src/define/ui/slider.ts @@ -1,11 +1,16 @@ import { SliderElement } from '../../ui/slider/slider-element'; +import { SliderFillElement } from '../../ui/slider/slider-fill-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'; +import { safeDefine } from '../safe-define'; -import './slider-fill'; -import './slider-thumb'; -import './slider-track'; -import './slider-value'; - -customElements.define(SliderElement.tagName, SliderElement); +// Parent slider first — sub-elements consume its context. +safeDefine(SliderElement); +safeDefine(SliderFillElement); +safeDefine(SliderThumbElement); +safeDefine(SliderTrackElement); +safeDefine(SliderValueElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/thumbnail.ts b/packages/html/src/define/ui/thumbnail.ts index 49476c67..fec1ad1b 100644 --- a/packages/html/src/define/ui/thumbnail.ts +++ b/packages/html/src/define/ui/thumbnail.ts @@ -1,6 +1,7 @@ import { ThumbnailElement } from '../../ui/thumbnail/thumbnail-element'; +import { safeDefine } from '../safe-define'; -customElements.define(ThumbnailElement.tagName, ThumbnailElement); +safeDefine(ThumbnailElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/time-group.ts b/packages/html/src/define/ui/time-group.ts index 5162569d..9fdf0afb 100644 --- a/packages/html/src/define/ui/time-group.ts +++ b/packages/html/src/define/ui/time-group.ts @@ -1,6 +1,7 @@ import { TimeGroupElement } from '../../ui/time/time-group-element'; +import { safeDefine } from '../safe-define'; -customElements.define(TimeGroupElement.tagName, TimeGroupElement); +safeDefine(TimeGroupElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/time-separator.ts b/packages/html/src/define/ui/time-separator.ts index 81eee001..42774603 100644 --- a/packages/html/src/define/ui/time-separator.ts +++ b/packages/html/src/define/ui/time-separator.ts @@ -1,6 +1,7 @@ import { TimeSeparatorElement } from '../../ui/time/time-separator-element'; +import { safeDefine } from '../safe-define'; -customElements.define(TimeSeparatorElement.tagName, TimeSeparatorElement); +safeDefine(TimeSeparatorElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/time-slider.ts b/packages/html/src/define/ui/time-slider.ts index 963372ad..25186875 100644 --- a/packages/html/src/define/ui/time-slider.ts +++ b/packages/html/src/define/ui/time-slider.ts @@ -1,12 +1,18 @@ +import { SliderBufferElement } from '../../ui/slider/slider-buffer-element'; +import { SliderFillElement } from '../../ui/slider/slider-fill-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'; import { TimeSliderElement } from '../../ui/time-slider/time-slider-element'; +import { safeDefine } from '../safe-define'; -import './slider-buffer'; -import './slider-fill'; -import './slider-thumb'; -import './slider-track'; -import './slider-value'; - -customElements.define(TimeSliderElement.tagName, TimeSliderElement); +// Parent slider first — sub-elements consume its context. +safeDefine(TimeSliderElement); +safeDefine(SliderBufferElement); +safeDefine(SliderFillElement); +safeDefine(SliderThumbElement); +safeDefine(SliderTrackElement); +safeDefine(SliderValueElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/time.ts b/packages/html/src/define/ui/time.ts index 04392bf2..96aa7c9c 100644 --- a/packages/html/src/define/ui/time.ts +++ b/packages/html/src/define/ui/time.ts @@ -1,9 +1,11 @@ import { TimeElement } from '../../ui/time/time-element'; +import { TimeGroupElement } from '../../ui/time/time-group-element'; +import { TimeSeparatorElement } from '../../ui/time/time-separator-element'; +import { safeDefine } from '../safe-define'; -import './time-group'; -import './time-separator'; - -customElements.define(TimeElement.tagName, TimeElement); +safeDefine(TimeElement); +safeDefine(TimeGroupElement); +safeDefine(TimeSeparatorElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/ui/volume-slider.ts b/packages/html/src/define/ui/volume-slider.ts index 262e6348..a607a9f5 100644 --- a/packages/html/src/define/ui/volume-slider.ts +++ b/packages/html/src/define/ui/volume-slider.ts @@ -1,11 +1,16 @@ +import { SliderFillElement } from '../../ui/slider/slider-fill-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'; import { VolumeSliderElement } from '../../ui/volume-slider/volume-slider-element'; +import { safeDefine } from '../safe-define'; -import './slider-fill'; -import './slider-thumb'; -import './slider-track'; -import './slider-value'; - -customElements.define(VolumeSliderElement.tagName, VolumeSliderElement); +// Parent slider first — sub-elements consume its context. +safeDefine(VolumeSliderElement); +safeDefine(SliderFillElement); +safeDefine(SliderThumbElement); +safeDefine(SliderTrackElement); +safeDefine(SliderValueElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/define/video/player.ts b/packages/html/src/define/video/player.ts index bebadbd8..7d07fb53 100644 --- a/packages/html/src/define/video/player.ts +++ b/packages/html/src/define/video/player.ts @@ -1,7 +1,8 @@ import { videoFeatures } from '@videojs/core/dom'; +import { MediaContainerElement } from '../../media/container-element'; import { createPlayer } from '../../player/create-player'; import { MediaElement } from '../../ui/media-element'; -import '../media/container'; +import { safeDefine } from '../safe-define'; const { ProviderMixin } = createPlayer({ features: videoFeatures, @@ -11,7 +12,9 @@ export class VideoPlayerElement extends ProviderMixin(MediaElement) { static readonly tagName = 'video-player'; } -customElements.define(VideoPlayerElement.tagName, VideoPlayerElement); +// Provider must be defined before consumer for context handshake during upgrade. +safeDefine(VideoPlayerElement); +safeDefine(MediaContainerElement); declare global { interface HTMLElementTagNameMap { diff --git a/packages/html/src/index.ts b/packages/html/src/index.ts index 1e6821b8..35da8c60 100644 --- a/packages/html/src/index.ts +++ b/packages/html/src/index.ts @@ -4,7 +4,8 @@ export * from '@videojs/core/dom'; // Store export type { Comparator, Selector } from '@videojs/store'; export { createSelector, shallowEqual } from '@videojs/store'; - +// Media +export { MediaContainerElement } from './media/container-element'; // Player export * from './player/context'; export * from './player/create-player'; diff --git a/packages/html/src/media/container-element.ts b/packages/html/src/media/container-element.ts new file mode 100644 index 00000000..eb84d971 --- /dev/null +++ b/packages/html/src/media/container-element.ts @@ -0,0 +1,9 @@ +import { playerContext } from '../player/context'; +import { createContainerMixin } from '../store/container-mixin'; +import { MediaElement } from '../ui/media-element'; + +const ContainerMixin = createContainerMixin(playerContext); + +export class MediaContainerElement extends ContainerMixin(MediaElement) { + static readonly tagName = 'media-container'; +} diff --git a/packages/html/tsdown.config.ts b/packages/html/tsdown.config.ts index 6205192e..c8e15a50 100644 --- a/packages/html/tsdown.config.ts +++ b/packages/html/tsdown.config.ts @@ -7,10 +7,12 @@ type BuildMode = 'dev' | 'default'; const buildModes: BuildMode[] = ['dev', 'default']; const defineEntries = Object.fromEntries( - globSync('src/define/**/*.ts').map((file) => { - const key = file.replace('src/', '').replace('.ts', ''); - return [key, file]; - }) + globSync('src/define/**/*.ts') + .filter((file) => !file.includes('.test.')) + .map((file) => { + const key = file.replace('src/', '').replace('.ts', ''); + return [key, file]; + }) ); const presetEntries = Object.fromEntries(