@fusion.dev/helix-headless 0.183.0 → 0.185.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.
@@ -1,5 +1,5 @@
1
1
  import { __decorate } from "tslib";
2
- import { AriaForwardController, anchorElement, customElement, DrilldownController, deepActiveElement, deepContains, filterKeeps, focusTarget, formatWords, HelixElement, HelixFilterEvent, inlineStep, KeyboardNavigationController, linkDescription, MIRROR_RULE, MixinCollection, MixinControllerOpen, markMatches, OpenPhaseController, onUnlessFalse, PopoverController, scrollItemIntoView, sideOf, slottedWords, tellDisclosure, typeaheadBy, unlinkDescription, words, } from '@fusion.dev/helix-core';
2
+ import { AriaForwardController, anchorElement, auditEntry, customElement, DrilldownController, deepActiveElement, deepContains, focusTarget, HelixElement, inlineStep, KeyboardNavigationController, linkDescription, MIRROR_RULE, MixinCollection, MixinControllerOpen, OpenPhaseController, onUnlessFalse, PopoverController, scrollItemIntoView, sideOf, slottedWords, tellDisclosure, typeaheadBy, unlinkDescription, words, } from '@fusion.dev/helix-core';
3
3
  import { caretLeft } from '@fusion.dev/helix-icons';
4
4
  import { html, nothing } from 'lit';
5
5
  import { property } from 'lit/decorators.js';
@@ -98,18 +98,13 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
98
98
  surface: () => this.partNamed('surface'),
99
99
  });
100
100
  this.settledOnce = false;
101
- this.filterPage = null;
102
- this.handleFilterInput = (event) => {
103
- event.stopPropagation();
104
- this.filterBy(event.target.value);
105
- };
106
101
  this.followRemoved = (records) => {
107
102
  if (!this.open) {
108
103
  return;
109
104
  }
110
105
  const active = deepActiveElement();
111
106
  const lost = active === null || active === document.body || !active.isConnected;
112
- const record = records.find((one) => [...one.removedNodes].some((node) => isItem(node) && (this.filterable ? node === this.highlighted : lost)));
107
+ const record = records.find((one) => [...one.removedNodes].some((node) => isItem(node) && (this.filters ? node === this.filters.highlighted : lost)));
113
108
  if (!record) {
114
109
  return;
115
110
  }
@@ -125,7 +120,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
125
120
  const next = after(record.nextSibling, 'nextSibling') ??
126
121
  after(record.previousSibling, 'previousSibling') ??
127
122
  items.at(-1);
128
- if (this.filterable || next) {
123
+ if (this.filters || next) {
129
124
  this.reach(next);
130
125
  return;
131
126
  }
@@ -188,23 +183,13 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
188
183
  const items = this.items;
189
184
  const at = items.findIndex((item) => item.active);
190
185
  const along = inlineStep(this, event.key);
191
- const typing = this.filterable && event.composedPath()[0] === this.field;
192
- const step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;
186
+ if (this.filters?.keydown(event, items, at)) {
187
+ event.preventDefault();
188
+ return;
189
+ }
193
190
  if (event.key === 'Escape') {
194
191
  event.stopPropagation();
195
- if (!this.filterable || this.filter === '') {
196
- this.leave();
197
- }
198
- else {
199
- this.filterBy('');
200
- }
201
- }
202
- else if (typing && step !== 0) {
203
- const count = items.length;
204
- this.reach(items[at < 0 ? (step > 0 ? 0 : count - 1) : (at + step + count) % count]);
205
- }
206
- else if (typing && event.key === 'Enter') {
207
- items[at]?.renderRoot.querySelector('[part="item"]')?.click();
192
+ this.leave();
208
193
  }
209
194
  else if (along === 1 && items[at]?.hasSubmenu) {
210
195
  this.enter(items[at]);
@@ -244,14 +229,6 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
244
229
  this.longPress.down(event);
245
230
  }
246
231
  };
247
- this.handleMouseDown = (event) => {
248
- if (this.filterable &&
249
- this.open &&
250
- event.composedPath()[0] !== this.field &&
251
- !this.fromTrigger(event)) {
252
- event.preventDefault();
253
- }
254
- };
255
232
  this.handlePointerMove = (event) => {
256
233
  this.longPress.move(event);
257
234
  };
@@ -406,48 +383,27 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
406
383
  return rows.filter((row) => !row.disabled && this.kept(row));
407
384
  }
