@fusion.dev/helix-headless 0.155.0 → 0.157.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 (74) hide show
  1. package/custom-elements.json +2695 -137
  2. package/dist/canvas/node/canvas-node.js +1 -1
  3. package/dist/canvas/root/canvas-root.d.ts +17 -2
  4. package/dist/canvas/root/canvas-root.js +23 -6
  5. package/dist/chat/composer/chat-composer.js +1 -1
  6. package/dist/chat/root/chat-root.d.ts +14 -1
  7. package/dist/chat/root/chat-root.js +14 -4
  8. package/dist/combobox/root/combobox-root.d.ts +10 -0
  9. package/dist/combobox/root/combobox-root.js +18 -3
  10. package/dist/command/root/command-root.d.ts +116 -1
  11. package/dist/command/root/command-root.js +124 -15
  12. package/dist/data-table/root/data-table-root.d.ts +25 -0
  13. package/dist/data-table/root/data-table-root.js +13 -3
  14. package/dist/date-input/root/date-input-root.d.ts +32 -1
  15. package/dist/date-input/root/date-input-root.js +7 -1
  16. package/dist/drilldown/item/drilldown-item.d.ts +49 -1
  17. package/dist/drilldown/item/drilldown-item.js +77 -17
  18. package/dist/drilldown/root/drilldown-root.d.ts +45 -5
  19. package/dist/drilldown/root/drilldown-root.js +51 -5
  20. package/dist/element-events.generated.js +2 -1
  21. package/dist/filter-input/menu/filter-menu.d.ts +1 -0
  22. package/dist/filter-input/menu/filter-menu.js +4 -4
  23. package/dist/graph/root/graph-root.d.ts +13 -0
  24. package/dist/graph/root/graph-root.js +16 -0
  25. package/dist/list/root/list-root.d.ts +74 -1
  26. package/dist/list/root/list-root.js +92 -4
  27. package/dist/list/selection.js +3 -3
  28. package/dist/map/root/map-root.d.ts +6 -0
  29. package/dist/map/root/map-root.js +8 -1
  30. package/dist/megamenu/root/megamenu-root.d.ts +50 -13
  31. package/dist/megamenu/root/megamenu-root.js +44 -6
  32. package/dist/menu/item/menu-item.d.ts +24 -1
  33. package/dist/menu/item/menu-item.js +42 -2
  34. package/dist/menu/root/menu-root.d.ts +120 -9
  35. package/dist/menu/root/menu-root.js +329 -26
  36. package/dist/notifications/root/notifications-root.d.ts +20 -0
  37. package/dist/notifications/root/notifications-root.js +32 -1
  38. package/dist/number-input/root/number-input-root.d.ts +63 -6
  39. package/dist/number-input/root/number-input-root.js +96 -11
  40. package/dist/object-inspector/root/object-inspector-root.d.ts +36 -1
  41. package/dist/object-inspector/root/object-inspector-root.js +26 -2
  42. package/dist/option/root/option-root.d.ts +32 -0
  43. package/dist/option/root/option-root.js +44 -5
  44. package/dist/outline/root/outline-root.d.ts +6 -2
  45. package/dist/outline/root/outline-root.js +7 -3
  46. package/dist/table/root/table-root.d.ts +20 -0
  47. package/dist/table/root/table-root.js +13 -3
  48. package/dist/time-input/root/time-input-root.d.ts +32 -1
  49. package/dist/time-input/root/time-input-root.js +9 -2
  50. package/dist/time-picker/root/time-picker-root.d.ts +63 -1
  51. package/dist/time-picker/root/time-picker-root.js +58 -7
  52. package/dist/timeline/item/timeline-item.d.ts +25 -0
  53. package/dist/timeline/item/timeline-item.js +27 -2
  54. package/dist/toast/item/toast-item.d.ts +26 -0
  55. package/dist/toast/item/toast-item.js +55 -3
  56. package/dist/toast/root/toast-root.d.ts +32 -0
  57. package/dist/toast/root/toast-root.js +47 -2
  58. package/dist/video/root/keys.d.ts +6 -2
  59. package/dist/video/root/keys.js +18 -1
  60. package/dist/video/root/video-root.d.ts +61 -1
  61. package/dist/video/root/video-root.js +79 -11
  62. package/dist/video-playlist/item/video-playlist-item.d.ts +15 -1
  63. package/dist/video-playlist/item/video-playlist-item.js +7 -0
  64. package/dist/video-playlist/menu/video-playlist-menu.d.ts +1 -0
  65. package/dist/video-playlist/menu/video-playlist-menu.js +1 -1
  66. package/dist/video-playlist/root/video-playlist-root.d.ts +43 -4
  67. package/dist/video-playlist/root/video-playlist-root.js +69 -2
  68. package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
  69. package/dist/virtual-list/root/virtual-list-root.js +3 -0
  70. package/lint.json +1 -1
  71. package/package.json +5 -5
  72. package/types/jsx.d.ts +3346 -266
  73. package/types/vue.d.ts +910 -66
  74. package/types/words.d.ts +124 -0
