@fusion.dev/helix-headless 0.209.0 → 0.211.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.
@@ -3,6 +3,8 @@ import { auditName, customElement, focusItemOf, formatWords, HelixActivateEvent,
3
3
  import { buildings, cloudRain, dotsNine, leaf, mountains, path, polygon, stack, target, } from '@fusion.dev/helix-icons';
4
4
  import { html, nothing } from 'lit';
5
5
  import { property, state } from 'lit/decorators.js';
6
+ import { html as staticHtml } from 'lit/static-html.js';
7
+ import '../../icon-button/index.js';
6
8
  import { live } from 'lit/directives/live.js';
7
9
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
8
10
  const MAP_TYPES = ['standard', 'hybrid', 'satellite'];
@@ -34,6 +36,7 @@ const TILTED = 50;
34
36
  const placed = (marker) => Number.isFinite(marker.lat) && Number.isFinite(marker.lng);
35
37
  const ASK_LAPSES = 3000;
36
38
  const SETTLES = 400;
39
+ const stopPress = (event) => event.stopPropagation();
37
40
  let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement) {
38
41
  constructor() {
39
42
  super(...arguments);
@@ -398,12 +401,16 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
398
401
  return nothing;
399
402
  }
400
403
  const entries = this.layerEntries;
401
- return html `<button
404
+ const button = this.controlTag;
405
+ const { material, raised } = this.controlAxes;
406
+ return staticHtml `<${button}
402
407
  part="layers-toggle"
408
+ exportparts="button: layers-toggle-button, live: layers-toggle-live, spinner: layers-toggle-spinner, text: layers-toggle-text"
403
409
  aria-controls="layers-menu"
404
- aria-expanded=${this.layersOpen ? 'true' : 'false'}
405
- aria-label=${this.layersLabel}
406
- type="button"
410
+ .expanded=${this.layersOpen}
411
+ label=${this.layersLabel}
412
+ material=${material ?? nothing}
413
+ ?raised=${raised}
407
414
  @click=${() => {
408
415
  this.layersOpen = !this.layersOpen;
409
416
  if (this.layersOpen) {
@@ -411,6 +418,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
411
418
  }
412
419
  }}
413
420
  @focusout=${this.handleMenuFocusOut}
421
+ @helix-press=${stopPress}
414
422
  @keydown=${this.handleMenuKeydown}
415
423
  >
416
424
  <slot name="layers-icon">
@@ -418,7 +426,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
418
426
  ${unsafeSVG(stack.content)}
419
427
  </svg>
420
428
  </slot>
421
- </button>
429
+ </${button}>
422
430
  <div
423
431
  part="layers-menu"
424
432
  id="layers-menu"
@@ -474,44 +482,98 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
474
482
  }
