@fusion.dev/helix-headless 0.165.0 → 0.166.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.
@@ -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, anchorElement, customElement, DrilldownController, deepActiveElement, deepContains, focusTarget, HelixElement, inlineStep, KeyboardNavigationController, linkDescription, MixinCollection, MixinControllerOpen, onUnlessFalse, PopoverController, scrollItemIntoView, sideOf, slottedWords, tellDisclosure, typeaheadBy, unlinkDescription, words, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, anchorElement, customElement, DrilldownController, deepActiveElement, deepContains, filterKeeps, focusTarget, HelixElement, HelixFilterEvent, inlineStep, KeyboardNavigationController, linkDescription, MixinCollection, MixinControllerOpen, markMatches, onUnlessFalse, PopoverController, scrollItemIntoView, sideOf, slottedWords, tellDisclosure, typeaheadBy, unlinkDescription, words, } from '@fusion.dev/helix-core';
8
8
  import { caretLeft } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
@@ -33,8 +33,12 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
33
33
  this.backLabel = 'Back';
34
34
  this.closeOnSelect = true;
35
35
  this.context = false;
36
+ this.filter = '';
37
+ this.filterable = false;
38
+ this.filterLabel = 'Filter';
36
39
  this.label = '';
37
40
  this.longPressLabel = 'Long press or press Alt+Down to show the menu';
41
+ this.noResultsText = 'No matches';
38
42
  this.offset = 4;
