@fusion.dev/helix-headless 0.144.0 → 0.145.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 (155) hide show
  1. package/custom-elements.json +8312 -881
  2. package/dist/accordion/item/accordion-item.d.ts +48 -12
  3. package/dist/accordion/item/accordion-item.js +26 -5
  4. package/dist/accordion/root/accordion-root.d.ts +6 -0
  5. package/dist/accordion/root/accordion-root.js +7 -1
  6. package/dist/announcement/root/announcement-root.d.ts +21 -1
  7. package/dist/announcement/root/announcement-root.js +36 -1
  8. package/dist/app-header/root/app-header-root.d.ts +7 -0
  9. package/dist/app-header/root/app-header-root.js +5 -1
  10. package/dist/app-header/sectioning.d.ts +7 -0
  11. package/dist/app-header/sectioning.js +26 -0
  12. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +32 -1
  13. package/dist/breadcrumbs/root/breadcrumbs-root.js +25 -2
  14. package/dist/canvas/root/canvas-root.d.ts +38 -3
  15. package/dist/canvas/root/canvas-root.js +121 -40
  16. package/dist/carousel/root/carousel-root.d.ts +90 -0
  17. package/dist/carousel/root/carousel-root.js +134 -12
  18. package/dist/chat/composer/chat-composer.d.ts +38 -6
  19. package/dist/chat/composer/chat-composer.js +42 -9
  20. package/dist/chat/message/chat-message.d.ts +32 -1
  21. package/dist/chat/message/chat-message.js +25 -9
  22. package/dist/chat/naming.d.ts +22 -0
  23. package/dist/chat/naming.js +12 -0
  24. package/dist/chat/relative-clock.d.ts +39 -0
  25. package/dist/chat/relative-clock.js +31 -0
  26. package/dist/chat/root/chat-root.d.ts +36 -0
  27. package/dist/chat/root/chat-root.js +24 -1
  28. package/dist/code-block/root/code-block-root.d.ts +8 -1
  29. package/dist/code-block/root/code-block-root.js +5 -5
  30. package/dist/copy-button/root/copy-button-root.d.ts +21 -1
  31. package/dist/copy-button/root/copy-button-root.js +2 -2
  32. package/dist/data-table/root/data-table-root.d.ts +150 -2
  33. package/dist/data-table/root/data-table-root.js +303 -29
  34. package/dist/data-table/tree.d.ts +40 -0
  35. package/dist/data-table/tree.js +55 -0
  36. package/dist/description-list/item/description-list-item.d.ts +17 -2
  37. package/dist/description-list/item/description-list-item.js +15 -2
  38. package/dist/description-list/root/description-list-root.d.ts +27 -1
  39. package/dist/description-list/root/description-list-root.js +26 -1
  40. package/dist/drilldown/item/drilldown-item.d.ts +11 -6
  41. package/dist/drilldown/item/drilldown-item.js +8 -5
  42. package/dist/drilldown/root/drilldown-root.d.ts +11 -0
  43. package/dist/drilldown/root/drilldown-root.js +8 -3
  44. package/dist/element-events.generated.js +53 -8
  45. package/dist/filter-input/menu/filter-menu.d.ts +3 -0
  46. package/dist/filter-input/menu/filter-menu.js +8 -4
  47. package/dist/image/root/image-root.js +9 -1
  48. package/dist/input/root/input-root.d.ts +1 -1
  49. package/dist/input/root/input-root.js +2 -2
  50. package/dist/list/item/list-item.d.ts +68 -1
  51. package/dist/list/item/list-item.js +66 -4
  52. package/dist/list/numbering.d.ts +14 -0
  53. package/dist/list/numbering.js +8 -0
  54. package/dist/list/root/list-root.d.ts +106 -9
  55. package/dist/list/root/list-root.js +189 -4
  56. package/dist/list/selection.d.ts +19 -0
  57. package/dist/list/selection.js +21 -0
  58. package/dist/listbox/root/listbox-root.js +1 -2
  59. package/dist/map/root/map-root.d.ts +6 -2
  60. package/dist/map/root/map-root.js +5 -2
  61. package/dist/megamenu/root/megamenu-root.d.ts +21 -0
  62. package/dist/megamenu/root/megamenu-root.js +16 -2
  63. package/dist/menu/index.d.ts +1 -0
  64. package/dist/menu/index.js +1 -0
  65. package/dist/menu/item/menu-item.d.ts +38 -11
  66. package/dist/menu/item/menu-item.js +33 -1
  67. package/dist/menu/root/menu-root.d.ts +25 -1
  68. package/dist/menu/root/menu-root.js +63 -6
  69. package/dist/menu/section/menu-section.d.ts +55 -0
  70. package/dist/menu/section/menu-section.js +60 -0
  71. package/dist/navigation/root/navigation-root.d.ts +16 -1
  72. package/dist/navigation/root/navigation-root.js +27 -1
  73. package/dist/notifications/root/notifications-root.d.ts +18 -0
  74. package/dist/notifications/root/notifications-root.js +17 -1
  75. package/dist/object-inspector/root/object-inspector-root.d.ts +97 -16
  76. package/dist/object-inspector/root/object-inspector-root.js +179 -79
  77. package/dist/object-inspector/rows.d.ts +57 -0
  78. package/dist/object-inspector/rows.js +86 -0
  79. package/dist/outline/levels.d.ts +31 -0
  80. package/dist/outline/levels.js +23 -0
  81. package/dist/outline/root/outline-root.d.ts +68 -3
  82. package/dist/outline/root/outline-root.js +73 -8
  83. package/dist/pagination/size/pagination-size.d.ts +11 -2
  84. package/dist/pagination/size/pagination-size.js +19 -4
  85. package/dist/radio/group/radio-group.d.ts +4 -10
  86. package/dist/radio/group/radio-group.js +5 -10
  87. package/dist/rating/root/rating-root.d.ts +1 -1
  88. package/dist/rating/root/rating-root.js +2 -2
  89. package/dist/scroll-area/root/scroll-area-root.d.ts +113 -0
  90. package/dist/scroll-area/root/scroll-area-root.js +218 -3
  91. package/dist/scroll-area/scrollbar.d.ts +65 -0
  92. package/dist/scroll-area/scrollbar.js +24 -0
  93. package/dist/select/root/select-root.js +1 -1
  94. package/dist/splitter/root/splitter-root.d.ts +13 -0
  95. package/dist/splitter/root/splitter-root.js +20 -2
  96. package/dist/splitter/snap.d.ts +9 -0
  97. package/dist/splitter/snap.js +12 -0
  98. package/dist/steps/reach.d.ts +15 -0
  99. package/dist/steps/reach.js +12 -0
  100. package/dist/steps/root/steps-root.d.ts +30 -0
  101. package/dist/steps/root/steps-root.js +53 -0
  102. package/dist/steps/step/step.d.ts +38 -2
  103. package/dist/steps/step/step.js +28 -6
  104. package/dist/table/column/table-column.d.ts +11 -0
  105. package/dist/table/column/table-column.js +6 -1
  106. package/dist/table/root/table-root.d.ts +101 -0
  107. package/dist/table/root/table-root.js +166 -5
  108. package/dist/table/row/table-row.d.ts +47 -1
  109. package/dist/table/row/table-row.js +39 -2
  110. package/dist/tabs/panel/tab-panel.d.ts +9 -5
  111. package/dist/tabs/panel/tab-panel.js +3 -4
  112. package/dist/tabs/root/tabs-root.d.ts +25 -1
  113. package/dist/tabs/root/tabs-root.js +21 -3
  114. package/dist/tabs/tab/tab.d.ts +36 -4
  115. package/dist/tabs/tab/tab.js +46 -3
  116. package/dist/timeline/group/timeline-group.d.ts +46 -0
  117. package/dist/timeline/group/timeline-group.js +37 -0
  118. package/dist/timeline/index.d.ts +1 -0
  119. package/dist/timeline/index.js +1 -0
  120. package/dist/timeline/root/timeline-root.d.ts +10 -3
  121. package/dist/toast/item/toast-item.d.ts +10 -17
  122. package/dist/toast/item/toast-item.js +15 -36
  123. package/dist/toolbar/root/toolbar-root.d.ts +13 -0
  124. package/dist/toolbar/root/toolbar-root.js +15 -5
  125. package/dist/tree/item/tree-item.d.ts +54 -1
  126. package/dist/tree/item/tree-item.js +89 -20
  127. package/dist/tree/root/tree-root.d.ts +37 -0
  128. package/dist/tree/root/tree-root.js +51 -6
  129. package/dist/tree/selection.d.ts +24 -0
  130. package/dist/tree/selection.js +26 -0
  131. package/dist/video/media-events.d.ts +49 -0
  132. package/dist/video/media-events.js +39 -0
  133. package/dist/video/root/video-root.d.ts +99 -13
  134. package/dist/video/root/video-root.js +84 -47
  135. package/dist/video-playlist/menu/video-playlist-menu.d.ts +3 -0
  136. package/dist/video-playlist/menu/video-playlist-menu.js +2 -1
  137. package/dist/video-playlist/order.d.ts +41 -0
  138. package/dist/video-playlist/order.js +34 -0
  139. package/dist/video-playlist/root/video-playlist-root.d.ts +70 -2
  140. package/dist/video-playlist/root/video-playlist-root.js +60 -16
  141. package/dist/virtual-list/controls.d.ts +4 -0
  142. package/dist/virtual-list/controls.js +30 -0
  143. package/dist/virtual-list/fill.d.ts +30 -0
  144. package/dist/virtual-list/fill.js +39 -0
  145. package/dist/virtual-list/keys.d.ts +15 -0
  146. package/dist/virtual-list/keys.js +24 -0
  147. package/dist/virtual-list/root/virtual-list-root.d.ts +155 -23
  148. package/dist/virtual-list/root/virtual-list-root.js +175 -34
  149. package/lint.json +1 -1
  150. package/package.json +5 -5
  151. package/types/jsx.d.ts +3778 -801
  152. package/types/svelte.d.ts +2 -0
  153. package/types/vue.d.ts +782 -215
  154. package/dist/listbox/range.d.ts +0 -17
  155. package/dist/listbox/range.js +0 -5