408
385
  get menuBox() {
409
- return this.filterable
386
+ return this.filters
410
387
  ? (this.renderRoot?.querySelector('#items') ?? null)
411
388
  : this.partNamed('surface');
412
389
  }
413
- get field() {
414
- return this.partNamed('search-field');
415
- }
416
- get filtering() {
417
- return this.filterable && this.filter.trim() !== '' && this.filter !== this.refused;
390
+ get filters() {
391
+ return this.filterable ? this.filterer : undefined;
418
392
  }
419
393
  kept(row) {
420
- return (!this.filtering ||
421
- this.holderOf(row) !== this.filterPage ||
422
- filterKeeps(slottedWords(row, ['']), this.filter.trim(), this.filterMatch));
394
+ return this.filters?.kept(row) ?? true;
423
395
  }
424
396
  requestFilter(value) {
425
- if (value === this.filter) {
426
- return;
427
- }
428
- this.filterPage = this.flies ? this.keyPage : this.drilldown.page;
429
- this.refused = this.dispatchEvent(new HelixFilterEvent({ value })) ? undefined : value;
430
- this.filter = value;
431
- }
432
- filterBy(value) {
433
- this.requestFilter(value);
434
- void this.updateComplete.then(() => this.reach(this.items[0]));
397
+ this.filterer?.request(value);
435
398
  }
436
399
  reach(row) {
437
- if (!this.filterable) {
400
+ const filters = this.filters;
401
+ if (!filters) {
438
402
  row?.focus();
439
403
  return;
440
404
  }
441
- if (this.highlighted && this.highlighted !== row) {
442
- this.highlighted.highlighted = false;
443
- }
444
- this.highlighted = row;
445
- const field = this.field;
446
- if (field && 'ariaActiveDescendantElement' in field) {
447
- field.ariaActiveDescendantElement = row ?? null;
448
- }
405
+ filters.highlight(row);
449
406
  if (row) {
450
- row.highlighted = true;
451
407
  this.revealRow(row);
452
408
  }
453
409
  }
@@ -460,7 +416,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
460
416
  }
461
417
  this.updateComplete.then(() => {
462
418
  const items = this.items;
463
- this.field?.focus();
419
+ this.filters?.field?.focus();
464
420
  this.reach(end === 'first' ? items[0] : items[items.length - 1]);
465
421
  });
466
422
  }
@@ -610,7 +566,6 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
610
566
  this.addEventListener('focusin', this.handleFocusIn);
611
567
  this.addEventListener('contextmenu', this.handleContextMenu);
612
568
  this.addEventListener('pointerdown', this.handlePointerDown);
613
- this.addEventListener('mousedown', this.handleMouseDown);
614
569
  this.addEventListener('pointermove', this.handlePointerMove);
615
570
  this.addEventListener('pointerup', this.handlePointerUp);
616
571
  this.addEventListener('pointercancel', this.handlePointerCancel);
@@ -635,10 +590,6 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
635
590
  this.removeEventListener('focusin', this.handleFocusIn);
636
591
  this.removeEventListener('contextmenu', this.handleContextMenu);
637
592
  this.removeEventListener('pointerdown', this.handlePointerDown);
638
- this.removeEventListener('mousedown', this.handleMouseDown);
639
- if (this.filterable) {
640
- markMatches(this, [], '');
641
- }
642
593
  this.removeEventListener('pointermove', this.handlePointerMove);
643
594
  this.removeEventListener('pointerup', this.handlePointerUp);
644
595
  this.removeEventListener('pointercancel', this.handlePointerCancel);
