@fusion.dev/helix-headless 0.154.0 → 0.156.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/custom-elements.json +3213 -179
- package/dist/accordion/item/accordion-item.d.ts +15 -0
- package/dist/accordion/item/accordion-item.js +12 -1
- package/dist/accordion/root/accordion-root.d.ts +2 -1
- package/dist/accordion/root/accordion-root.js +1 -0
- package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +36 -3
- package/dist/breadcrumbs/breadcrumb/breadcrumb.js +42 -2
- package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +15 -5
- package/dist/breadcrumbs/root/breadcrumbs-root.js +29 -4
- package/dist/button-group/root/button-group-root.d.ts +2 -0
- package/dist/button-group/root/button-group-root.js +1 -0
- package/dist/canvas/node/canvas-node.js +1 -1
- package/dist/canvas/root/canvas-root.d.ts +17 -2
- package/dist/canvas/root/canvas-root.js +23 -6
- package/dist/chat/composer/chat-composer.js +1 -1
- package/dist/chat/root/chat-root.d.ts +14 -1
- package/dist/chat/root/chat-root.js +14 -4
- package/dist/color-input/root/color-input-root.d.ts +9 -3
- package/dist/color-input/root/color-input-root.js +3 -1
- package/dist/color-picker/channels.d.ts +5 -3
- package/dist/color-picker/channels.js +79 -21
- package/dist/color-picker/root/color-picker-root.d.ts +58 -5
- package/dist/color-picker/root/color-picker-root.js +75 -13
- package/dist/combobox/root/combobox-root.d.ts +10 -0
- package/dist/combobox/root/combobox-root.js +18 -3
- package/dist/command/root/command-root.d.ts +116 -1
- package/dist/command/root/command-root.js +124 -15
- package/dist/data-table/root/data-table-root.d.ts +25 -0
- package/dist/data-table/root/data-table-root.js +13 -3
- package/dist/date-input/root/date-input-root.d.ts +33 -1
- package/dist/date-input/root/date-input-root.js +9 -2
- package/dist/date-picker/root/date-picker-root.d.ts +43 -13
- package/dist/date-picker/root/date-picker-root.js +54 -31
- package/dist/element-events.generated.js +3 -1
- package/dist/graph/root/graph-root.d.ts +13 -0
- package/dist/graph/root/graph-root.js +16 -0
- package/dist/list/root/list-root.d.ts +74 -1
- package/dist/list/root/list-root.js +92 -4
- package/dist/list/selection.js +3 -3
- package/dist/map/root/map-root.d.ts +6 -0
- package/dist/map/root/map-root.js +8 -1
- package/dist/navigation/root/navigation-root.d.ts +26 -0
- package/dist/navigation/root/navigation-root.js +58 -1
- package/dist/number-input/root/number-input-root.d.ts +63 -6
- package/dist/number-input/root/number-input-root.js +96 -11
- package/dist/object-inspector/root/object-inspector-root.d.ts +36 -1
- package/dist/object-inspector/root/object-inspector-root.js +26 -2
- package/dist/option/root/option-root.d.ts +32 -0
- package/dist/option/root/option-root.js +44 -5
- package/dist/outline/root/outline-root.d.ts +6 -2
- package/dist/outline/root/outline-root.js +7 -3
- package/dist/select/root/select-root.d.ts +29 -3
- package/dist/select/root/select-root.js +34 -9
- package/dist/slider/root/slider-root.d.ts +71 -9
- package/dist/slider/root/slider-root.js +129 -42
- package/dist/speed-dial/item/speed-dial-item.d.ts +29 -1
- package/dist/speed-dial/item/speed-dial-item.js +48 -7
- package/dist/speed-dial/root/speed-dial-root.d.ts +32 -6
- package/dist/speed-dial/root/speed-dial-root.js +50 -5
- package/dist/splitter/root/splitter-root.d.ts +22 -0
- package/dist/splitter/root/splitter-root.js +36 -7
- package/dist/steps/reach.d.ts +5 -1
- package/dist/steps/reach.js +2 -2
- package/dist/steps/root/steps-root.d.ts +37 -2
- package/dist/steps/root/steps-root.js +49 -7
- package/dist/steps/step/step.d.ts +6 -0
- package/dist/steps/step/step.js +4 -0
- package/dist/table/root/table-root.d.ts +20 -0
- package/dist/table/root/table-root.js +13 -3
- package/dist/tabs/root/tabs-root.d.ts +37 -0
- package/dist/tabs/root/tabs-root.js +92 -2
- package/dist/tabs/tab/tab.d.ts +38 -2
- package/dist/tabs/tab/tab.js +45 -4
- package/dist/tag/group/tag-group.d.ts +29 -3
- package/dist/tag/group/tag-group.js +42 -11
- package/dist/time-input/root/time-input-root.d.ts +32 -1
- package/dist/time-input/root/time-input-root.js +9 -2
- package/dist/time-picker/root/time-picker-root.d.ts +63 -1
- package/dist/time-picker/root/time-picker-root.js +58 -7
- package/dist/timeline/item/timeline-item.d.ts +25 -0
- package/dist/timeline/item/timeline-item.js +27 -2
- package/dist/toggle/root/toggle-root.d.ts +2 -0
- package/dist/toggle/root/toggle-root.js +1 -1
- package/dist/tree/item/tree-item.d.ts +10 -0
- package/dist/tree/item/tree-item.js +12 -0
- package/dist/tree/root/tree-root.d.ts +40 -1
- package/dist/tree/root/tree-root.js +120 -4
- package/dist/tree/selection.d.ts +6 -0
- package/dist/tree/selection.js +11 -0
- package/dist/video/root/keys.d.ts +6 -2
- package/dist/video/root/keys.js +18 -1
- package/dist/video/root/video-root.d.ts +60 -1
- package/dist/video/root/video-root.js +70 -2
- package/dist/video-playlist/item/video-playlist-item.d.ts +15 -1
- package/dist/video-playlist/item/video-playlist-item.js +7 -0
- package/dist/video-playlist/root/video-playlist-root.d.ts +43 -4
- package/dist/video-playlist/root/video-playlist-root.js +69 -2
- package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
- package/dist/virtual-list/root/virtual-list-root.js +3 -0
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +3885 -383
- package/types/vue.d.ts +1045 -107
- package/types/words.d.ts +146 -1
|
@@ -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 { commaList, customElement, deepActiveElement, drawStatus, focusItemOf, formatWords, HelixActionEvent, HelixElement, hostAria, inlineStep, KeyboardNavigationController, placeKey, pressedOwnControl, ReorderController, requestReorder, requestValue, rovingStop, StatusController, scrollingAncestor, scrollRoom, selectsAll, typeaheadByWords, whenHydrated, words, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { auditName, commaList, customElement, deepActiveElement, drawStatus, focusItemOf, formatWords, HelixActionEvent, HelixElement, HelixLoadMoreEvent, hostAria, inlineStep, KeyboardNavigationController, placeKey, pressedOwnControl, ReorderController, requestReorder, requestValue, rovingStop, StatusController, scrollingAncestor, scrollRoom, selectsAll, typeaheadByWords, whenHydrated, words, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import { HelixHeadlessListItem } from '../item/list-item.js';
|
|
@@ -15,7 +15,11 @@ let HelixHeadlessList = class HelixHeadlessList extends HelixElement {
|
|
|
15
15
|
super(...arguments);
|
|
16
16
|
this.canceledLabel = 'Canceled';
|
|
17
17
|
this.droppedLabel = 'Dropped';
|
|
18
|
+
this.emptyLabel = '';
|
|
18
19
|
this.label = '';
|
|
20
|
+
this.loading = false;
|
|
21
|
+
this.loadingLabel = 'Loading more';
|
|
22
|
+
this.loadMore = false;
|
|
19
23
|
this.movedLabel = '{name} moved to position {position} of {count}';
|
|
20
24
|
this.moveLabel = 'Move {name}';
|
|
21
25
|
this.pickedUpLabel = 'Picked up {name}';
|
|
@@ -81,7 +85,7 @@ let HelixHeadlessList = class HelixHeadlessList extends HelixElement {
|
|
|
81
85
|
}
|
|
82
86
|
if (selectsAll(event) && this.selectionMode === 'multiple') {
|
|
83
87
|
event.preventDefault();
|
|
84
|
-
const every = this.reachable.map((one) => this.valueFor(one)).join(',
|
|
88
|
+
const every = this.reachable.map((one) => this.valueFor(one)).join(',');
|
|
85
89
|
this.choose(every === this.value ? null : every);
|
|
86
90
|
}
|
|
87
91
|
else if (event.key === 'Escape' && this.value !== '' && this.selectionMode !== 'none') {
|
|
@@ -125,6 +129,10 @@ let HelixHeadlessList = class HelixHeadlessList extends HelixElement {
|
|
|
125
129
|
this.sync();
|
|
126
130
|
}
|
|
127
131
|
};
|
|
132
|
+
this.endInView = false;
|
|
133
|
+
this.handleEmptyChange = () => {
|
|
134
|
+
this.requestUpdate();
|
|
135
|
+
};
|
|
128
136
|
this.handleSlotChange = () => {
|
|
129
137
|
this.rowsChanged = true;
|
|
130
138
|
this.requestUpdate();
|
|
@@ -306,9 +314,15 @@ let HelixHeadlessList = class HelixHeadlessList extends HelixElement {
|
|
|
306
314
|
this.addEventListener('keydown', this.handleKeydown);
|
|
307
315
|
this.addEventListener('click', this.handleClick);
|
|
308
316
|
this.addEventListener('focusin', this.handleFocusin);
|
|
317
|
+
if (this.hasUpdated) {
|
|
318
|
+
this.watchEnd();
|
|
319
|
+
}
|
|
309
320
|
}
|
|
310
321
|
disconnectedCallback() {
|
|
311
322
|
super.disconnectedCallback();
|
|
323
|
+
this.endWatcher?.disconnect();
|
|
324
|
+
this.endWatcher = undefined;
|
|
325
|
+
this.watchedEnd = undefined;
|
|
312
326
|
this.removeEventListener('keydown', this.handleKeyCapture, true);
|
|
313
327
|
this.removeEventListener('keydown', this.handleKeydown);
|
|
314
328
|
this.removeEventListener('click', this.handleClick);
|
|
@@ -323,6 +337,17 @@ let HelixHeadlessList = class HelixHeadlessList extends HelixElement {
|
|
|
323
337
|
if (this.interactive || changed.get('selectionMode') !== undefined) {
|
|
324
338
|
this.sync();
|
|
325
339
|
}
|
|
340
|
+
if (this.interactive) {
|
|
341
|
+
auditName(this, () => null, 'Give it a `label`, or an `aria-label` that says what the rows are.', () => this.label !== '');
|
|
342
|
+
}
|
|
343
|
+
if (this.childrenKnown) {
|
|
344
|
+
this.toggleState('empty', this.rows.length === 0);
|
|
345
|
+
}
|
|
346
|
+
this.toggleState('loading', this.loading);
|
|
347
|
+
this.watchEnd();
|
|
348
|
+
if (this.endInView) {
|
|
349
|
+
this.askForMore();
|
|
350
|
+
}
|
|
326
351
|
if (this.reorderable || changed.get('reorderable') !== undefined) {
|
|
327
352
|
this.syncMoves();
|
|
328
353
|
this.refocus();
|
|
@@ -331,14 +356,64 @@ let HelixHeadlessList = class HelixHeadlessList extends HelixElement {
|
|
|
331
356
|
willUpdate(changed) {
|
|
332
357
|
super.willUpdate(changed);
|
|
333
358
|
hostAria(this.internals, { label: this.label, role: this.interactive ? 'grid' : 'list' });
|
|
359
|
+
this.internals.ariaBusy = this.loading ? 'true' : null;
|
|
334
360
|
this.internals.ariaMultiSelectable = this.selectionMode === 'multiple' ? 'true' : null;
|
|
335
361
|
}
|
|
336
362
|
get numbers() {
|
|
337
|
-
const counted = numbering(this.start, this.reversed, this.childrenKnown
|
|
363
|
+
const counted = numbering(this.start, this.reversed, this.childrenKnown
|
|
364
|
+
? this.knownChildren.filter((child) => !child.hasAttribute('slot')).length
|
|
365
|
+
: undefined);
|
|
338
366
|
return counted ? `--list-first: ${counted.first}; --list-step: ${counted.step}` : nothing;
|
|
339
367
|
}
|
|
340
368
|
render() {
|
|
341
|
-
return html `<slot style=${this.numbers} @slotchange=${this.handleSlotChange}></slot>${drawStatus(this.status)}${this.reorder.drawDragPreview()}`;
|
|
369
|
+
return html `<slot style=${this.numbers} @slotchange=${this.handleSlotChange}></slot>${this.drawLoading()}${this.drawEmpty()}${this.loadMore ? html `<span data-end aria-hidden="true"></span>` : nothing}${drawStatus(this.status)}${this.reorder.drawDragPreview()}`;
|
|
370
|
+
}
|
|
371
|
+
drawEmpty() {
|
|
372
|
+
if (!this.childrenKnown || this.rows.length > 0 || this.loading) {
|
|
373
|
+
return nothing;
|
|
374
|
+
}
|
|
375
|
+
const shown = this.emptyLabel !== '' || this.querySelector(':scope > [slot="empty"]') !== null;
|
|
376
|
+
const content = html `<slot name="empty" @slotchange=${this.handleEmptyChange}>${this.emptyLabel}</slot>`;
|
|
377
|
+
return this.interactive
|
|
378
|
+
? html `<div part="empty" role="row" ?hidden=${!shown}><div role="gridcell">${content}</div></div>`
|
|
379
|
+
: html `<div part="empty" role="listitem" ?hidden=${!shown}>${content}</div>`;
|
|
380
|
+
}
|
|
381
|
+
drawLoading() {
|
|
382
|
+
if (!this.loading) {
|
|
383
|
+
return nothing;
|
|
384
|
+
}
|
|
385
|
+
const content = html `<slot name="loading">${this.loadingLabel}</slot>`;
|
|
386
|
+
return this.interactive
|
|
387
|
+
? html `<div part="loading" role="row"><div role="gridcell">${content}</div></div>`
|
|
388
|
+
: html `<div part="loading" role="listitem">${content}</div>`;
|
|
389
|
+
}
|
|
390
|
+
watchEnd() {
|
|
391
|
+
const end = this.loadMore ? this.renderRoot.querySelector('[data-end]') : null;
|
|
392
|
+
if (end === this.watchedEnd) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
this.endWatcher?.disconnect();
|
|
396
|
+
this.endWatcher = undefined;
|
|
397
|
+
this.endInView = false;
|
|
398
|
+
this.watchedEnd = end ?? undefined;
|
|
399
|
+
if (!end || typeof IntersectionObserver === 'undefined') {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
this.endWatcher = new IntersectionObserver((entries) => {
|
|
403
|
+
this.endInView = entries.some((entry) => entry.isIntersecting);
|
|
404
|
+
if (this.endInView) {
|
|
405
|
+
this.askForMore();
|
|
406
|
+
}
|
|
407
|
+
}, { root: scrollingAncestor(this, { axis: 'y' }), rootMargin: '0px 0px 100% 0px' });
|
|
408
|
+
this.endWatcher.observe(end);
|
|
409
|
+
}
|
|
410
|
+
askForMore() {
|
|
411
|
+
const count = this.rows.length;
|
|
412
|
+
if (!this.loadMore || this.loading || this.askedAt === count) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
this.askedAt = count;
|
|
416
|
+
this.dispatchEvent(new HelixLoadMoreEvent());
|
|
342
417
|
}
|
|
343
418
|
};
|
|
344
419
|
__decorate([
|
|
@@ -349,12 +424,25 @@ __decorate([
|
|
|
349
424
|
words,
|
|
350
425
|
property({ attribute: 'dropped-label' })
|
|
351
426
|
], HelixHeadlessList.prototype, "droppedLabel", void 0);
|
|
427
|
+
__decorate([
|
|
428
|
+
property({ attribute: 'empty-label' })
|
|
429
|
+
], HelixHeadlessList.prototype, "emptyLabel", void 0);
|
|
352
430
|
__decorate([
|
|
353
431
|
property({ reflect: true })
|
|
354
432
|
], HelixHeadlessList.prototype, "kind", void 0);
|
|
355
433
|
__decorate([
|
|
356
434
|
property()
|
|
357
435
|
], HelixHeadlessList.prototype, "label", void 0);
|
|
436
|
+
__decorate([
|
|
437
|
+
property({ type: Boolean })
|
|
438
|
+
], HelixHeadlessList.prototype, "loading", void 0);
|
|
439
|
+
__decorate([
|
|
440
|
+
words,
|
|
441
|
+
property({ attribute: 'loading-label' })
|
|
442
|
+
], HelixHeadlessList.prototype, "loadingLabel", void 0);
|
|
443
|
+
__decorate([
|
|
444
|
+
property({ attribute: 'load-more', type: Boolean })
|
|
445
|
+
], HelixHeadlessList.prototype, "loadMore", void 0);
|
|
358
446
|
__decorate([
|
|
359
447
|
words,
|
|
360
448
|
property({ attribute: 'moved-label' })
|
package/dist/list/selection.js
CHANGED
|
@@ -7,7 +7,7 @@ export const choiceValue = ({ anchor, arrived = false, behavior, extend = false,
|
|
|
7
7
|
if (extend && mode === 'multiple') {
|
|
8
8
|
const from = anchor === undefined ? -1 : shown.indexOf(anchor);
|
|
9
9
|
const range = rangeAdd(now, shown, from, shown.indexOf(pressed));
|
|
10
|
-
return range ? range.join(',
|
|
10
|
+
return range ? range.join(',') : null;
|
|
11
11
|
}
|
|
12
12
|
if (behavior === 'replace' && !toggle) {
|
|
13
13
|
return now.length === 1 && now[0] === pressed ? null : pressed;
|
|
@@ -15,8 +15,8 @@ export const choiceValue = ({ anchor, arrived = false, behavior, extend = false,
|
|
|
15
15
|
if (arrived) {
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
|
-
const next = pressedValue(mode, value, pressed);
|
|
19
|
-
return next === value ? null : next;
|
|
18
|
+
const next = commaList(pressedValue(mode, value, pressed)).join(',');
|
|
19
|
+
return next === commaList(value).join(',') ? null : next;
|
|
20
20
|
};
|
|
21
21
|
export const rowStop = (count, at, step) => {
|
|
22
22
|
if (count <= 1) {
|
|
@@ -214,6 +214,12 @@ export declare class HelixHeadlessMap extends HelixHeadlessMap_base {
|
|
|
214
214
|
* above `0`.
|
|
215
215
|
*/
|
|
216
216
|
pitch: number;
|
|
217
|
+
/**
|
|
218
|
+
* What a screen reader calls the map in place of its role, after its
|
|
219
|
+
* name: "Edinburgh, map". Said only where the map has a name of its
|
|
220
|
+
* own, since the fallback name is the same word. Empty leaves the role.
|
|
221
|
+
*/
|
|
222
|
+
roleLabel: string;
|
|
217
223
|
/** The rotate left button's label. */
|
|
218
224
|
rotateLeftLabel: string;
|
|
219
225
|
/** The rotate right button's label. */
|
|
@@ -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 { customElement, focusItemOf, formatWords, HelixActivateEvent, HelixBeforeChangeEvent, HelixBeforeToggleEvent, HelixElement, HelixMapClusterEvent, HelixMapPanEvent, HelixMapPartChangeEvent, HelixMapReadyEvent, HelixMapRotateEvent, HelixMapTiltEvent, HelixMapZoomEvent, HelixToggleEvent, hostAria, KeyboardNavigationController, MixinViewport, requestChange, words, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { auditName, customElement, focusItemOf, formatWords, HelixActivateEvent, HelixBeforeChangeEvent, HelixBeforeToggleEvent, HelixElement, HelixMapClusterEvent, HelixMapPanEvent, HelixMapPartChangeEvent, HelixMapReadyEvent, HelixMapRotateEvent, HelixMapTiltEvent, HelixMapZoomEvent, HelixToggleEvent, hostAria, KeyboardNavigationController, MixinViewport, requestChange, words, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { buildings, cloudRain, dotsNine, leaf, mountains, path, polygon, stack, target, } from '@fusion.dev/helix-icons';
|
|
9
9
|
import { html, nothing } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
@@ -60,6 +60,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
|
|
|
60
60
|
this.movedUpLabel = 'Moved up';
|
|
61
61
|
this.northLabel = 'Facing north';
|
|
62
62
|
this.pitch = 0;
|
|
63
|
+
this.roleLabel = 'map';
|
|
63
64
|
this.rotateLeftLabel = 'Rotate left';
|
|
64
65
|
this.rotateRightLabel = 'Rotate right';
|
|
65
66
|
this.satelliteLabel = 'Satellite';
|
|
@@ -340,6 +341,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
|
|
|
340
341
|
firstUpdated(changed) {
|
|
341
342
|
super.firstUpdated(changed);
|
|
342
343
|
this.finishHydration();
|
|
344
|
+
auditName(this, () => null, 'Give it a `label` that says what the map shows, such as "Stores in Edinburgh".', () => this.label !== '');
|
|
343
345
|
}
|
|
344
346
|
updated(changed) {
|
|
345
347
|
super.updated(changed);
|
|
@@ -609,6 +611,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
|
|
|
609
611
|
part="region"
|
|
610
612
|
role="group"
|
|
611
613
|
aria-label=${this.label || this.getAttribute('aria-label') || this.fallbackLabel}
|
|
614
|
+
aria-roledescription=${(this.label || this.getAttribute('aria-label')) && this.roleLabel ? this.roleLabel : nothing}
|
|
612
615
|
aria-keyshortcuts=${this.keyShortcuts}
|
|
613
616
|
tabindex="0"
|
|
614
617
|
@focusin=${this.idle.stir}
|
|
@@ -705,6 +708,10 @@ __decorate([
|
|
|
705
708
|
__decorate([
|
|
706
709
|
property({ type: Number })
|
|
707
710
|
], HelixHeadlessMap.prototype, "pitch", void 0);
|
|
711
|
+
__decorate([
|
|
712
|
+
words,
|
|
713
|
+
property({ attribute: 'role-label' })
|
|
714
|
+
], HelixHeadlessMap.prototype, "roleLabel", void 0);
|
|
708
715
|
__decorate([
|
|
709
716
|
words,
|
|
710
717
|
property({ attribute: 'rotate-left-label' })
|
|
@@ -36,6 +36,15 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
36
36
|
* links and not a single control, and the keys across the other axis are
|
|
37
37
|
* left to the page and to an open panel.
|
|
38
38
|
*
|
|
39
|
+
* While a `Megamenu`, or another disclosure among the destinations, is open,
|
|
40
|
+
* the navigation is `:state(open)`, so a page can dim what lies under the
|
|
41
|
+
* panel, as Radix's navigation menu marks its open state.
|
|
42
|
+
*
|
|
43
|
+
* A screen reader lists the page's navigation landmarks by name, so two named
|
|
44
|
+
* alike cannot be told apart. Where another navigation in the same document
|
|
45
|
+
* has the same name, "Main" for two with no `label`, the later one warns once
|
|
46
|
+
* with the kind `duplicate-name`.
|
|
47
|
+
*
|
|
39
48
|
* @since 0.1.0
|
|
40
49
|
* @status stable
|
|
41
50
|
* @category navigation
|
|
@@ -47,6 +56,8 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
47
56
|
*
|
|
48
57
|
* @csspart indicator - The marker that moves to the current destination. It carries the destination's place and size as `--indicator-x`, `--indicator-y`, `--indicator-inline-size` and `--indicator-block-size`, and `data-placed` once it has been placed.
|
|
49
58
|
* @csspart nav - The navigation landmark.
|
|
59
|
+
*
|
|
60
|
+
* @cssstate open - While a megamenu or another disclosure among the destinations is open.
|
|
50
61
|
*/
|
|
51
62
|
export declare class HelixHeadlessNavigation extends HelixElement {
|
|
52
63
|
/**
|
|
@@ -80,6 +91,21 @@ export declare class HelixHeadlessNavigation extends HelixElement {
|
|
|
80
91
|
*/
|
|
81
92
|
orientation?: 'vertical';
|
|
82
93
|
private current;
|
|
94
|
+
/** The disclosures among the destinations that are open. */
|
|
95
|
+
private readonly opened;
|
|
96
|
+
/**
|
|
97
|
+
* Hears a disclosure among the destinations open or close. In capture,
|
|
98
|
+
* because `toggle` does not bubble.
|
|
99
|
+
*/
|
|
100
|
+
private readonly handleToggle;
|
|
101
|
+
/** The landmark's name as a screen reader lists it. */
|
|
102
|
+
private get landmarkName();
|
|
103
|
+
/**
|
|
104
|
+
* Warns once where another navigation in the same document has the same
|
|
105
|
+
* name, a task after the first render, so the page's other landmarks have
|
|
106
|
+
* rendered too.
|
|
107
|
+
*/
|
|
108
|
+
private auditTwin;
|
|
83
109
|
/**
|
|
84
110
|
* The destinations the consumer slotted, and the items of a slotted list in
|
|
85
111
|
* place of the list.
|
|
@@ -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, customElement, focusableElements, focusItemOf, HelixElement, IndicatorController, KeyboardNavigationController, words, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { AriaForwardController, customElement, focusableElements, focusItemOf, HelixElement, IndicatorController, KeyboardNavigationController, reportRuntime, words, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
const FOCUSABLE = 'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
@@ -23,6 +23,8 @@ const navigable = (item) => {
|
|
|
23
23
|
},
|
|
24
24
|
});
|
|
25
25
|
};
|
|
26
|
+
const connected = new Set();
|
|
27
|
+
const warned = new WeakSet();
|
|
26
28
|
let HelixHeadlessNavigation = class HelixHeadlessNavigation extends HelixElement {
|
|
27
29
|
constructor() {
|
|
28
30
|
super(...arguments);
|
|
@@ -53,8 +55,56 @@ let HelixHeadlessNavigation = class HelixHeadlessNavigation extends HelixElement
|
|
|
53
55
|
label: true,
|
|
54
56
|
target: () => this.partNamed('nav'),
|
|
55
57
|
});
|
|
58
|
+
this.opened = new Set();
|
|
59
|
+
this.handleToggle = (event) => {
|
|
60
|
+
const target = event.target;
|
|
61
|
+
if (target === this || target === null) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (event.newState === 'open') {
|
|
65
|
+
this.opened.add(target);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
this.opened.delete(target);
|
|
69
|
+
}
|
|
70
|
+
this.toggleState('open', this.opened.size > 0);
|
|
71
|
+
};
|
|
56
72
|
}
|
|
57
73
|
static { this.words = 'helix-navigation'; }
|
|
74
|
+
get landmarkName() {
|
|
75
|
+
const labeled = this.getAttribute('aria-labelledby');
|
|
76
|
+
if (labeled) {
|
|
77
|
+
const root = this.getRootNode();
|
|
78
|
+
return labeled
|
|
79
|
+
.split(/\s+/)
|
|
80
|
+
.map((id) => root.getElementById?.(id)?.textContent?.trim() ?? '')
|
|
81
|
+
.join(' ')
|
|
82
|
+
.trim();
|
|
83
|
+
}
|
|
84
|
+
return (this.getAttribute('aria-label') ?? this.label).trim();
|
|
85
|
+
}
|
|
86
|
+
auditTwin() {
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
if (!this.isConnected || warned.has(this)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const name = this.landmarkName;
|
|
92
|
+
const root = this.getRootNode();
|
|
93
|
+
const twin = [...connected].some((other) => other !== this &&
|
|
94
|
+
other.isConnected &&
|
|
95
|
+
(other.compareDocumentPosition(this) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0 &&
|
|
96
|
+
other.getRootNode() === root &&
|
|
97
|
+
other.landmarkName === name);
|
|
98
|
+
if (twin) {
|
|
99
|
+
warned.add(this);
|
|
100
|
+
reportRuntime({
|
|
101
|
+
kind: 'duplicate-name',
|
|
102
|
+
message: `<${this.localName}> has the name "${name}", which another navigation on the page has too, so a screen reader lists the two alike. Give each a \`label\` that says what it holds, such as "Footer".`,
|
|
103
|
+
tag: this.localName,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
58
108
|
get items() {
|
|
59
109
|
const slot = this.renderRoot.querySelector('slot:not([name])');
|
|
60
110
|
return (slot?.assignedElements() ?? []).flatMap((one) => isList(one) ? [...one.children] : [one]);
|
|
@@ -69,17 +119,24 @@ let HelixHeadlessNavigation = class HelixHeadlessNavigation extends HelixElement
|
|
|
69
119
|
}
|
|
70
120
|
connectedCallback() {
|
|
71
121
|
super.connectedCallback();
|
|
122
|
+
connected.add(this);
|
|
123
|
+
this.addEventListener('toggle', this.handleToggle, true);
|
|
72
124
|
if (this.hasUpdated) {
|
|
73
125
|
this.watch();
|
|
74
126
|
}
|
|
75
127
|
}
|
|
76
128
|
disconnectedCallback() {
|
|
77
129
|
super.disconnectedCallback();
|
|
130
|
+
connected.delete(this);
|
|
131
|
+
this.removeEventListener('toggle', this.handleToggle, true);
|
|
132
|
+
this.opened.clear();
|
|
133
|
+
this.toggleState('open', false);
|
|
78
134
|
this.current?.disconnect();
|
|
79
135
|
}
|
|
80
136
|
firstUpdated(changed) {
|
|
81
137
|
super.firstUpdated(changed);
|
|
82
138
|
this.watch();
|
|
139
|
+
this.auditTwin();
|
|
83
140
|
}
|
|
84
141
|
render() {
|
|
85
142
|
return html `<nav part="nav" aria-label=${this.label}>
|
|
@@ -25,17 +25,24 @@ declare const HelixHeadlessNumberInput_base: (new (...args: any[]) => import("@f
|
|
|
25
25
|
* The locale's group separators are read in every field, so `1,234` is a
|
|
26
26
|
* number in `en-US`.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
* `role
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* The field and its two buttons sit in a `role="group"`, and the field carries
|
|
29
|
+
* `role-label`, "number field" in English, as its `aria-roledescription`, as
|
|
30
|
+
* React Aria's NumberField does. On iOS and iPadOS the field is a plain text
|
|
31
|
+
* field, with no `role="spinbutton"`, which VoiceOver reads as a slider you
|
|
32
|
+
* cannot type in, and no role description, so VoiceOver says the field is
|
|
33
|
+
* required. React Aria's NumberField does the same.
|
|
32
34
|
*
|
|
33
35
|
* Validity follows the platform's rules: `min`, `max`, and `step` are checked
|
|
34
36
|
* and reported through the host's own `setValidity`, under the platform's flag
|
|
35
37
|
* names.
|
|
36
38
|
*
|
|
37
39
|
* Stepping is separate from typing, so a number is not corrected while you type
|
|
38
|
-
* it.
|
|
40
|
+
* it. While you type, the form holds the number the text reads as, so a field
|
|
41
|
+
* showing `$2,500.50` gives the form 2500.5, and text that is not a number
|
|
42
|
+
* gives it nothing, as a native number field does.
|
|
43
|
+
*
|
|
44
|
+
* Holding a stepper button down steps once, then again after 400 milliseconds,
|
|
45
|
+
* then every 60 milliseconds until you let go, as React Aria's NumberField does.
|
|
39
46
|
*
|
|
40
47
|
* **Keyboard.** The field is one tab stop, and the stepper buttons are not in
|
|
41
48
|
* the tab order.
|
|
@@ -56,9 +63,11 @@ declare const HelixHeadlessNumberInput_base: (new (...args: any[]) => import("@f
|
|
|
56
63
|
* @aliases NumberField, Stepper, InputNumber
|
|
57
64
|
* @documentation https://fusion.dev/helix/components/number-input
|
|
58
65
|
*
|
|
66
|
+
* @slot decrement-icon - Replaces the icon on the step down button.
|
|
59
67
|
* @slot end - Content after the number, such as a unit.
|
|
60
68
|
* @slot feedback - The message that says what is wrong, for content richer than a string. It shows while there is a message.
|
|
61
69
|
* @slot hint - Help under the control, for content richer than the `hint` text.
|
|
70
|
+
* @slot increment-icon - Replaces the icon on the step up button.
|
|
62
71
|
* @slot label - The label, for content richer than the `label` text.
|
|
63
72
|
* @slot start - Content before the number, such as a unit or an icon.
|
|
64
73
|
*
|
|
@@ -70,7 +79,7 @@ declare const HelixHeadlessNumberInput_base: (new (...args: any[]) => import("@f
|
|
|
70
79
|
* @csspart decrement - The button that steps the value down.
|
|
71
80
|
* @csspart decrement-mark - The icon on the step down button.
|
|
72
81
|
* @csspart feedback - The message that says what is wrong.
|
|
73
|
-
* @csspart field - The box that holds the whole control.
|
|
82
|
+
* @csspart field - The box that holds the whole control, a `role="group"` around the field and its buttons.
|
|
74
83
|
* @csspart hint - Help text under the field, shown while there is no feedback.
|
|
75
84
|
* @csspart increment - The button that steps the value up.
|
|
76
85
|
* @csspart increment-mark - The icon on the step up button.
|
|
@@ -175,6 +184,15 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
|
|
|
175
184
|
* Prevents editing. The value is still submitted with the form.
|
|
176
185
|
*/
|
|
177
186
|
readOnly: boolean;
|
|
187
|
+
/**
|
|
188
|
+
* The words screen readers announce for the field's role, set as its
|
|
189
|
+
* `aria-roledescription`. Translate it with the rest of your text, or empty
|
|
190
|
+
* it to leave the role as `spinbutton`.
|
|
191
|
+
*
|
|
192
|
+
* Never written on iOS and iPadOS, where a role description stops VoiceOver
|
|
193
|
+
* saying that the field is required.
|
|
194
|
+
*/
|
|
195
|
+
roleLabel: string;
|
|
178
196
|
/**
|
|
179
197
|
* Makes the label a grip that changes the value when it is dragged.
|
|
180
198
|
*
|
|
@@ -292,6 +310,26 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
|
|
|
292
310
|
* having to repeat both.
|
|
293
311
|
*/
|
|
294
312
|
stepDown(large?: boolean): void;
|
|
313
|
+
/** The timer of a held stepper button. */
|
|
314
|
+
private repeat;
|
|
315
|
+
/** Whether a pointer press stepped already, so the click that ends it does not step again. */
|
|
316
|
+
private pressStepped;
|
|
317
|
+
/**
|
|
318
|
+
* A stepper button pressed with a pointer steps at once, and again while it
|
|
319
|
+
* is held.
|
|
320
|
+
*
|
|
321
|
+
* Captured, so letting go outside the button still ends the hold. A click
|
|
322
|
+
* with no press before it, from a screen reader or a script, steps once in
|
|
323
|
+
* `handleStepperClick`.
|
|
324
|
+
*/
|
|
325
|
+
private readonly handleStepperDown;
|
|
326
|
+
/**
|
|
327
|
+
* Ends a hold. The click that follows a release arrives in the same task,
|
|
328
|
+
* so the press is forgotten after it.
|
|
329
|
+
*/
|
|
330
|
+
private readonly stopRepeat;
|
|
331
|
+
private readonly handleStepperClick;
|
|
332
|
+
disconnectedCallback(): void;
|
|
295
333
|
/**
|
|
296
334
|
* The label is the surface, which is where the convention already is.
|
|
297
335
|
*
|
|
@@ -391,6 +429,17 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
|
|
|
391
429
|
* what a reader pressing them meant.
|
|
392
430
|
*/
|
|
393
431
|
private readonly handleKeydown;
|
|
432
|
+
/**
|
|
433
|
+
* What the form holds for the field: the number the text reads as.
|
|
434
|
+
*
|
|
435
|
+
* The text as written where it is already a plain number, so a value of
|
|
436
|
+
* `0.50` is sent as written. A number written in the locale or the format,
|
|
437
|
+
* `$2,500.50` or `1.234,5`, is sent as the number, 2500.5 or 1234.5, so a
|
|
438
|
+
* form read while the field still has focus gets what a submit would. Text
|
|
439
|
+
* that is not a number is sent as empty, as a native number field sends it.
|
|
440
|
+
* The text itself is the state the browser restores.
|
|
441
|
+
*/
|
|
442
|
+
private submitted;
|
|
394
443
|
/**
|
|
395
444
|
* Reports the value and a verdict on it.
|
|
396
445
|
*
|
|
@@ -433,6 +482,14 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
|
|
|
433
482
|
private readonly handleKeypress;
|
|
434
483
|
/** @internal A single line of text counts against implicit submission. */
|
|
435
484
|
protected get blocksImplicitSubmission(): boolean;
|
|
485
|
+
/**
|
|
486
|
+
* The step down button's icon, as the `decrement-icon` slot's fallback, so an
|
|
487
|
+
* icon of your own replaces it by being slotted, and a tier above by
|
|
488
|
+
* overriding this.
|
|
489
|
+
*/
|
|
490
|
+
protected renderDecrementIcon(): TemplateResult;
|
|
491
|
+
/** The step up button's icon, behind the same seam as `renderDecrementIcon`. */
|
|
492
|
+
protected renderIncrementIcon(): TemplateResult;
|
|
436
493
|
protected render(): TemplateResult;
|
|
437
494
|
/**
|
|
438
495
|
* The value as a number, or `NaN` while the control is empty.
|