@fusion.dev/helix-headless 0.165.0 → 0.167.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 (57) hide show
  1. package/custom-elements.json +3235 -534
  2. package/dist/callout/root/callout-root.d.ts +8 -2
  3. package/dist/callout/root/callout-root.js +14 -1
  4. package/dist/canvas/arrange.d.ts +32 -0
  5. package/dist/canvas/arrange.js +144 -0
  6. package/dist/canvas/paste.d.ts +25 -0
  7. package/dist/canvas/paste.js +58 -0
  8. package/dist/canvas/root/canvas-root.d.ts +60 -3
  9. package/dist/canvas/root/canvas-root.js +166 -2
  10. package/dist/chat/composer/chat-composer.js +5 -1
  11. package/dist/code-block/root/code-block-root.d.ts +63 -0
  12. package/dist/code-block/root/code-block-root.js +117 -6
  13. package/dist/combobox/root/combobox-root.d.ts +6 -1
  14. package/dist/combobox/root/combobox-root.js +2 -2
  15. package/dist/date-input/root/date-input-root.d.ts +1 -1
  16. package/dist/element-events.generated.js +17 -2
  17. package/dist/file-input/root/file-input-root.d.ts +1 -1
  18. package/dist/file-input/root/file-input-root.js +1 -1
  19. package/dist/filter-input/menu/filter-menu.d.ts +1 -0
  20. package/dist/filter-input/menu/filter-menu.js +1 -1
  21. package/dist/format-date/root/format-date-root.d.ts +10 -3
  22. package/dist/format-date/root/format-date-root.js +6 -1
  23. package/dist/listbox/root/listbox-root.d.ts +12 -1
  24. package/dist/listbox/root/listbox-root.js +15 -6
  25. package/dist/menu/item/menu-item.d.ts +20 -0
  26. package/dist/menu/item/menu-item.js +53 -20
  27. package/dist/menu/root/menu-root.d.ts +84 -1
  28. package/dist/menu/root/menu-root.js +187 -44
  29. package/dist/object-inspector/root/object-inspector-root.d.ts +5 -0
  30. package/dist/object-inspector/root/object-inspector-root.js +5 -0
  31. package/dist/select/root/select-root.js +1 -1
  32. package/dist/steps/root/steps-root.d.ts +7 -0
  33. package/dist/steps/root/steps-root.js +7 -0
  34. package/dist/table/grid-controller.d.ts +10 -0
  35. package/dist/table/grid-controller.js +7 -0
  36. package/dist/table/root/table-root.d.ts +71 -1
  37. package/dist/table/root/table-root.js +122 -7
  38. package/dist/table/row/table-row.d.ts +17 -0
  39. package/dist/table/row/table-row.js +23 -0
  40. package/dist/time-input/root/time-input-root.d.ts +2 -1
  41. package/dist/time-input/root/time-input-root.js +1 -1
  42. package/dist/time-picker/root/time-picker-root.d.ts +48 -1
  43. package/dist/time-picker/root/time-picker-root.js +79 -21
  44. package/dist/video/root/video-root.d.ts +2 -0
  45. package/dist/video/root/video-root.js +4 -2
  46. package/dist/video-playlist/menu/video-playlist-menu.d.ts +1 -0
  47. package/dist/video-playlist/menu/video-playlist-menu.js +1 -1
  48. package/dist/video-playlist/root/video-playlist-root.js +15 -1
  49. package/dist/virtual-list/root/virtual-list-root.d.ts +70 -2
  50. package/dist/virtual-list/root/virtual-list-root.js +216 -32
  51. package/dist/virtual-list/sections.d.ts +27 -0
  52. package/dist/virtual-list/sections.js +43 -0
  53. package/lint.json +1 -1
  54. package/package.json +5 -5
  55. package/types/jsx.d.ts +716 -96
  56. package/types/vue.d.ts +148 -12
  57. package/types/words.d.ts +45 -0
@@ -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, treeArrow, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, anchorAt, assignSlots, auditName, 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, VirtualizeController, 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';
@@ -46,6 +46,7 @@ const lengthOf = (element) => textOf(element).length;
46
46
  const numberOf = (value) => Number(value.replaceAll(',', ''));
47
47
  const STATUS = new Set(['empty', 'error', 'loading']);
48
48
  const NAMED = new Set([...STATUS, 'heading']);
