@fusion.dev/helix-headless 0.2.0 → 0.4.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 (127) hide show
  1. package/custom-elements.json +234 -234
  2. package/dist/accordion/item/accordion-item.js +0 -108
  3. package/dist/accordion/root/accordion-root.js +0 -33
  4. package/dist/announcement/root/announcement-root.js +1 -76
  5. package/dist/app-header/root/app-header-root.js +0 -84
  6. package/dist/avatar/group/avatar-group.js +0 -77
  7. package/dist/avatar/root/avatar-root.js +0 -72
  8. package/dist/badge/root/badge-root.js +0 -67
  9. package/dist/bar-chart/root/bar-chart-root.js +3 -263
  10. package/dist/blockquote/root/blockquote-root.js +0 -46
  11. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
  12. package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
  13. package/dist/button/root/button-root.js +0 -215
  14. package/dist/button-group/root/button-group-root.js +0 -30
  15. package/dist/callout/root/callout-root.js +0 -52
  16. package/dist/card/root/card-root.js +0 -41
  17. package/dist/carousel/root/carousel-root.d.ts +2 -2
  18. package/dist/carousel/root/carousel-root.js +2 -362
  19. package/dist/chat/composer/chat-composer.js +0 -131
  20. package/dist/chat/message/chat-message.js +0 -137
  21. package/dist/chat/root/chat-root.js +0 -193
  22. package/dist/checkbox/root/checkbox-root.js +0 -113
  23. package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
  24. package/dist/code/root/code-root.js +0 -26
  25. package/dist/code-block/root/code-block-root.js +0 -100
  26. package/dist/color-input/root/color-input-root.js +0 -217
  27. package/dist/color-picker/color.js +0 -69
  28. package/dist/color-picker/root/color-picker-root.js +0 -272
  29. package/dist/combobox/root/combobox-root.js +0 -428
  30. package/dist/command/root/command-root.js +0 -177
  31. package/dist/copy-button/root/copy-button-root.js +0 -134
  32. package/dist/data-table/root/data-table-root.js +0 -336
  33. package/dist/data-table/root/order.js +0 -100
  34. package/dist/date-input/root/date-input-root.js +1 -398
  35. package/dist/date-picker/root/date-picker-root.js +0 -358
  36. package/dist/dialog/root/dialog-root.js +0 -189
  37. package/dist/divider/root/divider-root.js +0 -71
  38. package/dist/donut-chart/root/donut-chart-root.js +0 -214
  39. package/dist/drawer/root/drawer-root.js +0 -116
  40. package/dist/drilldown/item/drilldown-item.js +0 -82
  41. package/dist/drilldown/root/drilldown-root.js +0 -91
  42. package/dist/element-events.generated.js +0 -5
  43. package/dist/empty/root/empty-root.js +0 -35
  44. package/dist/file-input/root/file-input-root.js +0 -232
  45. package/dist/format-date/root/format-date-root.js +0 -95
  46. package/dist/heatmap/root/buckets.js +0 -57
  47. package/dist/heatmap/root/heatmap-root.js +0 -313
  48. package/dist/icon/root/icon-root.js +0 -48
  49. package/dist/icon-button/root/icon-button-root.js +0 -30
  50. package/dist/image/root/image-root.js +0 -174
  51. package/dist/index.js +0 -9
  52. package/dist/input/root/input-root.js +0 -228
  53. package/dist/kbd/root/kbd-root.js +0 -117
  54. package/dist/legend/root/legend-root.js +0 -73
  55. package/dist/line-chart/root/line-chart-root.js +0 -172
  56. package/dist/list/item/list-item.js +0 -92
  57. package/dist/list/root/list-root.js +0 -26
  58. package/dist/listbox/root/listbox-root.js +2 -429
  59. package/dist/loader/root/loader-root.js +0 -40
  60. package/dist/megamenu/root/megamenu-root.js +0 -284
  61. package/dist/menu/item/menu-item.js +1 -221
  62. package/dist/menu/root/menu-root.js +1 -171
  63. package/dist/meter/root/meter-root.js +1 -152
  64. package/dist/navigation/root/navigation-root.js +0 -56
  65. package/dist/notifications/notification/notification.js +0 -159
  66. package/dist/notifications/root/notifications-root.js +0 -148
  67. package/dist/number-input/root/number-input-root.js +1 -263
  68. package/dist/option/group/option-group.js +0 -26
  69. package/dist/option/root/option-root.js +0 -157
  70. package/dist/outline/root/outline-root.js +0 -292
  71. package/dist/pagination/root/pagination-root.js +0 -361
  72. package/dist/pagination/size/pagination-size.js +0 -104
  73. package/dist/pagination/summary/pagination-summary.js +0 -90
  74. package/dist/pin-input/root/pin-input-root.js +0 -276
  75. package/dist/popover/root/popover-root.js +0 -103
  76. package/dist/progress/root/progress-root.js +1 -133
  77. package/dist/qr-code/root/qr-code-root.js +0 -96
  78. package/dist/quote/root/quote-root.js +0 -25
  79. package/dist/radio/group/radio-group.js +0 -81
  80. package/dist/radio/root/radio-root.js +0 -85
  81. package/dist/rating/root/rating-root.js +0 -220
  82. package/dist/scroll-area/root/scroll-area-root.js +0 -100
  83. package/dist/select/root/select-root.js +2 -218
  84. package/dist/skeleton/root/skeleton-root.js +0 -20
  85. package/dist/slider/root/slider-root.js +1 -267
  86. package/dist/sparkline/root/sparkline-root.js +3 -164
  87. package/dist/speed-dial/item/speed-dial-item.js +0 -52
  88. package/dist/speed-dial/root/speed-dial-root.js +1 -116
  89. package/dist/splitter/root/splitter-root.js +0 -489
  90. package/dist/stat/group/stat-group.js +0 -18
  91. package/dist/stat/root/stat-root.js +0 -23
  92. package/dist/steps/root/steps-root.js +0 -62
  93. package/dist/steps/step/step.js +0 -131
  94. package/dist/switch/root/switch-root.js +0 -80
  95. package/dist/table/cell/table-cell.js +0 -23
  96. package/dist/table/column/table-column.js +0 -158
  97. package/dist/table/root/table-root.js +0 -82
  98. package/dist/table/row/table-row.js +0 -21
  99. package/dist/tabs/panel/tab-panel.js +0 -49
  100. package/dist/tabs/root/tabs-root.js +0 -98
  101. package/dist/tabs/tab/tab.js +0 -59
  102. package/dist/tag/root/tag-root.js +0 -31
  103. package/dist/textarea/root/textarea-root.js +0 -162
  104. package/dist/time-input/root/time-input-root.js +0 -283
  105. package/dist/time-picker/root/time-picker-root.js +0 -326
  106. package/dist/timeline/item/timeline-item.js +0 -66
  107. package/dist/timeline/root/timeline-root.js +0 -41
  108. package/dist/toast/item/toast-item.js +0 -165
  109. package/dist/toast/root/toast-root.js +0 -51
  110. package/dist/toggle/group/toggle-group.js +0 -150
  111. package/dist/toggle/root/toggle-root.js +0 -78
  112. package/dist/toolbar/root/toolbar-root.js +0 -116
  113. package/dist/tooltip/root/tooltip-root.js +0 -427
  114. package/dist/tour/root/tour-root.js +0 -130
  115. package/dist/tour/stop/tour-stop.js +1 -127
  116. package/dist/tree/item/tree-item.js +0 -198
  117. package/dist/tree/root/tree-root.js +0 -97
  118. package/dist/trend/root/trend-root.js +0 -53
  119. package/dist/video/root/video-root.d.ts +8 -12
  120. package/dist/video/root/video-root.js +5 -756
  121. package/dist/video-playlist/item/video-playlist-item.js +1 -90
  122. package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
  123. package/dist/video-playlist/root/video-playlist-root.js +0 -113
  124. package/dist/virtual-list/root/virtual-list-root.js +0 -105
  125. package/jsx.d.ts +1408 -2576
  126. package/package.json +6 -6
  127. package/vue.d.ts +1 -1