@@ -29,6 +29,11 @@ import '../../menu/index.js';
29
29
  * `readyState` of zero, and it would not play. They are copied so your light
30
30
  * DOM stays intact, and are copied again whenever the slot changes.
31
31
  *
32
+ * The player sends every media event of its inner video under the same name,
33
+ * without bubbling, as a `<video>` sends it. Listen on the player as you would
34
+ * on a video: `timeupdate`, `volumechange`, `loadedmetadata`, `error`, and the
35
+ * rest.
36
+ *
32
37
  * @since 0.1.0
33
38
  * @status stable
34
39
  * @category media
@@ -36,15 +41,52 @@ import '../../menu/index.js';
36
41
  * @documentation https://fusion.dev/helix/components/video
37
42
  *
38
43
  * @slot - `<source>` and `<track>` elements, which are copied into the video.
44
+ * @slot captions-icon - Replaces the glyph on the captions control.
39
45
  * @slot controls - Extra controls, added to the control bar before the fullscreen button.
46
+ * @slot download-icon - Replaces the glyph on the download control, on the bar or in the `overflow` menu.
47
+ * @slot exit-fullscreen-icon - Replaces the glyph on the fullscreen control while the player fills the screen.
48
+ * @slot forward-icon - Replaces the glyph on the control that skips forward, on the bar or in the `overflow` menu.
49
+ * @slot fullscreen-icon - Replaces the glyph on the fullscreen control.
40
50
  * @slot header - Content over the top left of the picture, such as a heading and a back button.