49
+ const ESTIMATE = 40;
49
50
  let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixElement) {
50
51
  constructor() {
51
52
  super(...arguments);
@@ -78,9 +79,11 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
78
79
  this.selectRowLabel = 'Select {row}';
79
80
  this.sortedLabel = '{direction, select, ascending {Sorted by {column}, ascending} descending {Sorted by {column}, descending} other {Sorted by {column}}}';
80
81
  this.unsortedLabel = 'Not sorted';
82
+ this.windowed = false;
81
83
  this.sortState = undefined;
82
84
  this.selectedState = undefined;
83
85
  this.expandedState = undefined;
86
+ this.memo = {};
84
87
  this.headed = false;
85
88
  this.aria = new AriaForwardController(this, {
86
89
  label: true,
@@ -95,9 +98,11 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
95
98
  },
96
99
  columns: () => this.tableColumns.filter((column) => column.shown),
97
100
  disclose: (at, forward) => this.disclose(at, forward),
101
+ drawn: () => (this.windowed ? new Set(this.drawnRows) : undefined),
98
102
  lead: () => this.lead,
99
103
  on: () => this.grid,
100
104
  part: (name) => this.partNamed(name),
105
+ reveal: (row) => this.revealRow(row),
101
106
  rows: () => (this.childrenKnown ? this.arrangedRows : []),
102
107
  rowWords: () => this.arrangedRows.map((row) => textOf(row.children[0])),
103
108
  selectAll: () => this.selectable &&
@@ -105,6 +110,15 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
105
110
  this.requestSelected(new Set([...this.chosen, ...this.enabledKeys])),
106
111
  selectRange: (to, from) => this.selectRange(to, from),
107
112
  });
113
+ this.virtualize = new VirtualizeController(this, {
114
+ container: () => (this.windowed ? this.partNamed('table') : null),
115
+ count: () => this.arrangedCount,
116
+ estimate: () => ESTIMATE,
117
+ measured: () => this.windowed,
118
+ rows: () => (this.windowed ? this.measuredRows() : []),
119
+ });
120
+ this.arrangedCount = 0;
121
+ this.drawnRows = [];
108
122
  this.reorder = new ReorderController(this, {
109
123
  axis: 'block',
110
124
  box: (key) => this.gripOf(this.rowFor(key))?.getBoundingClientRect(),
@@ -191,6 +205,11 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
191
205
  this.watcher = undefined;
192
206
  this.alignment = undefined;
193
207
  this.aligned = '';
208
+ this.nearEnd = () => {
209
+ if (this.windowed && this.loadMore && this.virtualize.endInSight(this.arrangedCount)) {
210
+ this.askForMore();
211
+ }
212
+ };
194
213
  this.endInView = false;
195
214
  }
196
215
  static { this.words = 'helix-table'; }
@@ -219,7 +238,23 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
219
238
  this.requestUpdate('sort', previous);
220
239
  }
221
240
  get nests() {
222
- return this.childrenKnown && this.keyed.some(({ row }) => (row.parent ?? '') !== '');
241
+ return this.remember('nests', () => this.childrenKnown && this.keyed.some(({ row }) => (row.parent ?? '') !== ''));
242
+ }
243
+ remember(key, compute) {
244
+ const memo = this.memo;
245
+ if (Object.keys(memo).length === 0) {
246
+ queueMicrotask(() => {
247
+ if (this.memo === memo) {
248
+ this.memo = {};
249
+ }
250
+ });
251
+ }
252
+ memo[key] ??= compute();
253
+ return memo[key];
254
+ }
255
+ requestUpdate(name, oldValue, options) {
256
+ this.memo = {};
257
+ super.requestUpdate(name, oldValue, options);
223
258
  }
224
259
  get open() {
225
260
  return new Set(commaList(this.expanded));
@@ -251,6 +286,44 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
251
286
  row.dispatchEvent(new HelixToggleEvent({ checked: next }));
252
287
  }
253
288
  }