@@ -9,17 +9,7 @@ import { airplay, arrowClockwise, arrowCounterClockwise, closedCaptioning, corne
9
9
  import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { html } from 'lit/static-html.js';
12
- /* Side-effect, for the menu rows this renders. Both tiers import it, because
13
- this tier writes the template and the styled tier inherits it without having
14
- a copy of its own to remind anyone. */
15
12
  import '../../menu/index.js';
16
- /**
17
- * Seconds as a clock, with an hour only when there is one.
18
- *
19
- * `0:42` and not `00:00:42`. A reader comparing the position to the duration is
20
- * reading two numbers at a glance, and leading zeroes that never change are
21
- * digits in the way of the ones that do.
22
- */
23
13
  const clock = (seconds) => {
24
14
  if (!Number.isFinite(seconds) || seconds < 0) {
25
15
  return '0:00';
@@ -33,318 +23,32 @@ const clock = (seconds) => {
33
23
  ? `${hours}:${String(minutes).padStart(2, '0')}:${padded}`
34
24
  : `${minutes}:${padded}`;
35
25
  };
36
- /**
37
- * Which player currently owns the page's one media session.
38
- *
39
- * There is exactly one `navigator.mediaSession` per page, and it decides what
40
- * the lock screen shows and where a headset's buttons land. Two players cannot
41
- * both have it, so the rule is written down rather than left to whichever
42
- * rendered last: **whoever played most recently owns it**, which is what a
43
- * reader means by "this one" when they press play on their headphones.
44
- *
45
- * Module scope, because the thing being shared is the page's and not any
46
- * element's. A player gives it up when it leaves the document, and only if it
47
- * still holds it: one that already lost the session to another must not clear
48
- * that other one's metadata on its way out.
49
- */
50
26
  let owner;
51
- /**
52
- * How far a skip control moves, in seconds.
53
- *
54
- * Ten, which is the number every player that has one uses, and the number the
55
- * platform names in `seekbackward` and `seekforward` when it hands over a
56
- * default. A different length here would be correct only on this player.
57
- */
58
27
  const SKIP_SECONDS = 10;
59
- /**
60
- * @summary Plays a video, with controls this library draws rather than the
61
- * browser's.
62
- *
63
- * @description
64
- * **The picture is a real `<video>`.** Decoding, buffering, captions, Picture
65
- * in Picture, AirPlay and the codecs a device happens to have are all the
66
- * platform's, and none of them can be had from anything else. What is replaced
67
- * is the control bar, which is the one part of a video element that cannot be
68
- * styled and looks different in every browser.
69
- *
70
- * **Every control is a real control.** The scrubber and the volume are
71
- * `<input type="range">`, so a reader gets arrow keys, Home and End, the
72
- * platform's own touch target and its screen reader announcement without this
73
- * element implementing any of it. Drawing a div and listening for pointer moves
74
- * is how a media player ends up unusable from a keyboard.
75
- *
76
- * **Native controls are the fallback, not the default.** Where a consumer sets
77
- * `controls`, the browser's bar is used and this one is not drawn: two sets of
78
- * controls over one video is worse than either alone, and a consumer who asked
79
- * for the native bar has a reason.
80
- *
81
- * The keys are the ones every player uses: Space and `k` to play or pause, the
82
- * arrows to seek and to change the volume, `m` to mute, `f` for fullscreen.
83
- * They are bound on this element rather than on the document, because a video
84
- * is not a palette: keys belong to it only while a reader is on it.
85
- *
86
- * **And none of them where `controls` is set.** The browser's own bar owns
87
- * those keys, and two handlers on one press is a video that seeks twice or
88
- * plays and immediately pauses. The focus stop goes with them, since the native
89
- * controls bring their own.
90
- *
91
- * **`<source>` and `<track>` are copied into the video, not slotted into it.**
92
- * A media element runs its resource selection over its own child *nodes*, and a
93
- * slot is one node: the sources and the caption tracks a consumer wrote were
94
- * assigned into the flattened tree and the video never saw any of them. It had
95
- * no text tracks, no current source and a `readyState` of zero, so a video
96
- * given sources this way did not merely lack captions, it did not play.
97
- *
98
- * They are copied rather than moved, because the consumer's light DOM is theirs
99
- * and a framework that re-renders it would find its children gone. The copies
100
- * are rebuilt whenever the slot changes and the video is asked to load again,
101
- * which is what makes a `<track>` added later take effect.
102
- *
103
- * **The lock screen and a headset's buttons work, and one player owns them.**
104
- * There is one `navigator.mediaSession` per page, so the rule is stated rather
105
- * than left to render order: whichever player was started last owns it, which
106
- * is what a reader means by "this one" when they press play on their
107
- * headphones. The title comes from `label` and the artwork from `poster`.
108
- *
109
- * **`remote` sends the picture somewhere else.** The Remote Playback API is
110
- * what Safari implements for AirPlay and Chrome for Cast, so one control covers
111
- * both and the platform supplies the device list. It is offered only once
112
- * something has answered `watchAvailability`: a button that opens an empty list
113
- * is worse than no button, and a consumer who wants none can set
114
- * `disableremoteplayback` on this element.
115
- *
116
- * The properties below are written by this component and read by whoever draws
117
- * it. A consumer sets none of them: they carry a number this element computed
118
- * and CSS cannot, which is what lets a stylesheet on this tier draw the thing
119
- * without measuring anything itself.
120
- *
121
- * @since 0.1.0
122
- * @status experimental
123
- * @category media
124
- * @aliases Player, Media, Movie
125
- * @documentation https://helix.fusion.dev/components/video
126
- *
127
- * @slot - `<source>` and `<track>` elements, which are copied into the video.
128
- * @slot back - Top left over the frame, for a way out of a set.
129
- * @slot controls - Added to the control bar, before fullscreen.
130
- * @slot poster - Shown over the video until it starts.
131
- *
132
- * @event {Event} ended - Relayed, because it does not cross a shadow boundary.
133
- * @event {HelixNextEvent} helix-next - Fired when the reader asks for the next item.
134
- * @event {HelixPreviousEvent} helix-previous - Fired when they ask for the previous one.
135
- * @event {Event} pause - Relayed for the same reason.
136
- * @event {Event} play - Relayed for the same reason.
137
- *
138
- * @csspart back - The region top left, for a way out of a set.
139
- * @csspart captions - Subtitles on and off.
140
- * @csspart captions-menu - The language menu, where there is more than one.
141
- * @csspart captions-menu-back - The control that returns from a submenu.
142
- * @csspart captions-menu-back-mark - The glyph on the back control of a submenu.
143
- * @csspart captions-menu-empty - Shown when there are no items to show.
144
- * @csspart captions-menu-header - The bar above a submenu, holding the way back and its name.
145
- * @csspart captions-menu-loading - Shown while the items are still on their way.
146
- * @csspart captions-menu-surface - The floating panel the menu is drawn on.
147
- * @csspart captions-menu-title - The name of the submenu that is showing.
148
- * @csspart controls - The bar along the bottom.
149
- * @csspart cue - The caption on screen now, drawn by this element.
150
- * @csspart download - Saves the file, where it was asked for.
151
- * @csspart entry - One row in a menu this player opens.
152
- * @csspart entry-item - The row, which is an `<a>` where `href` is set.
153
- * @csspart entry-mark - The trailing mark: a tick when checked, a caret when it opens.
154
- * @csspart entry-start - The leading column, which is a slot and is always there.
155
- * @csspart failure - What is said when the source will not play.
156
- * @csspart forward - The control that goes on ten seconds.
157
- * @csspart fullscreen - The whole screen, and back.
158
- * @csspart loading - What is said while the picture waits for more of itself.
159
- * @csspart loading-mark - The part of that which spins.
160
- * @csspart more - The button that opens it.
161
- * @csspart mute - Sound on and off.
162
- * @csspart next - Asks for the next item, where it was asked for.
163
- * @csspart overflow - The menu holding the controls kept off the bar.
164
- * @csspart overflow-back - The control that returns from a submenu.
165
- * @csspart overflow-back-mark - The glyph on the back control of a submenu.
166
- * @csspart overflow-empty - Shown when there are no items to show.
167
- * @csspart overflow-header - The bar above a submenu, holding the way back and its name.
168
- * @csspart overflow-loading - Shown while the items are still on their way.
169
- * @csspart overflow-surface - The floating panel the menu is drawn on.
170
- * @csspart overflow-title - The name of the submenu that is showing.
171
- * @csspart pip - Picture in picture, where it was asked for and exists.
172
- * @csspart play - Play and pause.
173
- * @csspart poster - What covers it before it plays.
174
- * @csspart previous - Asks for the previous one.
175
- * @csspart remote - Play on another device, where one is available.
176
- * @csspart rewind - The control that goes back ten seconds.
177
- * @csspart scrubber - Where it is, and where to go.
178
- * @csspart sound - The speaker and its level, which are one control.
179
- * @csspart speed - Playback speed, where it was asked for.
180
- * @csspart stage - The box holding the picture and its controls, and the focus
181
- * target the keys are bound to.
182
- * @csspart start - The mark over the picture before anything has played.
183
- * @csspart time - The position and the duration.
184
- * @csspart video - The video itself.
185
- * @csspart volume - How loud.
186
- *
187
- * @cssproperty --video-intrinsic-ratio - The media's own aspect ratio, once it is known. `--video-aspect-ratio` overrides it.
188
- * @cssproperty --video-level - The current volume, 0 to 1.
189
- * @cssproperty --video-progress - How far playback has reached, as a fraction of the duration.
190
- */
191
28
  let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
192
29
  constructor() {
193
30
  super(...arguments);
194
- /**
195
- * Whether it starts on its own, which a browser only allows while silent.
196
- *
197
- * Forwarded rather than acted on. Whether an unprompted play is permitted is
198
- * the browser's policy and not this element's, and `play()` already swallows
199
- * the refusal: what follows a refused autoplay is a paused player showing its
200
- * start control, which is the correct thing to be looking at.
201
- */
202
31
  this.autoplay = false;
203
- /**
204
- * Whether the browser draws the controls instead of this element.
205
- *
206
- * A fallback rather than a default. Two bars over one video is worse than
207
- * either alone, so this one is not drawn at all when it is set.
208
- */
209
32
  this.controls = false;
210
- /**
211
- * How the media is fetched across origins, which decides whether a track can
212
- * be read.
213
- */
214
33
  this.crossOrigin = null;
215
- /**
216
- * Whether it *starts* silent, which is what an autoplaying video must do.
217
- *
218
- * **Named for the platform's split, which is real.** The `muted` content
219
- * attribute reflects to `defaultMuted`; the `muted` property is live state a
220
- * reader's click moves without touching the attribute. One property called
221
- * `muted` bound to the attribute answers what the author wrote rather than
222
- * whether there is sound, which is a name matching the platform's while
223
- * meaning something else.
224
- */
225
34
  this.defaultMuted = false;
226
- /**
227
- * Whether the browser may offer picture in picture.
228
- */
229
35
  this.disablePictureInPicture = false;
230
- /**
231
- * Whether the browser may offer playback on a remote device.
232
- */
233
36
  this.disableRemotePlayback = false;
234
- /**
235
- * And what they are told when it will not play at all.
236
- */
237
37
  this.errorLabel = 'This video could not be played';
238
- /**
239
- * Controls beyond the ones every player has, named in the order they appear.
240
- *
241
- * A list rather than a property apiece. A bar is a row with a budget, and a
242
- * boolean per feature invites a consumer to switch all of them on: naming
243
- * them makes the row a decision, and makes the order theirs. Anything
244
- * unrecognized is skipped rather than throwing, since the list is markup and
245
- * a typo should cost a control, not a page.
246
- *
247
- * `pip` is offered only where the platform has it, checked rather than
248
- * assumed: it is absent on iPhone and behind a flag in Firefox, and a button
249
- * that cannot work is worse than one that is not there.
250
- *
251
- * The names are `speed`, `pip`, `remote` and `download`.
252
- *
253
- * **`download` is named and never assumed.** Offering the file is a decision
254
- * about the content rather than about the player, and plenty of consumers
255
- * decline it deliberately. It is also the one control whose success is not
256
- * ours to promise: the attribute is ignored cross-origin, so a file served
257
- * from somewhere else opens rather than saves, which is the browser's rule
258
- * and not something a player can work around.
259
- */
260
38
  this.extraControls = '';
261
- /**
262
- * How long the bar waits after the pointer stops before it goes, in ms.
263
- *
264
- * A wait rather than leaving with the pointer, or a reader moving across the
265
- * picture to reach a control watches the bar vanish under them. Zero turns
266
- * the wait off and the bar goes immediately; a large number keeps it up for
267
- * as long as anyone is likely to look.
268
- *
269
- * The wait is only ever about *idling*. A video that is paused keeps its bar
270
- * regardless, because a reader who stopped it is deciding what to do next.
271
- */
272
39
  this.idleDelay = 2500;
273
- /**
274
- * Accessible name, since a video is not described by its filename.
275
- */
276
40
  this.label = '';
277
- /**
278
- * What a reader is told while the picture is waiting for more of itself.
279
- */
280
41
  this.loadingLabel = 'Loading';
281
- /**
282
- * Whether it starts again at the end.
283
- */
284
42
  this.loop = false;
285
- /**
286
- * Controls that go behind one button instead of onto the bar, named the same
287
- * way.
288
- *
289
- * **Named rather than measured, and the reason is that a width cannot answer
290
- * this.** A media query asks about the window, and the bar's problem is its
291
- * own width: a video in a 320px sidebar on a desktop is cramped and a
292
- * full-bleed hero on a phone is not. A container query asks the right
293
- * question and still cannot answer it, because how many controls there are is
294
- * the consumer's, so the threshold is not a width but a width per control,
295
- * which is a guess. And a control that moves between the bar and a hidden
296
- * menu as the layout shifts is a control the reader has to find twice.
297
- *
298
- * So the consumer says where the fold is, which they can, having already
299
- * named the controls in order. A name in both lists is drawn once, on the
300
- * bar: two of the same control is worse than either placement.
301
- */
302
43
  this.overflowControls = '';
303
- /**
304
- * And while it would stop it.
305
- */
306
44
  this.pauseLabel = 'Pause';
307
- /**
308
- * What the play control is called while it would start the video.
309
- */
310
45
  this.playLabel = 'Play';
311
- /**
312
- * Whether it plays where it sits rather than taking over the screen.
313
- */
314
46
  this.playsInline = true;
315
- /**
316
- * A still shown before it plays, which the platform draws itself.
317
- */
318
47
  this.poster = '';
319
- /**
320
- * How much to fetch before anything is asked for.
321
- */
322
48
  this.preload = 'metadata';
323
- /**
324
- * The speeds offered, slowest first, as multipliers of normal.
325
- */
326
49
  this.speeds = '0.5 1 1.5 2';
327
- /**
328
- * Where the video is, for the common case of one file.
329
- */
330
50
  this.src = '';
331
- /**
332
- * Whether playback has stopped to wait for data.
333
- *
334
- * **Only ever after somebody asked for playback**, which is what `waiting`
335
- * means: the element wanted the next frame and did not have it. Before that,
336
- * a video that has loaded nothing is a poster with a play control over it,
337
- * which looks deliberate and is. A spinner there would say something is
338
- * happening when nothing is, on every video on the page.
339
- *
340
- * The case this covers is the one that looks like a bug: a picture frozen
341
- * mid-play, the scrubber stopped, and nothing on screen saying why.
342
- */
343
51
  this.buffering = false;
344
- /**
345
- * Whether the source failed, which is the other thing a frozen frame can
346
- * mean.
347
- */
348
52
  this.failed = false;
349
53
  this.playing = false;
350
54
  this.position = 0;
@@ -353,50 +57,13 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
353
57
  this.silent = false;
354
58
  this.full = false;
355
59
  this.captioned = false;
356
- /**
357
- * Which caption track is showing, or `-1` for none.
358
- *
359
- * `@state`, because it is read off the video rather than declared: a consumer
360
- * asking for a language would be naming a track that may not have loaded, and
361
- * `default` on the `<track>` is the platform's own way to say it.
362
- */
363
60
  this.captionIndex = -1;
364
- /**
365
- * How many caption tracks there are, which decides which control is drawn.
366
- *
367
- * A count and not a boolean. Tracks resolve after the element renders, so
368
- * this is what schedules the update that draws the control at all, and a
369
- * boolean goes false to true when the first track arrives and then never
370
- * moves again: a second one landing leaves the toggle in place and the
371
- * language menu unreachable on exactly the videos it exists for.
372
- */
373
61
  this.captionCount = 0;
374
62
  this.rate = 1;
375
- /**
376
- * Whether the reader has been still long enough for the bar to go.
377
- */
378
63
  this.idle = false;
379
- /**
380
- * Whether anything on the network offers to play this.
381
- */
382
64
  this.remoteAvailable = false;
383
- /**
384
- * Whether it is playing somewhere else right now.
385
- */
386
65
  this.remoteConnected = false;
387
- /**
388
- * The sources and caption tracks to copy into the video, as the consumer
389
- * wrote them.
390
- */
391
66
  this.media = [];
392
- /**
393
- * Rebuilds the copies from whatever the consumer has slotted.
394
- *
395
- * The attribute list is explicit rather than a blind copy of every attribute:
396
- * these are two elements with a known and small surface, and copying anything
397
- * a consumer happened to put on one would carry their framework's own markers
398
- * into a shadow root that has no use for them.
399
- */
400
67
  this.handleMedia = (event) => {
401
68
  const assigned = event.target.assignedElements();
402
69
  this.media = assigned
@@ -422,41 +89,16 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
422
89
  return;
423
90
  }
424
91
  this.knownDuration = video.duration;
425
- /* The video's own shape, handed to CSS as a number.
426
- *
427
- * `aspect-ratio: auto` cannot be used for this, and that is the whole
428
- * reason this exists: `auto` takes the element's intrinsic ratio, and a
429
- * `<video>` carrying a poster reports the *poster's*. A three-by-two still
430
- * over a sixteen-by-nine clip made the box three-by-two, so the poster
431
- * filled it and the video letterboxed inside, standing fifty pixels
432
- * shorter the moment it started. Measured at 488 by 325 where the video
433
- * wanted 488 by 275.
434
- *
435
- * Set once the metadata is in, which is the first moment the real ratio is
436
- * knowable at all. Until then the stylesheet's own default holds. */
437
92
  if (video.videoWidth > 0 && video.videoHeight > 0) {
438
93
  this.style.setProperty('--video-intrinsic-ratio', `${video.videoWidth} / ${video.videoHeight}`);
439
94
  }
440
- /* Read once the video has its tracks, not from the slot. A `<track>` is
441
- only a caption once the platform has parsed it, and a consumer can add
442
- one later. */
443
95
  this.captionCount = this.captionTracks.length;
444
- /* And what the first one is doing, which is not always nothing: a
445
- `<track default>` starts showing without anyone pressing anything, so a
446
- button that began at "off" reported the opposite of what was on screen. */
447
96
  const track = video.textTracks[0];
448
97
  if (track) {
449
- /* A `<track default>` arrives `showing`, which is the browser drawing it.
450
- Moved to `hidden` so the cues keep coming and we draw them instead. */
451
98
  if (track.mode === 'showing') {
452
99
  track.mode = 'hidden';
453
100
  }
454
101
  this.captioned = track.mode === 'hidden';
455
- /* Taken off whatever it was on before adding it here. This runs from
456
- `loadedmetadata`, which fires again for every source a reader moves to,
457
- and a consumer adding a `<track>` later gets another. Added without
458
- removing, a playlist of six left six listeners on tracks that between
459
- them fire for every cue in the video. */
460
102
  this.cued?.removeEventListener('cuechange', this.handleCue);
461
103
  this.cued = track;
462
104
  track.addEventListener('cuechange', this.handleCue);
@@ -466,24 +108,9 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
466
108
  this.handleTime = () => {
467
109
  this.position = this.video?.currentTime ?? 0;
468
110
  };
469
- /* Relayed, because none of these cross a shadow boundary on their own. The
470
- video is inside this element's shadow root, so a consumer listening on the
471
- element they wrote hears nothing at all without this. Input relays `change`
472
- for the same reason. */
473
- /* `emptied` is in the list for a reason worth keeping. Assigning `src` runs
474
- the media load algorithm, which stops playback and sets `paused` without
475
- firing `pause`: the only events are `emptied` and `loadstart`. Listening for
476
- `play` and `pause` alone, this element went on believing it was playing
477
- after a playlist pointed it at the next entry, so the start control it draws
478
- only when paused stayed hidden and there was nothing at all to press.
479
- Measured: `paused` true, `playing` true, no `pause` event in the log. */
480
111
  this.handleWaiting = () => {
481
112
  this.buffering = true;
482
113
  };
483
- /* Anything that means the picture can move again. `loadstart` is in the list
484
- because a new source clears the last one's failure: a playlist pointing the
485
- player somewhere else must not keep showing the error from the entry
486
- before. */
487
114
  this.handleReady = (event) => {
488
115
  this.buffering = false;
489
116
  if (event.type === 'loadstart') {
@@ -506,25 +133,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
506
133
  if (owner === this && typeof navigator !== 'undefined' && navigator.mediaSession) {
507
134
  navigator.mediaSession.playbackState = this.playing ? 'playing' : 'paused';
508
135
  }
509
- /* Non-bubbling, which is what the platform sends and what makes these reach
510
- a React consumer at all.
511
- *
512
- * `play`, `pause` and `ended` are in react-dom's `nonDelegatedEvents`, read
513
- * out of the installed 19.2.8 rather than remembered: it concatenates the
514
- * whole media list onto `beforetoggle cancel close invalid load scroll
515
- * scrollend toggle`. React attaches those directly to `<video>`, `<audio>`
516
- * and `<img>` and never to a custom element, so nothing at the root hears
517
- * them. `bindReactHandler` is what closes that, and `reactPropFor` skips
518
- * anything that bubbles, on the grounds that a bubbling relay is already
519
- * reaching React's delegated listener and a second binding would double
520
- * every call.
521
- *
522
- * So these three were sending `bubbles: true` into the one branch that
523
- * refuses to bind, and `onPlay`, `onPause` and `onEnded` on `<helix-video>`
524
- * were silent in React. The comment beside `reactPropFor` named this exact
525
- * trio as fixed while this line kept it broken. Matching the platform's own
526
- * propagation fixes it and is what `Image` already does with `load` and
527
- * `error`. */
528
136
  this.dispatchEvent(new Event(event.type, { bubbles: false, composed: false }));
529
137
  };
530
138
  this.handleVolume = () => {
@@ -535,35 +143,11 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
535
143
  this.loudness = video.volume;
536
144
  this.silent = video.muted;
537
145
  };
538
- /**
539
- * Draws the active cue ourselves, so it can be put where we want it.
540
- *
541
- * The browser renders cues inside the video's own box and there is no
542
- * standard way to inset that box: a caption lands under the control bar and
543
- * stays there. `::cue` styles the text and not the region, and the property
544
- * that moves the region is a `-webkit-` internal that two of the three
545
- * engines do not have.
546
- *
547
- * So the track is set to `hidden` rather than `showing`, which is the mode
548
- * that keeps the cues coming without drawing them, and what it reports is
549
- * rendered into a part of our own. That also makes captions styleable, which
550
- * `::cue` only half allows.
551
- */
552
146
  this.handleCue = () => {
553
- /* The track that is showing, not the first one. With one track those are
554
- the same and with two they are not, so a reader on the second language
555
- was shown the first one's words. */
556
147
  const track = this.captionTracks[this.captionIndex] ?? this.video?.textTracks[0];
557
148
  const active = track?.activeCues?.[0];
558
149
  this.cue = active instanceof VTTCue ? active.getCueAsHTML() : undefined;
559
150
  };
560
- /**
561
- * Restarts the wait, on anything that counts as the reader being here.
562
- *
563
- * Pointer movement and focus both count, and both have to: a reader using the
564
- * keyboard never moves a pointer, and a bar that only answered the mouse
565
- * would leave them nothing to look at.
566
- */
567
151
  this.stir = () => {
568
152
  this.idle = false;
569
153
  clearTimeout(this.idleTimer);
@@ -576,22 +160,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
576
160
  this.handleFullscreenChange = () => {
577
161
  this.full = document.fullscreenElement === this;
578
162
  };
579
- /**
580
- * The keys every player uses, bound to this element rather than the document.
581
- *
582
- * A video is not a command palette: its keys belong to it while a reader is
583
- * on it, and a page with two videos on it would otherwise have both answering
584
- * the same press.
585
- *
586
- * Nothing is taken while focus is in a range. The scrubber and the volume are
587
- * real inputs and the arrows are already theirs, so intercepting would mean
588
- * seeking twice for one press.
589
- */
590
163
  this.handleKeydown = (event) => {
591
- /* Nothing at all where the browser draws the controls. Its own bar owns
592
- Space, the arrows and the rest, and two handlers on one key is a video
593
- that seeks twice or plays and immediately pauses. A consumer who asked
594
- for the native controls asked for the native keyboard with them. */
595
164
  if (this.controls) {
596
165
  return;
597
166
  }
@@ -651,15 +220,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
651
220
  this.remoteConnected = this.video?.remote?.state === 'connected';
652
221
  };
653
222
  }
654
- /**
655
- * Whether the file this points at is ours to hand over.
656
- *
657
- * `blob:` and `data:` named rather than compared, because the rule `download`
658
- * actually follows is same-origin *or one of those two*: a clip a consumer
659
- * generated in the page has no origin to match and is theirs to save by
660
- * definition. Comparing origins alone called both foreign, so the one case
661
- * where a download certainly works was the one case it was not offered.
662
- */
663
223
  get localFile() {
664
224
  if (this.src === '') {
665
225
  return false;
@@ -677,53 +237,30 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
677
237
  return false;
678
238
  }
679
239
  }
680
- /** Whether this platform can do a named control at all. */
681
240
  offers(one) {
682
241
  if (one === 'pip') {
683
242
  return typeof document !== 'undefined' && document.pictureInPictureEnabled;
684
243
  }
685
- /* Availability, not support. A device that can play this has to exist for
686
- the button to mean anything: an AirPlay control with nothing to send to
687
- is a control that opens an empty list, which is worse than no control.
688
- `watchAvailability` is what answers that, and it answers late, so this
689
- reads a flag the watch keeps rather than asking. */
690
244
  if (one === 'remote') {
691
245
  return this.remoteAvailable;
692
246
  }
693
- /* A download needs something to download. `src` and not a slotted
694
- `<source>`: the element chose one of those and which one is the
695
- browser's business, so offering a specific file would be a guess about a
696
- decision already made. */
697
247
  if (one === 'download') {
698
248
  return this.src !== '';
699
249
  }
700
- /* Always offered, because there is nothing to check. Neither of these does
701
- anything to the video: they say the reader asked, and whatever holds the
702
- set answers. A player that hid them until it could prove a set existed
703
- would be a player that cannot know. */
704
250
  if (one === 'next' || one === 'previous') {
705
251
  return true;
706
252
  }
707
- /* A skip needs only a timeline, and a video that has none cannot be played
708
- at all, so there is nothing here to check either. */
709
253
  if (one === 'forward' || one === 'rewind') {
710
254
  return true;
711
255
  }
712
256
  return one === 'speed';
713
257
  }
714
- /** The extras asked for, in order, and only the ones this platform can do. */
715
258
  get extras() {
716
259
  return this.extraControls
717
260
  .split(/\s+/)
718
261
  .filter(Boolean)
719
262
  .filter((one) => this.offers(one));
720
263
  }
721
- /**
722
- * The controls asked for behind the overflow button, in order.
723
- *
724
- * A name in both lists is on the bar and not here. Drawing it twice is worse
725
- * than either placement, and the bar is the one a reader can see.
726
- */
727
264
  get overflow() {
728
265
  const onBar = new Set(this.extras);
729
266
  return this.overflowControls
@@ -731,35 +268,9 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
731
268
  .filter(Boolean)
732
269
  .filter((one) => !onBar.has(one) && this.offers(one));
733
270
  }
734
- /**
735
- * The media's own state, under the platform's own names.
736
- *
737
- * **Published because there is no other way in.** The `<video>` is inside
738
- * this shadow root and `querySelector` does not cross it, so a consumer
739
- * asking where the video has reached has nowhere else to ask.
740
- *
741
- * **Read from the element, with the mirror as the fallback.** The element is
742
- * the truth and the mirror is a render cache that a media event refreshes, so
743
- * reading the mirror alone would answer with whatever the last `timeupdate`
744
- * said rather than with where the video is now. The fallback is what answers
745
- * on the server and before the first render, where there is no element and
746
- * the mirror holds what markup asked for.
747
- *
748
- * **Accessors rather than reactive properties**, because none of these is a
749
- * setting: they are the media's state, and a `@property` would give each one
750
- * an attribute that a consumer could write and the element would then have to
751
- * disagree with. Where the platform makes one writable this writes it
752
- * through, and where the platform does not neither does this.
753
- */
754
271
  get currentSrc() {
755
272
  return this.video?.currentSrc ?? '';
756
273
  }
757
- /**
758
- * Where playback has reached, in seconds.
759
- *
760
- * Writing seeks, through the same clamp `seek` uses, because a `currentTime`
761
- * past the end is the one write the platform answers with an exception.
762
- */
763
274
  get currentTime() {
764
275
  return this.video?.currentTime ?? this.position;
765
276
  }
@@ -767,7 +278,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
767
278
  this.position = seconds;
768
279
  this.seek(seconds);
769
280
  }
770
- /** The rate playback returns to, which `defaultPlaybackRate` names natively. */
771
281
  get defaultPlaybackRate() {
772
282
  return this.video?.defaultPlaybackRate ?? 1;
773
283
  }
@@ -777,19 +287,15 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
777
287
  video.defaultPlaybackRate = rate;
778
288
  }
779
289
  }
780
- /** How long the media runs, in seconds, and `NaN` until that is known. */
781
290
  get duration() {
782
291
  return this.video?.duration ?? this.knownDuration;
783
292
  }
784
- /** Whether playback has run off the end. */
785
293
  get ended() {
786
294
  return this.video?.ended ?? false;
787
295
  }
788
- /** Whether playback is stopped, which is the resting state and not an error. */
789
296
  get paused() {
790
297
  return this.video?.paused ?? !this.playing;
791
298
  }
792
- /** How fast it plays, where 1 is the recorded speed. */
793
299
  get playbackRate() {
794
300
  return this.video?.playbackRate ?? this.rate;
795
301
  }
@@ -800,7 +306,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
800
306
  video.playbackRate = rate;
801
307
  }
802
308
  }
803
- /** Whether pitch is held steady when the rate changes. */
804
309
  get preservesPitch() {
805
310
  return this.video?.preservesPitch ?? true;
806
311
  }
@@ -810,25 +315,15 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
810
315
  video.preservesPitch = preserve;
811
316
  }