@@ -13,7 +13,7 @@ import '../../menu/index.js';
13
13
  import { HelixHeadlessMenu } from '../../menu/root/menu-root.js';
14
14
  import { relayMedia } from '../media-events.js';
15
15
  import { barControls, menuControls, ownFile } from './extra-controls.js';
16
- import { takeKey } from './keys.js';
16
+ import { PLAYER_KEYS, takeKey } from './keys.js';
17
17
  const clock = (seconds) => {
18
18
  if (!Number.isFinite(seconds) || seconds < 0) {
19
19
  return '0:00';
@@ -31,8 +31,10 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
31
31
  constructor() {
32
32
  super(...arguments);
33
33
  this.autoplay = false;
34
+ this.captionsHiddenLabel = 'Captions off';
34
35
  this.captionsLabel = 'Captions';
35
36
  this.captionsOffLabel = 'Off';
37
+ this.captionsShownLabel = 'Captions on';
36
38
  this.controls = false;
37
39
  this.crossOrigin = null;
38
40
  this.defaultMuted = false;
@@ -49,13 +51,16 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
49
51
  this.loop = false;
50
52
  this.mediaTitle = '';
51
53
  this.moreLabel = 'More controls';
54
+ this.mutedLabel = 'Muted';
52
55
  this.muteLabel = 'Mute';
53
56
  this.nextLabel = 'Next';
54
57
  this.overflowControls = '';
58
+ this.pausedLabel = 'Paused';
55
59
  this.pauseLabel = 'Pause';
56
60
  this.percentLabel = '{percent} percent';
57
61
  this.pipLabel = 'Picture in picture';
58
62
  this.playerLabel = 'Video';
63
+ this.playingLabel = 'Playing';
59
64
  this.playLabel = 'Play';
60
65
  this.playsInline = true;
61
66
  this.poster = '';
@@ -69,6 +74,8 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
69
74
  this.src = '';
70
75
  this.subtitlesLabel = 'Subtitles';
71
76
  this.timeLabel = '{position} of {duration}';
77
+ this.unmutedLabel = 'Unmuted';
78
+ this.volumeChangedLabel = 'Volume {percent} percent';
72
79
  this.volumeLabel = 'Volume';
73
80
  this.buffering = false;
74
81
  this.failed = false;
@@ -199,7 +206,9 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
199
206
  if (event.composedPath().some((node) => node instanceof HelixHeadlessMenu)) {
200
207
  return;
201
208
  }
209
+ const paused = video.paused;
202
210
  const taken = takeKey(event, video, {
211
+ captions: () => this.toggleCaptions(),
203
212
  fullscreen: () => void this.fullscreen.toggle(),
204
213
  seek: (seconds) => this.seek(seconds),
205
214
  skipBy: (seconds) => this.skipBy(seconds),
@@ -207,8 +216,10 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
207
216
  });
208
217
  if (taken) {
209
218
  event.preventDefault();
219
+ this.sayKey(event.key, paused, video);
210
220
  }
211
221
  };
222
+ this.said = '';
212
223
  this.captions = [];
213
224
  }
214
225
  static { this.words = 'helix-video'; }
@@ -414,6 +425,31 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
414
425
  });
415
426
  }
416
427
  }