475
483
  renderCompass(pan, rotate) {
476
484
  const { faceNorthLabel } = this;
477
- return html `<div part="compass">
485
+ const button = this.controlTag;
486
+ const { material, raised } = this.controlAxes;
487
+ const ground = material ?? nothing;
488
+ return staticHtml `<div part="compass">
478
489
  ${rotate
479
- ? html `<button part="rotate-left" aria-label=${this.rotateLeftLabel} type="button" @click=${() => this.rotate(-ROTATE_STEP)}>
490
+ ? staticHtml `<${button}
491
+ part="rotate-left"
492
+ exportparts="button: rotate-left-button, live: rotate-left-live, spinner: rotate-left-spinner, text: rotate-left-text"
493
+ label=${this.rotateLeftLabel}
494
+ material=${ground}
495
+ @click=${() => this.rotate(-ROTATE_STEP)}
496
+ @helix-press=${stopPress}
497
+ >
480
498
  <slot name="rotate-left-icon">↺</slot>
481
- </button>`
499
+ </${button}>`
482
500
  : nothing}
483
501
  ${pan
484
- ? html `<button part="pan-up" aria-label=${this.panLabel('up')} type="button" @click=${() => this.pan('up')}>
502
+ ? staticHtml `<${button}
503
+ part="pan-up"
504
+ exportparts="button: pan-up-button, live: pan-up-live, spinner: pan-up-spinner, text: pan-up-text"
505
+ label=${this.panLabel('up')}
506
+ material=${ground}
507
+ @click=${() => this.pan('up')}
508
+ @helix-press=${stopPress}
509
+ >
485
510
  <slot name="pan-up-icon">${this.panGlyph('up')}</slot>
486
- </button>`
511
+ </${button}>`
487
512
  : nothing}
488
513
  ${rotate
489
- ? html `<button part="rotate-right" aria-label=${this.rotateRightLabel} type="button" @click=${() => this.rotate(ROTATE_STEP)}>
514
+ ? staticHtml `<${button}
515
+ part="rotate-right"
516
+ exportparts="button: rotate-right-button, live: rotate-right-live, spinner: rotate-right-spinner, text: rotate-right-text"
517
+ label=${this.rotateRightLabel}
518
+ material=${ground}
519
+ @click=${() => this.rotate(ROTATE_STEP)}
520
+ @helix-press=${stopPress}
521
+ >
490
522
  <slot name="rotate-right-icon">↻</slot>
491
- </button>`
523
+ </${button}>`
492
524
  : nothing}
493
525
  ${pan
494
- ? html `<button part="pan-start" aria-label=${this.panLabel('start')} type="button" @click=${() => this.pan('start')}>
526
+ ? staticHtml `<${button}
527
+ part="pan-start"
528
+ exportparts="button: pan-start-button, live: pan-start-live, spinner: pan-start-spinner, text: pan-start-text"
529
+ label=${this.panLabel('start')}
530
+ material=${ground}
531
+ @click=${() => this.pan('start')}
532
+ @helix-press=${stopPress}
533
+ >
495
534
  <slot name="pan-start-icon">${this.panGlyph('start')}</slot>
496
- </button>`
535
+ </${button}>`
497
536
  : nothing}
498
- <button part="face-north" aria-label=${faceNorthLabel} type="button" style=${`--map-needle-turn: ${-this.bearing}deg`} @click=${() => this.faceNorth()}>
537
+ <${button}
538
+ part="face-north"
539
+ exportparts="button: face-north-button, live: face-north-live, spinner: face-north-spinner, text: face-north-text"
540
+ label=${faceNorthLabel}
541
+ material=${ground}
542
+ ?raised=${raised}
543
+ style=${`--map-needle-turn: ${-this.bearing}deg`}
544
+ @click=${() => this.faceNorth()}
545
+ @helix-press=${stopPress}
546
+ >
499
547
  <slot name="face-north-icon">
500
548
  <svg part="needle" aria-hidden="true" viewBox="0 0 16 16">
501
549
  <path data-end="north" d="M8 1.5 10.5 8h-5Z" />
502
550
  <path data-end="south" d="M8 14.5 5.5 8h5Z" />
503
551
  </svg>
504
552
  </slot>
505
- </button>
553
+ </${button}>
506
554
  ${pan
507
- ? html `<button part="pan-end" aria-label=${this.panLabel('end')} type="button" @click=${() => this.pan('end')}>
555
+ ? staticHtml `<${button}
556
+ part="pan-end"
557
+ exportparts="button: pan-end-button, live: pan-end-live, spinner: pan-end-spinner, text: pan-end-text"
558
+ label=${this.panLabel('end')}
559
+ material=${ground}
560
+ @click=${() => this.pan('end')}
561
+ @helix-press=${stopPress}
562
+ >
508
563
  <slot name="pan-end-icon">${this.panGlyph('end')}</slot>
509
- </button>`
564
+ </${button}>`
510
565
  : nothing}
511
566
  ${pan
512
- ? html `<button part="pan-down" aria-label=${this.panLabel('down')} type="button" @click=${() => this.pan('down')}>
567
+ ? staticHtml `<${button}
568
+ part="pan-down"
569
+ exportparts="button: pan-down-button, live: pan-down-live, spinner: pan-down-spinner, text: pan-down-text"
570
+ label=${this.panLabel('down')}
571
+ material=${ground}
572
+ @click=${() => this.pan('down')}
573
+ @helix-press=${stopPress}
574
+ >
513
575
  <slot name="pan-down-icon">${this.panGlyph('down')}</slot>
514
- </button>`
576
+ </${button}>`
515
577
  : nothing}
516
578
  </div>`;
517
579
  }
@@ -525,37 +587,62 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
525
587
  if (!pan && !zoom && !rotate && !tilt && !fullscreen && menu === nothing) {
526
588
  return nothing;
527
589
  }
528
- return html `<div part="controls" role="group" aria-label=${this.controlsLabel}>
590
+ const button = this.controlTag;
591
+ const { material, raised } = this.controlAxes;
592
+ const ground = material ?? nothing;
593
+ return staticHtml `<div part="controls" role="toolbar" aria-label=${this.controlsLabel}>
529
594
  ${fullscreen
530
- ? html `<button
595
+ ? staticHtml `<${button}
531
596
  part="fullscreen"
532
- aria-label=${this.fullscreenLabel}
597
+ exportparts="button: fullscreen-button, live: fullscreen-live, spinner: fullscreen-spinner, text: fullscreen-text"
533
598
  aria-pressed=${this.filled ? 'true' : 'false'}
534
- type="button"
599
+ label=${this.fullscreenLabel}
600
+ material=${ground}
601
+ ?raised=${raised}
535
602
  @click=${this.toggleFullscreen}
603
+ @helix-press=${stopPress}
536
604
  >
537
605
  <slot name="fullscreen-icon">⤢</slot>
538
- </button>`
606
+ </${button}>`
539
607
  : nothing}
540
608
  ${menu}
541
609
  ${tilt
542
- ? html `<button
610
+ ? staticHtml `<${button}
543
611
  part="tilt"
544
- aria-label=${this.tiltLabel}
612
+ exportparts="button: tilt-button, live: tilt-live, spinner: tilt-spinner, text: tilt-text"
545
613
  aria-pressed=${this.pitch > 0 ? 'true' : 'false'}
546
- type="button"
614
+ label=${this.tiltLabel}
615
+ material=${ground}
616
+ ?raised=${raised}
547
617
  @click=${() => this.tiltTo(this.pitch > 0 ? 0 : TILTED)}
618
+ @helix-press=${stopPress}
548
619
  >
549
620
  <slot name="tilt-icon">3D</slot>
550
- </button>`
621
+ </${button}>`
551
622
  : nothing}
552
623
  ${zoom
553
- ? html `<button part="zoom-in" aria-label=${this.zoomInLabel} type="button" @click=${() => this.zoom(1)}>
624
+ ? staticHtml `<${button}
625
+ part="zoom-in"
626
+ exportparts="button: zoom-in-button, live: zoom-in-live, spinner: zoom-in-spinner, text: zoom-in-text"
627
+ label=${this.zoomInLabel}
628
+ material=${ground}
629
+ ?raised=${raised}
630
+ @click=${() => this.zoom(1)}
631
+ @helix-press=${stopPress}
632
+ >
554
633
  <slot name="zoom-in-icon">+</slot>
555
- </button>
556
- <button part="zoom-out" aria-label=${this.zoomOutLabel} type="button" @click=${() => this.zoom(-1)}>
634
+ </${button}>
635
+ <${button}
636
+ part="zoom-out"
637
+ exportparts="button: zoom-out-button, live: zoom-out-live, spinner: zoom-out-spinner, text: zoom-out-text"
638
+ label=${this.zoomOutLabel}
639
+ material=${ground}
640
+ ?raised=${raised}
641
+ @click=${() => this.zoom(-1)}
642
+ @helix-press=${stopPress}
643
+ >
557
644
  <slot name="zoom-out-icon">−</slot>
558
- </button>`
645
+ </${button}>`
559
646
  : nothing}
560
647
  ${this.drawsCompass ? this.renderCompass(pan, rotate) : nothing}
561
648
  </div>`;
@@ -590,6 +677,9 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
590
677
  }