812
317
  }
813
- /** Whether it is currently moving to a new position. */
814
318
  get seeking() {
815
319
  return this.video?.seeking ?? false;
816
320
  }
817
- /** The media's own height in pixels, and zero until the metadata has loaded. */
818
321
  get videoHeight() {
819
322
  return this.video?.videoHeight ?? 0;
820
323
  }
821
- /** The media's own width in pixels, and zero until the metadata has loaded. */
822
324
  get videoWidth() {
823
325
  return this.video?.videoWidth ?? 0;
824
326
  }
825
- /**
826
- * Whether there is sound, now, which a reader's click moves.
827
- *
828
- * The live half of the pair above. Writing mutes or unmutes the media itself
829
- * rather than the attribute, which is what the platform does and what the
830
- * mute control here has always done directly.
831
- */
832
327
  get muted() {
833
328
  return this.video?.muted ?? this.defaultMuted;
834
329
  }
@@ -839,7 +334,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
839
334
  video.muted = quiet;
840
335
  }
841
336
  }
842
- /** How loud it plays, 0 to 1. */
843
337
  get volume() {
844
338
  return this.video?.volume ?? this.loudness;
845
339
  }
@@ -853,22 +347,12 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
853
347
  get video() {
854
348
  return this.renderRoot?.querySelector('[part="video"]') ?? null;
855
349
  }