@@ -667,7 +618,8 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
667
618
  if (isSection(child)) {
668
619
  child.onPage =
669
620
  top &&
670
- (!this.filtering || [...child.children].some((row) => isItem(row) && this.kept(row)));
621
+ (!this.filters?.filtering ||
622
+ [...child.children].some((row) => isItem(row) && this.kept(row)));
671
623
  for (const row of child.children) {
672
624
  if (row.isMenuItem === true) {
673
625
  this.mark(row, top);
@@ -684,14 +636,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
684
636
  this.toggleState('drilled', !this.flies && !this.drilldown.atRoot);
685
637
  this.markColumns();
686
638
  this.nameSurface();
687
- if (this.filterable) {
688
- markMatches(this, this.rowsOf(this.filterPage).filter((row) => this.kept(row)), this.filtering ? this.filter.trim() : '');
689
- const items = this.items;
690
- if (changed.has('filter') &&
691
- this.open &&
692
- !items.includes(this.highlighted)) {
693
- this.reach(items[0]);
694
- }
639
+ this.filterer?.updated();
640
+ if (this.filterable && !this.filterer) {
641
+ auditEntry(this, 'filterable', 'menu/filter', 'it shows no filter field', () => this.filterable && !this.filterer);
695
642
  }
696
643
  }
697
644
  nameSurface() {
@@ -729,7 +676,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
729
676
  mark(item, onPage) {
730
677
  const page = this.drilldown.page;
731
678
  item.onPage = onPage && this.kept(item);
732
- item.virtual = this.filterable;
679
+ item.virtual = this.filters !== undefined;
733
680
  const holds = page !== null && item.contains(page);
734
681
  item.flyout = this.flies;
735
682
  item.expanded = this.flies && holds;
@@ -753,16 +700,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
753
700
  }
754
701
  render() {
755
702
  const point = this.context && this.point;
756
- const filtering = this.filtering;
757
- const count = filtering
758
- ? this.rowsOf(this.filterPage).filter((row) => this.kept(row)).length
759
- : 0;
760
- const none = filtering && count === 0;
761
- this.announced = none
762
- ? this.noResultsText
763
- : filtering
764
- ? formatWords(this, 'resultsLabel', { count })
765
- : '';
703
+ const filters = this.filters;
704
+ this.announced = '';
705
+ const none = filters?.status();
766
706
  const rows = html `${this.flies || this.drilldown.atRoot
767
707
  ? nothing
768
708
  : html `<div part="header">
@@ -781,9 +721,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
781
721
  <span part="title" id="title">${this.drilldown.label}</span>
782
722
  </div>`}
783
723
  <slot @slotchange=${(event) => this.countRows(event)}></slot>
784
- ${none
785
- ? html `<p part="empty"><slot name="no-results">${this.noResultsText}</slot></p>`
786
- : this.renderStatus(this.hasRows)}`;
724
+ ${none ?? this.renderStatus(this.hasRows)}`;
787
725
  const busy = this.loading ? 'true' : nothing;
788
726
  const label = this.label || nothing;
789
727
  return html `<slot name="trigger" @click=${this.handleTriggerClick}></slot>${point
@@ -796,27 +734,14 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
796
734
  <div
797
735
  part="surface"
798
736
  popover="manual"
799
- role=${this.filterable ? nothing : 'menu'}
800
- aria-busy=${this.filterable ? nothing : busy}
801
- aria-label=${this.filterable ? nothing : label}
737
+ role=${filters ? nothing : 'menu'}
738
+ aria-busy=${filters ? nothing : busy}
739
+ aria-label=${filters ? nothing : label}
802
740
  data-drilled=${this.flies || this.drilldown.atRoot ? nothing : ''}
803
- data-filtered=${filtering ? '' : nothing}
741
+ data-filtered=${filters?.filtering ? '' : nothing}
804
742
  ?hidden=${!this.open}
805
743
  >
806
- ${this.filterable
807
- ? html `<input
808
- part="search-field"
809
- type="text"
810
- autocomplete="off"
811
- spellcheck="false"
812
- .value=${this.filter}
813
- placeholder=${this.filterLabel}
814
- aria-label=${this.filterLabel}
815
- aria-autocomplete="list"
816
- aria-controls="items"
817
- @input=${this.handleFilterInput}
818
- /><div id="items" role="menu" aria-busy=${busy} aria-label=${label}>${rows}</div>`
819
- : rows}
744
+ ${filters ? filters.drawFilter(rows, busy) : rows}
820
745
  </div>${this.renderLive(this.hasRows)}`;
821
746
  }
822
747
  };
@@ -243,13 +243,11 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
243
243
  * **It is also the only track list a server can send.** The shim has no tree
244
244
  * to walk, so a render there knows of no columns and this attribute is the
245
245
  * whole answer: a table given one lays its grid out in the first paint, and a
246
- * table sized only by its columns is one track wide until it upgrades.
246
+ * table sized only by its columns takes, until it upgrades, a track for each
247
+ * column at the width a column with no `width` takes (`helix.css`).
247
248
  *
248
- * **Data Table has none of this, and should not.** Its columns are its schema
249
- * rather than an option, so there is always a column to write a `width` on
250
- * and a list here would be a second way to say what that says. This table's
251
- * cells are markup, and a table written as rows of cells with no column
252
- * declared in it has nowhere else to be sized from.
249
+ * Data Table takes the same list, for the same reason: a data table that
250
+ * serves its first page as rows is laid out by it before it upgrades.
253
251
  */
254
252
  columns: string;
255
253
  /**
@@ -1,38 +1,12 @@
1
1
  import { __decorate } from "tslib";
2
- 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, VIRTUAL_RULE, VirtualizeController, whenHydrated, words, } from '@fusion.dev/helix-core';
2
+ 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, toggleEvery, treeArrow, VIRTUAL_RULE, VirtualizeController, whenHydrated, words, } from '@fusion.dev/helix-core';
3
3
  import { nothing } from 'lit';
4
4
  import { property, state } from 'lit/decorators.js';
5
5
  import { html } from 'lit/static-html.js';
6
6
  import '../../checkbox/index.js';
7
7
  import { edgeOf } from '../column/table-column.js';
8
8
  import { TableGridController } from '../grid-controller.js';
9
- const tracksOf = (list, wanted) => {
10
- const words = splitValues(list, ' ');
11
- const valid = (track) => typeof CSS === 'undefined' || CSS.supports('grid-template-columns', track);
12
- const tracks = [];
13
- let names = '';
14
- for (const one of words) {
15
- if (tracks.length >= wanted) {
16
- break;
17
- }
18
- if (one.startsWith('[')) {
19
- names += `${one} `;
20
- continue;
21
- }
22
- const repeat = /^repeat\(\s*(\d+)\s*,(.*)\)$/is.exec(one);
23
- if (repeat) {
24
- const inner = tracksOf(repeat[2] ?? '', wanted);
25
- for (let time = 0; time < Number(repeat[1]) && tracks.length < wanted; time += 1) {
26
- tracks.push(...inner.map((track, index) => (time === 0 && index === 0 ? names + track : track)));
27
- }
28
- }
29
- else if (!/^repeat\(/i.test(one)) {
30
- tracks.push(valid(names + one) ? names + one : '');
31
- }
32
- names = '';
33
- }
34
- return tracks.slice(0, wanted);
35
- };
9
+ import { tracksOf } from '../tracks.js';
36
10
  const SHORT = 32;
37
11
  const FLOOR = '14ch';
38
12
  const SAMPLE = 200;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The first `wanted` tracks of a track list, one entry each, so every declared
3
+ * column can take the one in its place.
4
+ *
5
+ * Split on the spaces between tracks and never inside a function, so
6
+ * `minmax(0, 1fr)` stays one track. A `repeat()` with a count is that many
7
+ * tracks, written out; an automatic one names no count and yields none. Line
8
+ * names go with the track after them.
9
+ *
10
+ * Everything above the split is a fact about a grid track list rather than
11
+ * about a value list, which is why only the split itself is shared.
12
+ *
13
+ * **A track the grid would refuse yields nothing either**, where the platform
14
+ * can say. One invalid track makes the whole `grid-template-columns` invalid,
15
+ * which a grid ignores in silence and draws as one column. That is the failure
16
+ * that once had `columns` deprecated, a list of headings handed over as widths,
17
+ * and dealt out a track at a time it costs the one column its width and no
18
+ * more.
19
+ */
20
+ export declare const tracksOf: (list: string, wanted: number) => string[];
@@ -0,0 +1,28 @@
1
+ import { splitValues } from '@fusion.dev/helix-core';
2
+ export const tracksOf = (list, wanted) => {
3
+ const words = splitValues(list, ' ');
4
+ const valid = (track) => typeof CSS === 'undefined' || CSS.supports('grid-template-columns', track);
5
+ const tracks = [];
6
+ let names = '';
7
+ for (const one of words) {
8
+ if (tracks.length >= wanted) {
9
+ break;
10
+ }
11
+ if (one.startsWith('[')) {
12
+ names += `${one} `;
13
+ continue;
14
+ }
15
+ const repeat = /^repeat\(\s*(\d+)\s*,(.*)\)$/is.exec(one);
16
+ if (repeat) {
17
+ const inner = tracksOf(repeat[2] ?? '', wanted);
18
+ for (let time = 0; time < Number(repeat[1]) && tracks.length < wanted; time += 1) {
19
+ tracks.push(...inner.map((track, index) => (time === 0 && index === 0 ? names + track : track)));
20
+ }
21
+ }
22
+ else if (!/^repeat\(/i.test(one)) {
23
+ tracks.push(valid(names + one) ? names + one : '');
24
+ }
25
+ names = '';
26
+ }
27
+ return tracks.slice(0, wanted);
28
+ };
@@ -217,6 +217,11 @@ export declare class HelixHeadlessTimeInput extends HelixHeadlessTimeInput_base
217
217
  * on why a wheel cannot do either.
218
218
  */
219
219
  mode?: 'multiple' | 'range';
220
+ /**
221
+ * The words on the list's button that chooses the current time, drawn with
222
+ * `show-now`.
223
+ */
224
+ nowLabel: string;
220
225
  /**
221
226
  * Placeholder text shown before a time is entered.
222
227
  */
@@ -252,6 +257,12 @@ export declare class HelixHeadlessTimeInput extends HelixHeadlessTimeInput_base
252
257
  * See Time Picker's `seconds`.
253
258
  */
254
259
  seconds: boolean;
260
+ /**
261
+ * Draws a button under the times in the list that chooses the current time,
262
+ * moved to the nearest time on offer. See Time Picker's `show-now`. Its words
263
+ * are `now-label`.
264
+ */
265
+ showNow: boolean;
255
266
  /**
256
267
  * How far apart the offered times are, in minutes, or in seconds with
257
268
  * `seconds`.
@@ -21,12 +21,14 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
21
21
  this.locale = '';
22
22
  this.max = '23:59';
23
23
  this.min = '00:00';
24
+ this.nowLabel = 'Now';
24
25
  this.placeholder = 'HH:mm';
25
26
  this.rangeLengthMessage = 'A range is two times.';
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.removeLabel = 'Remove {name}';
29
30
  this.seconds = false;
31
+ this.showNow = false;
30
32
  this.step = 15;
31
33
  this.timeStyle = 'local';
32
34
  this.triggerLabel = 'Choose a time';
@@ -246,7 +248,9 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
246
248
  locale=${handedLocale(this) || nothing}
247
249
  max=${this.max}
248
250
  min=${this.min}
251
+ now-label=${this.nowLabel}
249
252
  ?seconds=${this.seconds}
253
+ ?show-now=${this.showNow}
250
254
  step=${this.step}
251
255
  .value=${this.picked}
252
256
  @helix-before-change=${this.handlePicked}
@@ -400,6 +404,10 @@ __decorate([
400
404
  __decorate([
401
405
  property({ reflect: true })
402
406
  ], HelixHeadlessTimeInput.prototype, "mode", void 0);
407
+ __decorate([
408
+ words,
409
+ property({ attribute: 'now-label' })
410
+ ], HelixHeadlessTimeInput.prototype, "nowLabel", void 0);
403
411
  __decorate([
404
412
  words,
405
413
  property()
@@ -423,6 +431,9 @@ __decorate([
423
431
  __decorate([
424
432
  property({ type: Boolean })
425
433
  ], HelixHeadlessTimeInput.prototype, "seconds", void 0);
434
+ __decorate([
435
+ property({ attribute: 'show-now', type: Boolean })
436
+ ], HelixHeadlessTimeInput.prototype, "showNow", void 0);
426
437
  __decorate([
427
438
  property({ type: Number })
428
439
  ], HelixHeadlessTimeInput.prototype, "step", void 0);