@fusion.dev/helix-headless 0.163.0 → 0.164.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 (30) hide show
  1. package/custom-elements.json +809 -36
  2. package/dist/callout/root/callout-root.d.ts +43 -4
  3. package/dist/callout/root/callout-root.js +58 -2
  4. package/dist/code-block/patch.d.ts +42 -0
  5. package/dist/code-block/patch.js +118 -0
  6. package/dist/code-block/root/code-block-root.d.ts +37 -1
  7. package/dist/code-block/root/code-block-root.js +57 -9
  8. package/dist/color-picker/root/color-picker-root.d.ts +7 -3
  9. package/dist/color-picker/root/color-picker-root.js +7 -4
  10. package/dist/combobox/root/combobox-root.d.ts +1 -0
  11. package/dist/combobox/root/combobox-root.js +10 -1
  12. package/dist/data-table/root/data-table-root.d.ts +1 -0
  13. package/dist/data-table/root/data-table-root.js +1 -0
  14. package/dist/element-events.generated.js +7 -1
  15. package/dist/object-inspector/root/object-inspector-root.d.ts +23 -0
  16. package/dist/object-inspector/root/object-inspector-root.js +19 -1
  17. package/dist/table/grid-controller.d.ts +7 -0
  18. package/dist/table/grid-controller.js +15 -1
  19. package/dist/table/root/table-root.d.ts +10 -0
  20. package/dist/table/root/table-root.js +18 -1
  21. package/dist/video/root/video-root.js +2 -1
  22. package/dist/video-playlist/item/video-playlist-item.d.ts +8 -0
  23. package/dist/video-playlist/item/video-playlist-item.js +7 -0
  24. package/dist/video-playlist/root/video-playlist-root.d.ts +60 -0
  25. package/dist/video-playlist/root/video-playlist-root.js +55 -1
  26. package/lint.json +1 -1
  27. package/package.json +5 -5
  28. package/types/jsx.d.ts +340 -16
  29. package/types/vue.d.ts +86 -7
  30. package/types/words.d.ts +58 -0
@@ -26,6 +26,7 @@ declare const HelixHeadlessCombobox_base: (new (...args: any[]) => import("@fusi
26
26
  * | Key | What it does |
27
27
  * |---|---|
28
28
  * | Down, Up | On a closed field, opens the list on the chosen option, or the first or the last. In the open list, moves the highlight, round from the last option to the first. |
29
+ * | Alt with Down, Alt with Up | Opens the list without moving the highlight, and closes it, choosing nothing. |
29
30
  * | Page Down, Page Up | In the open list, moves the highlight by as many options as the list shows at once. |
30
31
  * | Enter | Chooses the highlighted option. With nothing left in the list, creates what was typed with `creatable`, or takes it with `allows-custom-value`. On a closed field, submits the form. |
31
32
  * | Escape | Closes the list and puts the chosen words back. On a closed field, a second Escape clears a single choice. |
@@ -154,7 +154,11 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
154
154
  return;
155
155
  }
156
156
  if (!this.open) {
157
- if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
157
+ if (event.key === 'ArrowDown' && event.altKey) {
158
+ event.preventDefault();
159
+ this.requestOpen(true);
160
+ }
161
+ else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
158
162
  event.preventDefault();
159
163
  const fallback = event.key === 'ArrowDown' ? 'first' : 'last';
160
164
  this.opening = this.values.length > 0 ? 'selected' : fallback;
@@ -162,6 +166,11 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
162
166
  }
163
167
  return;
164
168
  }