856
- /**
857
- * Starts playing, where the platform allows it.
858
- *
859
- * The rejection is swallowed on purpose: a browser refuses `play()` when
860
- * nothing the reader did asked for it, and that is a policy answer rather than
861
- * a fault. What follows a refused play is a paused player showing its start
862
- * control, which is the correct thing to be looking at.
863
- */
864
350
  play() {
865
351
  void this.video?.play().catch(() => { });
866
352
  }
867
- /** Stops, leaving the frame up. */
868
353
  pause() {
869
354
  this.video?.pause();
870
355
  }
871
- /** Toggles playback, which is what every key and the button both come to. */
872
356
  toggle() {
873
357
  const video = this.video;
874
358
  if (!video) {
@@ -881,7 +365,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
881
365
  this.pause();
882
366
  }
883
367
  }
884
- /** Moves to a point in seconds, clamped to what exists. */
885
368
  seek(seconds) {
886
369
  const video = this.video;
887
370
  if (!video || !Number.isFinite(video.duration)) {
@@ -889,25 +372,10 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
889
372
  }
890
373
  video.currentTime = Math.min(Math.max(seconds, 0), video.duration);
891
374
  }
892
- /**
893
- * The scrubber's new position, and the reason these two are named at all.
894
- *
895
- * **The stop is the first line.** `input` is composed by specification, so a
896
- * range inside this shadow root sends its own straight past the host to
897
- * whoever is listening on it, and this component declares no `input`: a
898
- * consumer binding one received the scrubber's and the volume rail's under a
899
- * name Video never promised, carrying a seconds count or a level between zero
900
- * and one rather than anything about the element.
901
- *
902
- * Every family that renders a native control needs this, not only the form
903
- * controls: a media component with two rails in its chrome is neither a form
904
- * control nor exempt from the mechanism.
905
- */
906
375
  takeScrub(event) {
907
376
  event.stopPropagation();
908
377
  this.seek(Number(event.target.value));
909
378
  }