289
+ get hasRows() {
290
+ return (this.windowed && this.arrangedCount > 0) || super.hasRows;
291
+ }
292
+ measuredRows() {
293
+ const rows = this.arrangedRows;
294
+ const at = new Map(rows.map((row, index) => [row, index]));
295
+ return this.drawnRows.flatMap((row) => {
296
+ const cell = [...row.children].find((child) => child.getAttribute('hidden') === null && child.getAttribute('slot') === null);
297
+ const index = at.get(row);
298
+ return cell instanceof HTMLElement && index !== undefined ? [{ element: cell, index }] : [];
299
+ });
300
+ }
301
+ keptRow(rows) {
302
+ const { first, last } = this.virtualize.window;
303
+ const active = this.getRootNode().activeElement;
304
+ const focused = this.grid
305
+ ? this.gridKeys.current().row
306
+ : rows.findIndex((row) => active !== null && row.contains(active));
307
+ return focused >= 0 && (focused < first || focused >= last) ? focused : undefined;
308
+ }
309
+ windowOf(rows) {
310
+ const { first, last } = this.virtualize.window;
311
+ const drawn = rows.slice(first, last);
312
+ const kept = this.keptRow(rows);
313
+ const row = kept === undefined ? undefined : rows[kept];
314
+ if (kept !== undefined && row) {
315
+ return kept < first ? [row, ...drawn] : [...drawn, row];
316
+ }
317
+ return drawn;
318
+ }
319
+ revealRow(row) {
320
+ if (!this.windowed) {
321
+ return;
322
+ }
323
+ const heading = this.gridKeys.cellAt({ column: 0, row: -1 });
324
+ this.virtualize.reveal(row, heading?.getBoundingClientRect().height ?? 0);
325
+ this.virtualize.measure();
326
+ }
254
327
  get lead() {
255
328
  return [
256
329
  ...(this.reorderable ? ['grip'] : []),
@@ -282,6 +355,9 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
282
355
  return this.requestSort(format(cycle(parse(this.sort), field)));
283
356
  }
284
357
  get arrangedRows() {
358
+ return this.remember('arranged', () => this.arrange());
359
+ }
360
+ arrange() {
285
361
  const rows = this.slotted;
286
362
  const spec = parse(this.sort);
287
363
  const declared = this.tableColumns;
@@ -381,10 +457,10 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
381
457
  });
382
458
  }
383
459
  get keyed() {
384
- return this.slotted.map((row, index) => ({
460
+ return this.remember('keyed', () => this.slotted.map((row, index) => ({
385
461
  key: row.value || String(index),
386
462
  row: row,
387
- }));
463
+ })));
388
464
  }
389
465
  get chosen() {
390
466
  return new Set(commaList(this.selected));
@@ -538,10 +614,26 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
538
614
  const tree = this.nests;
539
615
  const shapes = tree ? this.shape() : undefined;
540
616
  const open = this.open;
617
+ const drawn = this.windowed ? new Set(this.drawnRows) : undefined;
618
+ const places = drawn
619
+ ? new Map(this.arrangedRows.map((row, index) => [row, index + 2]))
620
+ : undefined;
541
621
  for (const { key, row } of this.keyed) {
542
622
  if (!row.isTableRow) {
543
623
  continue;
544
624
  }
625
+ if (drawn && !drawn.has(row)) {
626
+ const shape = shapes?.get(key);
627
+ whenHydrated(row, () => {
628
+ row.selectable = this.selectable;
629
+ row.gripLabel = this.reorderable ? row.gripLabel || ' ' : '';
630
+ row.level = shape?.level ?? 0;
631
+ row.branch = shape?.branch ?? false;
632
+ row.open = open.has(key);
633
+ });
634
+ continue;
635
+ }
636
+ const place = places?.get(row) ?? 0;
545
637
  const name = textOf(row.children[0]) || key;
546
638
  const label = formatWords(this, 'selectRowLabel', { row: name });
547
639
  const grip = this.reorderable ? formatWords(this, 'moveRowLabel', { row: name }) : '';
@@ -560,6 +652,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
560
652
  row.open = open.has(key);
561
653
  row.expandLabel = expand;
562
654
  row.expandWith = tree ? () => this.toggleRow(key) : undefined;
655
+ row.rowIndex = place;
563
656
  });
564
657
  }
565
658
  }
@@ -676,7 +769,11 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
676
769
  const columns = new Set(this.tableColumns);
677
770
  const headings = new Set(this.headings);
678
771
  const status = [...(this.children ?? [])].filter((child) => STATUS.has(child.getAttribute('slot') ?? ''));