428
+ sayKey(key, paused, video) {
429
+ if (key === ' ' || key === 'k') {
430
+ this.say(paused ? this.playingLabel : this.pausedLabel);
431
+ }
432
+ else if (key === 'm') {
433
+ this.say(video.muted ? this.mutedLabel : this.unmutedLabel);
434
+ }
435
+ else if (key === 'ArrowUp' || key === 'ArrowDown') {
436
+ this.say(formatWords(this, 'volumeChangedLabel', { percent: Math.round(video.volume * 100) }));
437
+ }
438
+ else if (key === 'c') {
439
+ if (this.captions.length > 0) {
440
+ this.say(this.captioned ? this.captionsShownLabel : this.captionsHiddenLabel);
441
+ }
442
+ }
443
+ else if (key !== 'f') {
444
+ this.say(formatWords(this, 'timeLabel', {
445
+ duration: clock(video.duration),
446
+ position: clock(video.currentTime),
447
+ }));
448
+ }
449
+ }
450
+ say(words) {
451
+ this.said = words !== '' && words === this.said ? `${words}\u00a0` : words;
452
+ }
417
453
  willUpdate(changed) {
418
454
  super.willUpdate(changed);
419
455
  if (!this.mediaRead && this.childrenKnown) {
@@ -574,7 +610,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
574
610
  part="entry"
575
611
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
576
612
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
577
- shortcut-separator: entry-shortcut-separator, start: entry-start"
613
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface"
578
614
  value="off"
579
615
  checkable
580
616
  exclusive
@@ -587,7 +623,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
587
623
  ${tracks.map((track, at) => html `<${this.tag('menu-item')} part="entry"
588
624
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
589
625
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
590
- shortcut-separator: entry-shortcut-separator, start: entry-start"
626
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface"
591
627
  value=${track.language || String(at)}
592
628
  checkable
593
629
  exclusive
@@ -616,7 +652,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
616
652
  return html `<${this.tag('menu-item')} part="entry"
617
653
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
618
654
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
619
- shortcut-separator: entry-shortcut-separator, start: entry-start"
655
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface"
620
656
  value="remote"
621
657
  @helix-select=${() => void this.remote.prompt()}
622
658
  >
@@ -628,7 +664,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
628
664
  return html `<${this.tag('menu-item')} part="entry"
629
665
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
630
666
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
631
- shortcut-separator: entry-shortcut-separator, start: entry-start" value="speed" @helix-select=${() => this.cycleSpeed()}>
667
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface" value="speed" @helix-select=${() => this.cycleSpeed()}>
632
668
  <slot name="speed-icon" slot="start">${drawIcon(gauge)}</slot>
633
669
  ${this.speedLabel}: ${this.rate}x
634
670
  </${this.tag('menu-item')}>`;
@@ -638,7 +674,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
638
674
  return html `<${this.tag('menu-item')} part="entry"
639
675
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
640
676
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
641
- shortcut-separator: entry-shortcut-separator, start: entry-start"
677
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface"
642
678
  value=${one}
643
679
  @helix-select=${() => this.skipBy(back ? -SKIP_SECONDS : SKIP_SECONDS)}
644
680
  >
@@ -652,7 +688,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
652
688
  return html `<${this.tag('menu-item')} part="entry"
653
689
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
654
690
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
655
- shortcut-separator: entry-shortcut-separator, start: entry-start" value="previous" @helix-select=${() => this.skip(true)}>
691
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface" value="previous" @helix-select=${() => this.skip(true)}>
656
692
  <slot name="previous-icon" slot="start">${drawIcon(skipBack)}</slot>
657
693
  ${this.previousLabel}
658
694
  </${this.tag('menu-item')}>`;
@@ -661,7 +697,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
661
697
  return html `<${this.tag('menu-item')} part="entry"
662
698
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
663
699
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
664
- shortcut-separator: entry-shortcut-separator, start: entry-start" value="next" @helix-select=${() => this.skip(false)}>
700
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface" value="next" @helix-select=${() => this.skip(false)}>
665
701
  <slot name="next-icon" slot="start">${drawIcon(skipForward)}</slot>
666
702
  ${this.nextLabel}
667
703
  </${this.tag('menu-item')}>`;
@@ -671,7 +707,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
671
707
  return html `<${this.tag('menu-item')} part="entry"
672
708
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
673
709
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
674
- shortcut-separator: entry-shortcut-separator, start: entry-start"
710
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface"
675
711
  ?download=${local}
676
712
  href=${this.src}
677
713
  rel=${local ? nothing : 'noreferrer'}
@@ -685,7 +721,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
685
721
  return html `<${this.tag('menu-item')} part="entry"
686
722
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
687
723
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
688
- shortcut-separator: entry-shortcut-separator, start: entry-start" value="pip" @helix-select=${() => void this.togglePip()}>
724
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface" value="pip" @helix-select=${() => void this.togglePip()}>
689
725
  <slot name="pip-icon" slot="start">${drawIcon(pictureInPicture)}</slot>
690
726
  ${this.pipLabel}
691
727
  </${this.tag('menu-item')}>`;
@@ -820,6 +856,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
820
856
  tabindex=${this.controls ? nothing : '0'}
821
857
  role=${this.controls ? nothing : 'group'}
822
858
  aria-label=${this.controls ? nothing : this.label || this.playerLabel}
859
+ aria-keyshortcuts=${this.controls ? nothing : PLAYER_KEYS}
823
860
  @keydown=${this.handleKeydown}
824
861
  @pointermove=${this.idler.stir}
825
862
  @pointerleave=${this.idler.leave}
@@ -883,7 +920,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
883
920
  ? html `<div part="loading">${this.loadingLabel}</div>`
884
921
  : nothing}
885
922
  ${''}
886
- <span part="live" role="status">${this.buffering && !this.failed ? this.loadingLabel : ''}</span>
923
+ <span part="live" role="status">${this.buffering && !this.failed ? this.loadingLabel : this.said}</span>
887
924
  ${this.failed
888
925
  ? html `<div
889
926
  part="failure"
@@ -900,6 +937,10 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
900
937
  __decorate([
901
938
  property({ reflect: true, type: Boolean })
902
939
  ], HelixHeadlessVideo.prototype, "autoplay", void 0);
940
+ __decorate([
941
+ words,
942
+ property({ attribute: 'captions-hidden-label' })
943
+ ], HelixHeadlessVideo.prototype, "captionsHiddenLabel", void 0);
903
944
  __decorate([
904
945
  words,
905
946
  property({ attribute: 'captions-label' })
@@ -908,6 +949,10 @@ __decorate([
908
949
  words,
909
950
  property({ attribute: 'captions-off-label' })
910
951
  ], HelixHeadlessVideo.prototype, "captionsOffLabel", void 0);
952
+ __decorate([
953
+ words,
954
+ property({ attribute: 'captions-shown-label' })
955
+ ], HelixHeadlessVideo.prototype, "captionsShownLabel", void 0);
911
956
  __decorate([
912
957
  property({ reflect: true, type: Boolean })
913
958
  ], HelixHeadlessVideo.prototype, "controls", void 0);
@@ -962,6 +1007,10 @@ __decorate([
962
1007
  words,
963
1008
  property({ attribute: 'more-label' })
964
1009
  ], HelixHeadlessVideo.prototype, "moreLabel", void 0);
1010
+ __decorate([
1011
+ words,
1012
+ property({ attribute: 'muted-label' })
1013
+ ], HelixHeadlessVideo.prototype, "mutedLabel", void 0);
965
1014
  __decorate([
966
1015
  words,
967
1016
  property({ attribute: 'mute-label' })
@@ -973,6 +1022,10 @@ __decorate([
973
1022
  __decorate([
974
1023
  property({ attribute: 'overflow-controls' })
975
1024
  ], HelixHeadlessVideo.prototype, "overflowControls", void 0);
1025
+ __decorate([
1026
+ words,
1027
+ property({ attribute: 'paused-label' })
1028
+ ], HelixHeadlessVideo.prototype, "pausedLabel", void 0);
976
1029
  __decorate([
977
1030
  words,
978
1031
  property({ attribute: 'pause-label' })
@@ -989,6 +1042,10 @@ __decorate([
989
1042
  words,
990
1043
  property({ attribute: 'player-label' })
991
1044
  ], HelixHeadlessVideo.prototype, "playerLabel", void 0);
1045
+ __decorate([
1046
+ words,
1047
+ property({ attribute: 'playing-label' })
1048
+ ], HelixHeadlessVideo.prototype, "playingLabel", void 0);
992
1049
  __decorate([
993
1050
  words,
994
1051
  property({ attribute: 'play-label' })
@@ -1039,6 +1096,14 @@ __decorate([
1039
1096
  words,
1040
1097
  property({ attribute: 'time-label' })
1041
1098
  ], HelixHeadlessVideo.prototype, "timeLabel", void 0);
1099
+ __decorate([
1100
+ words,
1101
+ property({ attribute: 'unmuted-label' })
1102
+ ], HelixHeadlessVideo.prototype, "unmutedLabel", void 0);
1103
+ __decorate([
1104
+ words,
1105
+ property({ attribute: 'volume-changed-label' })
1106
+ ], HelixHeadlessVideo.prototype, "volumeChangedLabel", void 0);
1042
1107
  __decorate([
1043
1108
  words,
1044
1109
  property({ attribute: 'volume-label' })
@@ -1100,6 +1165,9 @@ __decorate([
1100
1165
  __decorate([
1101
1166
  state()
1102
1167
  ], HelixHeadlessVideo.prototype, "media", void 0);
1168
+ __decorate([
1169
+ state()
1170
+ ], HelixHeadlessVideo.prototype, "said", void 0);
1103
1171
  HelixHeadlessVideo = __decorate([
1104
1172
  customElement('helix-headless-video')
1105
1173
  ], HelixHeadlessVideo);
@@ -1,5 +1,5 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
- import { type TemplateResult } from 'lit';
2
+ import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  /**
4
4
  * @summary One entry in a video playlist.
5
5
  *
@@ -34,6 +34,8 @@ import { type TemplateResult } from 'lit';
34
34
  * @csspart label - The name and the content after it.
35
35
  * @csspart media - The column at the start of the row.
36
36
  * @csspart thumbnail - The image of `poster`, shown when the `media` slot is empty.
37
+ *
38
+ * @cssstate current - The entry that is playing.
37
39
  */
38
40
  export declare class HelixHeadlessVideoPlaylistItem extends HelixElement {
39
41
  /**
@@ -78,6 +80,11 @@ export declare class HelixHeadlessVideoPlaylistItem extends HelixElement {
78
80
  * where there is no slot to ask. `Option.text` reads its own the same way.
79
81
  */
80
82
  get text(): string;
83
+ /**
84
+ * Focuses the entry's button, which is where the playlist's arrow keys send
85
+ * focus. The host itself takes no focus.
86
+ */
87
+ focus(options?: FocusOptions): void;
81
88
  private readonly choose;
82
89
  /**
83
90
  * What stands for the row: a thumbnail where there is one, and nothing where
@@ -88,6 +95,13 @@ export declare class HelixHeadlessVideoPlaylistItem extends HelixElement {
88
95
  * slots `media` for the same reasons and neither has to know about the other.
89
96
  */
90
97
  protected renderMedia(): TemplateResult;
98
+ /**
99
+ * Mirrors the playing entry where a stylesheet can select on the host,
100
+ * `helix-video-playlist-item:state(current)`. A custom state, because the
101
+ * playlist writes `current`, and an attribute it wrote would be markup a
102
+ * server never sent.
103
+ */
104
+ protected willUpdate(changed: PropertyValues): void;
91
105
  protected render(): TemplateResult;
92
106
  }
93
107
  declare global {
@@ -29,11 +29,18 @@ let HelixHeadlessVideoPlaylistItem = class HelixHeadlessVideoPlaylistItem extend
29
29
  .join('')
30
30
  .trim();
31
31
  }
32
+ focus(options) {
33
+ this.renderRoot?.querySelector('button')?.focus(options);
34
+ }
32
35
  renderMedia() {
33
36
  return html `<slot name="media">
34
37
  ${this.poster === '' ? nothing : html `<img part="thumbnail" alt="" src=${this.poster} />`}
35
38
  </slot>`;
36
39
  }
40
+ willUpdate(changed) {
41
+ super.willUpdate(changed);
42
+ this.toggleState('current', this.current);
43
+ }
37
44
  render() {
38
45
  return html `<div part="container" role="listitem">
39
46
  <button
@@ -30,6 +30,7 @@ import '../../menu/index.js';
30
30
  * @csspart entry-shortcut-key - One key of an entry's shortcut chord, inside `entry-shortcut`. The playlist's entries set none.
31
31
  * @csspart entry-shortcut-separator - The `+` between two keys of an entry's shortcut, where the platform writes one.
32
32
  * @csspart entry-start - The column at the start of an entry. It is a slot and is always present.
33
+ * @csspart entry-surface - The panel an entry's submenu opens in beside it. The playlist's entries have none.
33
34
  * @csspart menu - The menu that holds the entries.
34
35
  * @csspart menu-back - The button that goes back from a submenu.
35
36
  * @csspart menu-back-mark - The icon on a submenu's back button.
@@ -88,7 +88,7 @@ let HelixHeadlessVideoPlaylistMenu = class HelixHeadlessVideoPlaylistMenu extend
88
88
  part="entry"
89
89
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
90
90
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
91
- shortcut-separator: entry-shortcut-separator, start: entry-start"
91
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface"
92
92
  value=${entry.value}
93
93
  checkable
94
94
  ?checked=${entry.current}
@@ -35,10 +35,17 @@ declare const HelixHeadlessVideoPlaylist_base: (new (...args: any[]) => import("
35
35
  * your name, and only the title follows the entry.
36
36
  *
37
37
  * **Keyboard.** Each entry is a button in the tab order, and Enter or Space
38
- * plays it. The current entry carries `aria-current`. While focus is on the
39
- * player, the player's own keys work, and `Video` lists them. Its previous
40
- * and next controls move through the entries in the same order the playlist
41
- * does.
38
+ * plays it. The arrow keys move focus to the entry before or after, and Home
39
+ * and End to the first and the last, passing over disabled entries. The
40
+ * current entry carries `aria-current`. While focus is on the player, the
41
+ * player's own keys work, and `Video` lists them, and Shift with N or P plays
42
+ * the next or the previous entry. The player's previous and next controls
43
+ * move through the entries in the same order the playlist does.
44
+ *
45
+ * **What a screen reader hears.** A list named by `label`, with a button for
46
+ * each entry. When the playlist moves to another entry, by a press, by the
47
+ * player's controls, or at the end of a clip, a live region says
48
+ * `playing-label`, "Now playing" and the entry's name.
42
49
  *
43
50
  * @since 0.1.0
44
51
  * @status stable
@@ -54,7 +61,10 @@ declare const HelixHeadlessVideoPlaylist_base: (new (...args: any[]) => import("
54
61
  * @event {HelixEndEvent} helix-end - The last entry finished playing, and the playlist stopped there. Not sent while `repeat` is set.
55
62
  *
56
63
  * @csspart items - The list of entries.
64
+ * @csspart live - A live region that says which entry plays after the playlist moves.
57
65
  * @csspart player - The region that holds the player.
66
+ *
67
+ * @cssstate ended - The last entry finished and the playlist stopped there, until an entry plays again.
58
68
  */
59
69
  export declare class HelixHeadlessVideoPlaylist extends HelixHeadlessVideoPlaylist_base {
60
70
  /**
@@ -66,6 +76,17 @@ export declare class HelixHeadlessVideoPlaylist extends HelixHeadlessVideoPlayli
66
76
  * The list's name, read by screen readers.
67
77
  */
68
78
  label: string;
79
+ /**
80
+ * What the live region says when the playlist moves to another entry.
81
+ * `{title}` is the entry's name.
82
+ *
83
+ * **A move a reader did not make is said.** At the end of a clip the
84
+ * playlist starts the next one, and the only other trace of that is the
85
+ * `aria-current` on a button the reader is not on.
86
+ */
87
+ playingLabel: string;
88
+ /** The live region, rendered from the first move. */
89
+ private readonly status;
69
90
  /**
70
91
  * What happens when an entry ends with no next one to go to. `one` plays the
71
92
  * same entry again, and `all` goes from the last entry back to the first.
@@ -168,6 +189,24 @@ export declare class HelixHeadlessVideoPlaylist extends HelixHeadlessVideoPlayli
168
189
  */
169
190
  previous(): void;
170
191
  private readonly handleSelect;
192
+ /**
193
+ * The arrow keys, Home and End on an entry move focus to another one.
194
+ *
195
+ * **The entries the keys reach are the enabled ones**, because a disabled
196
+ * button cannot take focus and a key that landed on one would leave focus
197
+ * where it was. Up and Left go back, Down and Right go on, and Left and
198
+ * Right swap on a right-to-left page, so a list laid out in a row moves the
199
+ * way it reads.
200
+ */
201
+ private readonly handleKeydown;
202
+ /** The player playing again, after the end or anywhere, means the list has not ended. */
203
+ private readonly handlePlay;
204
+ /**
205
+ * Shift with N plays the next entry and Shift with P the previous one, while
206
+ * focus is on the player, the keys a reader knows from the common players.
207
+ * Not inside a field the player holds, where they are letters.
208
+ */
209
+ private takePlayerKey;
171
210
  private readonly handleNext;
172
211
  private readonly handlePrevious;
173
212
  /**
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, HelixElement, HelixEndEvent, MixinControllerActive, words, } from '@fusion.dev/helix-core';
7
+ import { customElement, drawStatus, formatWords, HelixElement, HelixEndEvent, inlineStep, MixinControllerActive, StatusController, words, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { HelixHeadlessVideoPlaylistItem } from '../item/video-playlist-item.js';
@@ -13,6 +13,8 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
13
13
  constructor() {
14
14
  super(...arguments);
15
15
  this.label = 'Playlist';
16
+ this.playingLabel = 'Now playing {title}';
17
+ this.status = new StatusController(this);
16
18
  this.shuffle = false;
17
19
  this.chased = false;
18
20
  this.handleSelect = (event) => {
@@ -23,6 +25,39 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
23
25
  this.chased = true;
24
26
  this.requestActive(event.detail.value);
25
27
  };
28
+ this.handleKeydown = (event) => {
29
+ const from = event.target;
30
+ if (this.takePlayerKey(event)) {
31
+ return;
32
+ }
33
+ if (!(from instanceof HelixHeadlessVideoPlaylistItem) ||
34
+ event.altKey ||
35
+ event.ctrlKey ||
36
+ event.metaKey ||
37
+ event.shiftKey) {
38
+ return;
39
+ }
40
+ const reachable = this.items.filter((one) => !one.disabled);
41
+ const at = reachable.indexOf(from);
42
+ const step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : inlineStep(this, event.key);
43
+ const to = event.key === 'Home'
44
+ ? 0
45
+ : event.key === 'End'
46
+ ? reachable.length - 1
47
+ : step !== 0 && at !== -1
48
+ ? Math.min(Math.max(at + step, 0), reachable.length - 1)
49
+ : undefined;
50
+ if (to === undefined) {
51
+ return;
52
+ }
53
+ event.preventDefault();
54
+ reachable[to]?.focus();
55
+ };
56
+ this.handlePlay = (event) => {
57
+ if (event.target === this.player) {
58
+ this.toggleState('ended', false);
59
+ }
60
+ };
26
61
  this.handleNext = () => {
27
62
  this.next();
28
63
  };
@@ -38,6 +73,7 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
38
73
  return;
39
74
  }
40
75
  if (this.following(1).next === undefined) {
76
+ this.toggleState('ended', true);
41
77
  this.dispatchEvent(new HelixEndEvent());
42
78
  return;
43
79
  }
@@ -91,19 +127,43 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
91
127
  previous() {
92
128
  this.step(-1);
93
129
  }
130
+ takePlayerKey(event) {
131
+ const player = this.player;
132
+ if (!player ||
133
+ event.target !== player ||
134
+ event.defaultPrevented ||
135
+ !event.shiftKey ||
136
+ event.altKey ||
137
+ event.ctrlKey ||
138
+ event.metaKey ||
139
+ (event.key !== 'N' && event.key !== 'P')) {
140
+ return false;
141
+ }
142
+ const origin = event.composedPath()[0];
143
+ if (origin?.tagName === 'INPUT' || origin?.isContentEditable) {
144
+ return false;
145
+ }
146
+ event.preventDefault();
147
+ this.step(event.key === 'N' ? 1 : -1);
148
+ return true;
149
+ }
94
150
  connectedCallback() {
95
151
  super.connectedCallback();
96
152
  this.addEventListener('helix-select', this.handleSelect);
153
+ this.addEventListener('keydown', this.handleKeydown);
97
154
  this.addEventListener('helix-next', this.handleNext);
98
155
  this.addEventListener('helix-previous', this.handlePrevious);
99
156
  this.addEventListener('ended', this.handleEnded, true);
157
+ this.addEventListener('play', this.handlePlay, true);
100
158
  }
101
159
  disconnectedCallback() {
102
160
  super.disconnectedCallback();
103
161
  this.removeEventListener('helix-select', this.handleSelect);
162
+ this.removeEventListener('keydown', this.handleKeydown);
104
163
  this.removeEventListener('helix-next', this.handleNext);
105
164
  this.removeEventListener('helix-previous', this.handlePrevious);
106
165
  this.removeEventListener('ended', this.handleEnded, true);
166
+ this.removeEventListener('play', this.handlePlay, true);
107
167
  }
108
168
  willUpdate(changed) {
109
169
  super.willUpdate(changed);
@@ -123,6 +183,7 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
123
183
  return;
124
184
  }
125
185
  if (player.src !== current.src) {
186
+ this.toggleState('ended', false);
126
187
  player.src = current.src;
127
188
  player.poster = current.poster;
128
189
  player.mediaTitle = current.text;
@@ -130,6 +191,7 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
130
191
  player.label = current.text;
131
192
  }
132
193
  if (this.chased) {
194
+ this.status.say(formatWords(this, 'playingLabel', { title: current.text }));
133
195
  void player.updateComplete?.then(() => player.play()).catch(() => { });
134
196
  }
135
197
  }
@@ -139,13 +201,18 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
139
201
  return html `<div part="player"><slot name="player"></slot></div>
140
202
  <div part="items" role="list" aria-label=${this.label}>
141
203
  <slot></slot>
142
- </div>`;
204
+ </div>
205
+ ${drawStatus(this.status)}`;
143
206
  }
144
207
  };
145
208
  __decorate([
146
209
  words,
147
210
  property()
148
211
  ], HelixHeadlessVideoPlaylist.prototype, "label", void 0);
212
+ __decorate([
213
+ words,
214
+ property({ attribute: 'playing-label' })
215
+ ], HelixHeadlessVideoPlaylist.prototype, "playingLabel", void 0);
149
216
  __decorate([
150
217
  property()
151
218
  ], HelixHeadlessVideoPlaylist.prototype, "repeat", void 0);
@@ -97,6 +97,11 @@ export interface VirtualListRange {
97
97
  * shows nothing, since a list whose items have not arrived yet has not said
98
98
  * it has none.
99
99
  *
100
+ * The list takes the `empty` state while it has read its items and holds none,
101
+ * and the `loading` state while `loading` is set, so your stylesheet can
102
+ * change the box around it for either. A list with no name is reported once
103
+ * in a development build. Name it with `label` or `aria-label`.
104
+ *
100
105
  * **Row height.** Rows are placed by `estimate` until they are drawn. Then the
101
106
  * list reads the drawn rows' height once and places every row by it, so a
102
107
  * wrong estimate still reaches the last item. Set `measured` for rows of
@@ -131,6 +136,9 @@ export interface VirtualListRange {
131
136
  * @csspart empty - The row shown when there are no items and none are loading, where `empty-label` or the `empty` slot gives it content.
132
137
  * @csspart loading - The row after the last item while `loading` is set.
133
138
  * @csspart viewport - The scrolling box. On this tier, it needs only a block size.
139
+ *
140
+ * @cssstate empty - While the list has read its items and holds none.
141
+ * @cssstate loading - While `loading` is set.
134
142
  */
135
143
  export declare class HelixHeadlessVirtualList extends HelixElement {
136
144
  /**
@@ -33,6 +33,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
33
33
  this.aria = new AriaForwardController(this, {
34
34
  label: true,
35
35
  target: () => this.partNamed('viewport'),
36
+ unnamed: 'Give it a `label`, or an `aria-label` that says what the items are.',
36
37
  });
37
38
  this.virtualize = new VirtualizeController(this, {
38
39
  container: () => this.partNamed('viewport'),
@@ -210,6 +211,8 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
210
211
  }
211
212
  updated(changed) {
212
213
  super.updated?.(changed);
214
+ this.toggleState('empty', this.readFor !== undefined && this.data.length === 0);
215
+ this.toggleState('loading', this.loading);
213
216
  this.virtualize.measure();
214
217
  this.place();
215
218
  this.virtualize.settle();