910
- /** The volume rail's new level, stopped for the reason `takeScrub` carries. */
911
379
  takeVolume(event) {
912
380
  event.stopPropagation();
913
381
  const video = this.video;
@@ -917,19 +385,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
917
385
  video.volume = Number(event.target.value);
918
386
  video.muted = false;
919
387
  }
920
- /**
921
- * Takes the page's media session, and tells the platform what is playing.
922
- *
923
- * On play rather than on render, which is the ownership rule: a page with two
924
- * players gives the session to whichever one the reader started, so a
925
- * headset's buttons land on what they are listening to rather than on
926
- * whichever came first in the markup.
927
- *
928
- * The handlers are what make a headset's play, pause and skip work at all.
929
- * Without them a platform falls back to guessing which element was meant,
930
- * which is right often enough to hide the fault and wrong exactly when there
931
- * are two.
932
- */
933
388
  claimSession() {
934
389
  if (typeof navigator === 'undefined' || !navigator.mediaSession) {
935
390
  return;
@@ -947,16 +402,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
947
402
  this.seek(details.seekTime);
948
403
  }
949
404
  });
950
- /* The skip keys, and only where the consumer has said there is something to
951
- skip to. A headset whose skip buttons do nothing is worse than one whose
952
- skip buttons are not offered: the platform draws them from whether a
953
- handler is set, so setting one unconditionally would put two dead keys on
954
- every lock screen showing this video. */
955
405
  const skips = `${this.extraControls} ${this.overflowControls}`.split(/\s+/);