51
+ * @slot more-icon - Replaces the glyph on the button that opens the `overflow` menu.
52
+ * @slot mute-icon - Replaces the glyph on the mute control while there is no sound.
53
+ * @slot next-icon - Replaces the glyph on the control that asks for the next video, on the bar or in the `overflow` menu.
54
+ * @slot pause-icon - Replaces the glyph on the play control while the video plays.
55
+ * @slot pip-icon - Replaces the glyph on the picture-in-picture control, on the bar or in the `overflow` menu.
56
+ * @slot play-icon - Replaces the glyph on the play control while the video is paused.
41
57
  * @slot poster - Shown over the video until it starts.
58
+ * @slot previous-icon - Replaces the glyph on the control that asks for the previous video, on the bar or in the `overflow` menu.
59
+ * @slot remote-icon - Replaces the glyph on the control that plays on another device, on the bar or in the `overflow` menu.
60
+ * @slot rewind-icon - Replaces the glyph on the control that skips back, on the bar or in the `overflow` menu.
61
+ * @slot speed-icon - Replaces the glyph beside the playback speed in the `overflow` menu. On the bar, the speed is shown as a number.
62
+ * @slot start-icon - Replaces the glyph on the large play button over the picture.
63
+ * @slot volume-icon - Replaces the glyph on the mute control while there is sound.
42
64
  *
43
- * @event {Event} ended - The video reached its end. Relayed from the inner video, since the native event does not cross the shadow boundary.
65
+ * @event {Event} abort - The media stopped loading before it finished, without an error.
66
+ * @event {Event} canplay - Enough has loaded to start playing.
67
+ * @event {Event} canplaythrough - Enough has loaded to play to the end without stopping, at the current rate.
68
+ * @event {Event} durationchange - The duration changed, which it does once the metadata is in.
69
+ * @event {Event} emptied - The media was reset, as when `src` changes.
70
+ * @event {Event} ended - The video reached its end.
71
+ * @event {Event} error - The source could not be loaded or played. The player shows `error-label` as well.
44
72
  * @event {HelixNextEvent} helix-next - The next video was requested. Load it in response.
45
73
  * @event {HelixPreviousEvent} helix-previous - The previous video was requested. Load it in response.
46
- * @event {Event} pause - Playback paused. Relayed from the inner video.
47
- * @event {Event} play - Playback started. Relayed from the inner video.
74
+ * @event {Event} loadeddata - The frame at the current position has loaded.
75
+ * @event {Event} loadedmetadata - The duration and the size of the video are known.
76
+ * @event {Event} loadstart - The video started looking for its media.
77
+ * @event {Event} pause - Playback paused.
78
+ * @event {Event} play - Playback was asked to start.
79
+ * @event {Event} playing - Playback started, or started again after waiting for data.
80
+ * @event {Event} progress - More of the media has downloaded.
81
+ * @event {Event} ratechange - The playback rate changed. Read `playbackRate`.
82
+ * @event {Event} resize - The video's own width or height changed. Read `videoWidth` and `videoHeight`.
83
+ * @event {Event} seeked - A move to a new position finished.
84
+ * @event {Event} seeking - A move to a new position started.
85
+ * @event {Event} stalled - The browser is trying to load more data and none is arriving.
86
+ * @event {Event} suspend - Loading paused on purpose, as when `preload` asks for no more.
87
+ * @event {Event} timeupdate - The position changed. Read `currentTime`.
88
+ * @event {Event} volumechange - The volume or the mute changed. Read `volume` and `muted`.
89
+ * @event {Event} waiting - Playback stopped to wait for more data.
48
90
  *
49
91
  * @csspart captions - The button that turns captions on and off.
50
92
  * @csspart captions-menu - The menu of caption languages, when the video has more than one track.
@@ -60,8 +102,11 @@ import '../../menu/index.js';
60
102
  * @csspart cue - The caption currently on screen, drawn by this element.
61
103
  * @csspart download - The button that downloads the file, when `extra-controls` includes it.
62
104
  * @csspart entry - One row in a menu that the player opens.
105
+ * @csspart entry-end - The trailing column of a row, which holds its shortcut. It is always rendered.
63
106
  * @csspart entry-item - The row, which is an `<a>` where `href` is set.
64
107
  * @csspart entry-mark - The trailing mark: a check mark when the row is checked, or a caret when it opens a submenu.
108
+ * @csspart entry-shortcut - The key drawn for a row's shortcut. The player's rows set none.
109
+ * @csspart entry-shortcut-kbd - The key inside `entry-shortcut`.
65
110
  * @csspart entry-start - The leading column that holds the `start` slot. It is always rendered.
