@editframe/elements 0.21.0-beta.0 → 0.23.7-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/dist/EF_FRAMEGEN.js +2 -3
  2. package/dist/attachContextRoot.d.ts +1 -0
  3. package/dist/attachContextRoot.js +9 -0
  4. package/dist/elements/ContextProxiesController.d.ts +1 -2
  5. package/dist/elements/EFAudio.js +2 -2
  6. package/dist/elements/EFCaptions.d.ts +1 -3
  7. package/dist/elements/EFCaptions.js +59 -51
  8. package/dist/elements/EFImage.js +2 -2
  9. package/dist/elements/EFMedia/AssetIdMediaEngine.js +1 -2
  10. package/dist/elements/EFMedia/AssetMediaEngine.js +1 -3
  11. package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +1 -1
  12. package/dist/elements/EFMedia/BufferedSeekingInput.js +2 -4
  13. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +4 -7
  14. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +1 -2
  15. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +5 -9
  16. package/dist/elements/EFMedia/shared/BufferUtils.js +1 -3
  17. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +4 -7
  18. package/dist/elements/EFMedia.d.ts +19 -0
  19. package/dist/elements/EFMedia.js +19 -2
  20. package/dist/elements/EFSourceMixin.js +1 -1
  21. package/dist/elements/EFSurface.js +1 -1
  22. package/dist/elements/EFTemporal.browsertest.d.ts +11 -0
  23. package/dist/elements/EFTemporal.d.ts +10 -0
  24. package/dist/elements/EFTemporal.js +82 -5
  25. package/dist/elements/EFThumbnailStrip.js +9 -16
  26. package/dist/elements/EFTimegroup.browsertest.d.ts +3 -3
  27. package/dist/elements/EFTimegroup.d.ts +35 -14
  28. package/dist/elements/EFTimegroup.js +73 -120
  29. package/dist/elements/EFVideo.d.ts +10 -0
  30. package/dist/elements/EFVideo.js +15 -2
  31. package/dist/elements/EFWaveform.js +10 -18
  32. package/dist/elements/SampleBuffer.js +1 -2
  33. package/dist/elements/TargetController.js +2 -2
  34. package/dist/elements/renderTemporalAudio.d.ts +10 -0
  35. package/dist/elements/renderTemporalAudio.js +35 -0
  36. package/dist/elements/updateAnimations.js +7 -10
  37. package/dist/gui/ContextMixin.d.ts +5 -5
  38. package/dist/gui/ContextMixin.js +151 -117
  39. package/dist/gui/Controllable.browsertest.d.ts +0 -0
  40. package/dist/gui/Controllable.d.ts +15 -0
  41. package/dist/gui/Controllable.js +9 -0
  42. package/dist/gui/EFConfiguration.js +1 -1
  43. package/dist/gui/EFControls.browsertest.d.ts +11 -0
  44. package/dist/gui/EFControls.d.ts +18 -4
  45. package/dist/gui/EFControls.js +67 -25
  46. package/dist/gui/EFDial.browsertest.d.ts +0 -0
  47. package/dist/gui/EFDial.d.ts +18 -0
  48. package/dist/gui/EFDial.js +141 -0
  49. package/dist/gui/EFFilmstrip.browsertest.d.ts +11 -0
  50. package/dist/gui/EFFilmstrip.d.ts +12 -2
  51. package/dist/gui/EFFilmstrip.js +140 -34
  52. package/dist/gui/EFFitScale.js +2 -4
  53. package/dist/gui/EFFocusOverlay.js +1 -1
  54. package/dist/gui/EFPause.browsertest.d.ts +0 -0
  55. package/dist/gui/EFPause.d.ts +23 -0
  56. package/dist/gui/EFPause.js +59 -0
  57. package/dist/gui/EFPlay.browsertest.d.ts +0 -0
  58. package/dist/gui/EFPlay.d.ts +23 -0
  59. package/dist/gui/EFPlay.js +59 -0
  60. package/dist/gui/EFPreview.d.ts +4 -0
  61. package/dist/gui/EFPreview.js +15 -6
  62. package/dist/gui/EFResizableBox.browsertest.d.ts +0 -0
  63. package/dist/gui/EFResizableBox.d.ts +34 -0
  64. package/dist/gui/EFResizableBox.js +547 -0
  65. package/dist/gui/EFScrubber.d.ts +9 -3
  66. package/dist/gui/EFScrubber.js +7 -7
  67. package/dist/gui/EFTimeDisplay.d.ts +7 -1
  68. package/dist/gui/EFTimeDisplay.js +5 -5
  69. package/dist/gui/EFToggleLoop.d.ts +9 -3
  70. package/dist/gui/EFToggleLoop.js +6 -4
  71. package/dist/gui/EFTogglePlay.d.ts +12 -4
  72. package/dist/gui/EFTogglePlay.js +24 -19
  73. package/dist/gui/EFWorkbench.js +1 -1
  74. package/dist/gui/PlaybackController.d.ts +67 -0
  75. package/dist/gui/PlaybackController.js +310 -0
  76. package/dist/gui/TWMixin.js +1 -1
  77. package/dist/gui/TargetOrContextMixin.d.ts +10 -0
  78. package/dist/gui/TargetOrContextMixin.js +98 -0
  79. package/dist/gui/efContext.d.ts +2 -2
  80. package/dist/index.d.ts +4 -0
  81. package/dist/index.js +5 -1
  82. package/dist/otel/setupBrowserTracing.d.ts +1 -1
  83. package/dist/otel/setupBrowserTracing.js +6 -4
  84. package/dist/otel/tracingHelpers.js +1 -2
  85. package/dist/style.css +1 -1
  86. package/package.json +5 -5
  87. package/src/elements/ContextProxiesController.ts +10 -10
  88. package/src/elements/EFAudio.ts +1 -0
  89. package/src/elements/EFCaptions.browsertest.ts +128 -58
  90. package/src/elements/EFCaptions.ts +60 -34
  91. package/src/elements/EFImage.browsertest.ts +1 -2
  92. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +3 -0
  93. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +1 -1
  94. package/src/elements/EFMedia.browsertest.ts +8 -15
  95. package/src/elements/EFMedia.ts +38 -7
  96. package/src/elements/EFSurface.browsertest.ts +2 -6
  97. package/src/elements/EFSurface.ts +1 -0
  98. package/src/elements/EFTemporal.browsertest.ts +58 -1
  99. package/src/elements/EFTemporal.ts +140 -4
  100. package/src/elements/EFThumbnailStrip.browsertest.ts +2 -8
  101. package/src/elements/EFThumbnailStrip.ts +1 -0
  102. package/src/elements/EFTimegroup.browsertest.ts +6 -7
  103. package/src/elements/EFTimegroup.ts +163 -244
  104. package/src/elements/EFVideo.browsertest.ts +143 -47
  105. package/src/elements/EFVideo.ts +26 -0
  106. package/src/elements/FetchContext.browsertest.ts +7 -2
  107. package/src/elements/TargetController.browsertest.ts +1 -0
  108. package/src/elements/TargetController.ts +1 -0
  109. package/src/elements/renderTemporalAudio.ts +108 -0
  110. package/src/elements/updateAnimations.browsertest.ts +181 -6
  111. package/src/elements/updateAnimations.ts +6 -6
  112. package/src/gui/ContextMixin.browsertest.ts +274 -27
  113. package/src/gui/ContextMixin.ts +230 -175
  114. package/src/gui/Controllable.browsertest.ts +258 -0
  115. package/src/gui/Controllable.ts +41 -0
  116. package/src/gui/EFControls.browsertest.ts +294 -80
  117. package/src/gui/EFControls.ts +139 -28
  118. package/src/gui/EFDial.browsertest.ts +84 -0
  119. package/src/gui/EFDial.ts +172 -0
  120. package/src/gui/EFFilmstrip.browsertest.ts +712 -0
  121. package/src/gui/EFFilmstrip.ts +213 -23
  122. package/src/gui/EFPause.browsertest.ts +202 -0
  123. package/src/gui/EFPause.ts +73 -0
  124. package/src/gui/EFPlay.browsertest.ts +202 -0
  125. package/src/gui/EFPlay.ts +73 -0
  126. package/src/gui/EFPreview.ts +20 -5
  127. package/src/gui/EFResizableBox.browsertest.ts +79 -0
  128. package/src/gui/EFResizableBox.ts +898 -0
  129. package/src/gui/EFScrubber.ts +7 -5
  130. package/src/gui/EFTimeDisplay.browsertest.ts +19 -19
  131. package/src/gui/EFTimeDisplay.ts +3 -1
  132. package/src/gui/EFToggleLoop.ts +6 -5
  133. package/src/gui/EFTogglePlay.ts +30 -23
  134. package/src/gui/PlaybackController.ts +522 -0
  135. package/src/gui/TWMixin.css +3 -0
  136. package/src/gui/TargetOrContextMixin.ts +185 -0
  137. package/src/gui/efContext.ts +2 -2
  138. package/src/otel/setupBrowserTracing.ts +17 -12
  139. package/test/cache-integration-verification.browsertest.ts +1 -1
  140. package/types.json +1 -1
  141. package/dist/elements/ContextProxiesController.js +0 -49
  142. /package/dist/_virtual/{_@oxc-project_runtime@0.93.0 → _@oxc-project_runtime@0.94.0}/helpers/decorate.js +0 -0