956
- /* The platform asks for a length and we answer with ours, or with its
957
- default where it did not say. A lock screen offering a skip that moves a
958
- different distance than the button on the bar is one player with two
959
- answers to the same gesture. */
960
406
  session.setActionHandler('seekbackward', (details) => {
961
407
  this.skipBy(-(details.seekOffset ?? SKIP_SECONDS));
962
408
  });
@@ -966,13 +412,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
966
412
  session.setActionHandler('nexttrack', skips.includes('next') ? () => this.skip(false) : null);
967
413
  session.setActionHandler('previoustrack', skips.includes('previous') ? () => this.skip(true) : null);
968
414
  }
969
- /**
970
- * Moves by a fixed length, clamped by `seek`.
971
- *
972
- * Separate from the arrow keys, which move by 5 and by 10 with Shift. Those
973
- * are a scrub; this is the gesture for "I missed that", and a reader who
974
- * presses it twice expects twenty seconds rather than an accelerating jump.
975
- */
976
415
  skipBy(seconds) {
977
416
  const video = this.video;
978
417
  if (!video) {
@@ -980,15 +419,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
980
419
  }
981
420
  this.seek(video.currentTime + seconds);
982
421
  }
983
- /**
984
- * The tracks a reader could choose between, in the order they were written.
985
- *
986
- * Filtered by kind, because `textTracks` also carries `chapters`, `metadata`
987
- * and `descriptions`, and a menu offering "Chapters" as a subtitle language
988
- * would be offering something that draws no words. Read off the video rather
989
- * than off the slot: a `<track>` added later is in one and not the other, and
990
- * the video is the one that has resolved them.
991
- */
992
422
  get captionTracks() {
993
423
  const tracks = this.video?.textTracks;
994
424
  if (!tracks) {
@@ -996,18 +426,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
996
426
  }
997
427
  return [...tracks].filter((one) => one.kind === 'subtitles' || one.kind === 'captions');
998
428
  }
