@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.
- package/api.json +1 -1
- package/custom-elements.json +1 -1
- package/dist/canvas/root/canvas-root.d.ts +39 -1
- package/dist/canvas/root/canvas-root.js +4 -0
- package/dist/graph/root/graph-root.d.ts +50 -10
- package/dist/graph/root/graph-root.js +24 -23
- package/dist/legend/root/legend-root.d.ts +9 -15
- package/dist/legend/root/legend-root.js +10 -20
- package/dist/map/root/map-root.d.ts +59 -2
- package/dist/map/root/map-root.js +123 -33
- package/dist/toolbar/root/toolbar-root.d.ts +5 -88
- package/dist/toolbar/root/toolbar-root.js +10 -227
- package/examples.json +1 -1
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +240 -10
- package/types/vue.d.ts +3 -3
|
@@ -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
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
499
|
+
</${button}>`
|
|
482
500
|
: nothing}
|
|
483
501
|
${pan
|
|
484
|
-
?
|
|
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
|
-
|
|
511
|
+
</${button}>`
|
|
487
512
|
: nothing}
|
|
488
513
|
${rotate
|
|
489
|
-
?
|
|
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
|
-
|
|
523
|
+
</${button}>`
|
|
492
524
|
: nothing}
|
|
493
525
|
${pan
|
|
494
|
-
?
|
|
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
|
-
|
|
535
|
+
</${button}>`
|
|
497
536
|
: nothing}
|
|
498
|
-
|
|
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
|
-
|
|
553
|
+
</${button}>
|
|
506
554
|
${pan
|
|
507
|
-
?
|
|
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
|
-
|
|
564
|
+
</${button}>`
|
|
510
565
|
: nothing}
|
|
511
566
|
${pan
|
|
512
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
595
|
+
? staticHtml `<${button}
|
|
531
596
|
part="fullscreen"
|
|
532
|
-
|
|
597
|
+
exportparts="button: fullscreen-button, live: fullscreen-live, spinner: fullscreen-spinner, text: fullscreen-text"
|
|
533
598
|
aria-pressed=${this.filled ? 'true' : 'false'}
|
|
534
|
-
|
|
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
|
-
|
|
606
|
+
</${button}>`
|
|
539
607
|
: nothing}
|
|
540
608
|
${menu}
|
|
541
609
|
${tilt
|
|
542
|
-
?
|
|
610
|
+
? staticHtml `<${button}
|
|
543
611
|
part="tilt"
|
|
544
|
-
|
|
612
|
+
exportparts="button: tilt-button, live: tilt-live, spinner: tilt-spinner, text: tilt-text"
|
|
545
613
|
aria-pressed=${this.pitch > 0 ? 'true' : 'false'}
|
|
546
|
-
|
|
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
|
-
|
|
621
|
+
</${button}>`
|
|
551
622
|
: nothing}
|
|
552
623
|
${zoom
|
|
553
|
-
?
|
|
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
|
-
|
|
556
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
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
|
-
|
|
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,
|
|
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.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
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.
|
|
62
|
+
<slot @slotchange=${() => this.tabStop.claim()}></slot>
|
|
280
63
|
</div>`;
|
|
281
64
|
}
|
|
282
65
|
};
|