@@ -1,22 +1,37 @@
1
1
  import { html, LitElement } from "lit";
2
2
  import { customElement } from "lit/decorators.js";
3
- import { beforeEach, describe, expect, test } from "vitest";
3
+ import { afterEach, describe, expect, test } from "vitest";
4
4
 
5
5
  import "../elements/EFTimegroup.js";
6
6
  import { EFTargetable } from "../elements/TargetController.js";
7
7
  import { ContextMixin } from "./ContextMixin.js";
8
8
  import "./EFControls.js";
9
- import { EFControls } from "./EFControls.js";
10
9
  import "./EFPreview.js";
11
-
12
- @customElement("test-context")
13
- class TestContext extends EFTargetable(ContextMixin(LitElement)) {
10
+ import "./EFTogglePlay.js";
11
+ import "./EFPreview.js";
12
+ import "./EFTogglePlay.js";
13
+ import "./EFTimeDisplay.js";
14
+ import type { EFTimegroup } from "../elements/EFTimegroup.js";
15
+ import type { EFConfiguration } from "./EFConfiguration.js";
16
+ import type { EFControls } from "./EFControls.js";
17
+ import type { EFPreview } from "./EFPreview.js";
18
+ import type { EFTimeDisplay } from "./EFTimeDisplay.js";
19
+ import type { EFTogglePlay } from "./EFTogglePlay.js";
20
+
21
+ @customElement("ef-controls-test-context")
22
+ class EFControlsTestContext extends EFTargetable(ContextMixin(LitElement)) {
14
23
  render() {
15
24
  return html`<slot></slot>`;
16
25
  }
17
26
  }
18
27
 
19
- beforeEach(() => {
28
+ declare global {
29
+ interface HTMLElementTagNameMap {
30
+ "ef-controls-test-context": EFControlsTestContext;
31
+ }
32
+ }
33
+
34
+ afterEach(() => {
20
35
  // Clean up localStorage
21
36
  for (let i = 0; i < localStorage.length; i++) {
22
37
  const key = localStorage.key(i);
@@ -30,28 +45,36 @@ beforeEach(() => {
30
45
  }
31
46
  });
32
47
 
33
- describe("EFControls", () => {
34
- test("should be defined", () => {
35
- expect(EFControls).toBeDefined();
36
- });
48
+ const makeElement = <
49
+ TagName extends keyof HTMLElementTagNameMap,
50
+ T extends HTMLElementTagNameMap[TagName],
51
+ >(
52
+ tagName: TagName,
53
+ attributes: Record<string, string> = {},
54
+ target: Element | null = null,
55
+ ) => {
56
+ const element = document.createElement(tagName);
57
+ for (const [key, value] of Object.entries(attributes)) {
58
+ element.setAttribute(key, value);
59
+ }
60
+ if (target) {
61
+ target.appendChild(element);
62
+ }
63
+ return element as T;
64
+ };
37
65
 
66
+ describe("EFControls", () => {
38
67
  test("can find and connect to target preview by ID", async () => {
39
- // Create a preview with an ID
40
- const preview = document.createElement("ef-preview") as any;
41
- preview.id = "test-preview";
42
-
43
- // Add a timegroup to the preview to give it duration
44
- const timegroup = document.createElement("ef-timegroup");
45
- timegroup.mode = "fixed";
46
- timegroup.duration = "10s";
47
- preview.appendChild(timegroup);
48
-
49
- document.body.appendChild(preview);
50
-
51
- // Create controls targeting the preview
52
- const controls = document.createElement("ef-controls");
53
- controls.target = "test-preview";
54
- document.body.appendChild(controls);
68
+ const preview = makeElement(
69
+ "ef-preview",
70
+ { id: "test-preview", mode: "fixed", duration: "10s" },
71
+ document.body,
72
+ );
73
+ const controls = makeElement(
74
+ "ef-controls",
75
+ { target: "test-preview" },
76
+ document.body,
77
+ );
55
78
 
56
79
  // Wait for both elements to complete their updates
57
80
  await preview.updateComplete;
@@ -62,62 +85,49 @@ describe("EFControls", () => {
62
85
  });
63
86
 
64
87
  test("handles missing target gracefully", async () => {
65
- const controls = document.createElement("ef-controls");
66
- controls.target = "nonexistent-preview";
67
- document.body.appendChild(controls);
88
+ const controls = makeElement(
89
+ "ef-controls",
90
+ { target: "nonexistent-preview" },
91
+ document.body,
92
+ );
68
93
 
69
- // Wait for the controller to attempt connection
70
94
  await controls.updateComplete;
71
95
 
72
- // Should have no target but not crash
73
96
  expect(controls.targetElement).toBe(null);
74
- // Note: EFControls with context proxying doesn't have playing property
75
- // It only proxies context requests to the target element
76
97
  });
77
98
 
78
99
  test("updates when target is set after connection", async () => {
79
- const controls = document.createElement("ef-controls") as EFControls;
80
- document.body.appendChild(controls);
100
+ const preview = makeElement(
101
+ "ef-preview",
102
+ { mode: "fixed", duration: "10s" },
103
+ document.body,
104
+ );
105
+ const controls = makeElement("ef-controls", {}, document.body);
81
106
 
82
107
  // Initially no target
83
108
  expect(controls.targetElement).toBe(null);
84
109
 
85
- // Create preview
86
- const preview = document.createElement("test-context") as TestContext;
87
- preview.id = "test-preview";
88
-
89
- const timegroup = document.createElement("ef-timegroup");
90
- timegroup.mode = "fixed";
91
- timegroup.duration = "10s";
92
- preview.appendChild(timegroup);
93
-
94
- document.body.appendChild(preview);
95
-
96
- // Set target after both are connected
97
110
  controls.target = "test-preview";
111
+ preview.id = "test-preview";
98
112
 
99
- // Wait for both elements to complete their updates
100
113
  await preview.updateComplete;
101
114
  await controls.updateComplete;
102
115
 
103
- // The controls should have found and connected to the target
104
116
  expect(controls.targetElement).toBe(preview);
105
117
  });
106
118
 
107
119
  test("disconnects from target when removed", async () => {
108
- const preview = document.createElement("test-context") as TestContext;
120
+ const preview = makeElement("ef-controls-test-context", {}, document.body);
109
121
  preview.id = "test-preview";
110
122
 
111
- const timegroup = document.createElement("ef-timegroup");
112
- timegroup.mode = "fixed";
113
- timegroup.duration = "10s";
114
- preview.appendChild(timegroup);
123
+ makeElement("ef-timegroup", { mode: "fixed", duration: "10s" }, preview);
115
124
 
116
- document.body.appendChild(preview);
117
-
118
- const controls = document.createElement("ef-controls") as EFControls;
125
+ const controls = makeElement(
126
+ "ef-controls",
127
+ { target: "test-preview" },
128
+ document.body,
129
+ );
119
130
  controls.target = "test-preview";
120
- document.body.appendChild(controls);
121
131
 
122
132
  // Wait for both elements to complete their updates
123
133
  await preview.updateComplete;
@@ -126,36 +136,240 @@ describe("EFControls", () => {
126
136
  // Should be connected
127
137
  expect(controls.targetElement).toBe(preview);
128
138
 
129
- // Disconnect the controls
130
- document.body.removeChild(controls);
139
+ controls.remove();
131
140
 
132
141
  // After disconnection, targetElement persists but should have no effect
133
142
  // (TargetController only clears targetElement when target is removed, not when consumer disconnects)
134
143
  expect(controls.targetElement).toBe(preview);
135
144
  });
136
145
 
137
- test.skip("works with child control elements - EFTogglePlay", async () => {
138
- // Import the control element
139
- await import("./EFTogglePlay.js");
140
-
141
- const preview = document.createElement("test-context") as TestContext;
142
- preview.id = "test-preview";
143
- preview.playing = false;
144
-
145
- const timegroup = document.createElement("ef-timegroup");
146
- timegroup.mode = "fixed";
147
- timegroup.duration = "10s";
148
- preview.appendChild(timegroup);
149
-
150
- document.body.appendChild(preview);
151
-
152
- const controls = document.createElement("ef-controls") as EFControls;
153
- controls.target = "test-preview";
146
+ describe("html as text", () => {
147
+ const htmlTest = test.extend<{
148
+ markup: string;
149
+ container: HTMLElement;
150
+ preview: EFPreview;
151
+ timegroup: EFTimegroup;
152
+ togglePlay: EFTogglePlay;
153
+ timeDisplay: EFTimeDisplay;
154
+ configuration: EFConfiguration;
155
+ controls: EFControls;
156
+ }>({
157
+ container: async ({ markup }, use) => {
158
+ const container = makeElement("div", {}, document.body);
159
+ container.innerHTML = markup;
160
+ await use(container);
161
+ container.remove();
162
+ },
163
+ markup: async ({}, use) => {
164
+ const markup = `
165
+ <ef-configuration>
166
+ <ef-preview id="test-preview-html">
167
+ <ef-timegroup mode="fixed" duration="10s" id="test-timegroup">
168
+ <ef-video src="https://www.youtube.com/watch?v=dQw4w9WgXcQ" />
169
+ </ef-timegroup>
170
+ </ef-preview>
171
+ <ef-controls target="test-preview-html">
172
+ <ef-toggle-play></ef-toggle-play>
173
+ <ef-time-display></ef-time-display>
174
+ </ef-controls>
175
+ </ef-configuration>
176
+ `;
177
+ use(markup);
178
+ },
179
+ preview: async ({ container }, use) => {
180
+ const preview = container.querySelector("ef-preview")!;
181
+ await use(preview);
182
+ },
183
+ timegroup: async ({ container }, use) => {
184
+ const timegroup = container.querySelector("ef-timegroup")!;
185
+ await use(timegroup);
186
+ },
187
+ togglePlay: async ({ container }, use) => {
188
+ const togglePlay = container.querySelector("ef-toggle-play")!;
189
+ await use(togglePlay);
190
+ },
191
+ timeDisplay: async ({ container }, use) => {
192
+ const timeDisplay = container.querySelector("ef-time-display")!;
193
+ await use(timeDisplay);
194
+ },
195
+ configuration: async ({ container }, use) => {
196
+ const configuration = container.querySelector("ef-configuration")!;
197
+ await use(configuration);
198
+ },
199
+ controls: async ({ container }, use) => {
200
+ const controls = container.querySelector("ef-controls")!;
201
+ await use(controls);
202
+ },
203
+ });
204
+
205
+ htmlTest(
206
+ "can find and connect to target preview by ID",
207
+ async ({ preview, controls, togglePlay }) => {
208
+ expect(controls.targetElement).toBe(preview);
209
+ expect(togglePlay.efContext).toBe(preview);
210
+ },
211
+ );
212
+ });
154
213
 
155
- const togglePlay = document.createElement("ef-toggle-play");
156
- controls.appendChild(togglePlay);
214
+ describe("context propagation", () => {
215
+ const contextTest = test.extend<{
216
+ context: EFControlsTestContext;
217
+ controls: EFControls;
218
+ togglePlay: EFTogglePlay;
219
+ timeDisplay: EFTimeDisplay;
220
+ configuration: EFConfiguration;
221
+ }>({
222
+ configuration: async ({}, use) => {
223
+ const configuration = makeElement(
224
+ "ef-configuration",
225
+ {},
226
+ document.body,
227
+ );
228
+ use(configuration);
229
+ },
230
+ context: async ({ configuration }, use) => {
231
+ const context = makeElement(
232
+ "ef-controls-test-context",
233
+ { id: "test-preview" },
234
+ configuration,
235
+ );
236
+ makeElement(
237
+ "ef-timegroup",
238
+ { mode: "fixed", duration: "10s" },
239
+ context,
240
+ );
241
+ use(context);
242
+ },
243
+ controls: async ({ configuration }, use) => {
244
+ const controls = makeElement(
245
+ "ef-controls",
246
+ { target: "test-preview" },
247
+ configuration,
248
+ );
249
+ use(controls);
250
+ },
251
+ togglePlay: async ({ controls }, use) => {
252
+ const togglePlay = makeElement("ef-toggle-play", {}, controls);
253
+ use(togglePlay);
254
+ },
255
+ timeDisplay: async ({ controls }, use) => {
256
+ const timeDisplay = makeElement("ef-time-display", {}, controls);
257
+ use(timeDisplay);
258
+ },
259
+ });
260
+
261
+ contextTest(
262
+ "propagates changes to play state",
263
+ async ({ context, controls, togglePlay }) => {
264
+ context.play();
265
+ await context.updateComplete;
266
+ await controls.updateComplete;
267
+ await togglePlay.updateComplete;
268
+ expect(togglePlay.playing).toBe(true);
269
+ context.pause();
270
+ },
271
+ );
272
+
273
+ contextTest(
274
+ "propagates changes from toggle play button",
275
+ async ({ context, togglePlay }) => {
276
+ context.play();
277
+ await togglePlay.updateComplete;
278
+ await context.updateComplete;
279
+ expect(togglePlay.playing).toBe(true);
280
+ togglePlay.click();
281
+ await togglePlay.updateComplete;
282
+ expect(context.playing).toBe(false);
283
+ },
284
+ );
285
+
286
+ contextTest(
287
+ "efContext is consumed by toggle-play",
288
+ async ({ controls, context, togglePlay }) => {
289
+ await togglePlay.updateComplete;
290
+ expect(togglePlay.efContext).toBe(context);
291
+
292
+ controls.setAttribute("target", "no-target");
293
+ await togglePlay.updateComplete;
294
+ expect(togglePlay.efContext).toBeNull();
295
+
296
+ controls.setAttribute("target", "test-preview");
297
+ await togglePlay.updateComplete;
298
+ expect(togglePlay.efContext).toBe(context);
299
+ },
300
+ );
301
+
302
+ contextTest(
303
+ "propagates context from controls to toggle-play",
304
+ async ({ context, controls, togglePlay }) => {
305
+ context.play();
306
+ await controls.updateComplete;
307
+ await togglePlay.updateComplete;
308
+ expect(togglePlay.efContext).toBe(context);
309
+
310
+ // Pause before disconnecting to ensure clean state
311
+ context.pause();
312
+ await context.updateComplete;
313
+ await togglePlay.updateComplete;
314
+
315
+ controls.setAttribute("target", "no-target");
316
+ // Wait for context changes to propagate through multiple cycles
317
+ await new Promise((resolve) => requestAnimationFrame(resolve));
318
+ await controls.updateComplete;
319
+ await togglePlay.updateComplete;
320
+ await new Promise((resolve) => requestAnimationFrame(resolve));
321
+ await controls.updateComplete;
322
+ await togglePlay.updateComplete;
323
+ expect(controls.targetElement).toBeNull();
324
+ expect(togglePlay.efContext).toBeNull();
325
+ expect(togglePlay.playing).toBe(false);
326
+ // Test to ensure play context is no longer updated
327
+ context.pause();
328
+ await context.updateComplete;
329
+ context.play();
330
+ await context.updateComplete;
331
+ await togglePlay.updateComplete;
332
+ expect(togglePlay.playing).toBe(false);
333
+ },
334
+ );
335
+
336
+ contextTest(
337
+ "propagates changes to time display",
338
+ async ({ context, timeDisplay }) => {
339
+ await context.updateComplete;
340
+ await timeDisplay.updateComplete;
341
+ expect(timeDisplay.shadowRoot?.textContent?.trim()).toBe("0:00 / 0:10");
342
+
343
+ const timegroup = context.querySelector("ef-timegroup");
344
+ if (timegroup) {
345
+ await timegroup.seek(1000);
346
+ } else {
347
+ context.currentTimeMs = 1000;
348
+ }
349
+
350
+ await new Promise((resolve) => requestAnimationFrame(resolve));
351
+ await context.updateComplete;
352
+ await timeDisplay.updateComplete;
353
+ expect(timeDisplay.shadowRoot?.textContent?.trim()).toBe("0:01 / 0:10");
354
+ },
355
+ );
356
+ });
157
357
 
158
- document.body.appendChild(controls);
358
+ test("works with child control elements - EFTogglePlay", async () => {
359
+ const preview = makeElement(
360
+ "ef-controls-test-context",
361
+ { id: "test-preview" },
362
+ document.body,
363
+ );
364
+ makeElement("ef-timegroup", { mode: "fixed", duration: "10s" }, preview);
365
+
366
+ const controls = makeElement(
367
+ "ef-controls",
368
+ { target: "test-preview" },
369
+ document.body,
370
+ );
371
+
372
+ const togglePlay = makeElement("ef-toggle-play", {}, controls);
159
373
 
160
374
  // Wait for all elements to complete their updates
161
375
  await preview.updateComplete;
@@ -1,19 +1,82 @@
1
- import { css, html, LitElement } from "lit";
1
+ import { type Context, createContext, provide } from "@lit/context";
2
+ import { css, LitElement, type PropertyValueMap } from "lit";
2
3
  import { customElement, property, state } from "lit/decorators.js";
3
-
4
- import { ContextProxyController } from "../elements/ContextProxiesController.js";
4
+ import { attachContextRoot } from "../attachContextRoot.js";
5
+ import type { TemporalMixinInterface } from "../elements/EFTemporal.js";
5
6
  import { TargetController } from "../elements/TargetController.js";
6
- import type { ContextMixinInterface } from "./ContextMixin.js";
7
- import { targetTimegroupContext } from "./ContextMixin.js";
7
+ import { targetTemporalContext } from "./ContextMixin.js";
8
+ import type { ControllableInterface } from "./Controllable.js";
8
9
  import { currentTimeContext } from "./currentTimeContext.js";
9
10
  import { durationContext } from "./durationContext.js";
10
- import { efConfigurationContext } from "./EFConfiguration.js";
11
11
  import { efContext } from "./efContext.js";
12
- import { fetchContext } from "./fetchContext.js";
13
- import { focusContext } from "./focusContext.js";
12
+ import { type FocusContext, focusContext } from "./focusContext.js";
14
13
  import { focusedElementContext } from "./focusedElementContext.js";
15
14
  import { loopContext, playingContext } from "./playingContext.js";
16
15
 
16
+ attachContextRoot();
17
+
18
+ class ContextRequestEvent extends Event {
19
+ context: Context<any, any>;
20
+ contextTarget: Element;
21
+ callback: (proxy: EFControls, value: any) => void;
22
+ subscribe: boolean;
23
+
24
+ constructor(
25
+ context: Context<any, any>,
26
+ contextTarget: Element,
27
+ callback: (proxy: EFControls, value: any) => void,
28
+ subscribe: boolean,
29
+ ) {
30
+ super("context-request", { bubbles: true, composed: true });
31
+ this.context = context;
32
+ this.contextTarget = contextTarget;
33
+ this.callback = callback;
34
+ this.subscribe = subscribe ?? false;
35
+ }
36
+ }
37
+
38
+ const proxiedContexts = [
39
+ [
40
+ // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback
41
+ (proxy: EFControls, value: boolean) => (proxy.playing = value),
42
+ playingContext,
43
+ ],
44
+ [
45
+ // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback
46
+ (proxy: EFControls, value: boolean) => (proxy.loop = value),
47
+ loopContext,
48
+ ],
49
+ [
50
+ // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback
51
+ (proxy: EFControls, value: number) => (proxy.currentTimeMs = value),
52
+ currentTimeContext,
53
+ ],
54
+ [
55
+ // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback
56
+ (proxy: EFControls, value: number) => (proxy.durationMs = value),
57
+ durationContext,
58
+ ],
59
+ [
60
+ (proxy: EFControls, value: TemporalMixinInterface) =>
61
+ // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback
62
+ (proxy.targetTemporal = value),
63
+ targetTemporalContext,
64
+ ],
65
+ [
66
+ // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback
67
+ (proxy: EFControls, value: HTMLElement) => (proxy.focusedElement = value),
68
+ focusedElementContext,
69
+ ],
70
+ [
71
+ (proxy: EFControls, value: HTMLElement) =>
72
+ // biome-ignore lint/suspicious/noAssignInExpressions: Intentional assignment in callback
73
+ (proxy.focusContext.focusedElement = value),
74
+ focusContext,
75
+ ],
76
+ ] as const;
77
+
78
+ export const testContext = createContext<string | null>("test");
79
+
17
80
  /**
18
81
  * EFControls provides a way to control an ef-preview element that is not a direct ancestor.
19
82
  * It bridges the contexts from a target preview element to its children controls.
@@ -40,6 +103,10 @@ export class EFControls extends LitElement {
40
103
  }
41
104
  `;
42
105
 
106
+ createRenderRoot() {
107
+ return this;
108
+ }
109
+
43
110
  /**
44
111
  * The ID of the ef-preview element to control
45
112
  */
@@ -49,31 +116,75 @@ export class EFControls extends LitElement {
49
116
  /**
50
117
  * The target element (set by TargetController)
51
118
  */
119
+ @provide({ context: efContext })
120
+ @state()
121
+ targetElement: ControllableInterface | null = null;
122
+
123
+ @provide({ context: playingContext })
124
+ @state()
125
+ playing = false;
126
+
127
+ @provide({ context: loopContext })
128
+ @state()
129
+ loop = false;
130
+
131
+ @provide({ context: currentTimeContext })
52
132
  @state()
53
- targetElement: ContextMixinInterface | null = null;
133
+ currentTimeMs = 0;
134
+
135
+ @provide({ context: durationContext })
136
+ @state()
137
+ durationMs = 0;
138
+
139
+ @provide({ context: targetTemporalContext })
140
+ @state()
141
+ targetTemporal: TemporalMixinInterface | null = null;
142
+
143
+ @provide({ context: focusedElementContext })
144
+ @state()
145
+ focusedElement?: HTMLElement;
146
+
147
+ @provide({ context: focusContext })
148
+ focusContext = this as FocusContext;
54
149
 
55
150
  // @ts-expect-error controller is intentionally not referenced directly
151
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used for side effects
56
152
  #targetController = new TargetController(this);
57
153
 
58
- // @ts-expect-error controller is intentionally not referenced directly
59
- #contextProxyController = new ContextProxyController(this, {
60
- target: () => this.targetElement,
61
- contexts: [
62
- playingContext,
63
- loopContext,
64
- currentTimeContext,
65
- durationContext,
66
- targetTimegroupContext,
67
- focusedElementContext,
68
- efContext,
69
- fetchContext,
70
- focusContext,
71
- efConfigurationContext,
72
- ],
73
- });
74
-
75
- render() {
76
- return html`<slot></slot>`;
154
+ #proxyUnsubscribeMap = new Map<Context<any, any>, () => void>();
155
+
156
+ #unsubscribe() {
157
+ for (const unsubscribe of this.#proxyUnsubscribeMap.values()) {
158
+ unsubscribe();
159
+ }
160
+ this.#proxyUnsubscribeMap.clear();
161
+ }
162
+
163
+ updated(changedProperties: PropertyValueMap<this>) {
164
+ super.updated(changedProperties);
165
+ if (changedProperties.has("targetElement")) {
166
+ this.#unsubscribe();
167
+
168
+ if (this.targetElement) {
169
+ for (const [callback, context] of proxiedContexts) {
170
+ const event = new ContextRequestEvent(
171
+ context,
172
+ this,
173
+ (value, unsubscribe) => {
174
+ callback(this, value as never);
175
+ this.#proxyUnsubscribeMap.set(context, unsubscribe);
176
+ },
177
+ true,
178
+ );
179
+ this.targetElement.dispatchEvent(event);
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ disconnectedCallback() {
186
+ super.disconnectedCallback();
187
+ this.#unsubscribe();
77
188
  }
78
189
  }
79
190