999
- /**
1000
- * Shows one track and disables every other, or shows none at `-1`.
1001
- *
1002
- * Every other, rather than only the one that was on. Two tracks in `hidden`
1003
- * both fire `cuechange`, so the rendered cue becomes whichever fired last and
1004
- * a reader who switched language gets the two interleaved. Measured as two
1005
- * subtitles alternating line by line.
1006
- *
1007
- * `hidden`, never `showing`. Hidden is the mode where a track still fires
1008
- * `cuechange` and still fills `activeCues`, and the browser draws nothing,
1009
- * which is exactly what is wanted when something else is doing the drawing.
1010
- */
1011
429
  showCaption(index) {
1012
430
  const tracks = this.captionTracks;
1013
431
  tracks.forEach((track, at) => {
@@ -1017,29 +435,15 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1017
435
  this.captioned = index >= 0;
1018
436
  this.cue = undefined;
1019
437
  }
1020
- /** Turns captions on or off, which is what the button means with one track. */
1021
438
  toggleCaptions() {
1022
439
  if (this.captionTracks.length === 0) {
1023
440
  return;
1024
441
  }
1025
442
  this.showCaption(this.captioned ? -1 : 0);
1026
443
  }
1027
- /**
1028
- * Says the reader asked for another item, and does nothing else.
1029
- *
1030
- * There is nothing else it could do. A `<video>` plays one resource, so what
1031
- * comes before or after it is a fact this element does not hold: a playlist
1032
- * has it, or the consumer does. Announcing is the whole of the contribution,
1033
- * and the event bubbles so a wrapper hears it.
1034
- *
1035
- * The same call answers the button, the row in the overflow menu, and the
1036
- * skip keys on a headset, which is why the media session's handlers point
1037
- * here rather than at anything of their own.
1038
- */
1039
444
  skip(back) {
1040
445
  this.dispatchEvent(back ? new HelixPreviousEvent() : new HelixNextEvent());
1041
446
  }
1042
- /** Steps through the speeds offered, which is what one button can do. */
1043
447
  cycleSpeed() {
1044
448
  const speeds = this.speeds
1045
449
  .split(/\s+/)
@@ -1054,18 +458,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1054
458
  video.playbackRate = next;
1055
459
  this.rate = next;
1056
460
  }
1057
- /**
1058
- * Starts watching for something to play this on, and stops on the way out.
1059
- *
1060
- * The Remote Playback API is the standard one, and it is what Safari
1061
- * implements for AirPlay and Chrome for Cast: one call covers both, where the
1062
- * older `webkitShowPlaybackTargetPicker` covers only the first. Firefox has
1063
- * neither, and `remote` simply being absent is what turns the control off
1064
- * there.
1065
- *
1066
- * `watchAvailability` returns an id that has to be canceled, or the callback
1067
- * outlives the element that owns it.
1068
- */
1069
461
  async watchRemote() {
1070
462
  const remote = this.video?.remote;
1071
463
  if (!remote?.watchAvailability) {
@@ -1077,18 +469,13 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1077
469
  });
1078
470
  }
1079
471
  catch {
1080
- /* Refused where the media is not remotable, which is a fact about the
1081
- source rather than an error to report: the control stays off. */
1082
472
  }
1083
473
  }
1084
- /** Opens the platform's own device picker. It has no list of its own. */
1085
474
  async promptRemote() {
1086
475
  try {
1087
476
  await this.video?.remote?.prompt();
1088
477
  }
1089
478
  catch {
1090
- /* Dismissing the picker rejects, which is a reader saying no rather than
1091
- anything going wrong. */
1092
479
  }
1093
480
  }
1094
481
  async togglePip() {
@@ -1102,21 +489,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1102
489
  }
1103
490
  await video.requestPictureInPicture();
1104
491
  }