591
678
  return false;
592
679
  }
680
+ get controlTag() {
681
+ return this.tag('icon-button');
682
+ }
593
683
  panEvent(detail) {
594
684
  this.ask('pan', this.sideOf(detail.to));
595
685
  return new HelixMapPanEvent(detail);
@@ -1,4 +1,4 @@
1
- import { AriaForwardController, HelixElement } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, HelixElement, ToolbarController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  /**
4
4
  * @summary A group of controls that share one tab stop.
@@ -94,13 +94,6 @@ export declare class HelixHeadlessToolbar extends HelixElement {
94
94
  label: string;
95
95
  /** Carries a host `aria-label` and `aria-labelledby` to the toolbar. */
96
96
  protected readonly aria: AriaForwardController;
97
- /**
98
- * Whether the stop has been written onto controls this package does not
99
- * know, which happens on the first focus. From then on, every move of the
100
- * stop has to write them too, or the one at `0` can be removed or disabled
101
- * and leave every other one at `-1`.
102
- */
103
- private wroteForeign;
104
97
  /**
105
98
  * The direction the arrow keys move focus in.
106
99
  */
@@ -110,77 +103,12 @@ export declare class HelixHeadlessToolbar extends HelixElement {
110
103
  * wrap around from one end to the other.
111
104
  */
112
105
  stopAtEnds: boolean;
113
- private watcher;
114
- /**
115
- * The children that take part in the toolbar, in document order, each with
116
- * the elements in it that take focus.
117
- *
118
- * A test on the element rather than a list of tag names: a toolbar holds
119
- * anything, including elements this package has never heard of. A child
120
- * that takes no focus and holds nothing that does is skipped. Client only,
121
- * there being no tree to walk on the server.
122
- */
123
- private get controls();
124
- /**
125
- * The toolbar's children, a slot among them read as the elements assigned
126
- * to it.
127
- *
128
- * **For a toolbar a component renders in its own shadow root** and passes a
129
- * page's controls into through a slot, as Terminal's header does with its
130
- * `actions`: read as a child, the slot holds only its fallback, and the
131
- * page's controls kept tab stops of their own beside the toolbar's one.
132
- */
133
- private get members();
134
- /** The elements in a child's light DOM that take focus. */
135
- private within;
136
- /**
137
- * The stops of a child that takes focus itself: the child, or the members of
138
- * a composite that owns its tab stop, such as a toggle group, which the
139
- * toolbar enters at a member and moves the stop of through `focusable`.
140
- */
141
- private inside;
142
- /**
143
- * Where focus lands on arriving at a control. A nested composite is entered
144
- * at the stop it holds, which for a toggle group is the chosen toggle.
145
- */
146
- private entry;
147
- /** The control a press or a focus came from, by its path. */
148
- private origin;
149
106
  /**
150
- * Moves the single tab stop to one control.
151
- *
152
- * Prefers the control's own `focusable` property to writing `tabindex` on it.
153
- * The attribute is a change to markup the consumer rendered, which a React
154
- * consumer's hydration reports and does not patch up; a control that offers
155
- * the property moves the stop inside its own shadow root instead.
156
- *
157
- * `tabindex` stays as the fallback, because a toolbar has to hold controls
158
- * this package has never heard of.
159
- */
160
- private setTabStop;
161
- /**
162
- * Sets the first tab stop once every custom control has upgraded.
163
- *
164
- * A custom element has no properties at all before it upgrades, so a pass
165
- * that ran first would find no `focusable`, take the `tabindex` fallback, and
166
- * write the very attribute this exists to avoid.
167
- */
168
- private claimTabStop;
169
- private readonly handleFocusIn;
170
- /**
171
- * Gives the tab stop back to a control that still exists.
172
- *
173
- * `controls` excludes anything disabled or hidden, so disabling the control
174
- * holding the stop drops it out of the list while every other control is
175
- * already at `-1`, leaving a toolbar the keyboard cannot reach at all. A Save
176
- * button disabling itself once the form is clean is enough to do it.
177
- *
178
- * `foreign` stays false, the same as the first claim, until a focus has
179
- * written `tabindex="-1"` onto foreign controls. From then on `setTabStop`
180
- * writes them as well, so removing or disabling the native button that held
181
- * the stop hands it to the first one left.
107
+ * The one tab stop and the arrow keys, shared with every toolbar a
108
+ * component draws: the controls are the children, a slot among them read as
109
+ * the elements assigned to it.
182
110
  */
183
- private readonly restoreTabStop;
111
+ protected readonly tabStop: ToolbarController;
184
112
  /**
185
113
  * Whether this toolbar sits inside another, which then owns the keys and the
186
114
  * tab stop: a toolbar inside a toolbar held the reader's arrows inside it.
@@ -195,18 +123,7 @@ export declare class HelixHeadlessToolbar extends HelixElement {
195
123
  * so a reflected default cannot appear on the host after hydration.
196
124
  */
197
125
  private get axis();
198
- /**
199
- * The toolbar's own keys, from inside a nested composite.
200
- *
201
- * In capture, ahead of the composite, which answers every arrow itself and
202
- * would otherwise hold the reader inside it. Only the keys along the
203
- * toolbar's axis and Home and End are taken, so the arrows across it still
204
- * move inside the composite.
205
- */
206
- private readonly handleCompositeKeydown;
207
- private readonly handleKeydown;
208
126
  connectedCallback(): void;
209
- disconnectedCallback(): void;
210
127
  protected firstUpdated(changed: PropertyValues): void;
211
128
  protected willUpdate(changed: PropertyValues): void;
212
129
  protected render(): TemplateResult;
@@ -1,44 +1,8 @@
1
1
  var HelixHeadlessToolbar_1;
2
2
  import { __decorate } from "tslib";
3
- import { AriaForwardController, closestInstance, customElement, HelixElement, inlineStep, keyClaimed, toolbarAround, words, } from '@fusion.dev/helix-core';
3
+ import { AriaForwardController, closestInstance, customElement, HelixElement, ToolbarController, toolbarAround, words, } from '@fusion.dev/helix-core';
4
4
  import { html, nothing } from 'lit';
5
5
  import { property } from 'lit/decorators.js';
6
- const KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End']);
7
- const ownsTabStop = (node) => 'focusable' in node;
8
- const NATIVE = 'a[href], button, input, select, textarea, [tabindex], [contenteditable=""], [contenteditable="true"]';
9
- const TABBABLE = NATIVE.replace('[tabindex]', '[tabindex]:not([tabindex="-1"])');
10
- const innerStops = new WeakMap();
11
- const takesFocus = (node) => {
12
- if (!(node instanceof HTMLElement) ||
13
- node.hasAttribute('disabled') ||
14
- node.getAttribute('aria-hidden') === 'true') {
15
- return false;
16
- }
17
- return (node.matches(NATIVE) ||
18
- ownsTabStop(node) ||
19
- node.shadowRoot?.delegatesFocus === true ||
20
- node.shadowRoot?.querySelector(TABBABLE) != null ||
21
- innerStops.get(node)?.isConnected === true);
22
- };
23
- const stopOf = (node) => {
24
- if (node.hasAttribute('tabindex')) {
25
- return node;
26
- }
27
- const held = innerStops.get(node);
28
- if (held?.isConnected && held.getRootNode() === node.shadowRoot) {
29
- return held;
30
- }
31
- const inner = node.localName.includes('-')
32
- ? node.shadowRoot?.querySelector(TABBABLE)
33
- : null;
34
- if (!inner) {
35
- return node;
36
- }
37
- innerStops.set(node, inner);
38
- return inner;
39
- };
40
- const flattens = (node) => ownsTabStop(node) &&
41
- node.selectionMode === 'multiple';
42
6
  let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
43
7
  constructor() {
44
8
  super(...arguments);
@@ -47,180 +11,16 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
47
11
  label: true,
48
12
  target: () => this.partNamed('toolbar'),
49
13
  });
50
- this.wroteForeign = false;
51
14
  this.stopAtEnds = false;
52
- this.handleFocusIn = (event) => {
53
- if (this.nested) {
54
- return;
55
- }
56
- const control = this.origin(event);
57
- if (control) {
58
- this.setTabStop(control);
59
- }
60
- };
61
- this.restoreTabStop = () => {
62
- if (this.nested) {
63
- return;
64
- }
65
- const controls = this.controls;
66
- const held = controls.some((control) => {
67
- if (control.group) {
68
- return control.group.focusable;
69
- }
70
- const composite = control.stops.length !== 1;
71
- const element = composite ? control.child : control.stops[0];
72
- if (ownsTabStop(element)) {
73
- return element.focusable;
74
- }
75
- return composite || stopOf(element).tabIndex === 0;
76
- });
77
- if (controls.length > 0 && !held) {
78
- this.setTabStop(controls[0], false);
79
- }
80
- };
81
- this.handleCompositeKeydown = (event) => {
82
- if (this.nested) {
83
- return;
84
- }
85
- const control = this.origin(event);
86
- if (!control || (control.stops.length === 1 && !control.group)) {
87
- return;
88
- }
89
- this.handleKeydown(event);
90
- if (event.defaultPrevented) {
91
- event.stopPropagation();
92
- }
93
- };
94
- this.handleKeydown = (event) => {
95
- if (this.nested || !KEYS.has(event.key) || keyClaimed(event)) {
96
- return;
97
- }
98
- const along = inlineStep(this, event.key);
99
- const forward = this.axis === 'horizontal' ? along === 1 : event.key === 'ArrowDown';
100
- const back = this.axis === 'horizontal' ? along === -1 : event.key === 'ArrowUp';
101
- const controls = this.controls;
102
- const origin = this.origin(event);
103
- const current = origin ? controls.findIndex((control) => control.child === origin.child) : -1;
104
- if (current === -1) {
105
- return;
106
- }
107
- let next = current;
108
- const last = controls.length - 1;
109
- if (forward) {
110
- next = this.stopAtEnds ? Math.min(current + 1, last) : (current + 1) % controls.length;
111
- }
112
- else if (back) {
113
- next = this.stopAtEnds
114
- ? Math.max(current - 1, 0)
115
- : (current - 1 + controls.length) % controls.length;
116
- }
117
- else if (event.key === 'Home') {
118
- next = 0;
119
- }
120
- else if (event.key === 'End') {
121
- next = controls.length - 1;
122
- }
123
- else {
124
- return;
125
- }
126
- event.preventDefault();
127
- const target = controls[next];
128
- this.setTabStop(target);
129
- const element = target ? this.entry(target) : undefined;
130
- if (element && 'focusItem' in element && typeof element.focusItem === 'function') {
131
- element.focusItem();
132
- }
133
- else if (element) {
134
- ;
135
- (ownsTabStop(element) ? element : stopOf(element)).focus();
136
- }
137
- };
15
+ this.tabStop = new ToolbarController(this, {
16
+ arrows: () => this.axis,
17
+ members: () => [...this.children],
18
+ nested: () => this.nested,
19
+ stopAtEnds: () => this.stopAtEnds,
20
+ });
138
21
  }