679
- assignSlots(this.renderRoot, [...columns, ...headings, ...status, ...this.arrangedRows], (child) => columns.has(child)
772
+ const arranged = this.arrangedRows;
773
+ this.arrangedCount = arranged.length;
774
+ const rows = this.windowed ? this.windowOf(arranged) : arranged;
775
+ this.drawnRows = this.windowed ? rows : [];
776
+ assignSlots(this.renderRoot, [...columns, ...headings, ...status, ...rows], (child) => columns.has(child)
680
777
  ? 'columns'
681
778
  : headings.has(child)
682
779
  ? 'heading'
@@ -719,14 +816,22 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
719
816
  this.markSorted();
720
817
  this.markSelected();
721
818
  this.nameByHeading();
819
+ auditName(this, () => null, 'Give it a `label`, or put a heading in its `heading` slot, that says what the rows are.', () => this.label !== '' || this.headings.length > 0);
722
820
  if (this.childrenKnown) {
723
821
  this.gridKeys.mark();
724
822
  this.gridKeys.place();
725
823
  }
824
+ if (this.windowed) {
825
+ this.virtualize.measure();
826
+ this.virtualize.settle();
827
+ }
726
828
  this.watchEnd();
727
829
  }
728
830
  watchEnd() {
729
- const end = this.loadMore ? this.renderRoot.querySelector('[data-end]') : null;
831
+ if (this.windowed) {
832
+ this.nearEnd();
833
+ }
834
+ const end = this.loadMore && !this.windowed ? this.renderRoot.querySelector('[data-end]') : null;
730
835
  if (end === this.watchedEnd) {
731
836
  if (this.endInView) {
732
837
  this.askForMore();
@@ -810,6 +915,9 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
810
915
  render() {
811
916
  const template = this.template;
812
917
  const grid = this.grid;
918
+ const windowed = this.windowed && this.childrenKnown;
919
+ const count = windowed ? this.arrangedRows.length : 0;
920
+ const room = windowed ? this.virtualize.spacers(this.keptRow(this.arrangedRows)) : undefined;
813
921
  const start = grid ? html `<span part="grid-start" tabindex="-1"></span>` : nothing;
814
922
  const end = grid ? html `<span part="grid-end" tabindex="-1"></span>` : nothing;
815
923
  return html `<slot name="heading"></slot>${start}<div
@@ -818,15 +926,19 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
818
926
  aria-multiselectable=${grid && this.selectable ? (this.single ? 'false' : 'true') : nothing}
819
927
  aria-busy=${this.loading ? 'true' : nothing}
820
928
  aria-label=${this.label || this.fallbackLabel}
929
+ aria-rowcount=${windowed ? count + 1 : nothing}
821
930
  style=${template ? `--table-template: ${template}` : ''}
931
+ @scroll=${this.nearEnd}
822
932
  >
823
- <div part="head" role="row">${this.renderGripHead()}${this.renderSelectAll()}${this.renderExpandHead()}<slot name="columns"></slot></div>
933
+ <div part="head" role="row" aria-rowindex=${windowed ? 1 : nothing}>${this.renderGripHead()}${this.renderSelectAll()}${this.renderExpandHead()}<slot name="columns"></slot></div>
934
+ ${room ? html `<div part="before" role="presentation" style="block-size: ${room.before}px"></div>` : nothing}
824
935
  <slot
825
936
  @slotchange=${(event) => {
826
937
  this.countRows(event);
827
938
  this.handleSlotChange();
828
939
  }}
829
940
  ></slot>
941
+ ${room ? html `<div part="after" role="presentation" style="block-size: ${room.after}px"></div>` : nothing}
830
942
  ${this.errorText === ''
831
943
  ? this.renderStatus(this.hasRows, grid ? 'gridcell' : 'cell')
832
944
  : html `<p part="error" role="row"><span role=${grid ? 'gridcell' : 'cell'}><slot name="error">${this.errorText}</slot></span></p>`}
@@ -948,6 +1060,9 @@ __decorate([
948
1060
  words,
949
1061
  property({ attribute: 'unsorted-label' })
950
1062
  ], HelixHeadlessTable.prototype, "unsortedLabel", void 0);
1063
+ __decorate([
1064
+ property({ reflect: true, type: Boolean })
1065
+ ], HelixHeadlessTable.prototype, "windowed", void 0);
951
1066
  __decorate([
952
1067
  state()
953
1068
  ], HelixHeadlessTable.prototype, "sortState", void 0);
@@ -157,6 +157,12 @@ export declare class HelixHeadlessTableRow extends HelixElement {
157
157
  * table. Not a consumer's to set.
158
158
  */
159
159
  level: number;
160
+ /**
161
+ * @internal The row's place among the table's rows, the headings being 1,
162
+ * in a `windowed` table, which draws only some of them, and 0 otherwise.
163
+ * Written by the table. Not a consumer's to set.
164
+ */
165
+ rowIndex: number;
160
166
  /** @internal Whether rows sit under this one. Written by the table. */
161
167
  branch: boolean;
162
168
  /** @internal Whether the rows under this one are shown. Written by the table. */
@@ -177,6 +183,17 @@ export declare class HelixHeadlessTableRow extends HelixElement {
177
183
  */
178
184
  alignCells(rules: CSSStyleSheet): void;
179
185
  protected willUpdate(changed: PropertyValues): void;
186
+ /**
187
+ * Names the row by its header cell, the cell with `header` among cells of
188
+ * values, or by its first cell where it has none, as React Aria's rows are
189
+ * named.
190
+ *
191
+ * **Named from its contents, a row read out its checkbox's name too**, "Select
192
+ * 412 412 main Passed" in a `selectable` table, since the checkbox is a cell of
193
+ * the row's own. Not on a server, which has no cells to point at, and not
194
+ * where a page named the row itself.
195
+ */
196
+ private nameByHeader;
180
197
  connectedCallback(): void;
181
198
  disconnectedCallback(): void;
182
199
  /**
@@ -31,6 +31,7 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
31
31
  this.gridCell = false;
32
32
  this.selectStop = false;
33
33
  this.level = 0;
34
+ this.rowIndex = 0;
34
35
  this.branch = false;
35
36
  this.open = false;
36
37
  this.expandLabel = '';
@@ -53,6 +54,7 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
53
54
  followLink(this.ownerDocument, { href: this.href, rel: this.rel, target: this.target }, event);
54
55
  };
55
56
  this.handleSlotChange = () => {
57
+ this.requestUpdate();
56
58
  for (const cell of this.children) {
57
59
  if ('isTableCell' in cell) {
58
60
  ;
@@ -75,8 +77,10 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
75
77
  willUpdate(changed) {
76
78
  super.willUpdate(changed);
77
79
  hostAria(this.internals, { role: 'row' });
80
+ this.nameByHeader();
78
81
  this.internals.ariaSelected = this.gridCell && this.selectable ? String(this.selected) : null;
79
82
  this.internals.ariaDisabled = this.disabled ? 'true' : null;
83
+ this.internals.ariaRowIndex = this.rowIndex > 0 ? String(this.rowIndex) : null;
80
84
  const tree = this.gridCell && this.level > 0;
81
85
  this.internals.ariaLevel = tree ? String(this.level) : null;
82
86
  this.internals.ariaExpanded = tree && this.branch ? String(this.open) : null;
@@ -87,6 +91,22 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
87
91
  this.toggleState('drop-after', this.move === 'drop-after');
88
92
  this.toggleState('drop-before', this.move === 'drop-before');
89
93
  }
94
+ nameByHeader() {
95
+ const cells = this.childrenKnown
96
+ ? this.knownChildren.filter((child) => 'isTableCell' in child && !child.hasAttribute('hidden'))
97
+ : [];
98
+ const values = cells.some((cell) => !cell.hasAttribute('header'));
99
+ const header = (values ? cells.find((cell) => cell.hasAttribute('header')) : undefined) ?? cells[0];
100
+ const own = this.hasAttribute('aria-label') || this.hasAttribute('aria-labelledby');
101
+ if ('ariaLabelledByElements' in this.internals) {
102
+ hostAria(this.internals, { labelledBy: header && !own ? [header] : [] });
103
+ }
104
+ else {
105
+ hostAria(this.internals, {
106
+ label: header && !own ? (header.textContent ?? '').replace(/\s+/g, ' ').trim() : '',
107
+ });
108
+ }
109
+ }
90
110
  connectedCallback() {
91
111
  super.connectedCallback();
92
112
  this.addEventListener('click', this.handleClick);
@@ -210,6 +230,9 @@ __decorate([
210
230
  __decorate([
211
231
  state()
212
232
  ], HelixHeadlessTableRow.prototype, "level", void 0);
233
+ __decorate([
234
+ state()
235
+ ], HelixHeadlessTableRow.prototype, "rowIndex", void 0);
213
236
  __decorate([
214
237
  state()
215
238
  ], HelixHeadlessTableRow.prototype, "branch", void 0);
@@ -56,7 +56,7 @@ declare const HelixHeadlessTimeInput_base: (new (...args: any[]) => import("@fus
56
56
  * | Home, End on a segment | Goes to the segment's first or last value. |
57
57
  * | Digits on a segment | Fill it, and move to the next once it can take no more. |
58
58
  * | A letter on the half of the day | Picks the half whose name starts with it, such as A for AM. |
59
- * | Backspace, Delete on a segment | Empty the segment. |
59
+ * | Backspace, Delete on a segment | Take off its last digit, or move to the segment before where it is empty. |
60
60
  * | Left, Right on a segment | Move to the previous or the next segment, along the line of text. |
61
61
  *
62
62
  * In the list, the keys are `TimePicker`'s.
@@ -123,6 +123,7 @@ declare const HelixHeadlessTimeInput_base: (new (...args: any[]) => import("@fus
123
123
  * @csspart picker-list-loading - The text shown while the options load.
124
124
  * @csspart picker-list-search - The region that holds the filter field.
125
125
  * @csspart picker-list-search-field - The filter field.
126
+ * @csspart picker-now - The picker's button that chooses the current time, which Time Input does not draw.
126
127
  * @csspart picker-option - One time in the picker. Carries `data-between` inside a range.
127
128
  * @csspart picker-option-between - A time inside a chosen range, between its two ends. Set together with `picker-option`.
128
129
  * @csspart picker-option-description - The caption under the label, when there is one.
@@ -233,7 +233,7 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
233
233
  list-filter-icon: picker-list-filter-icon, list-filter-mark: picker-list-filter-mark,
234
234
  list-hint: picker-list-hint, list-items: picker-list-items, list-label: picker-list-label,
235
235
  list-live: picker-list-live, list-loading: picker-list-loading, list-search: picker-list-search,
236
- list-search-field: picker-list-search-field, option: picker-option,
236
+ list-search-field: picker-list-search-field, now: picker-now, option: picker-option,
237
237
  option-between: picker-option-between, option-item-active: picker-option-item-active,
238
238
  option-item-selected: picker-option-item-selected,
239
239
  option-description: picker-option-description, option-item: picker-option-item,
@@ -14,7 +14,9 @@ declare const HelixHeadlessTimePicker_base: (new (...args: any[]) => import("@fu
14
14
  * between the wheels, and Tab does too.
15
15
  *
16
16
  * `hour-cycle` writes every time on a twelve or a twenty-four hour clock
17
- * whatever the locale, and `seconds` holds times to the second.
17
+ * whatever the locale, and `seconds` holds times to the second. `show-now`
18
+ * draws a button that chooses the current time, on the step and inside the
19
+ * bounds.
18
20
  *
19
21
  * The step keeps the list usable. At the default of 15 minutes a day is 96
20
22
  * rows, which people can scroll or type into. At 1 minute it is 1,440. Use the
@@ -103,6 +105,7 @@ declare const HelixHeadlessTimePicker_base: (new (...args: any[]) => import("@fu
103
105
  * @csspart list-loading - The text shown while the options load.
104
106
  * @csspart list-search - The region that holds the filter field.
105
107
  * @csspart list-search-field - The filter field.
108
+ * @csspart now - The button that chooses the current time, drawn with `show-now`.
106
109
  * @csspart option - One time in the list. It carries `data-between` inside a range, and inside the run to the time under the pointer while a range waits for its second end.
107
110
  * @csspart option-between - A time inside a chosen range, excluding its ends, beside `option`.
108
111
  * @csspart option-description - The caption under the time's label, when there is one.
@@ -242,6 +245,11 @@ export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_bas
242
245
  * whenever a mode is set.
243
246
  */
244
247
  mode?: 'multiple' | 'range';
248
+ /**
249
+ * The words on the button that chooses the current time, drawn with
250
+ * `show-now`.
251
+ */
252
+ nowLabel: string;
245
253
  /**
246
254
  * The validation message for a time after `max`. `{max}` is replaced with
247
255
  * `max`, written in `locale`.
@@ -322,6 +330,35 @@ export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_bas
322
330
  * sequence.
323
331
  */
324
332
  private readonly handleChange;
333
+ /**
334
+ * Requests the times a pick announced, kept to what the mode allows, and
335
+ * returns whether the value changed.
336
+ */
337
+ private choose;
338
+ /**
339
+ * Chooses the current time, as a press on the `show-now` button does, and
340
+ * returns whether the value changed.
341
+ *
342
+ * The time is read off the reader's clock and moved to the nearest time on
343
+ * offer that is not barred, so it falls on the `step` and inside `min` and
344
+ * `max`. It is requested as a pick is, so `helix-before-change` can cancel
345
+ * it. With a `mode` the time joins the times held, and a range that holds
346
+ * two starts over from it, as a third pick does.
347
+ */
348
+ selectNow(): boolean;
349
+ /**
350
+ * Puts the list's highlight on the offered time nearest one held between two
351
+ * steps, so the arrows move from there.
352
+ *
353
+ * No row holds `14:07` at a step of 15, so the list had nothing to move
354
+ * from: Down went to the top of the list, and in a list that chooses as it
355
+ * moves, the value became 00:00. Down now goes to 14:15.
356
+ */
357
+ private highlightNearest;
358
+ private readonly handleListFocus;
359
+ private readonly handleNow;
360
+ /** The button that chooses the current time, drawn with `show-now`. */
361
+ private renderNow;
325
362
  /**
326
363
  * The time under the pointer while a range holds one end, which the list
327
364
  * bands the range to before it is chosen, as Date Picker does.
@@ -409,6 +446,16 @@ export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_bas
409
446
  * the wheels.
410
447
  */
411
448
  seconds: boolean;
449
+ /**
450
+ * Draws a button under the times that chooses the current time, as
451
+ * `selectNow()` does.
452
+ *
453
+ * For a form about something happening now, such as a shift starting or a
454
+ * dose taken, where the reader would otherwise scroll to the time on the
455
+ * clock. The button is a tab stop after the times, and its words are
456
+ * `now-label`.
457
+ */
458
+ showNow: boolean;
412
459
  /**
413
460
  * How far apart the offered times are, in minutes, or in seconds with
414
461
  * `seconds`.
@@ -23,6 +23,7 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
23
23
  this.manual = false;
24
24
  this.max = '23:59';
25
25
  this.min = '00:00';
26
+ this.nowLabel = 'Now';
26
27
  this.rangeOverflowMessage = 'Choose a time at or before {max}.';
27
28
  this.rangeUnderflowMessage = 'Choose a time at or after {min}.';
28
29
  this.handleListInput = (event) => {
@@ -33,25 +34,13 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
33
34
  if (this.isDisabled) {
34
35
  return;
35
36
  }
36
- const announced = event.detail.value;
37
- if (announced.split(',').some((one) => this.barred(one.trim()))) {
38
- return;
39
- }
40
- if (this.mode !== 'range') {
41
- this.requestValue(announced);
42
- return;
43
- }
44
- const next = announced
45
- .split(',')
46
- .map((one) => one.trim())
47
- .filter(Boolean);
48
- if (next.length <= 2) {
49
- this.requestValue(next.sort().join(','));
50
- return;
51
- }
52
- const before = new Set(this.held);
53
- const added = next.find((one) => !before.has(one));
54
- this.requestValue(added ?? '');
37
+ this.choose(event.detail.value);
38
+ };
39
+ this.handleListFocus = () => {
40
+ this.highlightNearest();
41
+ };
42
+ this.handleNow = () => {
43
+ this.selectNow();
55
44
  };
56
45
  this.previewed = '';
57
46
  this.handleListHover = (event) => {
@@ -93,6 +82,7 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
93
82
  this.readOnly = false;
94
83
  this.required = false;
95
84
  this.seconds = false;
85
+ this.showNow = false;
96
86
  this.step = 15;
97
87
  this.stepMismatchMessage = 'Choose one of the times on offer.';
98
88
  this.unavailableMessage = 'Choose a time that is available.';
@@ -155,6 +145,65 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
155
145
  get held() {
156
146
  return commaList(this.value);
157
147
  }
148
+ choose(announced) {
149
+ if (announced.split(',').some((one) => this.barred(one.trim()))) {
150
+ return false;
151
+ }
152
+ if (this.mode !== 'range') {
153
+ return this.requestValue(announced);
154
+ }
155
+ const next = announced
156
+ .split(',')
157
+ .map((one) => one.trim())
158
+ .filter(Boolean);
159
+ if (next.length <= 2) {
160
+ return this.requestValue(next.sort().join(','));
161
+ }
162
+ const before = new Set(this.held);
163
+ const added = next.find((one) => !before.has(one));
164
+ return this.requestValue(added ?? '');
165
+ }
166
+ selectNow() {
167
+ if (this.isDisabled) {
168
+ return false;
169
+ }
170
+ const clock = new Date();
171
+ const at = clock.getHours() * 3600 + clock.getMinutes() * 60 + clock.getSeconds();
172
+ const open = this.times.filter((time) => !this.barredAt(time));
173
+ if (open.length === 0) {
174
+ return false;
175
+ }
176
+ const now = this.valueFor(nearestTime(open, at));
177
+ if (this.mode === undefined) {
178
+ return this.requestValue(now);
179
+ }
180
+ if (this.held.includes(now)) {
181
+ return false;
182
+ }
183
+ return this.choose([...this.held, now].join(','));
184
+ }
185
+ highlightNearest() {
186
+ const at = this.mode === undefined ? valueAsSeconds(this.value) : null;
187
+ if (at === null || this.times.includes(at)) {
188
+ return;
189
+ }
190
+ const open = this.times.filter((time) => !this.barredAt(time));
191
+ if (open.length === 0) {
192
+ return;
193
+ }
194
+ this.partNamed('list')?.highlightValue?.(this.valueFor(nearestTime(open, at)));
195
+ }
196
+ renderNow() {
197
+ if (!this.showNow) {
198
+ return nothing;
199
+ }
200
+ return html `<button
201
+ part="now"
202
+ type="button"
203
+ ?disabled=${this.isDisabled || this.readOnly}
204
+ @click=${this.handleNow}
205
+ >${this.nowLabel}</button>`;
206
+ }
158
207
  between(at) {
159
208
  if (this.mode !== 'range') {
160
209
  return false;
@@ -191,6 +240,7 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
191
240
  return;
192
241
  }
193
242
  if (this.variant !== 'wheels') {
243
+ this.highlightNearest();
194
244
  this.partNamed('list')?.reveal?.();
195
245
  return;
196
246
  }
@@ -421,6 +471,7 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
421
471
  ?manual=${this.manual}
422
472
  ?multiple=${this.mode !== undefined}
423
473
  .value=${this.value}
474
+ @focusin=${this.handleListFocus}
424
475
  @helix-before-change=${this.handleChange}
425
476
  @input=${this.handleListInput}
426
477
  @pointerleave=${this.handleListLeave}
@@ -446,9 +497,9 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
446
497
  }
447
498
  render() {
448
499
  if (this.variant === 'wheels' && this.mode === undefined) {
449
- return html `${this.renderSlottedLabel()}${this.renderSpinner()}${this.renderHint()}${this.renderPickerFeedback()}`;
500
+ return html `${this.renderSlottedLabel()}${this.renderSpinner()}${this.renderNow()}${this.renderHint()}${this.renderPickerFeedback()}`;
450
501
  }
451
- return html `${this.renderSlottedLabel()}${this.renderList()}${this.renderHint()}${this.renderPickerFeedback()}`;
502
+ return html `${this.renderSlottedLabel()}${this.renderList()}${this.renderNow()}${this.renderHint()}${this.renderPickerFeedback()}`;
452
503
  }
453
504
  get valueAsDate() {
454
505
  const at = valueAsSeconds(this.value);
@@ -512,6 +563,10 @@ __decorate([
512
563
  __decorate([
513
564
  property({ reflect: true })
514
565
  ], HelixHeadlessTimePicker.prototype, "mode", void 0);
566
+ __decorate([
567
+ words,
568
+ property({ attribute: 'now-label' })
569
+ ], HelixHeadlessTimePicker.prototype, "nowLabel", void 0);
515
570
  __decorate([
516
571
  words,
517
572
  property({ attribute: 'range-overflow-message' })
@@ -535,6 +590,9 @@ __decorate([
535
590
  __decorate([
536
591
  property({ type: Boolean })
537
592
  ], HelixHeadlessTimePicker.prototype, "seconds", void 0);
593
+ __decorate([
594
+ property({ attribute: 'show-now', type: Boolean })
595
+ ], HelixHeadlessTimePicker.prototype, "showNow", void 0);
538
596
  __decorate([
539
597
  property({ type: Number })
540
598
  ], HelixHeadlessTimePicker.prototype, "step", void 0);
@@ -115,6 +115,7 @@ import '../../menu/index.js';
115
115
  * @csspart captions-menu-header - The bar above a submenu, holding the way back and its name.
116
116
  * @csspart captions-menu-live - The inner menu's `live` part, a live region that repeats the empty and loading text for screen readers.
117
117
  * @csspart captions-menu-loading - The text shown while the items are loading.
118
+ * @csspart captions-menu-search-field - The inner menu's filter field, drawn with `filterable`.
118
119
  * @csspart captions-menu-surface - The floating panel the menu is drawn on.
119
120
  * @csspart captions-menu-title - The name of the submenu that is showing.
120
121
  * @csspart controls - The control bar along the bottom.
@@ -148,6 +149,7 @@ import '../../menu/index.js';
148
149
  * @csspart overflow-header - The bar above a submenu, holding the way back and its name.
149
150
  * @csspart overflow-live - The inner menu's `live` part, a live region that repeats the empty and loading text for screen readers.
150
151
  * @csspart overflow-loading - The text shown while the items are loading.
152
+ * @csspart overflow-search-field - The inner menu's filter field, drawn with `filterable`.
151
153
  * @csspart overflow-surface - The floating panel the menu is drawn on.
152
154
  * @csspart overflow-title - The name of the submenu that is showing.
153
155
  * @csspart pip - The picture-in-picture button, when `extra-controls` includes it and the browser supports it.