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(