@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.
- package/custom-elements.json +234 -234
- package/dist/accordion/item/accordion-item.js +0 -108
- package/dist/accordion/root/accordion-root.js +0 -33
- package/dist/announcement/root/announcement-root.js +1 -76
- package/dist/app-header/root/app-header-root.js +0 -84
- package/dist/avatar/group/avatar-group.js +0 -77
- package/dist/avatar/root/avatar-root.js +0 -72
- package/dist/badge/root/badge-root.js +0 -67
- package/dist/bar-chart/root/bar-chart-root.js +3 -263
- package/dist/blockquote/root/blockquote-root.js +0 -46
- package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
- package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
- package/dist/button/root/button-root.js +0 -215
- package/dist/button-group/root/button-group-root.js +0 -30
- package/dist/callout/root/callout-root.js +0 -52
- package/dist/card/root/card-root.js +0 -41
- package/dist/carousel/root/carousel-root.d.ts +2 -2
- package/dist/carousel/root/carousel-root.js +2 -362
- package/dist/chat/composer/chat-composer.js +0 -131
- package/dist/chat/message/chat-message.js +0 -137
- package/dist/chat/root/chat-root.js +0 -193
- package/dist/checkbox/root/checkbox-root.js +0 -113
- package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
- package/dist/code/root/code-root.js +0 -26
- package/dist/code-block/root/code-block-root.js +0 -100
- package/dist/color-input/root/color-input-root.js +0 -217
- package/dist/color-picker/color.js +0 -69
- package/dist/color-picker/root/color-picker-root.js +0 -272
- package/dist/combobox/root/combobox-root.js +0 -428
- package/dist/command/root/command-root.js +0 -177
- package/dist/copy-button/root/copy-button-root.js +0 -134
- package/dist/data-table/root/data-table-root.js +0 -336
- package/dist/data-table/root/order.js +0 -100
- package/dist/date-input/root/date-input-root.js +1 -398
- package/dist/date-picker/root/date-picker-root.js +0 -358
- package/dist/dialog/root/dialog-root.js +0 -189
- package/dist/divider/root/divider-root.js +0 -71
- package/dist/donut-chart/root/donut-chart-root.js +0 -214
- package/dist/drawer/root/drawer-root.js +0 -116
- package/dist/drilldown/item/drilldown-item.js +0 -82
- package/dist/drilldown/root/drilldown-root.js +0 -91
- package/dist/element-events.generated.js +0 -5
- package/dist/empty/root/empty-root.js +0 -35
- package/dist/file-input/root/file-input-root.js +0 -232
- package/dist/format-date/root/format-date-root.js +0 -95
- package/dist/heatmap/root/buckets.js +0 -57
- package/dist/heatmap/root/heatmap-root.js +0 -313
- package/dist/icon/root/icon-root.js +0 -48
- package/dist/icon-button/root/icon-button-root.js +0 -30
- package/dist/image/root/image-root.js +0 -174
- package/dist/index.js +0 -9
- package/dist/input/root/input-root.js +0 -228
- package/dist/kbd/root/kbd-root.js +0 -117
- package/dist/legend/root/legend-root.js +0 -73
- package/dist/line-chart/root/line-chart-root.js +0 -172
- package/dist/list/item/list-item.js +0 -92
- package/dist/list/root/list-root.js +0 -26
- package/dist/listbox/root/listbox-root.js +2 -429
- package/dist/loader/root/loader-root.js +0 -40
- package/dist/megamenu/root/megamenu-root.js +0 -284
- package/dist/menu/item/menu-item.js +1 -221
- package/dist/menu/root/menu-root.js +1 -171
- package/dist/meter/root/meter-root.js +1 -152
- package/dist/navigation/root/navigation-root.js +0 -56
- package/dist/notifications/notification/notification.js +0 -159
- package/dist/notifications/root/notifications-root.js +0 -148
- package/dist/number-input/root/number-input-root.js +1 -263
- package/dist/option/group/option-group.js +0 -26
- package/dist/option/root/option-root.js +0 -157
- package/dist/outline/root/outline-root.js +0 -292
- package/dist/pagination/root/pagination-root.js +0 -361
- package/dist/pagination/size/pagination-size.js +0 -104
- package/dist/pagination/summary/pagination-summary.js +0 -90
- package/dist/pin-input/root/pin-input-root.js +0 -276
- package/dist/popover/root/popover-root.js +0 -103
- package/dist/progress/root/progress-root.js +1 -133
- package/dist/qr-code/root/qr-code-root.js +0 -96
- package/dist/quote/root/quote-root.js +0 -25
- package/dist/radio/group/radio-group.js +0 -81
- package/dist/radio/root/radio-root.js +0 -85
- package/dist/rating/root/rating-root.js +0 -220
- package/dist/scroll-area/root/scroll-area-root.js +0 -100
- package/dist/select/root/select-root.js +2 -218
- package/dist/skeleton/root/skeleton-root.js +0 -20
- package/dist/slider/root/slider-root.js +1 -267
- package/dist/sparkline/root/sparkline-root.js +3 -164
- package/dist/speed-dial/item/speed-dial-item.js +0 -52
- package/dist/speed-dial/root/speed-dial-root.js +1 -116
- package/dist/splitter/root/splitter-root.js +0 -489
- package/dist/stat/group/stat-group.js +0 -18
- package/dist/stat/root/stat-root.js +0 -23
- package/dist/steps/root/steps-root.js +0 -62
- package/dist/steps/step/step.js +0 -131
- package/dist/switch/root/switch-root.js +0 -80
- package/dist/table/cell/table-cell.js +0 -23
- package/dist/table/column/table-column.js +0 -158
- package/dist/table/root/table-root.js +0 -82
- package/dist/table/row/table-row.js +0 -21
- package/dist/tabs/panel/tab-panel.js +0 -49
- package/dist/tabs/root/tabs-root.js +0 -98
- package/dist/tabs/tab/tab.js +0 -59
- package/dist/tag/root/tag-root.js +0 -31
- package/dist/textarea/root/textarea-root.js +0 -162
- package/dist/time-input/root/time-input-root.js +0 -283
- package/dist/time-picker/root/time-picker-root.js +0 -326
- package/dist/timeline/item/timeline-item.js +0 -66
- package/dist/timeline/root/timeline-root.js +0 -41
- package/dist/toast/item/toast-item.js +0 -165
- package/dist/toast/root/toast-root.js +0 -51
- package/dist/toggle/group/toggle-group.js +0 -150
- package/dist/toggle/root/toggle-root.js +0 -78
- package/dist/toolbar/root/toolbar-root.js +0 -116
- package/dist/tooltip/root/tooltip-root.js +0 -427
- package/dist/tour/root/tour-root.js +0 -130
- package/dist/tour/stop/tour-stop.js +1 -127
- package/dist/tree/item/tree-item.js +0 -198
- package/dist/tree/root/tree-root.js +0 -97
- package/dist/trend/root/trend-root.js +0 -53
- package/dist/video/root/video-root.d.ts +8 -12
- package/dist/video/root/video-root.js +5 -756
- package/dist/video-playlist/item/video-playlist-item.js +1 -90
- package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
- package/dist/video-playlist/root/video-playlist-root.js +0 -113
- package/dist/virtual-list/root/virtual-list-root.js +0 -105
- package/jsx.d.ts +1408 -2576
- package/package.json +6 -6
- 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));
|