39
43
  this.longPress = new LongPress(this, {
40
44
  onLongPress: () => {
@@ -68,7 +72,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
68
72
  });
69
73
  this.aria = new AriaForwardController(this, {
70
74
  label: true,
71
- target: () => this.partNamed('surface'),
75
+ target: () => this.menuBox,
72
76
  });
73
77
  this.panel = new PopoverController(this, {
74
78
  anchor: () => this.context && this.point
@@ -89,6 +93,11 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
89
93
  surface: () => this.partNamed('surface'),
90
94
  });
91
95
  this.settledOnce = false;
96
+ this.filterPage = null;
97
+ this.handleFilterInput = (event) => {
98
+ event.stopPropagation();
99
+ this.filterBy(event.target.value);
100
+ };
92
101
  this.handleContextMenu = (event) => {
93
102
  if (!this.context) {
94
103
  return;
@@ -129,14 +138,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
129
138
  };
130
139
  this.handleFocusIn = (event) => {
131
140
  const item = this.items.find((row) => row.contains(event.target));
132
- if (!item) {
133
- return;
141
+ if (item) {
142
+ this.revealRow(item);
134
143
  }
135
- const holder = this.flies ? this.holderOf(item) : null;
136
- const surface = holder
137
- ? holder.shadowRoot?.querySelector('[part="surface"]')
138
- : this.partNamed('surface');
139
- scrollItemIntoView(item, { padding: 4, ...(surface ? { scroller: surface } : {}) });
140
144
  };
141
145
  this.handleKeydown = (event) => {
142
146
  if (!this.open) {
@@ -146,9 +150,23 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
146
150
  const items = this.items;
147
151
  const at = items.findIndex((item) => item.active);
148
152
  const along = inlineStep(this, event.key);
153
+ const typing = this.filterable && event.composedPath()[0] === this.field;
154
+ const step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;
149
155
  if (event.key === 'Escape') {
150
156
  event.stopPropagation();
151
- this.leave();
157
+ if (!this.filterable || this.filter === '') {
158
+ this.leave();
159
+ }
160
+ else {
161
+ this.filterBy('');
162
+ }
163
+ }
164
+ else if (typing && step !== 0) {
165
+ const count = items.length;
166
+ this.reach(items[at < 0 ? (step > 0 ? 0 : count - 1) : (at + step + count) % count]);
167
+ }
168
+ else if (typing && event.key === 'Enter') {
169
+ items[at]?.renderRoot.querySelector('[part="item"]')?.click();
152
170
  }
153
171
  else if (along === 1 && items[at]?.hasSubmenu) {
154
172
  this.enter(items[at]);
@@ -188,6 +206,14 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
188
206
  this.longPress.down(event);
189
207
  }
190
208
  };
209
+ this.handleMouseDown = (event) => {
210
+ if (this.filterable &&
211
+ this.open &&
212
+ event.composedPath()[0] !== this.field &&
213
+ !this.fromTrigger(event)) {
214
+ event.preventDefault();
215
+ }
216
+ };
191
217
  this.handlePointerMove = (event) => {
192
218
  this.longPress.move(event);
193
219
  };
@@ -209,7 +235,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
209
235
  }
210
236
  };
211
237
  this.handleHover = (event) => {
212
- if (!this.flies || !this.open || event.pointerType !== 'mouse') {
238
+ if (!(this.flies || this.filterable) || !this.open || event.pointerType !== 'mouse') {
213
239
  return;
214
240
  }
215
241
  const row = event
@@ -221,7 +247,10 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
221
247
  this.hovered = row;
222
248
  this.grace = undefined;
223
249
  if (!row.disabled) {
224
- row.focus();
250
+ this.reach(row);
251
+ }
252
+ if (!this.flies) {
253
+ return;
225
254
  }
226
255
  clearTimeout(this.hoverTimer);
227
256
  this.hoverTimer = setTimeout(() => {
@@ -334,7 +363,53 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
334
363
  }
335
364
  get items() {
336
365
  const rows = this.flies ? this.rowsOf(this.keyPage) : this.drilldown.rows;
337
- return rows.filter((row) => !row.disabled);
366
+ return rows.filter((row) => !row.disabled && this.kept(row));
367
+ }
368
+ get menuBox() {
369
+ return this.filterable
370
+ ? (this.renderRoot?.querySelector('#items') ?? null)
371
+ : this.partNamed('surface');
372
+ }
373
+ get field() {
374
+ return this.partNamed('search-field');
375
+ }
376
+ get filtering() {
377
+ return this.filterable && this.filter.trim() !== '' && this.filter !== this.refused;
378
+ }
379
+ kept(row) {
380
+ return (!this.filtering ||
381
+ this.holderOf(row) !== this.filterPage ||
382
+ filterKeeps(slottedWords(row, ['']), this.filter.trim(), this.filterMatch));
383
+ }
384
+ requestFilter(value) {
385
+ if (value === this.filter) {
386
+ return;
387
+ }
388
+ this.filterPage = this.flies ? this.keyPage : this.drilldown.page;
389
+ this.refused = this.dispatchEvent(new HelixFilterEvent({ value })) ? undefined : value;
390
+ this.filter = value;
391
+ }
392
+ filterBy(value) {
393
+ this.requestFilter(value);
394
+ void this.updateComplete.then(() => this.reach(this.items[0]));
395
+ }
396
+ reach(row) {
397
+ if (!this.filterable) {
398
+ row?.focus();
399
+ return;
400
+ }
401
+ if (this.highlighted && this.highlighted !== row) {
402
+ this.highlighted.highlighted = false;
403
+ }
404
+ this.highlighted = row;
405
+ const field = this.field;
406
+ if (field && 'ariaActiveDescendantElement' in field) {
407
+ field.ariaActiveDescendantElement = row ?? null;
408
+ }
409
+ if (row) {
410
+ row.highlighted = true;
411
+ this.revealRow(row);
412
+ }
338
413
  }
339
414
  show() {
340
415
  this.openAt('first');
@@ -345,7 +420,8 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
345
420
  }
346
421
  this.updateComplete.then(() => {
347
422
  const items = this.items;
348
- (end === 'first' ? items[0] : items[items.length - 1])?.focus();
423
+ this.field?.focus();
424
+ this.reach(end === 'first' ? items[0] : items[items.length - 1]);
349
425
  });
350
426
  }
351
427
  hide() {
@@ -355,6 +431,8 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
355
431
  this.panel.hide();
356
432
  this.drilldown.reset();
357
433
  this.stopHover();
434
+ this.requestFilter('');
435
+ this.reach(undefined);
358
436
  if (deepContains(this, deepActiveElement())) {
359
437
  ;
360
438
  (this.context ? (this.returnTo ?? this.trigger) : this.trigger).focus();
@@ -377,18 +455,20 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
377
455
  if (!this.drilldown.enter(item)) {
378
456
  return;
379
457
  }
458
+ this.requestFilter('');
380
459
  void this.updateComplete.then(async () => {
381
460
  if (!this.flies) {
382
- this.items[0]?.focusItem();
461
+ this.reach(this.items[0]);
383
462
  return;
384
463
  }
385
464
  await item.updateComplete;
386
465
  const first = this.rowsOf(item).find((row) => !row.disabled);
387
466
  await first?.updateComplete;
388
- first?.focusItem();
467
+ this.reach(first);
389
468
  });
390
469
  }
391
470
  leave() {
471
+ this.requestFilter('');
392
472
  if (this.flies) {
393
473
  const holder = this.keyPage;
394
474
  if (!holder) {
@@ -396,7 +476,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
396
476
  return;
397
477
  }
398
478
  this.openPage(this.holderOf(holder));
399
- this.updateComplete.then(() => holder.focus());
479
+ this.updateComplete.then(() => this.reach(holder));
400
480
  return;
401
481
  }
402
482
  if (this.drilldown.atRoot) {
@@ -405,7 +485,14 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
405
485
  }
406
486
  const leaving = this.drilldown.page;
407
487
  this.drilldown.back();
408
- this.updateComplete.then(() => leaving?.focus());
488
+ this.updateComplete.then(() => this.reach(leaving ?? undefined));
489
+ }
490
+ revealRow(item) {
491
+ const holder = this.flies ? this.holderOf(item) : null;
492
+ const surface = holder
493
+ ? holder.shadowRoot?.querySelector('[part="surface"]')
494
+ : this.partNamed('surface');
495
+ scrollItemIntoView(item, { padding: 4, ...(surface ? { scroller: surface } : {}) });
409
496
  }
410
497
  handleClosedKeydown(event) {
411
498
  if (this.context && event.shiftKey && event.key === 'F10' && this.fromTrigger(event)) {
@@ -462,6 +549,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
462
549
  this.addEventListener('focusin', this.handleFocusIn);
463
550
  this.addEventListener('contextmenu', this.handleContextMenu);
464
551
  this.addEventListener('pointerdown', this.handlePointerDown);
552
+ this.addEventListener('mousedown', this.handleMouseDown);
465
553
  this.addEventListener('pointermove', this.handlePointerMove);
466
554
  this.addEventListener('pointerup', this.handlePointerUp);
467
555
  this.addEventListener('pointercancel', this.handlePointerCancel);
@@ -484,6 +572,10 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
484
572
  this.removeEventListener('focusin', this.handleFocusIn);
485
573
  this.removeEventListener('contextmenu', this.handleContextMenu);
486
574
  this.removeEventListener('pointerdown', this.handlePointerDown);
575
+ this.removeEventListener('mousedown', this.handleMouseDown);
576
+ if (this.filterable) {
577
+ markMatches(this, [], '');
578
+ }
487
579
  this.removeEventListener('pointermove', this.handlePointerMove);
488
580
  this.removeEventListener('pointerup', this.handlePointerUp);
489
581
  this.removeEventListener('pointercancel', this.handlePointerCancel);
@@ -505,7 +597,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
505
597
  const top = this.flies || this.drilldown.atRoot;
506
598
  for (const child of this.children) {
507
599
  if (isSection(child)) {
508
- child.onPage = top;
600
+ child.onPage =
601
+ top &&
602
+ (!this.filtering || [...child.children].some((row) => isItem(row) && this.kept(row)));
509
603
  for (const row of child.children) {
510
604
  if (row.isMenuItem === true) {
511
605
  this.mark(row, top);
@@ -522,9 +616,18 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
522
616
  this.toggleState('drilled', !this.flies && !this.drilldown.atRoot);
523
617
  this.markColumns();
524
618
  this.nameSurface();
619
+ if (this.filterable) {
620
+ markMatches(this, this.rowsOf(this.filterPage).filter((row) => this.kept(row)), this.filtering ? this.filter.trim() : '');
621
+ const items = this.items;
622
+ if (changed.has('filter') &&
623
+ this.open &&
624
+ !items.includes(this.highlighted)) {
625
+ this.reach(items[0]);
626
+ }
627
+ }
525
628
  }
526
629
  nameSurface() {
527
- const surface = this.partNamed('surface');
630
+ const surface = this.menuBox;
528
631
  if (!surface || !('ariaLabelledByElements' in surface)) {
529
632
  return;
530
633
  }
@@ -544,7 +647,8 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
544
647
  }
545
648
  mark(item, onPage) {
546
649
  const page = this.drilldown.page;
547
- item.onPage = onPage;
650
+ item.onPage = onPage && this.kept(item);
651
+ item.virtual = this.filterable;
548
652
  const holds = page !== null && item.contains(page);
549
653
  item.flyout = this.flies;
550
654
  item.expanded = this.flies && holds;
@@ -553,7 +657,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
553
657
  for (const child of item.children) {
554
658
  const nested = child;
555
659
  if (nested.isMenuItem === true && child.slot === 'submenu') {
556
- this.mark(nested, onPage && holds);
660
+ this.mark(nested, item.onPage && holds);
557
661
  }
558
662
  }
559
663
  return;
@@ -568,6 +672,32 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
568
672
  }
569
673
  render() {
570
674
  const point = this.context && this.point;
675
+ const filtering = this.filtering;
676
+ const none = filtering && !this.rowsOf(this.filterPage).some((row) => this.kept(row));
677
+ this.announced = none ? this.noResultsText : '';
678
+ const rows = html `${this.flies || this.drilldown.atRoot
679
+ ? nothing
680
+ : html `<div part="header">
681
+ <button
682
+ part="back"
683
+ type="button"
684
+ role="menuitem"
685
+ tabindex="-1"
686
+ aria-label=${this.backLabel}
687
+ @click=${() => this.leave()}
688
+ >
689
+ <svg part="back-mark" data-mirror fill="currentColor" viewBox=${caretLeft.viewBox} aria-hidden="true">
690
+ ${unsafeSVG(caretLeft.content)}
691
+ </svg>
692
+ </button>
693
+ <span part="title">${this.drilldown.label}</span>
694
+ </div>`}
695
+ <slot @slotchange=${(event) => this.countRows(event)}></slot>
696
+ ${none
697
+ ? html `<p part="empty"><slot name="no-results">${this.noResultsText}</slot></p>`
698
+ : this.renderStatus(this.hasRows)}`;
699
+ const busy = this.loading ? 'true' : nothing;
700
+ const label = this.label || nothing;
571
701
  return html `<slot name="trigger" @click=${this.handleTriggerClick}></slot>${point
572
702
  ? html `<span
573
703
  id="point"
@@ -578,31 +708,27 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
578
708
  <div
579
709
  part="surface"
580
710
  popover="manual"
581
- role="menu"
582
- aria-busy=${this.loading ? 'true' : nothing}
583
- aria-label=${this.label || nothing}
711
+ role=${this.filterable ? nothing : 'menu'}
712
+ aria-busy=${this.filterable ? nothing : busy}
713
+ aria-label=${this.filterable ? nothing : label}
584
714
  data-drilled=${this.flies || this.drilldown.atRoot ? nothing : ''}
715
+ data-filtered=${filtering ? '' : nothing}
585
716
  ?hidden=${!this.open}
586
717
  >
587
- ${this.flies || this.drilldown.atRoot
588
- ? nothing
589
- : html `<div part="header">
590
- <button
591
- part="back"
592
- type="button"
593
- role="menuitem"
594
- tabindex="-1"
595
- aria-label=${this.backLabel}
596
- @click=${() => this.leave()}
597
- >
598
- <svg part="back-mark" data-mirror fill="currentColor" viewBox=${caretLeft.viewBox} aria-hidden="true">
599
- ${unsafeSVG(caretLeft.content)}
600
- </svg>
601
- </button>
602
- <span part="title">${this.drilldown.label}</span>
603
- </div>`}
604
- <slot @slotchange=${(event) => this.countRows(event)}></slot>
605
- ${this.renderStatus(this.hasRows)}
718
+ ${this.filterable
719
+ ? html `<input
720
+ part="search-field"
721
+ type="text"
722
+ autocomplete="off"
723
+ spellcheck="false"
724
+ .value=${this.filter}
725
+ placeholder=${this.filterLabel}
726
+ aria-label=${this.filterLabel}
727
+ aria-autocomplete="list"
728
+ aria-controls="items"
729
+ @input=${this.handleFilterInput}
730
+ /><div id="items" role="menu" aria-busy=${busy} aria-label=${label}>${rows}</div>`
731
+ : rows}
606
732
  </div>${this.renderLive(this.hasRows)}`;
607
733
  }
608
734
  };
@@ -616,6 +742,19 @@ __decorate([
616
742
  __decorate([
617
743
  property({ reflect: true, type: Boolean })
618
744
  ], HelixHeadlessMenu.prototype, "context", void 0);
745
+ __decorate([
746
+ property()
747
+ ], HelixHeadlessMenu.prototype, "filter", void 0);
748
+ __decorate([
749
+ property({ type: Boolean })
750
+ ], HelixHeadlessMenu.prototype, "filterable", void 0);
751
+ __decorate([
752
+ words,
753
+ property({ attribute: 'filter-label' })
754
+ ], HelixHeadlessMenu.prototype, "filterLabel", void 0);
755
+ __decorate([
756
+ property({ attribute: false })
757
+ ], HelixHeadlessMenu.prototype, "filterMatch", void 0);
619
758
  __decorate([
620
759
  property()
621
760
  ], HelixHeadlessMenu.prototype, "label", void 0);
@@ -623,6 +762,10 @@ __decorate([
623
762
  words,
624
763
  property({ attribute: 'long-press-label' })
625
764
  ], HelixHeadlessMenu.prototype, "longPressLabel", void 0);
765
+ __decorate([
766
+ words,
767
+ property({ attribute: 'no-results-text' })
768
+ ], HelixHeadlessMenu.prototype, "noResultsText", void 0);
626
769
  __decorate([
627
770
  property({ type: Number })
628
771
  ], HelixHeadlessMenu.prototype, "offset", void 0);
@@ -390,6 +390,11 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
390
390
  private counted;
391
391
  /** Says how many entries the filter matched, and nothing for an empty one. */
392
392
  private tell;
393
+ /**
394
+ * Moves focus to the row the keys move from, or to the tree while that row
395
+ * is scrolled out of the window, as Tab reaches them.
396
+ */
397
+ focus(options?: FocusOptions): void;
393
398
  protected firstUpdated(changed: PropertyValues): void;
394
399
  protected updated(changed: PropertyValues): void;
395
400
  /**
@@ -268,6 +268,11 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
268
268
  this.status.say(this.counted('result', this.found.size));
269
269
  }
270
270
  }
271
+ focus(options) {
272
+ this.renderRoot
273
+ ?.querySelector('#tree[tabindex="0"], #tree [tabindex="0"]')
274
+ ?.focus(options);
275
+ }
271
276
  firstUpdated(changed) {
272
277
  super.firstUpdated(changed);
273
278
  this.finishHydration();
@@ -158,6 +158,13 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
158
158
  * calls to `next()` in one task then move two stages.
159
159
  */
160
160
  private get at();
161
+ /**
162
+ * Requests a new current stage, and returns whether it moved.
163
+ *
164
+ * Where only a step's `current` says where the reader is, `active` is first
165
+ * written with that step, so `helix-activate` names it as `previous`.
166
+ */
167
+ requestActive(next: string, cause?: string): boolean;
161
168
  /**
162
169
  * Moves to the next stage that can be gone to, as Ark UI's `goToNextStep`
163
170
  * does. Returns whether it moved: not past the last stage, not into a stage
@@ -65,6 +65,13 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
65
65
  const item = this.resolveActive(steps);
66
66
  return item ? steps.indexOf(item) : -1;
67
67
  }
68
+ requestActive(next, cause) {
69
+ const here = this.active === '' ? this.steps[this.at]?.value : undefined;
70
+ if (here) {
71
+ this.active = here;
72
+ }
73
+ return super.requestActive(next, cause);
74
+ }
68
75
  next() {
69
76
  return this.step(1);
70
77
  }
@@ -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.
@@ -594,7 +594,8 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
594
594
  }
595
595
  return html `<${this.tag('menu')} part="captions-menu"
596
596
  exportparts="back: captions-menu-back, back-mark: captions-menu-back-mark, empty: captions-menu-empty,
597
- header: captions-menu-header, live: captions-menu-live, loading: captions-menu-loading, surface: captions-menu-surface,
597
+ header: captions-menu-header, live: captions-menu-live, loading: captions-menu-loading,
598
+ search-field: captions-menu-search-field, surface: captions-menu-surface,
598
599
  title: captions-menu-title" label=${this.subtitlesLabel} placement="top-end"
599
600
  @toggle=${(event) => this.heardMenu('captions', event)}>
600
601
  <button
@@ -804,7 +805,8 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
804
805
  ? nothing
805
806
  : html `<${this.tag('menu')} part="overflow"
806
807
  exportparts="back: overflow-back, back-mark: overflow-back-mark, empty: overflow-empty,
807
- header: overflow-header, live: overflow-live, loading: overflow-loading, surface: overflow-surface,
808
+ header: overflow-header, live: overflow-live, loading: overflow-loading,
809
+ search-field: overflow-search-field, surface: overflow-surface,
808
810
  title: overflow-title" label=${this.moreLabel} placement="top-end"
809
811
  @toggle=${(event) => this.heardMenu('overflow', event)}>
810
812
  <button
@@ -38,6 +38,7 @@ import '../../menu/index.js';
38
38
  * @csspart menu-header - The bar above a submenu, holding the back button and the submenu's name.
39
39
  * @csspart menu-live - The inner menu's `live` part, the region screen readers announce its empty and loading messages from.
40
40
  * @csspart menu-loading - Shown while the items are loading.
41
+ * @csspart menu-search-field - The inner menu's filter field, drawn with `filterable`.
41
42
  * @csspart menu-surface - The floating panel the menu is drawn on.
42
43
  * @csspart menu-title - The name of the submenu that is shown.
43
44
  * @csspart poster - An entry's poster image, if it has one.
@@ -84,7 +84,7 @@ let HelixHeadlessVideoPlaylistMenu = class HelixHeadlessVideoPlaylistMenu extend
84
84
  renderMenu() {
85
85
  return html `<${this.tag('menu')} part="menu"
86
86
  exportparts="back: menu-back, back-mark: menu-back-mark, empty: menu-empty, header: menu-header,
87
- live: menu-live, loading: menu-loading, surface: menu-surface, title: menu-title" label=${this.label} placement=${this.placement}
87
+ live: menu-live, loading: menu-loading, search-field: menu-search-field, surface: menu-surface, title: menu-title" label=${this.label} placement=${this.placement}
88
88
  @toggle=${this.heardToggle}>
89
89
  <slot name="trigger" slot="trigger">
90
90
  <button
@@ -47,8 +47,16 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
47
47
  if (event.defaultPrevented) {
48
48
  return;
49
49
  }
50
+ const value = event.detail.value;
51
+ const player = this.player;
52
+ if (value === this.active) {
53
+ if (player && player.paused !== false) {
54
+ void Promise.resolve(player.play()).catch(() => { });
55
+ }
56
+ return;
57
+ }
50
58
  this.chased = true;
51
- this.requestActive(event.detail.value, 'press');
59
+ this.requestActive(value, 'press');
52
60
  };
53
61
  this.handleFocusIn = (event) => {
54
62
  const entry = event.target;
@@ -259,6 +267,12 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
259
267
  if (changed.has('shuffle') && !this.shuffle) {
260
268
  this.dealt = undefined;
261
269
  }
270
+ if (this.active === '' && this.childrenKnown) {
271
+ const opening = this.resolveActive(this.items);
272
+ if (opening) {
273
+ this.active = opening.value;
274
+ }
275
+ }
262
276
  }
263
277
  updated(changed) {
264
278
  super.updated?.(changed);