@fusion.dev/helix-headless 0.209.0 → 0.210.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,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
  };