169
+ if (event.key === 'ArrowUp' && event.altKey) {
170
+ event.preventDefault();
171
+ this.close();
172
+ return;
173
+ }
165
174
  if (this.moveAroundCreate(event)) {
166
175
  void this.updateComplete.then(() => this.linkActive());
167
176
  return;
@@ -215,6 +215,7 @@ declare const HelixHeadlessDataTable_base: (new (...args: any[]) => import("@fus
215
215
  *
216
216
  * @cssstate empty - While there are no rows to show and none are loading.
217
217
  * @cssstate error - While `error-text` is set.
218
+ * @cssstate loading - While `loading` is set, as the rows are on their way.
218
219
  */
219
220
  export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base {
220
221
  /**
@@ -984,6 +984,7 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
984
984
  this.markEmpty();
985
985
  this.toggleState('error', this.errorText !== '');
986
986
  this.toggleState('empty', !this.loading && !this.hasRows);
987
+ this.toggleState('loading', this.loading);
987
988
  this.assign();
988
989
  this.announceSort();
989
990
  this.quietControls();
@@ -335,7 +335,13 @@ registerElementEvents({
335
335
  'volumechange',
336
336
  'waiting',
337
337
  ],
338
- 'helix-headless-video-playlist': ['helix-activate', 'helix-before-activate', 'helix-end'],
338
+ 'helix-headless-video-playlist': [
339
+ 'helix-activate',
340
+ 'helix-before-activate',
341
+ 'helix-before-reorder',
342
+ 'helix-end',
343
+ 'helix-reorder',
344
+ ],
339
345
  'helix-headless-video-playlist-item': ['helix-select'],
340
346
  'helix-headless-video-playlist-menu': ['helix-select'],
341
347
  'helix-headless-virtual-list': [
@@ -99,6 +99,8 @@ export type ObjectInspectorRenderer = (value: unknown, entry: ObjectInspectorEnt
99
99
  * @aliases JSON viewer, Object viewer, Inspector, Data explorer
100
100
  * @documentation https://fusion.dev/helix/components/object-inspector
101
101
  *
102
+ * @slot empty - Shown in the tree while it has no entries to show. Defaults to `empty-label`, or `no-matches-label` while a filter matched none.
103
+ *
102
104
  * @event {HelixActivateEvent} helix-activate - The active row moved, by a key or a click. The detail's `value` is its path.
103
105
  * @event {HelixBeforeActivateEvent} helix-before-activate - The active row is about to move, with its new path. Cancelable: cancel it to keep the row where it is.
104
106
  * @event {HelixBeforeCloseEvent} helix-before-close - A reader is about to close the active branch. Cancelable: cancel it to keep the branch open.
@@ -128,6 +130,7 @@ export type ObjectInspectorRenderer = (value: unknown, entry: ObjectInspectorEnt
128
130
  * @csspart copy-tooltip-right - The tooltip while it is right of the copy button. Set alongside `copy-tooltip`.
129
131
  * @csspart copy-tooltip-top - The tooltip while it is above the copy button. Set alongside `copy-tooltip`.
130
132
  * @csspart count - The number of entries a branch holds.
133
+ * @csspart empty - The row shown in the tree while it has no entries to show: no value, or a filter that matched none. It holds the `empty` slot.
131
134
  * @csspart frame - The tree and the copy button beside it.
132
135
  * @csspart header - The bar that holds the label, when there is one.
133
136
  * @csspart key - A property name or an array index.
@@ -146,6 +149,10 @@ export type ObjectInspectorRenderer = (value: unknown, entry: ObjectInspectorEnt
146
149
  * @csspart value - What `renderValue` drew for an entry, in place of its own drawing.
147
150
  *
148
151
  * @cssproperty --object-inspector-level - How deep a row is, from 0 at the root, for indenting it.
152
+ *
153
+ * @cssstate empty - While the tree has no entries to show: no value, or a filter that matched none.
154
+ * @cssstate filtered - While `filter` holds text.
155
+ * @cssstate matched - While `filter` holds text and matched at least one entry.
149
156
  */
150
157
  export declare class HelixHeadlessObjectInspector extends HelixElement {
151
158
  /**
@@ -183,6 +190,11 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
183
190
  * wall of text this exists to avoid.
184
191
  */
185
192
  defaultDepth: number;
193
+ /**
194
+ * What the tree says while there is no value to show, such as before a
195
+ * fetch answers. The `empty` slot replaces it.
196
+ */
197
+ emptyLabel: string;
186
198
  /**
187
199
  * The open branches, by path, in no set order.
188
200
  *
@@ -254,6 +266,11 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
254
266
  * reach a value.
255
267
  */
256
268
  noCopy: boolean;
269
+ /**
270
+ * What the tree says while a filter matched no entry. The `empty` slot
271
+ * replaces it.
272
+ */
273
+ noMatchesLabel: string;
257
274
  /**
258
275
  * Draws an entry's value your way, given the value and its entry. Set it as
259
276
  * a property.
@@ -358,6 +375,12 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
358
375
  private seed;
359
376
  /** Opens the branches around each path, where the value has them. */
360
377
  private openAround;
378
+ /**
379
+ * Whether the tree has no entries to show. Not while a value named by `for`
380
+ * is still to be read, as on a server and while hydrating what one sent,
381
+ * where it says nothing yet about the value.
382
+ */
383
+ private get blank();
361
384
  protected willUpdate(changed: PropertyValues): void;
362
385
  /**
363
386
  * A count in its plural message, `items-label`, `keys-label` or
@@ -32,6 +32,7 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
32
32
  this.copyLabel = 'Copy {name}';
33
33
  this.copyValueLabel = 'Copy value';
34
34
  this.defaultDepth = 1;
35
+ this.emptyLabel = 'Nothing to show';
35
36
  this.filter = '';
36
37
  this.filterable = false;
37
38
  this.filterLabel = 'Filter';
@@ -40,6 +41,7 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
40
41
  this.keysLabel = '{count, plural, one {# key} other {# keys}}';
41
42
  this.label = '';
42
43
  this.noCopy = false;
44
+ this.noMatchesLabel = 'No matches';
43
45
  this.resultsLabel = '{count, plural, one {# result} other {# results}}';
44
46
  this.treeLabel = 'Value';
45
47
  this.value = undefined;
@@ -213,6 +215,9 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
213
215
  openAround(paths) {
214
216
  this.opened = withAncestors(this.opened, this.branches, paths);
215
217
  }
218
+ get blank() {
219
+ return this.rows.length === 0 && (this.for === '' || this.loadedFor === this.for);
220
+ }
216
221
  willUpdate(changed) {
217
222
  super.willUpdate(changed);
218
223
  this.hadFocus = this.hasFocusedRow();
@@ -248,6 +253,9 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
248
253
  if (changed.has('filter') && this.hasUpdated) {
249
254
  this.tell();
250
255
  }
256
+ this.toggleState('empty', this.blank);
257
+ this.toggleState('filtered', this.found !== undefined);
258
+ this.toggleState('matched', (this.found?.size ?? 0) > 0);
251
259
  }
252
260
  counted(which, count) {
253
261
  const one = this[`${which}Label`];
@@ -526,7 +534,9 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
526
534
  @scroll=${() => this.virtualize.measure()}
527
535
  >
528
536
  ${this.windowed ? html `<div part="before" role="presentation" style="block-size: ${before}px"></div>` : nothing}
529
- ${repeat(shown, ([row]) => row.path, ([row, at]) => this.renderRow(row, at))}
537
+ ${repeat(shown, ([row]) => row.path, ([row, at]) => this.renderRow(row, at))}${this.blank
538
+ ? html `<div part="empty" role="row"><div role="gridcell"><slot name="empty">${this.found ? this.noMatchesLabel : this.emptyLabel}</slot></div></div>`
539
+ : nothing}
530
540
  ${this.windowed ? html `<div part="after" role="presentation" style="block-size: ${after}px"></div>` : nothing}
531
541
  </div>
532
542
  ${this.noCopy ? nothing : this.renderCopyButton(rows[this.cursor])}
@@ -548,6 +558,10 @@ __decorate([
548
558
  __decorate([
549
559
  property({ attribute: 'default-depth', type: Number })
550
560
  ], HelixHeadlessObjectInspector.prototype, "defaultDepth", void 0);
561
+ __decorate([
562
+ words,
563
+ property({ attribute: 'empty-label' })
564
+ ], HelixHeadlessObjectInspector.prototype, "emptyLabel", void 0);
551
565
  __decorate([
552
566
  property({ attribute: false })
553
567
  ], HelixHeadlessObjectInspector.prototype, "expanded", null);
@@ -584,6 +598,10 @@ __decorate([
584
598
  __decorate([
585
599
  property({ attribute: 'no-copy', type: Boolean })
586
600
  ], HelixHeadlessObjectInspector.prototype, "noCopy", void 0);
601
+ __decorate([
602
+ words,
603
+ property({ attribute: 'no-matches-label' })
604
+ ], HelixHeadlessObjectInspector.prototype, "noMatchesLabel", void 0);
587
605
  __decorate([
588
606
  property({ attribute: false })
589
607
  ], HelixHeadlessObjectInspector.prototype, "renderValue", void 0);
@@ -42,6 +42,13 @@ export interface TableGridOptions {
42
42
  * carry the selection selects the row there.
43
43
  */
44
44
  arrive?: (to: number, control: boolean) => void;
45
+ /**
46
+ * Right or Left on the disclosure cell of drawn row `row`, `forward` for
47
+ * the key that moves on along the line. Says that it opened or shut the
48
+ * row, names the drawn row to move to, or says it did neither, which moves
49
+ * along the row as the key does on any cell.
50
+ */
51
+ disclose?: (row: number, forward: boolean) => boolean | number;
45
52
  /** Whether the table is a grid now. */
46
53
  on: () => boolean;
47
54
  }
@@ -71,10 +71,24 @@ export class TableGridController {
71
71
  }
72
72
  return;
73
73
  }
74
+ const step = inlineStep(this.host, event.key);
75
+ if (step !== 0 &&
76
+ place.row >= 0 &&
77
+ !(event.shiftKey || event.ctrlKey || event.metaKey) &&
78
+ this.options.lead()[place.column] === 'expand') {
79
+ const done = this.options.disclose?.(place.row, step > 0) ?? false;
80
+ if (done !== false) {
81
+ event.preventDefault();
82
+ if (typeof done === 'number') {
83
+ this.moveTo({ column: place.column, row: done });
84
+ }
85
+ return;
86
+ }
87
+ }
74
88
  const to = gridMove(event.key, place, { last: this.options.rows().length - 1, top: this.top, width: this.widthOf(place.row) }, {
75
89
  control: event.ctrlKey || event.metaKey,
76
90
  page: event.key.startsWith('Page') ? this.pageFrom(place.cell) : 1,
77
- step: inlineStep(this.host, event.key),
91
+ step,
78
92
  });
79
93
  if (!to) {
80
94
  return;
@@ -111,6 +111,8 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
111
111
  * | Enter, Space on a sortable heading | Sorts by the column. |
112
112
  * | Space on a row, Enter or Space on its checkbox cell | Selects the row, in a `selectable` table. |
113
113
  * | Enter or Space on a disclosure cell | Shows or hides the rows under the row, in a table whose rows nest. |
114
+ * | Right on a disclosure cell | Shows the rows under a shut row, and otherwise moves one cell along the row. Left does, on a right-to-left page. |
115
+ * | Left on a disclosure cell | Hides the rows under an open row, and moves to the row this one sits under from any other. |
114
116
  * | Enter or Space on a grip cell | Picks the row up, or drops it, in a `reorderable` table. Up, Down, Home, and End move a row picked up, and Escape puts it back. |
115
117
  * | Down, Up, Page Down, Page Up under `selection-behavior="replace"` | Selects the row it moves to alone. Control or Command with the key moves without selecting. |
116
118
  * | Shift with Down or Up | Selects the rows it moves across, in a `selectable` table. |
@@ -184,6 +186,7 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
184
186
  *
185
187
  * @cssstate empty - While there are no rows and none are loading.
186
188
  * @cssstate error - While `error-text` is set.
189
+ * @cssstate loading - While `loading` is set, as the rows are on their way.
187
190
  */
188
191
  export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
189
192
  /**
@@ -534,6 +537,13 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
534
537
  private selectRange;
535
538
  /** One row's checkbox, ticked or not. It becomes the row a range runs from. */
536
539
  private chooseRow;
540
+ /**
541
+ * Right or Left on a disclosure cell, as on a tree's item and as Data
542
+ * Table's row headers do: on to open a shut row, back to shut an open one.
543
+ * Back on a row that is shut, or has no rows under it, moves to the row it
544
+ * sits under. Anything else moves along the row.
545
+ */
546
+ private disclose;
537
547
  /**
538
548
  * Enter, Space or F2 on a cell of a `grid` table, and whether it did
539
549
  * anything. Left alone, Enter and F2 move into the cell's controls.
@@ -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 { AriaForwardController, anchorAt, assignSlots, commaList, customElement, cycle, everyState, followLink, format, formatWords, HelixBeforeToggleEvent, HelixElement, HelixLoadMoreEvent, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, HelixToggleEvent, inRowOrder, MixinCollection, onAndroid, parse, ReorderController, rangeAdd, reflectElements, requestChange, scrollingAncestor, sort, splitValues, toggleEvery, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, anchorAt, assignSlots, commaList, customElement, cycle, everyState, followLink, format, formatWords, HelixBeforeToggleEvent, HelixElement, HelixLoadMoreEvent, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, HelixToggleEvent, inRowOrder, MixinCollection, onAndroid, parse, ReorderController, rangeAdd, reflectElements, requestChange, scrollingAncestor, sort, splitValues, toggleEvery, treeArrow, whenHydrated, words, } from '@fusion.dev/helix-core';
8
8
  import { nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { html } from 'lit/static-html.js';
@@ -94,6 +94,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
94
94
  }
95
95
  },
96
96
  columns: () => this.tableColumns.filter((column) => column.shown),
97
+ disclose: (at, forward) => this.disclose(at, forward),
97
98
  lead: () => this.lead,
98
99
  on: () => this.grid,
99
100
  part: (name) => this.partNamed(name),
@@ -471,6 +472,21 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
471
472
  }
472
473
  this.requestSelected(keys);
473
474
  }
475
+ disclose(at, forward) {
476
+ const rows = this.arrangedRows;
477
+ const row = rows[at];
478
+ const found = this.keyed.find((one) => one.row === row);
479
+ if (!(row && found)) {
480
+ return false;
481
+ }
482
+ const arrow = treeArrow({ branch: row.branch ?? false, open: this.open.has(found.key) }, forward ? 1 : -1);
483
+ if (arrow === 'open' || arrow === 'close') {
484
+ this.toggleRow(found.key);
485
+ return true;
486
+ }
487
+ const parent = arrow === 'parent' ? rows.indexOf(this.rowFor(row.parent ?? '')) : -1;
488
+ return parent >= 0 ? parent : false;
489
+ }
474
490
  activate(place, event) {
475
491
  const leads = this.lead;
476
492
  const lead = leads[place.column] === 'select';
@@ -697,6 +713,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
697
713
  super.updated(changed);
698
714
  this.toggleState('error', this.errorText !== '');
699
715
  this.toggleState('empty', !this.loading && !this.hasRows);
716
+ this.toggleState('loading', this.loading);
700
717
  this.assign();
701
718
  this.alignCells();
702
719
  this.markSorted();
@@ -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 { activeCue, adoptTrack, CueController, captionsAmong, captionTracks, copiesOf, customElement, drawIcon, FullscreenController, formatWords, HelixElement, HelixNextEvent, HelixPreviousEvent, IdleController, MediaSessionController, onUnlessFalse, RemotePlaybackController, reportRuntime, SKIP_SECONDS, sameCopies, showCaption, words, writtenList, writtenNumbers, } from '@fusion.dev/helix-core';
7
+ import { activeCue, adoptTrack, auditName, CueController, captionsAmong, captionTracks, copiesOf, customElement, drawIcon, FullscreenController, formatWords, HelixElement, HelixNextEvent, HelixPreviousEvent, IdleController, MediaSessionController, onUnlessFalse, RemotePlaybackController, reportRuntime, SKIP_SECONDS, sameCopies, showCaption, words, writtenList, writtenNumbers, } from '@fusion.dev/helix-core';
8
8
  import { airplay, arrowClockwise, arrowCounterClockwise, closedCaptioning, cornersIn, cornersOut, dotsThreeVertical, download, gauge, pause, pictureInPicture, play, skipBack, skipForward, speakerHigh, speakerX, } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -464,6 +464,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
464
464
  firstUpdated(changed) {
465
465
  super.firstUpdated?.(changed);
466
466
  this.finishHydration();
467
+ auditName(this, () => null, 'Give it a `label` that says what the video shows, such as "Product tour".', () => this.label !== '');
467
468
  const video = this.video;
468
469
  if (video) {
469
470
  if (this.defaultMuted || this.silent) {
@@ -38,6 +38,9 @@ import { type PropertyValues, type TemplateResult } from 'lit';
38
38
  * @csspart thumbnail - The image of `poster`, shown when the `media` slot is empty.
39
39
  *
40
40
  * @cssstate current - The entry that is playing.
41
+ * @cssstate dragging - The entry a pointer is dragging, in a `reorderable` playlist.
42
+ * @cssstate drop-after - The entry the dragged one would land after, which the drop indicator is drawn under.
43
+ * @cssstate drop-before - The entry the dragged one would land before, which the drop indicator is drawn over.
41
44
  */
42
45
  export declare class HelixHeadlessVideoPlaylistItem extends HelixElement {
43
46
  /**
@@ -61,6 +64,11 @@ export declare class HelixHeadlessVideoPlaylistItem extends HelixElement {
61
64
  * current one, and the arrow keys reach the rest.
62
65
  */
63
66
  focusable?: boolean;
67
+ /**
68
+ * @internal Set by a `reorderable` playlist: where this entry stands in a
69
+ * move, which it says through its states.
70
+ */
71
+ move: 'dragging' | 'drop-after' | 'drop-before' | undefined;
64
72
  /**
65
73
  * The image shown before this entry plays, also used as the row's thumbnail.
66
74
  *
@@ -12,6 +12,7 @@ let HelixHeadlessVideoPlaylistItem = class HelixHeadlessVideoPlaylistItem extend
12
12
  super(...arguments);
13
13
  this.current = false;
14
14
  this.disabled = false;
15
+ this.move = undefined;
15
16
  this.poster = '';
16
17
  this.src = '';
17
18
  this.value = '';
@@ -40,6 +41,9 @@ let HelixHeadlessVideoPlaylistItem = class HelixHeadlessVideoPlaylistItem extend
40
41
  willUpdate(changed) {
41
42
  super.willUpdate(changed);
42
43
  this.toggleState('current', this.current);
44
+ this.toggleState('dragging', this.move === 'dragging');
45
+ this.toggleState('drop-after', this.move === 'drop-after');
46
+ this.toggleState('drop-before', this.move === 'drop-before');
43
47
  }
44
48
  render() {
45
49
  return html `<div part="container" role="listitem">
@@ -70,6 +74,9 @@ __decorate([
70
74
  __decorate([
71
75
  property({ attribute: false })
72
76
  ], HelixHeadlessVideoPlaylistItem.prototype, "focusable", void 0);
77
+ __decorate([
78
+ property({ attribute: false })
79
+ ], HelixHeadlessVideoPlaylistItem.prototype, "move", void 0);
73
80
  __decorate([
74
81
  property()
75
82
  ], HelixHeadlessVideoPlaylistItem.prototype, "poster", void 0);
@@ -54,6 +54,21 @@ declare const HelixHeadlessVideoPlaylist_base: (new (...args: any[]) => import("
54
54
  * the next or the previous entry. The player's previous and next controls
55
55
  * move through the entries in the same order the playlist does.
56
56
  *
57
+ * **Moving entries.** `reorderable` lets a reader change the order of the
58
+ * queue:
59
+ *
60
+ * - Alt with Up or Down moves the focused entry a place, and Alt with Home or
61
+ * End moves it to either end, as the APG's rearrangeable listbox does.
62
+ * Focus stays on it.
63
+ * - A pointer drags an entry, with a drop indicator, and the page scrolls near
64
+ * an edge. A press that does not travel still plays the entry.
65
+ * - A drop sends `helix-before-reorder`, which you can refuse, and then
66
+ * `helix-reorder`, whose `keys` hold every entry's `value` in the new order.
67
+ * **You move the entries**: the playlist moves no element of yours, and the
68
+ * next entry is the one after the current entry in the order you leave.
69
+ * - Each move is said in the live region, in words you can change:
70
+ * `moved-label`, `dropped-label`, `canceled-label`, and `picked-up-label`.
71
+ *
57
72
  * **What a screen reader hears.** A list named by `label`, with a button for
58
73
  * each entry. When the playlist moves to another entry, by a press, by the
59
74
  * player's controls, or at the end of a clip, a live region says
@@ -71,7 +86,9 @@ declare const HelixHeadlessVideoPlaylist_base: (new (...args: any[]) => import("
71
86
  *
72
87
  * @event {HelixActivateEvent} helix-activate - The current entry changed. The playlist already holds the new entry when this is sent, and `detail.previous` is the entry it left. `currentEntry` is the entry itself, with its `src`, `poster` and name.
73
88
  * @event {HelixBeforeActivateEvent} helix-before-activate - The current entry is about to change, and `detail.cause` says why: `press`, `next`, `previous`, `ended`, or `removed`. Cancelable: cancel it to keep the current entry.
89
+ * @event {HelixBeforeReorderEvent} helix-before-reorder - An entry was moved to a new place, in a `reorderable` playlist, with every entry's value in the new order in `detail.keys`. Cancelable: cancel it to keep the entry where it is.
74
90
  * @event {HelixEndEvent} helix-end - The last entry finished playing, and the playlist stopped there. Not sent while `repeat` is set.
91
+ * @event {HelixReorderEvent} helix-reorder - Fired after `helix-before-reorder` when nobody canceled it. Move your entries to the order in `keys`. Cancelable: cancel it to say the move did not happen.
75
92
  *
76
93
  * @csspart empty - The box that holds the `empty` slot, drawn while there are no entries.
77
94
  * @csspart items - The list of entries.
@@ -86,6 +103,16 @@ export declare class HelixHeadlessVideoPlaylist extends HelixHeadlessVideoPlayli
86
103
  * `registerWords('de', { 'helix-video-playlist': { label: 'Wiedergabeliste' } })`.
87
104
  */
88
105
  static words: string;
106
+ /**
107
+ * What screen readers hear when a move is put back, in a `reorderable`
108
+ * playlist. `{name}` is the entry's words.
109
+ */
110
+ canceledLabel: string;
111
+ /**
112
+ * What screen readers hear when a moved entry is let go, in a `reorderable`
113
+ * playlist. `{name}` is the entry's words.
114
+ */
115
+ droppedLabel: string;
89
116
  /**
90
117
  * What the playlist says while it has no entries. The `empty` slot replaces it.
91
118
  */
@@ -94,12 +121,23 @@ export declare class HelixHeadlessVideoPlaylist extends HelixHeadlessVideoPlayli
94
121
  * The list's name, read by screen readers.
95
122
  */
96
123
  label: string;
124
+ /**
125
+ * What screen readers hear each time a key moves an entry, in a
126
+ * `reorderable` playlist. `{name}` is the entry's words, `{position}` its
127
+ * new place, counting from 1, and `{count}` the number of entries.
128
+ */
129
+ movedLabel: string;
97
130
  /**
98
131
  * Stays on an entry when its clip ends, where the playlist would otherwise
99
132
  * play the next one. The player's controls, a press, and `next()` still
100
133
  * move. `repeat="one"` still plays the entry again.
101
134
  */
102
135
  noAdvance: boolean;
136
+ /**
137
+ * What screen readers hear when a pointer picks up an entry, in a
138
+ * `reorderable` playlist. `{name}` is the entry's words.
139
+ */
140
+ pickedUpLabel: string;
103
141
  /**
104
142
  * What the live region says when the playlist moves to another entry.
105
143
  * `{title}` is the entry's name.
@@ -109,8 +147,30 @@ export declare class HelixHeadlessVideoPlaylist extends HelixHeadlessVideoPlayli
109
147
  * `aria-current` on a button the reader is not on.
110
148
  */
111
149
  playingLabel: string;
150
+ /**
151
+ * Lets a reader move the entries, with Alt and the arrow keys or by
152
+ * dragging one. The playlist moves no entry itself: you move your entries
153
+ * on `helix-reorder`.
154
+ */
155
+ reorderable: boolean;
112
156
  /** The live region, rendered from the first move. */
113
157
  private readonly status;
158
+ /** The entry an event passed through, where it may move. */
159
+ private entryOf;
160
+ /** The entry with that value. */
161
+ private entryFor;
162
+ /**
163
+ * Moving the entries, as a listbox moves its options: Alt with the arrows
164
+ * on the focused entry, or a pointer dragging a whole entry, where a press
165
+ * that does not travel still plays it.
166
+ */
167
+ private readonly reorder;
168
+ /**
169
+ * Tells each entry where it stands in a move, which it says through its
170
+ * states for the styled tier to draw: the entry dragged, and the one the
171
+ * drop indicator is before or after.
172
+ */
173
+ private markMoves;
114
174
  /**
115
175
  * What happens when an entry ends with no next one to go to. `one` plays the
116
176
  * same entry again, and `all` goes from the last entry back to the first.
@@ -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, drawStatus, formatWords, HelixElement, HelixEndEvent, inlineStep, MixinControllerActive, StatusController, words, } from '@fusion.dev/helix-core';
7
+ import { customElement, drawStatus, formatWords, HelixElement, HelixEndEvent, inlineStep, MixinControllerActive, ReorderController, StatusController, words, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { HelixHeadlessVideoPlaylistItem } from '../item/video-playlist-item.js';
@@ -12,11 +12,30 @@ import { playOrder, stepTo } from '../order.js';
12
12
  let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinControllerActive(HelixElement) {
13
13
  constructor() {
14
14
  super(...arguments);
15
+ this.canceledLabel = 'Canceled';
16
+ this.droppedLabel = 'Dropped';
15
17
  this.emptyLabel = 'No videos';
16
18
  this.label = 'Playlist';
19
+ this.movedLabel = '{name} moved to position {position} of {count}';
17
20
  this.noAdvance = false;
21
+ this.pickedUpLabel = 'Picked up {name}';
18
22
  this.playingLabel = 'Now playing {title}';
23
+ this.reorderable = false;
19
24
  this.status = new StatusController(this);
25
+ this.reorder = new ReorderController(this, {
26
+ axis: 'block',
27
+ box: (value) => this.entryFor(value)?.getBoundingClientRect(),
28
+ current: (event) => this.entryOf(event)?.value,
29
+ grip: (event) => {
30
+ const entry = this.entryOf(event);
31
+ return entry ? { element: entry, key: entry.value } : undefined;
32
+ },
33
+ holds: false,
34
+ keys: () => this.items.map((one) => one.value),
35
+ name: (value) => this.entryFor(value)?.text || value,
36
+ refocus: (value) => this.entryFor(value)?.focus(),
37
+ say: (words) => this.status.say(words),
38
+ });
20
39
  this.shuffle = false;
21
40
  this.chased = false;
22
41
  this.known = {
@@ -116,6 +135,21 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
116
135
  };
117
136
  }
118
137
  static { this.words = 'helix-video-playlist'; }
138
+ entryOf(event) {
139
+ const items = this.items;
140
+ return event
141
+ .composedPath()
142
+ .find((node) => items.includes(node) &&
143
+ !node.disabled);
144
+ }
145
+ entryFor(value) {
146
+ return this.items.find((one) => one.value === value);
147
+ }
148
+ markMoves() {
149
+ for (const entry of this.items) {
150
+ entry.move = this.reorderable ? this.reorder.moveOf(entry.value) : undefined;
151
+ }
152
+ }
119
153
  get items() {
120
154
  return [...this.querySelectorAll('*')].filter((one) => one instanceof HelixHeadlessVideoPlaylistItem && one.value !== '');
121
155
  }
@@ -228,6 +262,7 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
228
262
  }
229
263
  updated(changed) {
230
264
  super.updated?.(changed);
265
+ this.markMoves();
231
266
  const items = this.items;
232
267
  const current = this.resolveActive(items);
233
268
  for (const item of items) {
@@ -270,6 +305,14 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
270
305
  ${drawStatus(this.status)}`;
271
306
  }
272
307
  };
308
+ __decorate([
309
+ words,
310
+ property({ attribute: 'canceled-label' })
311
+ ], HelixHeadlessVideoPlaylist.prototype, "canceledLabel", void 0);
312
+ __decorate([
313
+ words,
314
+ property({ attribute: 'dropped-label' })
315
+ ], HelixHeadlessVideoPlaylist.prototype, "droppedLabel", void 0);
273
316
  __decorate([
274
317
  words,
275
318
  property({ attribute: 'empty-label' })
@@ -278,13 +321,24 @@ __decorate([
278
321
  words,
279
322
  property()
280
323
  ], HelixHeadlessVideoPlaylist.prototype, "label", void 0);
324
+ __decorate([
325
+ words,
326
+ property({ attribute: 'moved-label' })
327
+ ], HelixHeadlessVideoPlaylist.prototype, "movedLabel", void 0);
281
328
  __decorate([
282
329
  property({ attribute: 'no-advance', type: Boolean })
283
330
  ], HelixHeadlessVideoPlaylist.prototype, "noAdvance", void 0);
331
+ __decorate([
332
+ words,
333
+ property({ attribute: 'picked-up-label' })
334
+ ], HelixHeadlessVideoPlaylist.prototype, "pickedUpLabel", void 0);
284
335
  __decorate([
285
336
  words,
286
337
  property({ attribute: 'playing-label' })
287
338
  ], HelixHeadlessVideoPlaylist.prototype, "playingLabel", void 0);
339
+ __decorate([
340
+ property({ reflect: true, type: Boolean })
341
+ ], HelixHeadlessVideoPlaylist.prototype, "reorderable", void 0);
288
342
  __decorate([
289
343
  property()
290
344
  ], HelixHeadlessVideoPlaylist.prototype, "repeat", void 0);