139
22
  static { HelixHeadlessToolbar_1 = this; }
140
23
  static { this.words = 'helix-toolbar'; }
141
- get controls() {
142
- const controls = [];
143
- for (const child of this.members) {
144
- if (!(child instanceof HTMLElement) ||
145
- child.hasAttribute('disabled') ||
146
- child.getAttribute('aria-hidden') === 'true') {
147
- continue;
148
- }
149
- if (takesFocus(child)) {
150
- const stops = this.inside(child);
151
- if (stops.length > 1 && flattens(child)) {
152
- for (const stop of stops) {
153
- controls.push({ child: stop, group: child, stops: [stop] });
154
- }
155
- continue;
156
- }
157
- controls.push({ child, stops });
158
- continue;
159
- }
160
- for (const stop of this.within(child)) {
161
- controls.push({ child: stop, stops: [stop] });
162
- }
163
- }
164
- return controls;
165
- }
166
- get members() {
167
- return [...this.children].flatMap((child) => child instanceof HTMLSlotElement ? child.assignedElements({ flatten: true }) : [child]);
168
- }
169
- within(child) {
170
- return [...child.querySelectorAll('*')].filter(takesFocus);
171
- }
172
- inside(child) {
173
- if (!ownsTabStop(child)) {
174
- return [child];
175
- }
176
- const members = this.within(child);
177
- return members.length > 1 ? members : [child];
178
- }
179
- entry(control) {
180
- const { stops } = control;
181
- if (stops.length === 1) {
182
- return stops[0];
183
- }
184
- return (stops.find((stop) => (ownsTabStop(stop) && stop.focusable) ||
185
- stop.selected === true) ?? stops[0]);
186
- }
187
- origin(event) {
188
- const path = event.composedPath();
189
- return this.controls.find((control) => control.stops.some((stop) => path.includes(stop)));
190
- }
191
- setTabStop(active, foreign = true) {
192
- if (foreign) {
193
- this.wroteForeign = true;
194
- }
195
- for (const control of this.controls) {
196
- if (control.group) {
197
- control.group.focusable = control.group === active?.group;
198
- continue;
199
- }
200
- if (control.stops.length !== 1) {
201
- if (ownsTabStop(control.child)) {
202
- control.child.focusable = control.child === active?.child;
203
- }
204
- continue;
205
- }
206
- const element = control.stops[0];
207
- const stop = control.child === active?.child;
208
- if (ownsTabStop(element)) {
209
- element.focusable = stop;
210
- }
211
- else if (foreign || this.wroteForeign) {
212
- stopOf(element).tabIndex = stop ? 0 : -1;
213
- }
214
- }
215
- }
216
- async claimTabStop() {
217
- if (this.nested) {
218
- return;
219
- }
220
- const pending = [...new Set(this.members.map((child) => child.localName))].filter((name) => name.includes('-'));
221
- await Promise.all(pending.map((name) => customElements.whenDefined(name)));
222
- this.setTabStop(this.controls[0], false);
223
- }
224
24
  get nested() {
225
25
  const parent = this.parentElement;
226
26
  if (!parent) {
@@ -234,27 +34,10 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
234
34
  connectedCallback() {
235
35
  super.connectedCallback();
236
36
  this.requestUpdate();
237
- this.addEventListener('keydown', this.handleCompositeKeydown, true);
238
- this.addEventListener('keydown', this.handleKeydown);
239
- this.addEventListener('focusin', this.handleFocusIn);
240
- this.watcher = new MutationObserver(this.restoreTabStop);
241
- this.watcher.observe(this, {
242
- attributeFilter: ['disabled', 'aria-hidden'],
243
- attributes: true,
244
- subtree: true,
245
- });
246
- }
247
- disconnectedCallback() {
248
- super.disconnectedCallback();
249
- this.removeEventListener('keydown', this.handleCompositeKeydown, true);
250
- this.removeEventListener('keydown', this.handleKeydown);
251
- this.removeEventListener('focusin', this.handleFocusIn);
252
- this.watcher?.disconnect();
253
- this.watcher = undefined;
254
37
  }
255
38
  firstUpdated(changed) {
256
39
  super.firstUpdated(changed);
257
- void this.claimTabStop();
40
+ void this.tabStop.claim();
258
41
  }
259
42
  willUpdate(changed) {
260
43
  super.willUpdate(changed);
@@ -262,7 +45,7 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
262
45
  this.toggleState('horizontal', this.axis === 'horizontal');
263
46
  this.toggleState('vertical', this.axis === 'vertical');
264
47
  if (this.hasUpdated && changed.has('orientation')) {
265
- for (const child of this.members) {
48
+ for (const child of this.tabStop.members) {
266
49
  ;
267
50
  child.requestUpdate?.();
268
51
  }
@@ -276,7 +59,7 @@ let HelixHeadlessToolbar = class HelixHeadlessToolbar extends HelixElement {
276
59
  aria-label=${this.label}
277
60
  aria-orientation=${nested ? nothing : this.axis}
278
61
  >
279
- <slot @slotchange=${() => this.claimTabStop()}></slot>
62
+ <slot @slotchange=${() => this.tabStop.claim()}></slot>
280
63
  </div>`;
281
64
  }
282
65
  };