@fusion.dev/helix-headless 0.1.0 → 0.3.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
@@ -7,77 +7,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, HelixSelectEvent } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary One entry in a video playlist.
12
- *
13
- * @description
14
- * **It holds what the player needs and nothing about playing.** A source, a
15
- * poster and a name: the entry knows which file it stands for, and the playlist
16
- * is what hands that to a player. Nothing here touches a `<video>`, which is
17
- * what keeps a playlist usable with a player the consumer configured
18
- * themselves.
19
- *
20
- * **A real `<button>`, in a `listitem`.** A playlist is a list of things to
21
- * choose, and the list roles are the honest ones: `list` and `listitem` say how
22
- * many there are and where you are in them, which `listbox` would too, at the
23
- * cost of the pattern a listbox obliges. `Option` records that trap in full,
24
- * that a listbox has to keep focus elsewhere and say which item is current with
25
- * `aria-activedescendant`. Nothing here needs that, because focus really moves
26
- * to the button.
27
- *
28
- * `aria-current` rather than `aria-selected`, which belongs to the roles that
29
- * have a selection. What is true of the entry playing now is that it is the one
30
- * the player is on.
31
- *
32
- * @since 0.1.0
33
- * @status experimental
34
- * @category media
35
- * @documentation https://helix.fusion.dev/components/video-playlist
36
- *
37
- * @slot - The entry's name.
38
- * @slot media - A thumbnail. Falls back to an image of `poster`.
39
- * @slot meta - After the name. A duration, or a date.
40
- *
41
- * @event {HelixSelectEvent} helix-select - Fired when the entry is chosen. Cancelable: refusing it takes the decision and nothing moves.
42
- *
43
- * @csspart container - The `listitem`, which the button sits in.
44
- * @csspart item - The row, which is a button.
45
- * @csspart label - The name and whatever follows it.
46
- * @csspart media - The leading column.
47
- * @csspart thumbnail - The image `poster` falls back to.
48
- */
49
10
  let HelixHeadlessVideoPlaylistItem = class HelixHeadlessVideoPlaylistItem extends HelixElement {
50
11
  constructor() {
51
12
  super(...arguments);
52
- /**
53
- * Whether the player is on this entry.
54
- *
55
- * Written by the playlist rather than by the entry, the way an option's
56
- * selection is: an entry that made itself current would be a second answer to
57
- * which one is playing.
58
- */
59
13
  this.current = false;
60
- /**
61
- * Whether the entry can be chosen.
62
- */
63
14
  this.disabled = false;
64
- /**
65
- * What covers the picture before this entry plays, and the row's thumbnail.
66
- *
67
- * One property doing both, because they are the same picture: the frame that
68
- * stands for the clip stands for it in the list too. It is the `media` slot's
69
- * fallback rather than something drawn beside it, which is this library's rule
70
- * for a content property, so a consumer with something better to show slots it
71
- * and the two can never both appear.
72
- */
73
15
  this.poster = '';
74
- /**
75
- * The file this entry plays.
76
- */
77
16
  this.src = '';
78
- /**
79
- * What identifies the entry, and what `active` is compared against.
80
- */
81
17
  this.value = '';
82
18
  this.choose = () => {
83
19
  if (this.disabled) {
@@ -86,17 +22,6 @@ let HelixHeadlessVideoPlaylistItem = class HelixHeadlessVideoPlaylistItem extend
86
22
  this.dispatchEvent(new HelixSelectEvent({ value: this.value }));
87
23
  };
88
24
  }
89
- /**
90
- * The entry's name, which is the words and not everything in it.
91
- *
92
- * `textContent` takes the whole subtree: a row with a duration in `meta`
93
- * reads as "His Girl Friday0:06", and that string reaches the player and the
94
- * lock screen. Only the nodes the consumer left unslotted are the name;
95
- * anything named is a column beside it.
96
- *
97
- * Light DOM rather than `assignedNodes`, so it answers on the server too,
98
- * where there is no slot to ask. `Option.text` reads its own the same way.
99
- */
100
25
  get text() {
101
26
  return [...this.childNodes]
102
27
  .filter((node) => !(node instanceof Element) || !node.hasAttribute('slot'))
@@ -104,26 +29,12 @@ let HelixHeadlessVideoPlaylistItem = class HelixHeadlessVideoPlaylistItem extend
104
29
  .join('')
105
30
  .trim();
106
31
  }
107
- /**
108
- * What stands for the row: a thumbnail where there is one, and nothing where
109
- * there is not.
110
- *
111
- * A seam rather than inline, so a tier can draw a duration badge over it or
112
- * a placeholder in its place without reimplementing the row. A consumer
113
- * slots `media` for the same reasons and neither has to know about the other.
114
- */
115
32
  renderMedia() {
116
33
  return html `<slot name="media">
117
- ${
118
- // `alt=""`, because the row's own words already name it and a
119
- // thumbnail repeating them is the same thing said twice.
120
- this.poster === '' ? nothing : html `<img part="thumbnail" alt="" src=${this.poster} />`}
34
+ ${this.poster === '' ? nothing : html `<img part="thumbnail" alt="" src=${this.poster} />`}
121
35
  </slot>`;
122
36
  }
123
37
  render() {
124
- /* `aria-current="true"`, and absent rather than `"false"` otherwise. A
125
- reader is told which one is playing; being told about each of the others
126
- that it is not playing is the same fact eleven times. */
127
38
  return html `<div part="container" role="listitem">
128
39
  <button
129
40
  part="item"
@@ -12,91 +12,11 @@ import { html } from 'lit/static-html.js';
12
12
  import { HelixHeadlessVideoPlaylist } from '../root/video-playlist-root.js';
13
13
  import '../../menu/index.js';
14
14
  const ITEMS = 'helix-video-playlist-item, helix-headless-video-playlist-item';
15
- /**
16
- * @summary The playlist's entries, as a menu inside the player's control bar.
17
- *
18
- * @description
19
- * The entries again, reachable without leaving the picture. A playlist beside
20
- * the player is a list a reader scrolls to; the same list in the control bar is
21
- * one they open over the video, which is what a full-screen player has and what
22
- * a phone has room for.
23
- *
24
- * **It is placed by the consumer, not rendered by the playlist**, and that is
25
- * forced rather than chosen. A shadow root cannot project into a sibling's
26
- * light DOM, so nothing the playlist renders can land inside the player's
27
- * control bar. What can is an element the consumer writes there, which is what
28
- * `Video`'s `controls` slot is for:
29
- *
30
- * ```html
31
- * <helix-video-playlist>
32
- * <helix-video slot="player" controls>
33
- * <helix-video-playlist-menu slot="controls"></helix-video-playlist-menu>
34
- * </helix-video>
35
- * <helix-video-playlist-item value="one" src="/one.mp4">One</helix-video-playlist-item>
36
- * </helix-video-playlist>
37
- * ```
38
- *
39
- * So it finds its playlist upward rather than being handed one, and that chain
40
- * crosses a shadow boundary in the ordinary arrangement, which is why it is
41
- * `closestInstance` and not `closest`.
42
- *
43
- * **Choosing dispatches `helix-select`, the same event an entry dispatches.**
44
- * It never writes `active` on the playlist, which looks like the shorter path
45
- * and skips the cancelable request: an assignment applies the change with no
46
- * `helix-before-activate` for anyone to refuse. Announcing is what an entry
47
- * does, the playlist already listens for it, and a consumer refusing a
48
- * selection gets `preventDefault()` on one event rather than two paths that
49
- * behave differently.
50
- *
51
- * @since 0.1.0
52
- * @status experimental
53
- * @category media
54
- * @aliases Episode Menu, Queue Menu, Up Next
55
- * @documentation https://helix.fusion.dev/components/video-playlist
56
- *
57
- * @slot trigger - Replaces the button that opens the menu.
58
- *
59
- * @event {HelixSelectEvent} helix-select - Fired with the chosen entry's value. The playlist listens for it. Cancelable: refusing it takes the decision and nothing moves.
60
- *
61
- * @csspart entry - One entry in the menu.
62
- * @csspart entry-item - The row, which is an `<a>` where `href` is set.
63
- * @csspart entry-mark - The trailing mark: a tick when checked, a caret when it opens
64
- * @csspart entry-start - The leading column, which is a slot and is always there.
65
- * @csspart menu - The menu that holds the entries.
66
- * @csspart menu-back - The control that returns from a submenu.
67
- * @csspart menu-back-mark - The glyph on the back control of a submenu.
68
- * @csspart menu-empty - Shown when there are no items to show.
69
- * @csspart menu-header - The bar above a submenu, holding the way back and its name.
70
- * @csspart menu-loading - Shown while the items are still on their way.
71
- * @csspart menu-surface - The floating panel the menu is drawn on.
72
- * @csspart menu-title - The name of the submenu that is showing.
73
- * @csspart poster - An entry's still, where it has one.
74
- * @csspart trigger - The button that opens it.
75
- */
76
15
  let HelixHeadlessVideoPlaylistMenu = class HelixHeadlessVideoPlaylistMenu extends HelixElement {
77
16
  constructor() {
78
17
  super(...arguments);
79
- /**
80
- * The menu's accessible name, and its trigger's.
81
- *
82
- * "Episodes" suits a series and is wrong for a set of camera angles, and the
83
- * component cannot know which one it is in.
84
- */
85
18
  this.label = 'Episodes';
86
- /**
87
- * Where the menu opens relative to its trigger.
88
- *
89
- * `top-end` because a control bar sits at the foot of the picture, and a menu
90
- * opening downward from there opens off the player.
91
- */
92
19
  this.placement = 'top-end';
93
- /* `requestUpdate` rather than a counter held in `@state`.
94
- *
95
- * The entries are the playlist's light DOM rather than this element's, so
96
- * nothing about them schedules an update here: an entry added, removed, or
97
- * made current changes a tree this element only looks at. A bumped `@state`
98
- * field is the usual way to say "render again", and here it would be a field
99
- * nothing reads, which the linter is right to call out. */
100
20
  this.reread = () => {
101
21
  this.requestUpdate();
102
22
  };
@@ -107,8 +27,6 @@ let HelixHeadlessVideoPlaylistMenu = class HelixHeadlessVideoPlaylistMenu extend
107
27
  #observer;
108
28
  connectedCallback() {
109
29
  super.connectedCallback();
110
- /* After the first update, because the playlist is found by walking the tree
111
- and the server has no tree to walk. */
112
30
  this.updateComplete.then(() => this.watch());
113
31
  }
114
32
  disconnectedCallback() {
@@ -117,46 +35,14 @@ let HelixHeadlessVideoPlaylistMenu = class HelixHeadlessVideoPlaylistMenu extend
117
35
  this.#observer?.disconnect();
118
36
  this.#observer = undefined;
119
37
  }
120
- /* Resolved once, on the client, and never during a render.
121
- *
122
- * `closestInstance` walks `parentNode` and steps across a `ShadowRoot` by
123
- * following its `host`, and the server has no `ShadowRoot` global, so
124
- * reaching for it inside `render` throws
125
- * `ReferenceError: Can't find variable: ShadowRoot` from inside Lit's own
126
- * result collector. A component that walks the tree to render cannot render
127
- * on a server, and the server has no tree to walk anyway. Set after the first
128
- * update, and `undefined` on the server, where an empty menu is the honest
129
- * output. */
130
38
  #playlist;
131
- /** The playlist this menu belongs to, found upward through the slots. */
132
39
  get playlist() {
133
40
  return this.#playlist;
134
41
  }
135
- /** The playlist's entries, in the order it holds them. */
136
42
  get entries() {
137
43
  const playlist = this.playlist;
138
44
  return playlist ? [...playlist.querySelectorAll(ITEMS)] : [];
139
45
  }
140
- /**
141
- * Watches the playlist for entries arriving, and re-reads it on every open.
142
- *
143
- * Two signals rather than one, because the two facts change on different
144
- * clocks and only one of them is observable.
145
- *
146
- * **Which entries exist** is the playlist's light DOM, and a `MutationObserver`
147
- * sees it directly.
148
- *
149
- * **Which entry is current** is `active`, a property with no attribute, so
150
- * there is nothing to observe: events travel up from the playlist and this
151
- * element is below it, and mirroring `active` to a `data-` name is ruled out
152
- * on its own merits, `active` being a payload rather than a boolean fact. A
153
- * `data-active` on every host is what `tree-root.ssr.spec.ts` fails on.
154
- *
155
- * So it is re-read at the moment it is looked at. `helix-before-open` comes from the
156
- * menu in this element's own shadow root, which is the one direction events
157
- * do arrive from, and a closed menu showing a stale mark is a thing nobody
158
- * can see.
159
- */
160
46
  watch() {
161
47
  this.#playlist ??= closestInstance(this, HelixHeadlessVideoPlaylist) ?? undefined;
162
48
  const playlist = this.#playlist;
@@ -171,7 +57,6 @@ let HelixHeadlessVideoPlaylistMenu = class HelixHeadlessVideoPlaylistMenu extend
171
57
  render() {
172
58
  return this.renderMenu();
173
59
  }
174
- /** The menu itself, behind a method so a styled tier can render another one. */
175
60
  renderMenu() {
176
61
  const active = this.playlist?.active;
177
62
  return html `<${this.tag('menu')} part="menu"
@@ -188,27 +73,11 @@ let HelixHeadlessVideoPlaylistMenu = class HelixHeadlessVideoPlaylistMenu extend
188
73
  ?disabled=${entry.disabled}
189
74
  role="menuitemradio"
190
75
  @helix-select=${(event) => {
191
- /* Stopped and re-announced from the host rather than let through.
192
- What travels up is then this element's own event, so a consumer
193
- listening on the menu sees what the playlist sees, and
194
- `preventDefault()` refuses the selection in one place. */
195
76
  event.stopPropagation();
196
77
  this.choose(entry.value);
197
78
  }}
198
79
  >
199
- ${
200
- /* The still, where there is one: a reader picks an episode by
201
- recognizing it faster than by reading its title, and this menu
202
- opens over the picture where reading is hardest.
203
- *
204
- * In `start`, which is free: `Menu.Item` renders its tick *after*
205
- * the label as `part="mark"`, so the leading column has nothing to
206
- * collide with. `alt=""`, because the name is in the same row and a
207
- * still repeating it is the same thing said twice.
208
- *
209
- * Omitted rather than empty where there is no poster, an `<img>`
210
- * with no `src` being a broken-image glyph in every engine. */
211
- entry.poster === ''
80
+ ${entry.poster === ''
212
81
  ? nothing
213
82
  : html `<img alt="" part="poster" slot="start" src=${entry.poster} />`}
214
83
  ${entry.text}
@@ -7,79 +7,12 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, MixinControllerActive } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary A set of clips and the one player they take turns in.
12
- *
13
- * @description
14
- * **The player is slotted, never rendered here.** A consumer's player carries
15
- * their appearance, their extra controls, their captions and their poster
16
- * treatment, and a playlist that rendered its own would either lose all of that
17
- * or grow a property for each of them. `Sandbox` takes its preview and `Menu`
18
- * its trigger the same way.
19
- *
20
- * **The entries are markup rather than an array property.** A list of objects
21
- * would be the shorter API and it does not survive the trip: React skips every
22
- * property it would have to apply imperatively while hydrating, so an array
23
- * handed to a custom element is missing on the first paint of every
24
- * server-rendered page. `using-the-elements.mdx` carries the measurement. Markup
25
- * is also what makes each entry a real button with a real name in it.
26
- *
27
- * **Nothing here plays anything.** It sets `src` on the player and lets the
28
- * player be a player. What the playlist owns is which entry is current, and it
29
- * moves that on three signals: an entry pressed, `helix-next` or
30
- * `helix-previous` from the player, and the player reaching the end.
31
- *
32
- * `ended` advances without asking, because a set of clips that stopped between
33
- * each one is a set the reader has to nurse. The clip that follows starts where
34
- * the last one left off, which is `autoplay` in the only sense that matters
35
- * here: the reader already pressed play once.
36
- *
37
- * @since 0.1.0
38
- * @status experimental
39
- * @category media
40
- * @aliases Playlist, Queue, Up next
41
- * @documentation https://helix.fusion.dev/components/video-playlist
42
- *
43
- * @slot - The entries.
44
- * @slot player - The `helix-video` they play in.
45
- *
46
- * @event {HelixActivateEvent} helix-activate - Fired once the current one has moved, so the element already holds it.
47
- * @event {HelixBeforeActivateEvent} helix-before-activate - Fired before the current entry changes, and cancelable.
48
- *
49
- * @csspart items - The list of entries.
50
- * @csspart player - The region holding the player.
51
- */
52
10
  let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinControllerActive(HelixElement) {
53
11
  constructor() {
54
12
  super(...arguments);
55
- /**
56
- * Accessible name for the list, which usually has no visible heading.
57
- */
58
13
  this.label = 'Playlist';
59
- /**
60
- * Whether the entry changed because somebody moved through the playlist.
61
- *
62
- * All three ways it moves are the reader going somewhere: pressing an entry,
63
- * pressing next or previous, or a clip ending and the next one following it.
64
- * Every one of those means play, and the one case that does not is the first
65
- * render, where the playlist is simply pointing the player at its opening
66
- * entry and nobody has asked for anything yet.
67
- *
68
- * A press is also what makes the play legal. Autoplay is refused when nothing
69
- * the reader did asked for it, so starting here works because this is inside
70
- * the gesture that chose the entry, and where it is refused anyway the player
71
- * shows its start control and waits.
72
- */
73
14
  this.chased = false;
74
15
  this.handleSelect = (event) => {
75
- /* Stopped and not relayed. The entry says it was chosen, this element says
76
- what is playing, and a consumer hearing both would hear one press twice
77
- under two names.
78
-
79
- `stopImmediatePropagation`, not `stopPropagation`. The consumer's listener
80
- is on this same element, and stopping propagation only keeps an event off
81
- the *next* node: every listener already bound here still runs, so the
82
- entry's selection reaches them anyway. Command carries the longer note. */
83
16
  event.stopImmediatePropagation();
84
17
  this.chased = true;
85
18
  this.requestActive(event.detail.value);
@@ -97,38 +30,16 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
97
30
  this.step(1);
98
31
  };
99
32
  }
100
- /**
101
- * The entries, in the order the consumer wrote them.
102
- *
103
- * **Found by tag and filtered on the property, never on `[value]`.** React
104
- * assigns a matching property rather than an attribute, so a playlist written
105
- * in JSX carries no `value` attribute anywhere and `querySelectorAll('[value]')`
106
- * returns nothing: every entry vanishes from the list, the active one
107
- * included, and nothing throws, an empty playlist being a legal playlist.
108
- *
109
- * Reading `one.value` answers the same question the same way whether the
110
- * consumer wrote markup or set a property, which is the only form that works
111
- * for both.
112
- */
113
33
  get items() {
114
34
  return [
115
35
  ...this.querySelectorAll('helix-video-playlist-item, helix-headless-video-playlist-item'),
116
36
  ].filter((one) => one.value !== '');
117
37
  }
118
- /**
119
- * The player, whichever tag the consumer slotted.
120
- *
121
- * Asked of the slot rather than matched by tag name: the headless tier's
122
- * player is `helix-headless-video` and the styled tier's is `helix-video`, and
123
- * a consumer bringing a third under either name should still be driven. What
124
- * is needed of it is `src`, which is the whole of the contract.
125
- */
126
38
  get player() {
127
39
  const slot = this.renderRoot?.querySelector('slot[name="player"]');
128
40
  const first = slot?.assignedElements()[0];
129
41
  return (first ?? null);
130
42
  }
131
- /** Moves by one, and does nothing at either end. */
132
43
  step(by) {
133
44
  const items = this.items.filter((one) => !one.disabled);
134
45
  const at = items.findIndex((one) => one.value === this.resolveActive(items)?.value);
@@ -142,10 +53,6 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
142
53
  this.addEventListener('helix-select', this.handleSelect);
143
54
  this.addEventListener('helix-next', this.handleNext);
144
55
  this.addEventListener('helix-previous', this.handlePrevious);
145
- /* `ended` is a media event and does not compose out of the player's shadow
146
- root on its own. The player relays it, which is why listening here works
147
- at all, and why this listens on the element rather than on a `<video>` it
148
- has no reference to. */
149
56
  this.addEventListener('ended', this.handleEnded);
150
57
  }
151
58
  disconnectedCallback() {
@@ -155,14 +62,6 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
155
62
  this.removeEventListener('helix-previous', this.handlePrevious);
156
63
  this.removeEventListener('ended', this.handleEnded);
157
64
  }
158
- /**
159
- * Points the player at the current entry, and tells the entries which is on.
160
- *
161
- * In `updated` rather than in the handlers, so the three signals that move the
162
- * current entry all arrive at one place that applies it. The server never runs
163
- * this, which is correct: the entries are light DOM it cannot see, and what it
164
- * renders is the player with whatever `src` the consumer wrote.
165
- */
166
65
  updated(changed) {
167
66
  super.updated?.(changed);
168
67
  const items = this.items;
@@ -174,23 +73,11 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
174
73
  if (!player || !current) {
175
74
  return;
176
75
  }
177
- /* Only on a change. Writing `src` with the value it already holds reloads
178
- the resource in every engine, so a playlist that assigned on each update
179
- would restart the clip whenever anything else about it rendered. */
180
76
  if (player.src !== current.src) {
181
77
  player.src = current.src;
182
78
  player.poster = current.poster;
183
79
  player.label = current.text;
184
80
  if (this.chased) {
185
- /* After the player has rendered, not now. `src` here is the element's
186
- property, and the `<video>` inside it only takes the new source on
187
- its next render: playing at this point plays the outgoing clip for an
188
- instant and the load that follows stops it again. Measured, and the
189
- reading was a player left paused on the entry that had just been
190
- chosen.
191
-
192
- A microtask later is still inside the press that got us here, so the
193
- platform still counts it as asked for. */
194
81
  void player.updateComplete?.then(() => {
195
82
  player.play();
196
83
  });
@@ -7,113 +7,19 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, VirtualizeController } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
- /**
11
- * @summary Renders the rows of a long set that are on screen, and no others.
12
- *
13
- * @description
14
- * **The set arrives as data, which is what makes this a virtual list rather
15
- * than a box that hides children.** `for` names a
16
- * `<script type="application/json">` holding an array of objects, and a
17
- * `<template>` child says what one row looks like: elements carrying
18
- * `data-field="name"` are filled with that field of the item.
19
- *
20
- * Both halves are markup, and that is the whole reason for the shape.
21
- * `VirtualizeController` says why the obvious alternatives are shut: an `items`
22
- * property cannot survive React hydration, which skips every property it would
23
- * have to apply imperatively, and light-DOM children exist by the time anything
24
- * could decide not to create them. Text in the document is neither. A server
25
- * emits it, React re-emits it as a child rather than applying it, and it is one
26
- * node whatever the row count.
27
- *
28
- * **The accessible version is not a `<table>`**, the way it is for the charts
29
- * that share this `for="<id>"` shape: ten thousand rows in one is the cost this
30
- * exists to avoid. It is the rendered window plus `aria-setsize` and
31
- * `aria-posinset`, which is what says "four thousand of ten thousand" rather
32
- * than "four of twenty".
33
- *
34
- * **Rows are light DOM, deliberately.** They are cloned from the consumer's
35
- * template and appended to the host, so a document rule reaches them, the base
36
- * layer applies, and a class in the template means what it says. Cloned into
37
- * the shadow root they would be markup the consumer wrote and could not style,
38
- * which is the one thing a row template must not be.
39
- *
40
- * **Reading the source is client-only**, because a server has no document to
41
- * query. The window renders empty from the server and fills on the first update,
42
- * which is the same client-side enhancement rule every tree-reading component
43
- * here follows.
44
- *
45
- * **On this tier the consumer gives the viewport its box**, and the component
46
- * cannot do it for them: `helix/headless` ships no stylesheet, and a height is a
47
- * decision rather than a fact this could publish. So a base list wants two rules
48
- * before it is a list at all:
49
- *
50
- * ```css
51
- * helix-headless-virtual-list::part(viewport) {
52
- * block-size: 12rem;
53
- * overflow-y: auto;
54
- * }
55
- * ```
56
- *
57
- * **Without them it is not a scroller**, and the failure is worse than it
58
- * sounds. `scrollTop` is always zero, so nothing ever windows; and the box has
59
- * no height of its own, so `clientHeight` is a function of the rows just drawn.
60
- * Measuring then moves what is being measured, and the two need not converge:
61
- * measured in Chromium at 500 rows, it settled into a two-cycle between 378 and
62
- * 379 rows and stayed there, at two and a half thousand passes in eight seconds.
63
- * `VirtualizeController` gives up after ten passes and says so, so this now
64
- * draws a bad window rather than stopping the page, but a bad window is still
65
- * the answer until the rules are written.
66
- *
67
- * The styled tier writes them for you, with `--virtual-list-block-size`.
68
- *
69
- * @category layout
70
- * @since 0.1.0
71
- * @status experimental
72
- * @documentation https://helix.fusion.dev/components/virtual-list
73
- *
74
- * @csspart after - The space standing in for rows below the window.
75
- * @csspart before - The space standing in for rows above the window.
76
- * @csspart viewport - The scrolling box. Needs a block size and an overflow on this tier.
77
- */
78
10
  let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixElement {
79
11
  constructor() {
80
12
  super(...arguments);
81
- /**
82
- * How tall one row is, in pixels, used to map scroll offset to an index.
83
- */
84
13
  this.estimate = 32;
85
- /**
86
- * Id of the element holding the rows, as JSON in a script.
87
- */
88
14
  this.for = '';
89
- /**
90
- * What the list is called, since the rendered rows are only a window on it.
91
- */
92
15
  this.label = '';
93
- /**
94
- * The parsed source.
95
- *
96
- * Empty on a server, which has nothing to read.
97
- */
98
16
  this.rows = [];
99
- /* No `overscan` property. The controller's four is a frame of fast scrolling
100
- at a 32 pixel row, which is a fact about the platform rather than a
101
- preference. */
102
17
  this.virtualize = new VirtualizeController(this, {
103
18
  container: () => this.renderRoot.querySelector('[part="viewport"]'),
104
19
  count: () => this.rows.length,
105
20
  estimate: () => this.estimate,
106
21
  });
107
22
  }
108
- /**
109
- * Reads the source named by `for`, once the document exists to read.
110
- *
111
- * `firstUpdated` rather than `connectedCallback`, per the server rules: the
112
- * shim has attributes and no tree, so `getElementById` is undefined there.
113
- * A source that is missing or not an array leaves the list empty rather than
114
- * throwing, because a consumer whose data has not arrived yet is the ordinary
115
- * case and an empty list says so by itself.
116
- */
117
23
  firstUpdated(changed) {
118
24
  super.firstUpdated?.(changed);
119
25
  this.read();
@@ -138,18 +44,9 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
138
44
  this.rows = Array.isArray(parsed) ? parsed : [];
139
45
  }
140
46
  catch {
141
- /* Left empty rather than thrown. The source is a consumer's markup, and a
142
- component that throws on it takes the page down over a comma. */
143
47
  this.rows = [];
144
48
  }
145
49
  }
146
- /**
147
- * Puts the windowed rows into the light DOM, and takes the rest out.
148
- *
149
- * Cleared and re-cloned rather than reconciled: a window is twenty-odd rows
150
- * and cloning that many is well under a frame. A reconciler here would carry
151
- * keys and identity for a set nothing selects into.
152
- */
153
50
  place() {
154
51
  const template = this.querySelector('template');
155
52
  if (!template) {
@@ -172,8 +69,6 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
172
69
  for (const hole of clone.querySelectorAll('[data-field]')) {
173
70
  hole.textContent = row[hole.dataset.field ?? ''] ?? '';
174
71
  }
175
- /* The real index and the real total, not the position in the window. A
176
- reader arrowing through this should hear where they are in the set. */
177
72
  for (const element of clone.children) {
178
73
  element.setAttribute('role', 'listitem');
179
74
  element.setAttribute('aria-posinset', String(at + 1));