1105
- /**
1106
- * Into fullscreen and back, and quiet about being refused.
1107
- *
1108
- * `requestFullscreen` rejects rather than returning false, and the two
1109
- * refusals a consumer will actually meet are a Permissions Policy that does
1110
- * not grant `fullscreen` to the frame the player is in, and a call without
1111
- * user activation. Both are `TypeError: Permissions check failed`, which is
1112
- * the same message for two different causes.
1113
- *
1114
- * Caught rather than `void`ed, or a refusal becomes an unhandled rejection
1115
- * naming neither the element nor the reason. The state stays honest either
1116
- * way, because `aria-pressed` follows `fullscreenchange` rather than this
1117
- * call, so a refused request leaves a button that correctly reports "not
1118
- * fullscreen".
1119
- */
1120
492
  async toggleFullscreen() {
1121
493
  try {
1122
494
  if (document.fullscreenElement === this) {
@@ -1126,9 +498,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1126
498
  await this.requestFullscreen();
1127
499
  }
1128
500
  catch (error) {
1129
- /* Reported rather than swallowed, and reported once. A player embedded in
1130
- a frame without the permission is the common case, and the fix is on
1131
- the frame rather than here, so the message has to say which. */
1132
501
  console.warn(`<${this.localName}> could not enter fullscreen. If it is inside an iframe, that frame needs allow="fullscreen".`, error);
1133
502
  }
1134
503
  }
@@ -1142,14 +511,10 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1142
511
  this.cued?.removeEventListener('cuechange', this.handleCue);
1143
512
  this.cued = undefined;
1144
513
  document.removeEventListener('fullscreenchange', this.handleFullscreenChange);
1145
- /* The watch outlives the element unless it is canceled, and a callback
1146
- holding a removed element is a leak with a `@state` write on the end. */
1147
514
  if (this.remoteWatch !== undefined) {
1148
515
  void this.video?.remote?.cancelWatchAvailability?.(this.remoteWatch);
1149
516
  this.remoteWatch = undefined;
1150
517
  }
1151
- /* And the page's one media session goes back to nobody, but only if this
1152
- element is the one holding it. */
1153
518
  if (owner === this) {
1154
519
  owner = undefined;
1155
520
  if (typeof navigator !== 'undefined' && navigator.mediaSession) {
@@ -1169,47 +534,16 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1169
534
  void this.watchRemote();
1170
535
  }
1171
536
  }
1172
- /**
1173
- * Mirrors playback onto the host as `data-playing`.
1174
- *
1175
- * Because CSS cannot ask. `paused` is a property of a `<video>` and never an
1176
- * attribute, so `video:not([paused])` matches every video that has ever
1177
- * existed and a rule written on it hides the bar always, including on the
1178
- * paused video it was written to keep the bar up for.
1179
- *
1180
- * **A `data-` attribute is right here because of when it is written.** A fact
1181
- * that can be true on the first update mirrors to a custom state instead, or
1182
- * a consumer's server sends markup the element then grows an attribute on and
1183
- * React reports a mismatch it will not patch up. Nothing plays until somebody
1184
- * presses play, so this one is written long past hydration and there is
1185
- * nothing for it to disagree with. `data-closed` on `Toast.Item` and
1186
- * `data-scrolled` on `AppHeader` are the same case; `:state(open)` and
1187
- * `:state(checked)` are the other one.
1188
- */
1189
537
  updated(changed) {
1190
538
  super.updated?.(changed);
1191
539
  this.toggleAttribute('data-playing', this.playing);
1192
- /* `data-idle` rather than a hover rule alone, so the stylesheet can say
1193
- "playing, and nobody has moved for a while" instead of "playing and the
1194
- pointer is not on it this instant". */
1195
540
  this.toggleAttribute('data-idle', this.idle && this.playing);
1196
- /* A media element decides what to play once, when it is created or when it
1197
- is told to look again. Copies that arrive after that are markup it has
1198
- already walked past. */
1199
541
  if (changed.has('media')) {
1200
542
  const video = this.video;
1201
543
  video?.load();
1202
544
  this.captionCount = this.captionTracks.length;
1203
545
  }
1204
546
  }
1205
- /**
1206
- * One named control, as a button on the bar.
1207
- *
1208
- * A method rather than four branches inlined, because the same four names are
1209
- * asked for twice now, once for the bar and once for the menu behind it, and
1210
- * two lists that have to stay in step is how a control ends up offered in one
1211
- * place and not the other.
1212
- */
1213
547
  renderExtra(one) {
1214
548
  if (one === 'remote') {
1215
549
  return html `<button
@@ -1222,9 +556,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1222
556
  ${drawIcon(airplay)}
1223
557
  </button>`;
1224
558
  }
1225
- /* Two templates rather than one with the name bound in, for the reason
1226
- written under `previous` below: a part is read from the source as a
1227
- literal, so `part=${one}` is a part no check can prove anything about. */
1228
559
  if (one === 'rewind') {
1229
560
  return html `<button
1230
561
  part="rewind"
@@ -1255,10 +586,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1255
586
  ${this.rate}x
1256
587
  </button>`;
1257
588
  }
1258
- /* Two templates rather than one with the name bound in, because a part is
1259
- read from the source as a literal: `part=${one}` is a part no stylesheet
1260
- can be proved to reach, and nothing can match it against the
1261
- documentation either. */
1262
589
  if (one === 'previous') {
1263
590
  return html `<button
1264
591
  part="previous"
@@ -1280,19 +607,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1280
607
  </button>`;
1281
608
  }
1282
609
  if (one === 'download') {
1283
- /* A real anchor, because saving a file is the browser's job and there is
1284
- no scripted version of it worth having.
1285
- *
1286
- * **`download` only where it means something.** The attribute is honored
1287
- * for a same-origin file and ignored for any other, whatever headers come
1288
- * back, so on a video served from a CDN it does nothing and the press
1289
- * *navigates*: measured, clicking it took the whole page to the mp4, which
1290
- * reads as the player disappearing. Writing an attribute the browser will
1291
- * ignore is a lie in the markup, so it is written only when it holds.
1292
- *
1293
- * A new tab where it does not. That is the best a page can do with someone
1294
- * else's file, and it is the difference between handing the reader the
1295
- * video and taking away the one they were watching. */
1296
610
  const local = this.localFile;
1297
611
  return html `<a
1298
612
  part="download"
@@ -1314,24 +628,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1314
628
  ${drawIcon(pictureInPicture)}
1315
629
  </button>`;
1316
630
  }
1317
- /**
1318
- * A toggle for one track, a menu for several, and nothing for none.
1319
- *
1320
- * No menu at one, because a menu of a single language plus "Off" is a
1321
- * two-state control wearing a popover, and the button already says which
1322
- * state it is in through `aria-pressed`. The moment there are two the button
1323
- * cannot say which one is on, so it stops being a button.
1324
- *
1325
- * The label comes off the track: `label` where the markup gave one, and
1326
- * `srclang` where it did not, because a `<track>` with neither is a track
1327
- * nobody can choose between and that is the markup's fault to fix, not this
1328
- * component's to paper over with "Track 2".
1329
- */
1330
631
  renderCaptions() {
1331
- /* Branched on the state rather than on the getter, so the render that draws
1332
- this is one the state scheduled. Reading the tracks for their labels is
1333
- fine either way: by the time this runs, the count that got us here was
1334
- taken from the same list. */
1335
632
  const tracks = this.captionTracks;
1336
633
  if (this.captionCount === 0) {
1337
634
  return nothing;
@@ -1375,14 +672,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1375
672
  </${this.tag('menu-item')}>`)}
1376
673
  </${this.tag('menu')}>`;
1377
674
  }
1378
- /*
1379
- * The same control as a row in the overflow menu.
1380
- *
1381
- * The label is words rather than a mark, which is the difference between a
1382
- * bar and a menu: a row has the width for a name and a reader scanning one is
1383
- * reading rather than recognizing. `download` is a linked row, which is what
1384
- * `Menu.Item`'s `href` is for.
1385
- */
1386
675
  renderOverflowItem(one) {
1387
676
  if (one === 'remote') {
1388
677
  return html `<${this.tag('menu-item')} part="entry"
@@ -1511,16 +800,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1511
800
  </button>
1512
801
  ${this.overflow.map((one) => this.renderOverflowItem(one))}
1513
802
  </${this.tag('menu')}>`}
1514
- ${
1515
- /* Whatever else belongs on this bar, put here by whoever knows.
1516
- A playlist's dropdown is the case this was written for and the video
1517
- deliberately knows nothing about it: `helix-video-playlist` renders a
1518
- menu of its own entries into this slot, so the popover machinery and
1519
- the notion of a set of clips both stay in the component that is about
1520
- a set of clips. Before fullscreen, which is always the last thing on
1521
- a control bar because it is the one that changes what the bar is
1522
- attached to. */
1523
- ''}
803
+ ${''}
1524
804
  <slot name="controls"></slot>
1525
805
  <button
1526
806
  part="fullscreen"
@@ -1534,11 +814,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1534
814
  </div>`;
1535
815
  }
1536
816
  render() {
1537
- /* One focusable box around the lot, which is where the keys land. A video
1538
- without native controls is not focusable at all, and the buttons under it
1539
- are, so without this a reader could tab to Play and never to the picture
1540
- the keys are about. `role="group"` with a name so what they landed on
1541
- announces as something rather than as an unnamed div. */
1542
817
  return html `<div
1543
818
  part="stage"
1544
819
  tabindex=${this.controls ? nothing : '0'}
@@ -1547,8 +822,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1547
822
  @keydown=${this.handleKeydown}
1548
823
  @pointermove=${this.stir}
1549
824
  @pointerleave=${() => {
1550
- /* Leaving is the one case that does not wait: the reader has gone
1551
- somewhere else, and there is nothing to reach for. */
1552
825
  clearTimeout(this.idleTimer);
1553
826
  this.idle = true;
1554
827
  }}
@@ -1604,21 +877,10 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1604
877
  the first one ends it. -->
1605
878
  <span aria-hidden="true" hidden><slot @slotchange=${this.handleMedia}></slot></span>
1606
879
  ${this.cue === undefined ? nothing : html `<p aria-live="off" part="cue">${this.cue}</p>`}
1607
- ${
1608
- /* Top left, over the frame, and empty unless something is slotted.
1609
- A player inside a set has somewhere to go back to and a player on its
1610
- own does not, so the video draws whatever it is handed and decides
1611
- nothing: a `back` property would be this component holding an opinion
1612
- about navigation, which is the consumer's. */
1613
- ''}
880
+ ${''}
1614
881
  <div part="back"><slot name="back"></slot></div>
1615
882
  <slot name="poster" part="poster"></slot>
1616
- ${
1617
- /* A live region rather than a spinning box, because the reader who most
1618
- needs to be told the picture is waiting is the one who cannot see it
1619
- spin. `polite`, so it waits its turn, and it goes when the frame moves
1620
- again. */
1621
- this.buffering && !this.failed
883
+ ${this.buffering && !this.failed
1622
884
  ? html `<div
1623
885
  part="loading"
1624
886
  role="status"
@@ -1628,11 +890,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1628
890
  ${this.loadingLabel}
1629
891
  </div>`
1630
892
  : nothing}
1631
- ${
1632
- /* And the other thing a frozen frame can mean. `alert`, because nothing
1633
- further is going to happen and a reader waiting politely for a video
1634
- that will never load is waiting forever. */
1635
- this.failed
893
+ ${this.failed
1636
894
  ? html `<div
1637
895
  part="failure"
1638
896
  role="alert"
@@ -1640,16 +898,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
1640
898
  ${this.errorLabel}
1641
899
  </div>`
1642
900
  : nothing}
1643
- ${
1644
- /* Whenever it is not playing, which is what the mark means: press this
1645
- to play. Not only before the first play, which would make the mark
1646
- mean two different things at two moments that are the same state.
1647
- Every player a reader has used shows it on pause, and the one who
1648
- paused to study a frame clears it by pressing the thing in the way.
1649
-
1650
- Not drawn at all where the browser draws its own controls, since that
1651
- bar has a play button and two would be two. */
1652
- this.controls ||
901
+ ${this.controls ||
1653
902
  this.playing ||
1654
903
  this.buffering ||
1655
904
  this.failed ||