66
111
  * @csspart failure - The message shown when the source cannot be played.
67
112
  * @csspart forward - The button that skips forward ten seconds.
@@ -109,10 +154,10 @@ export declare class HelixHeadlessVideo extends HelixElement {
109
154
  * Whether the video starts playing on its own. Browsers allow this only while
110
155
  * it is muted.
111
156
  *
112
- * Forwarded rather than acted on. Whether an unprompted play is permitted is
113
- * the browser's policy and not this element's, and `play()` already swallows
114
- * the refusal: what follows a refused autoplay is a paused player showing its
115
- * start control, which is the correct thing to be looking at.
157
+ * Forwarded to the video, which decides. Whether an unprompted play is
158
+ * permitted is the browser's policy, and what follows a refused autoplay is a
159
+ * paused player showing its start control, which is the correct thing to be
160
+ * looking at.
116
161
  */
117
162
  autoplay: boolean;
118
163
  /**
@@ -282,6 +327,11 @@ export declare class HelixHeadlessVideo extends HelixElement {
282
327
  * What the control that floats the video over other windows is called.
283
328
  */
284
329
  pipLabel: string;
330
+ /**
331
+ * The name of the player when `label` is not set. The player is a group, and
332
+ * this is what a screen reader calls it on arriving.
333
+ */
334
+ playerLabel: string;
285
335
  /**
286
336
  * The name of the play control while pressing it would start the video.
287
337
  */
@@ -340,6 +390,14 @@ export declare class HelixHeadlessVideo extends HelixElement {
340
390
  * behind a hero, a grid of eight thumbnails, anything where the video is
341
391
  * decoration rather than something to start.
342
392
  *
393
+ * **It is for the pointer, and the bar's Play is for everyone else.** Both
394
+ * play the video, so a keyboard met two stops called "Play" one after the
395
+ * other and a screen reader read the same button twice. The large one is out
396
+ * of the tab order and hidden from assistive technology while the bar is
397
+ * drawn, which is whenever it is: it shows only while the video is not
398
+ * playing, and the bar does not fade then. A press on it leaves focus on the
399
+ * player, so the keys go on working once it has gone.
400
+ *
343
401
  * A value rather than a boolean, so the resting state is the attribute's
344
402
  * absence: a boolean defaulting to on would have to be spelled as its own
345
403
  * negation.
@@ -510,14 +568,19 @@ export declare class HelixHeadlessVideo extends HelixElement {
510
568
  set volume(level: number);
511
569
  private get video();
512
570
  /**
513
- * Starts playback, if the browser allows it.
571
+ * Starts playback, and returns the video's own promise: it settles when
572
+ * playback starts, and rejects when the browser refuses.
573
+ *
574
+ * **A refusal is not reported as an unhandled rejection.** A browser refuses
575
+ * `play()` when nothing the reader did asked for it, and that is a policy
576
+ * answer: what follows is a paused player showing its start control. So the
577
+ * promise is marked handled here, and a caller who awaits it still hears the
578
+ * refusal.
514
579
  *
515
- * The rejection is swallowed on purpose: a browser refuses `play()` when
516
- * nothing the reader did asked for it, and that is a policy answer rather than
517
- * a fault. What follows a refused play is a paused player showing its start
518
- * control, which is the correct thing to be looking at.
580
+ * Before the first render there is no video yet, and the promise waits for
581
+ * one.
519
582
  */
520
- play(): void;
583
+ play(): Promise<void>;
521
584
  /** Pauses playback and leaves the current frame showing. */
522
585
  pause(): void;
523
586
  /**
@@ -645,6 +708,16 @@ export declare class HelixHeadlessVideo extends HelixElement {
645
708
  * mouse left resting until the bar faded.
646
709
  */
647
710
  private readonly handlePicture;
711
+ /**
712
+ * A press on the large play button plays, and leaves focus on the player.
713
+ *
714
+ * The button takes focus from the press and is gone once the video plays,
715
+ * which drops focus to the page, and the keys bound to the player stop
716
+ * answering. Moved to the stage first, they keep working. The press came from
717
+ * a pointer, the button being out of the tab order, so the stage draws no
718
+ * ring for it.
719
+ */
720
+ private readonly handleStart;
648
721
  /**
649
722
  * The keys every player uses, bound to this element rather than the document.
650
723
  *
@@ -716,6 +789,19 @@ export declare class HelixHeadlessVideo extends HelixElement {
716
789
  private heardMenu;
717
790
  protected renderOverflowItem(one: string): TemplateResult;
718
791
  protected renderBar(): TemplateResult;
792
+ /**
793
+ * The large play button over the picture, or nothing.
794
+ *
795
+ * Drawn whenever the video is not playing, which is what the mark means:
796
+ * press this to play. Not only before the first play, which would make the
797
+ * mark mean two different things at two moments that are the same state.
798
+ * Every player a reader has used shows it on pause, and the one who paused to
799
+ * study a frame clears it by pressing the thing in the way.
800
+ *
801
+ * Not drawn at all where the browser draws its own controls, since that bar
802
+ * has a play button and two would be two.
803
+ */
804
+ protected renderStart(): TemplateResult | typeof nothing;
719
805
  protected render(): TemplateResult;
720
806
  }
721
807
  declare global {
@@ -11,6 +11,7 @@ import { property, state } from 'lit/decorators.js';
11
11
  import { html } from 'lit/static-html.js';
12
12
  import '../../menu/index.js';
13
13
  import { HelixHeadlessMenu } from '../../menu/root/menu-root.js';
14
+ import { relayMedia } from '../media-events.js';
14
15
  import { barControls, menuControls, ownFile } from './extra-controls.js';
15
16
  import { takeKey } from './keys.js';
16
17
  const clock = (seconds) => {
@@ -53,6 +54,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
53
54
  this.pauseLabel = 'Pause';
54
55
  this.percentLabel = '{percent} percent';
55
56
  this.pipLabel = 'Picture in picture';
57
+ this.playerLabel = 'Video';
56
58
  this.playLabel = 'Play';
57
59
  this.playsInline = true;
58
60
  this.poster = '';
@@ -149,7 +151,6 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
149
151
  this.claimSession();
150
152
  }
151
153
  this.session.report(this.playing);
152
- this.dispatchEvent(new Event(event.type, { bubbles: false, composed: false }));
153
154
  };
154
155
  this.handleVolume = () => {
155
156
  const video = this.video;
@@ -179,6 +180,10 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
179
180
  }
180
181
  this.toggle();
181
182
  };
183
+ this.handleStart = () => {
184
+ this.partNamed('stage')?.focus({ preventScroll: true });
185
+ this.toggle();
186
+ };
182
187
  this.handleKeydown = (event) => {
183
188
  if (this.controls) {
184
189
  return;
@@ -296,7 +301,14 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
296
301
  return this.partNamed('video');
297
302
  }
298
303
  play() {
299
- void this.video?.play().catch(() => { });
304
+ const video = this.video;
305
+ const started = video
306
+ ? video.play()
307
+ : this.updateComplete.then(async () => {
308
+ await this.video?.play();
309
+ });
310
+ started.catch(() => { });
311
+ return started;
300
312
  }
301
313
  pause() {
302
314
  this.video?.pause();
@@ -307,7 +319,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
307
319
  return;
308
320
  }
309
321
  if (video.paused) {
310
- this.play();
322
+ void this.play();
311
323
  }
312
324
  else {
313
325
  this.pause();
@@ -419,6 +431,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
419
431
  this.silent = video.muted;
420
432
  this.loudness = video.volume;
421
433
  void this.remote.attach(video);
434
+ relayMedia(video, this);
422
435
  }
423
436
  }
424
437
  updated(changed) {
@@ -438,7 +451,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
438
451
  aria-pressed=${this.remoteConnected ? 'true' : 'false'}
439
452
  @click=${() => void this.remote.prompt()}
440
453
  >
441
- ${drawIcon(airplay)}
454
+ <slot name="remote-icon">${drawIcon(airplay)}</slot>
442
455
  </button>`;
443
456
  }
444
457
  if (one === 'rewind') {
@@ -448,7 +461,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
448
461
  aria-label=${fillTemplate(this.rewindLabel, { seconds: SKIP_SECONDS })}
449
462
  @click=${() => this.skipBy(-SKIP_SECONDS)}
450
463
  >
451
- ${drawIcon(arrowCounterClockwise)}
464
+ <slot name="rewind-icon">${drawIcon(arrowCounterClockwise)}</slot>
452
465
  </button>`;
453
466
  }
454
467
  if (one === 'forward') {
@@ -458,7 +471,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
458
471
  aria-label=${fillTemplate(this.forwardLabel, { seconds: SKIP_SECONDS })}
459
472
  @click=${() => this.skipBy(SKIP_SECONDS)}
460
473
  >
461
- ${drawIcon(arrowClockwise)}
474
+ <slot name="forward-icon">${drawIcon(arrowClockwise)}</slot>
462
475
  </button>`;
463
476
  }
464
477
  if (one === 'speed') {
@@ -478,7 +491,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
478
491
  aria-label=${this.previousLabel}
479
492
  @click=${() => this.skip(true)}
480
493
  >
481
- ${drawIcon(skipBack)}
494
+ <slot name="previous-icon">${drawIcon(skipBack)}</slot>
482
495
  </button>`;
483
496
  }
484
497
  if (one === 'next') {
@@ -488,7 +501,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
488
501
  aria-label=${this.nextLabel}
489
502
  @click=${() => this.skip(false)}
490
503
  >
491
- ${drawIcon(skipForward)}
504
+ <slot name="next-icon">${drawIcon(skipForward)}</slot>
492
505
  </button>`;
493
506
  }
494
507
  if (one === 'download') {
@@ -501,7 +514,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
501
514
  rel=${local ? nothing : 'noreferrer'}
502
515
  aria-label=${this.downloadLabel}
503
516
  >
504
- ${drawIcon(download)}
517
+ <slot name="download-icon">${drawIcon(download)}</slot>
505
518
  </a>`;
506
519
  }
507
520
  return html `<button
@@ -510,7 +523,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
510
523
  aria-label=${this.pipLabel}
511
524
  @click=${() => void this.togglePip()}
512
525
  >
513
- ${drawIcon(pictureInPicture)}
526
+ <slot name="pip-icon">${drawIcon(pictureInPicture)}</slot>
514
527
  </button>`;
515
528
  }
516
529
  renderCaptions() {
@@ -526,7 +539,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
526
539
  aria-pressed=${this.captioned ? 'true' : 'false'}
527
540
  @click=${() => this.toggleCaptions()}
528
541
  >
529
- ${drawIcon(closedCaptioning)}
542
+ <slot name="captions-icon">${drawIcon(closedCaptioning)}</slot>
530
543
  </button>`;
531
544
  }
532
545
  return html `<${this.tag('menu')} part="captions-menu"
@@ -542,11 +555,12 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
542
555
  aria-haspopup="menu"
543
556
  aria-expanded=${this.menuOpen === 'captions' ? 'true' : 'false'}
544
557
  >
545
- ${drawIcon(closedCaptioning)}
558
+ <slot name="captions-icon">${drawIcon(closedCaptioning)}</slot>
546
559
  </button>
547
560
  <${this.tag('menu-item')}
548
561
  part="entry"
549
- exportparts="item: entry-item, mark: entry-mark, start: entry-start"
562
+ exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
563
+ shortcut-kbd: entry-shortcut-kbd, start: entry-start"
550
564
  value="off"
551
565
  checkable
552
566
  exclusive
@@ -557,7 +571,8 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
557
571
  ${this.captionsOffLabel}
558
572
  </${this.tag('menu-item')}>
559
573
  ${tracks.map((track, at) => html `<${this.tag('menu-item')} part="entry"
560
- exportparts="item: entry-item, mark: entry-mark, start: entry-start"
574
+ exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
575
+ shortcut-kbd: entry-shortcut-kbd, start: entry-start"
561
576
  value=${track.language || String(at)}
562
577
  checkable
563
578
  exclusive
@@ -584,63 +599,72 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
584
599
  renderOverflowItem(one) {
585
600
  if (one === 'remote') {
586
601
  return html `<${this.tag('menu-item')} part="entry"
587
- exportparts="item: entry-item, mark: entry-mark, start: entry-start"
602
+ exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
603
+ shortcut-kbd: entry-shortcut-kbd, start: entry-start"
588
604
  value="remote"
589
605
  @helix-select=${() => void this.remote.prompt()}
590
606
  >
591
- ${drawIcon(airplay, { slot: 'start' })}
607
+ <slot name="remote-icon" slot="start">${drawIcon(airplay)}</slot>
592
608
  ${this.remoteLabel}
593
609
  </${this.tag('menu-item')}>`;
594
610
  }
595
611
  if (one === 'speed') {
596
612
  return html `<${this.tag('menu-item')} part="entry"
597
- exportparts="item: entry-item, mark: entry-mark, start: entry-start" value="speed" @helix-select=${() => this.cycleSpeed()}>
598
- ${drawIcon(gauge, { slot: 'start' })}
613
+ exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
614
+ shortcut-kbd: entry-shortcut-kbd, start: entry-start" value="speed" @helix-select=${() => this.cycleSpeed()}>
615
+ <slot name="speed-icon" slot="start">${drawIcon(gauge)}</slot>
599
616
  ${this.speedLabel}: ${this.rate}x
600
617
  </${this.tag('menu-item')}>`;
601
618
  }
602
619
  if (one === 'rewind' || one === 'forward') {
603
620
  const back = one === 'rewind';
604
621
  return html `<${this.tag('menu-item')} part="entry"
605
- exportparts="item: entry-item, mark: entry-mark, start: entry-start"
622
+ exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
623
+ shortcut-kbd: entry-shortcut-kbd, start: entry-start"
606
624
  value=${one}
607
625
  @helix-select=${() => this.skipBy(back ? -SKIP_SECONDS : SKIP_SECONDS)}
608
626
  >
609
- ${drawIcon(back ? arrowCounterClockwise : arrowClockwise, { slot: 'start' })}
627
+ ${back
628
+ ? html `<slot name="rewind-icon" slot="start">${drawIcon(arrowCounterClockwise)}</slot>`
629
+ : html `<slot name="forward-icon" slot="start">${drawIcon(arrowClockwise)}</slot>`}
610
630
  ${fillTemplate(back ? this.rewindLabel : this.forwardLabel, { seconds: SKIP_SECONDS })}
611
631
  </${this.tag('menu-item')}>`;
612
632
  }
613
633
  if (one === 'previous') {
614
634
  return html `<${this.tag('menu-item')} part="entry"
615
- exportparts="item: entry-item, mark: entry-mark, start: entry-start" value="previous" @helix-select=${() => this.skip(true)}>
616
- ${drawIcon(skipBack, { slot: 'start' })}
635
+ exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
636
+ shortcut-kbd: entry-shortcut-kbd, start: entry-start" value="previous" @helix-select=${() => this.skip(true)}>
637
+ <slot name="previous-icon" slot="start">${drawIcon(skipBack)}</slot>
617
638
  ${this.previousLabel}
618
639
  </${this.tag('menu-item')}>`;
619
640
  }
620
641
  if (one === 'next') {
621
642
  return html `<${this.tag('menu-item')} part="entry"
622
- exportparts="item: entry-item, mark: entry-mark, start: entry-start" value="next" @helix-select=${() => this.skip(false)}>
623
- ${drawIcon(skipForward, { slot: 'start' })}
643
+ exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
644
+ shortcut-kbd: entry-shortcut-kbd, start: entry-start" value="next" @helix-select=${() => this.skip(false)}>
645
+ <slot name="next-icon" slot="start">${drawIcon(skipForward)}</slot>
624
646
  ${this.nextLabel}
625
647
  </${this.tag('menu-item')}>`;
626
648
  }
627
649
  if (one === 'download') {
628
650
  const local = ownFile(this.src);
629
651
  return html `<${this.tag('menu-item')} part="entry"
630
- exportparts="item: entry-item, mark: entry-mark, start: entry-start"
652
+ exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
653
+ shortcut-kbd: entry-shortcut-kbd, start: entry-start"
631
654
  ?download=${local}
632
655
  href=${this.src}
633
656
  rel=${local ? nothing : 'noreferrer'}
634
657
  target=${local ? nothing : '_blank'}
635
658
  value="download"
636
659
  >
637
- ${drawIcon(download, { slot: 'start' })}
660
+ <slot name="download-icon" slot="start">${drawIcon(download)}</slot>
638
661
  ${this.downloadLabel}
639
662
  </${this.tag('menu-item')}>`;
640
663
  }
641
664
  return html `<${this.tag('menu-item')} part="entry"
642
- exportparts="item: entry-item, mark: entry-mark, start: entry-start" value="pip" @helix-select=${() => void this.togglePip()}>
643
- ${drawIcon(pictureInPicture, { slot: 'start' })}
665
+ exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
666
+ shortcut-kbd: entry-shortcut-kbd, start: entry-start" value="pip" @helix-select=${() => void this.togglePip()}>
667
+ <slot name="pip-icon" slot="start">${drawIcon(pictureInPicture)}</slot>
644
668
  ${this.pipLabel}
645
669
  </${this.tag('menu-item')}>`;
646
670
  }
@@ -653,7 +677,9 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
653
677
  aria-label=${this.playing ? this.pauseLabel : this.playLabel}
654
678
  @click=${() => this.toggle()}
655
679
  >
656
- ${drawIcon(this.playing ? pause : play)}
680
+ ${this.playing
681
+ ? html `<slot name="pause-icon">${drawIcon(pause)}</slot>`
682
+ : html `<slot name="play-icon">${drawIcon(play)}</slot>`}
657
683
  </button>
658
684
  <span part="time">${clock(this.position)} / ${clock(this.knownDuration)}</span>
659
685
  <!-- Left to right on every page. A player's timeline is a tape running
@@ -691,7 +717,9 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
691
717
  }
692
718
  }}
693
719
  >
694
- ${drawIcon(this.silent || this.loudness === 0 ? speakerX : speakerHigh)}
720
+ ${this.silent || this.loudness === 0
721
+ ? html `<slot name="mute-icon">${drawIcon(speakerX)}</slot>`
722
+ : html `<slot name="volume-icon">${drawIcon(speakerHigh)}</slot>`}
695
723
  </button>
696
724
  <input
697
725
  part="volume"
@@ -725,7 +753,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
725
753
  aria-haspopup="menu"
726
754
  aria-expanded=${this.menuOpen === 'overflow' ? 'true' : 'false'}
727
755
  >
728
- ${drawIcon(dotsThreeVertical)}
756
+ <slot name="more-icon">${drawIcon(dotsThreeVertical)}</slot>
729
757
  </button>
730
758
  ${this.overflow.map((one) => this.renderOverflowItem(one))}
731
759
  </${this.tag('menu')}>`}
@@ -738,16 +766,35 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
738
766
  aria-pressed=${this.full ? 'true' : 'false'}
739
767
  @click=${() => void this.fullscreen.toggle()}
740
768
  >
741
- ${drawIcon(this.full ? cornersIn : cornersOut)}
769
+ ${this.full
770
+ ? html `<slot name="exit-fullscreen-icon">${drawIcon(cornersIn)}</slot>`
771
+ : html `<slot name="fullscreen-icon">${drawIcon(cornersOut)}</slot>`}
742
772
  </button>
743
773
  </div>`;
744
774
  }
775
+ renderStart() {
776
+ if (this.controls ||
777
+ this.playing ||
778
+ this.buffering ||
779
+ this.failed ||
780
+ this.startControl === 'none') {
781
+ return nothing;
782
+ }
783
+ return html `<button
784
+ part="start"
785
+ type="button"
786
+ tabindex="-1"
787
+ aria-hidden="true"
788
+ aria-label=${this.playLabel}
789
+ @click=${this.handleStart}
790
+ ><slot name="start-icon">${drawIcon(play)}</slot></button>`;
791
+ }
745
792
  render() {
746
793
  return html `<div
747
794
  part="stage"
748
795
  tabindex=${this.controls ? nothing : '0'}
749
796
  role=${this.controls ? nothing : 'group'}
750
- aria-label=${this.controls ? nothing : this.label || 'Video'}
797
+ aria-label=${this.controls ? nothing : this.label || this.playerLabel}
751
798
  @keydown=${this.handleKeydown}
752
799
  @pointermove=${this.idler.stir}
753
800
  @pointerleave=${this.idler.leave}
@@ -820,20 +867,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
820
867
  ${this.errorLabel}
821
868
  </div>`
822
869
  : nothing}
823
- ${this.controls ||
824
- this.playing ||
825
- this.buffering ||
826
- this.failed ||
827
- this.startControl === 'none'
828
- ? nothing
829
- : html `<button
830
- part="start"
831
- type="button"
832
- aria-label=${this.playLabel}
833
- @click=${() => this.toggle()}
834
- >
835
- ${drawIcon(play)}
836
- </button>`}
870
+ ${this.renderStart()}
837
871
  ${this.controls ? nothing : this.renderBar()}
838
872
  </div>`;
839
873
  }
@@ -910,6 +944,9 @@ __decorate([
910
944
  __decorate([
911
945
  property({ attribute: 'pip-label' })
912
946
  ], HelixHeadlessVideo.prototype, "pipLabel", void 0);
947
+ __decorate([
948
+ property({ attribute: 'player-label' })
949
+ ], HelixHeadlessVideo.prototype, "playerLabel", void 0);
913
950
  __decorate([
914
951
  property({ attribute: 'play-label' })
915
952
  ], HelixHeadlessVideo.prototype, "playLabel", void 0);
@@ -22,8 +22,11 @@ import '../../menu/index.js';
22
22
  * @event {HelixSelectEvent} helix-select - An entry was chosen. The event carries its value, and the playlist listens for it. Cancelable: cancel it to keep the current entry.
23
23
  *
24
24
  * @csspart entry - One entry in the menu.
25
+ * @csspart entry-end - The column at the end of an entry, which holds its shortcut. It is always present.
25
26
  * @csspart entry-item - The entry's row. It is an `<a>` when `href` is set.
26
27
  * @csspart entry-mark - The mark at the end of an entry: a check mark when it is checked, or a caret when it opens a submenu.
28
+ * @csspart entry-shortcut - The key drawn for an entry's shortcut. The playlist's entries set none.
29
+ * @csspart entry-shortcut-kbd - The key inside `entry-shortcut`.
27
30
  * @csspart entry-start - The column at the start of an entry. It is a slot and is always present.
28
31
  * @csspart menu - The menu that holds the entries.
29
32
  * @csspart menu-back - The button that goes back from a submenu.
@@ -85,7 +85,8 @@ let HelixHeadlessVideoPlaylistMenu = class HelixHeadlessVideoPlaylistMenu extend
85
85
  </slot>
86
86
  ${this.entries.map((entry) => html `<${this.tag('menu-item')}
87
87
  part="entry"
88
- exportparts="item: entry-item, mark: entry-mark, start: entry-start"
88
+ exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
89
+ shortcut-kbd: entry-shortcut-kbd, start: entry-start"
89
90
  value=${entry.value}
90
91
  checkable
91
92
  ?checked=${entry.current}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The order a playlist plays its entries in, and where a step from one lands.
3
+ *
4
+ * Separable because every answer is taken from a list of values and two
5
+ * settings, before anything moves: nothing here reaches a player or an entry.
6
+ * The playlist asks it which value comes next and applies the answer.
7
+ */
8
+ /**
9
+ * How a playlist goes on at its end. `one` plays the same entry again when it
10
+ * ends, and `all` goes from the last entry back to the first.
11
+ */
12
+ export type Repeat = 'all' | 'one';
13
+ /**
14
+ * A shuffled copy of a list, with `first` leading where it is in the list.
15
+ *
16
+ * `first` leads because a reader turning shuffle on is watching something, and
17
+ * a shuffle that could put that entry anywhere would bring it round again
18
+ * before the rest have played.
19
+ *
20
+ * Fisher and Yates' shuffle, which gives every order of the rest the same
21
+ * chance. `random` is the source of chance, `Math.random` unless a caller
22
+ * passes one.
23
+ */
24
+ export declare const shuffled: <T>(items: readonly T[], first?: T, random?: () => number) => T[];
25
+ /**
26
+ * The order to play in.
27
+ *
28
+ * As written without shuffle. With it, the shuffled order already `held` for
29
+ * as long as it covers the same entries, so stepping back retraces what was
30
+ * played; a new one, led by `current`, once an entry arrives or leaves.
31
+ */
32
+ export declare const playOrder: (values: readonly string[], shuffle: boolean, held: readonly string[] | undefined, current: string | undefined, random?: () => number) => string[];
33
+ /**
34
+ * Where a step of `by` from `current` lands in `order`, or `undefined` past an
35
+ * end.
36
+ *
37
+ * `wrap` goes round instead of stopping, which is what `repeat="all"` asks
38
+ * for. With no current entry, a step forward lands on the first one and a step
39
+ * back on nothing, as a list that has not started has nothing behind it.
40
+ */
41
+ export declare const stepTo: (order: readonly string[], current: string | undefined, by: number, wrap: boolean) => string | undefined;
@@ -0,0 +1,34 @@
1
+ export const shuffled = (items, first, random = Math.random) => {
2
+ const rest = items.filter((one) => one !== first);
3
+ for (let at = rest.length - 1; at > 0; at -= 1) {
4
+ const swap = Math.floor(random() * (at + 1));
5
+ const held = rest[at];
6
+ rest[at] = rest[swap];
7
+ rest[swap] = held;
8
+ }
9
+ return first !== undefined && items.includes(first) ? [first, ...rest] : rest;
10
+ };
11
+ const sameValues = (one, other) => one.length === other.length && one.every((value) => other.includes(value));
12
+ export const playOrder = (values, shuffle, held, current, random = Math.random) => {
13
+ if (!shuffle) {
14
+ return [...values];
15
+ }
16
+ if (held !== undefined && sameValues(held, values)) {
17
+ return [...held];
18
+ }
19
+ return shuffled(values, current, random);
20
+ };
21
+ export const stepTo = (order, current, by, wrap) => {
22
+ const at = current === undefined ? -1 : order.indexOf(current);
23
+ if (at < 0) {
24
+ return by > 0 ? order[0] : undefined;
25
+ }
26
+ const to = at + by;
27
+ if (to >= 0 && to < order.length) {
28
+ return order[to];
29
+ }
30
+ if (!wrap || order.length === 0) {
31
+ return undefined;
32
+ }
33
+ return order[((to % order.length) + order.length) % order.length];
34
+ };