@fusion.dev/helix-headless 0.149.1 → 0.151.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 +1211 -110
- package/dist/area-chart/root/area-chart-root.d.ts +36 -0
- package/dist/area-chart/root/area-chart-root.js +17 -2
- package/dist/bar-chart/root/bar-chart-root.d.ts +28 -4
- package/dist/bar-chart/root/bar-chart-root.js +17 -4
- package/dist/button/root/button-root.d.ts +5 -0
- package/dist/button/root/button-root.js +3 -0
- package/dist/callout/root/callout-root.d.ts +20 -0
- package/dist/callout/root/callout-root.js +19 -4
- package/dist/chat/composer/chat-composer.d.ts +30 -0
- package/dist/chat/composer/chat-composer.js +47 -2
- package/dist/chat/root/chat-root.d.ts +10 -0
- package/dist/chat/root/chat-root.js +14 -0
- package/dist/color-input/root/color-input-root.d.ts +1 -0
- package/dist/color-picker/root/color-picker-root.js +1 -0
- package/dist/combobox/root/combobox-root.d.ts +12 -1
- package/dist/combobox/root/combobox-root.js +8 -0
- package/dist/copy-button/root/copy-button-root.d.ts +4 -3
- package/dist/copy-button/root/copy-button-root.js +5 -1
- package/dist/data-table/root/data-table-root.d.ts +34 -2
- package/dist/data-table/root/data-table-root.js +63 -2
- package/dist/date-input/root/date-input-root.d.ts +1 -0
- package/dist/divider/root/divider-root.d.ts +8 -6
- package/dist/divider/root/divider-root.js +10 -2
- package/dist/element-events.generated.js +5 -1
- package/dist/filter-input/chip/filter-chip.d.ts +2 -1
- package/dist/filter-input/root/filter-input-root.d.ts +5 -0
- package/dist/filter-input/root/filter-input-root.js +7 -0
- package/dist/kbd/root/kbd-root.d.ts +7 -1
- package/dist/kbd/root/kbd-root.js +8 -4
- package/dist/line-chart/root/line-chart-root.d.ts +36 -0
- package/dist/line-chart/root/line-chart-root.js +15 -2
- package/dist/number-input/root/number-input-root.d.ts +6 -0
- package/dist/number-input/root/number-input-root.js +14 -3
- package/dist/option/root/option-root.d.ts +17 -8
- package/dist/option/root/option-root.js +18 -4
- package/dist/radio/group/radio-group.d.ts +8 -1
- package/dist/radio/group/radio-group.js +15 -1
- package/dist/scatter-chart/root/scatter-chart-root.d.ts +18 -0
- package/dist/scatter-chart/root/scatter-chart-root.js +38 -15
- package/dist/slider/root/slider-root.d.ts +8 -0
- package/dist/slider/root/slider-root.js +6 -3
- package/dist/table/column/table-column.js +3 -2
- package/dist/table/grid-controller.d.ts +14 -2
- package/dist/table/grid-controller.js +27 -1
- package/dist/table/root/table-root.d.ts +30 -4
- package/dist/table/root/table-root.js +45 -7
- package/dist/table/row/table-row.d.ts +12 -2
- package/dist/table/row/table-row.js +8 -2
- package/dist/tag/group/tag-group.d.ts +22 -1
- package/dist/tag/group/tag-group.js +63 -3
- package/dist/tag/root/tag-root.d.ts +9 -0
- package/dist/tag/root/tag-root.js +5 -1
- package/dist/time-input/root/time-input-root.d.ts +1 -0
- package/dist/toggle/group/toggle-group.d.ts +6 -0
- package/dist/toggle/group/toggle-group.js +16 -1
- package/dist/toggle/root/toggle-root.d.ts +21 -3
- package/dist/toggle/root/toggle-root.js +22 -5
- package/dist/video/root/video-root.d.ts +6 -0
- package/dist/video/root/video-root.js +5 -1
- package/dist/video-playlist/root/video-playlist-root.d.ts +4 -0
- package/dist/video-playlist/root/video-playlist-root.js +4 -1
- package/dist/virtual-list/root/virtual-list-root.d.ts +23 -1
- package/dist/virtual-list/root/virtual-list-root.js +21 -2
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +808 -156
- package/types/vue.d.ts +223 -59
- package/types/words.d.ts +4 -2
|
@@ -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, assignSlots, commaList, customElement, cycle, everyState, followLink, format, formatWords, HelixElement, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, inRowOrder, MixinCollection, onAndroid, parse, ReorderController, reflectElements, requestChange, sort, splitValues, toggleEvery, whenHydrated, words, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { AriaForwardController, anchorAt, assignSlots, commaList, customElement, cycle, everyState, followLink, format, formatWords, HelixElement, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, inRowOrder, MixinCollection, onAndroid, parse, ReorderController, rangeAdd, reflectElements, requestChange, sort, splitValues, toggleEvery, whenHydrated, words, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { nothing } from 'lit';
|
|
9
9
|
import { property, state } from 'lit/decorators.js';
|
|
10
10
|
import { html } from 'lit/static-html.js';
|
|
@@ -44,9 +44,12 @@ const SAMPLE = 200;
|
|
|
44
44
|
const textOf = (element) => (element?.textContent ?? '').replace(/\s+/g, ' ').trim();
|
|
45
45
|
const lengthOf = (element) => textOf(element).length;
|
|
46
46
|
const numberOf = (value) => Number(value.replaceAll(',', ''));
|
|
47
|
+
const STATUS = new Set(['empty', 'loading']);
|
|
48
|
+
const NAMED = new Set([...STATUS, 'heading']);
|
|
47
49
|
let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixElement) {
|
|
48
50
|
constructor() {
|
|
49
51
|
super(...arguments);
|
|
52
|
+
this.statusSlots = true;
|
|
50
53
|
this.ascendingLabel = 'ascending';
|
|
51
54
|
this.canceledLabel = 'Canceled';
|
|
52
55
|
this.columns = '';
|
|
@@ -82,7 +85,9 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
82
85
|
on: () => this.grid,
|
|
83
86
|
part: (name) => this.partNamed(name),
|
|
84
87
|
rows: () => (this.childrenKnown ? this.arrangedRows : []),
|
|
85
|
-
|
|
88
|
+
rowWords: () => this.arrangedRows.map((row) => textOf(row.children[0])),
|
|
89
|
+
selectAll: () => this.selectable && this.requestSelected(new Set([...this.chosen, ...this.enabledKeys])),
|
|
90
|
+
selectRange: (to, from) => this.selectRange(to, from),
|
|
86
91
|
});
|
|
87
92
|
this.reorder = new ReorderController(this, {
|
|
88
93
|
axis: 'block',
|
|
@@ -125,6 +130,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
125
130
|
event.preventDefault();
|
|
126
131
|
this.chooseRow(found.key, event.detail.checked);
|
|
127
132
|
};
|
|
133
|
+
this.anchorKey = undefined;
|
|
128
134
|
this.handleHeadClick = (event) => {
|
|
129
135
|
const path = event.composedPath();
|
|
130
136
|
if (!path.some((node) => node instanceof Element && node.getAttribute('part') === 'sort')) {
|
|
@@ -260,10 +266,35 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
260
266
|
get everyKey() {
|
|
261
267
|
return this.keyed.map(({ key }) => key);
|
|
262
268
|
}
|
|
269
|
+
get enabledKeys() {
|
|
270
|
+
return this.keyed.filter(({ row }) => !row.disabled).map(({ key }) => key);
|
|
271
|
+
}
|
|
272
|
+
isDisabled(key) {
|
|
273
|
+
return this.keyed.find((one) => one.key === key)?.row.disabled === true;
|
|
274
|
+
}
|
|
263
275
|
chooseAll() {
|
|
264
|
-
this.requestSelected(new Set(toggleEvery(this.chosen, this.
|
|
276
|
+
this.requestSelected(new Set(toggleEvery(this.chosen, this.enabledKeys)));
|
|
277
|
+
}
|
|
278
|
+
selectRange(to, from) {
|
|
279
|
+
if (!this.selectable) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
const keyOf = new Map(this.keyed.map(({ key, row }) => [row, key]));
|
|
283
|
+
const keys = this.arrangedRows.map((row) => keyOf.get(row) ?? '');
|
|
284
|
+
const { anchor, anchorKey } = anchorAt(keys, this.anchorKey, from);
|
|
285
|
+
this.anchorKey = anchorKey;
|
|
286
|
+
const now = this.chosen;
|
|
287
|
+
const next = rangeAdd([...now], keys, anchor, to)?.filter((key) => now.has(key) || !this.isDisabled(key));
|
|
288
|
+
if (next && next.length > now.size) {
|
|
289
|
+
this.requestSelected(new Set(next));
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
265
292
|
}
|
|
266
293
|
chooseRow(key, on) {
|
|
294
|
+
if (this.isDisabled(key)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
this.anchorKey = key;
|
|
267
298
|
const keys = this.chosen;
|
|
268
299
|
if (on) {
|
|
269
300
|
keys.add(key);
|
|
@@ -298,7 +329,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
298
329
|
this.chooseRow(found.key, !this.chosen.has(found.key));
|
|
299
330
|
return true;
|
|
300
331
|
}
|
|
301
|
-
if (event.key === 'Enter' && row?.href) {
|
|
332
|
+
if (event.key === 'Enter' && row?.href && !row.disabled) {
|
|
302
333
|
followLink(this.ownerDocument, { href: row.href, rel: row.rel, target: row.target }, event);
|
|
303
334
|
return true;
|
|
304
335
|
}
|
|
@@ -418,7 +449,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
418
449
|
}; }
|
|
419
450
|
get slotted() {
|
|
420
451
|
const columns = new Set(this.tableColumns);
|
|
421
|
-
return [...(this.children ?? [])].filter((child) => !columns.has(child) && child.getAttribute('slot')
|
|
452
|
+
return [...(this.children ?? [])].filter((child) => !columns.has(child) && !NAMED.has(child.getAttribute('slot') ?? ''));
|
|
422
453
|
}
|
|
423
454
|
assign() {
|
|
424
455
|
if (this.assignsByName) {
|
|
@@ -429,7 +460,14 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
429
460
|
}
|
|
430
461
|
const columns = new Set(this.tableColumns);
|
|
431
462
|
const headings = new Set(this.headings);
|
|
432
|
-
|
|
463
|
+
const status = [...(this.children ?? [])].filter((child) => STATUS.has(child.getAttribute('slot') ?? ''));
|
|
464
|
+
assignSlots(this.renderRoot, [...columns, ...headings, ...status, ...this.arrangedRows], (child) => columns.has(child)
|
|
465
|
+
? 'columns'
|
|
466
|
+
: headings.has(child)
|
|
467
|
+
? 'heading'
|
|
468
|
+
: status.includes(child)
|
|
469
|
+
? (child.getAttribute('slot') ?? '')
|
|
470
|
+
: '');
|
|
433
471
|
}
|
|
434
472
|
alignCells() {
|
|
435
473
|
if (typeof CSSStyleSheet === 'undefined' || !('replaceSync' in CSSStyleSheet.prototype)) {
|
|
@@ -483,7 +521,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
483
521
|
if (!this.selectable) {
|
|
484
522
|
return nothing;
|
|
485
523
|
}
|
|
486
|
-
const every = this.childrenKnown ? this.
|
|
524
|
+
const every = this.childrenKnown ? this.enabledKeys : [];
|
|
487
525
|
const { checked, indeterminate } = everyState(this.chosen, every);
|
|
488
526
|
const checkbox = this.tag('checkbox');
|
|
489
527
|
const grid = this.grid;
|
|
@@ -13,7 +13,8 @@ export type TableRowMove = 'dragging' | 'drop-after' | 'drop-before';
|
|
|
13
13
|
* it, or make it interactive.
|
|
14
14
|
*
|
|
15
15
|
* In a `selectable` table the row draws a checkbox cell before its own cells,
|
|
16
|
-
* and `value` is the key the table's `selected` lists it by.
|
|
16
|
+
* and `value` is the key the table's `selected` lists it by. `disabled` keeps
|
|
17
|
+
* the row out of the selection and out of its link.
|
|
17
18
|
*
|
|
18
19
|
* In a `reorderable` table the row draws a grip cell before those, holding a
|
|
19
20
|
* button named by the table's `move-row-label`, and `value` is the key
|
|
@@ -56,7 +57,7 @@ export type TableRowMove = 'dragging' | 'drop-after' | 'drop-before';
|
|
|
56
57
|
* @cssstate dragging - The row is being moved, in a `reorderable` table.
|
|
57
58
|
* @cssstate drop-after - The row being moved goes after this one when it is dropped.
|
|
58
59
|
* @cssstate drop-before - The row being moved goes before this one when it is dropped.
|
|
59
|
-
* @cssstate link - While the row has `href`, so a press on it follows the link.
|
|
60
|
+
* @cssstate link - While the row has `href` and is not `disabled`, so a press on it follows the link.
|
|
60
61
|
* @cssstate selected - While the row is selected, in a `selectable` table.
|
|
61
62
|
*/
|
|
62
63
|
export declare class HelixHeadlessTableRow extends HelixElement {
|
|
@@ -65,6 +66,15 @@ export declare class HelixHeadlessTableRow extends HelixElement {
|
|
|
65
66
|
* finds its columns. Not a consumer's to read or set.
|
|
66
67
|
*/
|
|
67
68
|
readonly isTableRow = true;
|
|
69
|
+
/**
|
|
70
|
+
* Takes the row out of the table's selection and out of its link: the row's
|
|
71
|
+
* checkbox is disabled, Space, Shift ranges, and the checkbox in the heading
|
|
72
|
+
* pass over it, and a press does not follow `href`. A row already selected
|
|
73
|
+
* stays selected. A screen reader hears it as unavailable, and in a `grid`
|
|
74
|
+
* table the arrow keys still reach it, as a disabled cell in a grid is
|
|
75
|
+
* reached.
|
|
76
|
+
*/
|
|
77
|
+
disabled: boolean;
|
|
68
78
|
/**
|
|
69
79
|
* The URL a press on the row follows, which makes the whole row a link.
|
|
70
80
|
* Empty, the default, leaves presses to the row's cells.
|
|
@@ -15,6 +15,7 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
|
|
|
15
15
|
constructor() {
|
|
16
16
|
super(...arguments);
|
|
17
17
|
this.isTableRow = true;
|
|
18
|
+
this.disabled = false;
|
|
18
19
|
this.href = '';
|
|
19
20
|
this.rel = '';
|
|
20
21
|
this.target = '';
|
|
@@ -30,7 +31,7 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
|
|
|
30
31
|
this.selectStop = false;
|
|
31
32
|
this.columnRules = undefined;
|
|
32
33
|
this.handleClick = (event) => {
|
|
33
|
-
if (this.href === '' || event.button !== 0 || event.defaultPrevented) {
|
|
34
|
+
if (this.disabled || this.href === '' || event.button !== 0 || event.defaultPrevented) {
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
36
37
|
const cell = event
|
|
@@ -68,8 +69,9 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
|
|
|
68
69
|
super.willUpdate(changed);
|
|
69
70
|
hostAria(this.internals, { role: 'row' });
|
|
70
71
|
this.internals.ariaSelected = this.gridCell && this.selectable ? String(this.selected) : null;
|
|
72
|
+
this.internals.ariaDisabled = this.disabled ? 'true' : null;
|
|
71
73
|
this.toggleState('selected', this.selectable && this.selected);
|
|
72
|
-
this.toggleState('link', this.href !== '');
|
|
74
|
+
this.toggleState('link', this.href !== '' && !this.disabled);
|
|
73
75
|
this.toggleState('dragging', this.move === 'dragging');
|
|
74
76
|
this.toggleState('drop-after', this.move === 'drop-after');
|
|
75
77
|
this.toggleState('drop-before', this.move === 'drop-before');
|
|
@@ -100,6 +102,7 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
|
|
|
100
102
|
mark-checked: checkbox-mark-checked, mark-dash: checkbox-mark-dash,
|
|
101
103
|
mark-indeterminate: checkbox-mark-indeterminate"
|
|
102
104
|
aria-label=${this.selectLabel}
|
|
105
|
+
?disabled=${this.disabled}
|
|
103
106
|
.checked=${this.selected}
|
|
104
107
|
></${checkbox}
|
|
105
108
|
></span>`;
|
|
@@ -126,6 +129,9 @@ let HelixHeadlessTableRow = class HelixHeadlessTableRow extends HelixElement {
|
|
|
126
129
|
return html `${this.renderGrip()}${this.renderSelect()}<slot @slotchange=${this.handleSlotChange}></slot>`;
|
|
127
130
|
}
|
|
128
131
|
};
|
|
132
|
+
__decorate([
|
|
133
|
+
property({ reflect: true, type: Boolean })
|
|
134
|
+
], HelixHeadlessTableRow.prototype, "disabled", void 0);
|
|
129
135
|
__decorate([
|
|
130
136
|
property()
|
|
131
137
|
], HelixHeadlessTableRow.prototype, "href", void 0);
|
|
@@ -15,7 +15,10 @@ export type TagSelectionMode = SelectionMode;
|
|
|
15
15
|
* focus moves to the next tag, or the previous one after the last.
|
|
16
16
|
*
|
|
17
17
|
* `selection-mode` lets a reader select tags, with Space, Enter or a press:
|
|
18
|
-
* `single` selects one at a time and `multiple` any number. `
|
|
18
|
+
* `single` selects one at a time and `multiple` any number. In `multiple`,
|
|
19
|
+
* Shift with an arrow, Home or End selects the range from the tag last
|
|
20
|
+
* pressed to the tag focus moves to, and Control or Command with A selects
|
|
21
|
+
* every tag that is not disabled. `value` holds the
|
|
19
22
|
* selected tags' values, separated by commas, and a change asks with
|
|
20
23
|
* `helix-before-change` and tells with `change`. Enter on a tag with an
|
|
21
24
|
* `href` follows its link. A `disabled` tag is passed over. A selected tag
|
|
@@ -30,6 +33,8 @@ export type TagSelectionMode = SelectionMode;
|
|
|
30
33
|
* set is a `group`, and focus moves to its `empty` part,
|
|
31
34
|
* which holds the `empty` slot: put words there such as "No filters" for a
|
|
32
35
|
* reader to hear. With nothing slotted it says `empty-label`, "No tags".
|
|
36
|
+
* Where only disabled tags are left, the first of them holds the tab stop and
|
|
37
|
+
* takes focus, so the reader stays in the set.
|
|
33
38
|
*
|
|
34
39
|
* @since 0.1.0
|
|
35
40
|
* @status stable
|
|
@@ -77,6 +82,12 @@ export declare class HelixHeadlessTagGroup extends HelixElement {
|
|
|
77
82
|
value: string | readonly string[];
|
|
78
83
|
/** The tag the reader was last on, which keeps the tab stop. */
|
|
79
84
|
private last;
|
|
85
|
+
/**
|
|
86
|
+
* Where a range chosen with Shift and an arrow starts, and where it reached,
|
|
87
|
+
* in `multiple`: the tag last pressed or moved to without Shift.
|
|
88
|
+
*/
|
|
89
|
+
private anchor;
|
|
90
|
+
private reached;
|
|
80
91
|
/**
|
|
81
92
|
* The arrow keys, Home, End and typed letters, moving focus and choosing
|
|
82
93
|
* nothing. From the focused tag rather than the selection, since a reader
|
|
@@ -119,6 +130,16 @@ export declare class HelixHeadlessTagGroup extends HelixElement {
|
|
|
119
130
|
* has moved.
|
|
120
131
|
*/
|
|
121
132
|
protected press(tag: HelixHeadlessTag): void;
|
|
133
|
+
/** The reachable tags from one to the other, either way round. */
|
|
134
|
+
private span;
|
|
135
|
+
/**
|
|
136
|
+
* Shift and an arrow, Home or End, in `multiple`: the range from the anchor
|
|
137
|
+
* to the tag focus moved to is selected, in place of the range the last such
|
|
138
|
+
* key selected, as React Aria's selection extends.
|
|
139
|
+
*/
|
|
140
|
+
private extend;
|
|
141
|
+
/** Asks for `value` to hold these tags' values, in their order. */
|
|
142
|
+
private choose;
|
|
122
143
|
private readonly handleKeydown;
|
|
123
144
|
private readonly handleClick;
|
|
124
145
|
/**
|
|
@@ -23,7 +23,29 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
|
|
|
23
23
|
});
|
|
24
24
|
this.handleKeydown = (event) => {
|
|
25
25
|
const tag = this.tagOf(event);
|
|
26
|
-
if (!tag || tag.disabled || event.altKey
|
|
26
|
+
if (!tag || tag.disabled || event.altKey) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const multiple = this.selectionMode === 'multiple';
|
|
30
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') {
|
|
31
|
+
if (multiple) {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
this.choose(this.reachable);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (event.ctrlKey || event.metaKey) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (MOVES.has(event.key)) {
|
|
41
|
+
const to = this.last;
|
|
42
|
+
if (multiple && event.shiftKey && to && to !== tag) {
|
|
43
|
+
this.extend(tag, to);
|
|
44
|
+
}
|
|
45
|
+
else if (!event.shiftKey) {
|
|
46
|
+
this.anchor = to;
|
|
47
|
+
this.reached = to;
|
|
48
|
+
}
|
|
27
49
|
return;
|
|
28
50
|
}
|
|
29
51
|
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
@@ -70,7 +92,15 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
|
|
|
70
92
|
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
|
71
93
|
}
|
|
72
94
|
const active = document.activeElement;
|
|
73
|
-
if (!had ||
|
|
95
|
+
if (!had || (active !== null && active !== document.body)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const [first] = this.tags;
|
|
99
|
+
if (first) {
|
|
100
|
+
if (this.reachable.length === 0) {
|
|
101
|
+
this.sync();
|
|
102
|
+
first.focusItem();
|
|
103
|
+
}
|
|
74
104
|
return;
|
|
75
105
|
}
|
|
76
106
|
this.requestUpdate();
|
|
@@ -126,7 +156,10 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
|
|
|
126
156
|
selected: chosen.has(this.valueFor(tag)),
|
|
127
157
|
tag,
|
|
128
158
|
}));
|
|
129
|
-
const holder = rovingStop(items, items.find((item) => item.tag === this.last), {
|
|
159
|
+
const holder = rovingStop(items, items.find((item) => item.tag === this.last), {
|
|
160
|
+
disabled: (item) => item.disabled,
|
|
161
|
+
selected: (item) => item.selected,
|
|
162
|
+
})?.tag ?? tags[0];
|
|
130
163
|
for (const tag of tags) {
|
|
131
164
|
whenHydrated(tag, () => {
|
|
132
165
|
tag.focusable = tag === holder;
|
|
@@ -144,10 +177,36 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
|
|
|
144
177
|
if (mode === 'none' || tag.disabled) {
|
|
145
178
|
return;
|
|
146
179
|
}
|
|
180
|
+
this.anchor = tag;
|
|
181
|
+
this.reached = tag;
|
|
147
182
|
requestValue(this, pressedValue(mode, this.written, this.valueFor(tag)), (value) => {
|
|
148
183
|
this.value = value;
|
|
149
184
|
});
|
|
150
185
|
}
|
|
186
|
+
span(from, to) {
|
|
187
|
+
const tags = this.reachable;
|
|
188
|
+
const [start, end] = [tags.indexOf(from), tags.indexOf(to)].sort((a, b) => a - b);
|
|
189
|
+
return start === undefined || end === undefined || start < 0 ? [] : tags.slice(start, end + 1);
|
|
190
|
+
}
|
|
191
|
+
extend(from, to) {
|
|
192
|
+
const anchor = this.anchor?.isConnected ? this.anchor : from;
|
|
193
|
+
const before = new Set(this.span(anchor, this.reached?.isConnected ? this.reached : from));
|
|
194
|
+
const after = this.span(anchor, to);
|
|
195
|
+
this.anchor = anchor;
|
|
196
|
+
this.reached = to;
|
|
197
|
+
const kept = this.tags.filter((tag) => commaList(this.written).includes(this.valueFor(tag)) && !before.has(tag));
|
|
198
|
+
const chosen = [...new Set([...kept, ...after])];
|
|
199
|
+
this.choose(this.tags.filter((tag) => chosen.includes(tag)));
|
|
200
|
+
}
|
|
201
|
+
choose(tags) {
|
|
202
|
+
const next = [...new Set(tags.map((tag) => this.valueFor(tag)))].join(', ');
|
|
203
|
+
if (next === this.written) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
requestValue(this, next, (value) => {
|
|
207
|
+
this.value = value;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
151
210
|
connectedCallback() {
|
|
152
211
|
super.connectedCallback();
|
|
153
212
|
this.addEventListener('keydown', this.handleKeydown);
|
|
@@ -199,3 +258,4 @@ HelixHeadlessTagGroup = __decorate([
|
|
|
199
258
|
customElement('helix-headless-tag-group')
|
|
200
259
|
], HelixHeadlessTagGroup);
|
|
201
260
|
export { HelixHeadlessTagGroup };
|
|
261
|
+
const MOVES = new Set(['ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'End', 'Home']);
|
|
@@ -70,6 +70,15 @@ export declare class HelixHeadlessTag extends HelixHeadlessTag_base {
|
|
|
70
70
|
* tag's words in it, so a translation puts them where its language does.
|
|
71
71
|
*/
|
|
72
72
|
dismissLabel: string;
|
|
73
|
+
/**
|
|
74
|
+
* Whether the remove button is a tab stop of its own, outside a `Tag.Group`.
|
|
75
|
+
*
|
|
76
|
+
* A field that holds tags as its tokens, such as a `multiple` Combobox,
|
|
77
|
+
* writes `false`: the field's text is its one tab stop, and the arrow keys
|
|
78
|
+
* move from the text to the tags. No attribute: it is coordination between
|
|
79
|
+
* elements, as `focusable` is.
|
|
80
|
+
*/
|
|
81
|
+
dismissTabStop: boolean;
|
|
73
82
|
/**
|
|
74
83
|
* Whether the tag holds its group's tab stop.
|
|
75
84
|
*
|
|
@@ -12,6 +12,7 @@ let HelixHeadlessTag = class HelixHeadlessTag extends MixinDismissible(HelixElem
|
|
|
12
12
|
super(...arguments);
|
|
13
13
|
this.disabled = false;
|
|
14
14
|
this.dismissLabel = 'Remove {label}';
|
|
15
|
+
this.dismissTabStop = true;
|
|
15
16
|
this.href = '';
|
|
16
17
|
this.value = '';
|
|
17
18
|
}
|
|
@@ -87,7 +88,7 @@ let HelixHeadlessTag = class HelixHeadlessTag extends MixinDismissible(HelixElem
|
|
|
87
88
|
label: this.removeName,
|
|
88
89
|
mark: html `<slot name="dismiss-icon">${drawDismissGlyph()}</slot>`,
|
|
89
90
|
onDismiss: () => this.dismiss(),
|
|
90
|
-
tabindex: this.grouped ? '-1' : undefined,
|
|
91
|
+
tabindex: this.grouped || !this.dismissTabStop ? '-1' : undefined,
|
|
91
92
|
});
|
|
92
93
|
}
|
|
93
94
|
};
|
|
@@ -98,6 +99,9 @@ __decorate([
|
|
|
98
99
|
words,
|
|
99
100
|
property({ attribute: 'dismiss-label' })
|
|
100
101
|
], HelixHeadlessTag.prototype, "dismissLabel", void 0);
|
|
102
|
+
__decorate([
|
|
103
|
+
property({ attribute: false })
|
|
104
|
+
], HelixHeadlessTag.prototype, "dismissTabStop", void 0);
|
|
101
105
|
__decorate([
|
|
102
106
|
property({ attribute: false })
|
|
103
107
|
], HelixHeadlessTag.prototype, "focusable", void 0);
|
|
@@ -42,6 +42,7 @@ declare const HelixHeadlessTimeInput_base: (new (...args: any[]) => import("@fus
|
|
|
42
42
|
*
|
|
43
43
|
* @slot feedback - The message that says what is wrong with the value.
|
|
44
44
|
* @slot hint - Help under the field.
|
|
45
|
+
* @slot label - The label, for content richer than the `label` text.
|
|
45
46
|
* @slot panel-footer - Content in the panel under the times, such as a note or an action.
|
|
46
47
|
* @slot panel-header - Content in the panel above the times, such as a heading or a shortcut.
|
|
47
48
|
* @slot trigger - The icon on the field that shows a list opens.
|
|
@@ -191,7 +191,13 @@ export declare class HelixHeadlessToggleGroup extends HelixHeadlessToggleGroup_b
|
|
|
191
191
|
private readonly indicator;
|
|
192
192
|
protected readonly keys: KeyboardNavigationController;
|
|
193
193
|
protected readonly cascade: DisabledCascadeController;
|
|
194
|
+
/**
|
|
195
|
+
* The toggles, in light-DOM order, at any depth, so a toggle inside a layout
|
|
196
|
+
* wrapper of yours is one of the group's. A toggle held by a group nested
|
|
197
|
+
* inside this one is that group's.
|
|
198
|
+
*/
|
|
194
199
|
private get items();
|
|
200
|
+
private observer;
|
|
195
201
|
private readonly handleSelection;
|
|
196
202
|
/**
|
|
197
203
|
* Whether the group holds an answer the form can submit: a value an item
|
|
@@ -101,7 +101,10 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
|
|
|
101
101
|
null);
|
|
102
102
|
}
|
|
103
103
|
get items() {
|
|
104
|
-
|
|
104
|
+
if (this.children === undefined) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
return [...this.querySelectorAll('*')].filter((element) => element instanceof HelixHeadlessToggle && nearestGroup(element) === this);
|
|
105
108
|
}
|
|
106
109
|
get answered() {
|
|
107
110
|
if (this.value === '') {
|
|
@@ -118,11 +121,15 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
|
|
|
118
121
|
super.connectedCallback();
|
|
119
122
|
this.addEventListener('helix-select', this.handleSelection);
|
|
120
123
|
this.addEventListener('focusin', this.noteFocus);
|
|
124
|
+
this.observer = new MutationObserver(this.handleItems);
|
|
125
|
+
this.observer.observe(this, { childList: true, subtree: true });
|
|
121
126
|
}
|
|
122
127
|
disconnectedCallback() {
|
|
123
128
|
super.disconnectedCallback();
|
|
124
129
|
this.removeEventListener('helix-select', this.handleSelection);
|
|
125
130
|
this.removeEventListener('focusin', this.noteFocus);
|
|
131
|
+
this.observer?.disconnect();
|
|
132
|
+
this.observer = undefined;
|
|
126
133
|
}
|
|
127
134
|
firstUpdated(changed) {
|
|
128
135
|
super.firstUpdated(changed);
|
|
@@ -146,6 +153,7 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
|
|
|
146
153
|
this.adopt();
|
|
147
154
|
hostAria(this.internals, { label: this.label, role: this.groupRole });
|
|
148
155
|
this.internals.ariaOrientation = this.orientation ?? null;
|
|
156
|
+
this.internals.ariaRequired = this.required && !this.multiple ? 'true' : null;
|
|
149
157
|
if (!this.answered) {
|
|
150
158
|
this.internals.setFormValue(null);
|
|
151
159
|
}
|
|
@@ -203,3 +211,10 @@ HelixHeadlessToggleGroup = __decorate([
|
|
|
203
211
|
customElement('helix-headless-toggle-group')
|
|
204
212
|
], HelixHeadlessToggleGroup);
|
|
205
213
|
export { HelixHeadlessToggleGroup };
|
|
214
|
+
function nearestGroup(toggle) {
|
|
215
|
+
let parent = toggle.parentElement;
|
|
216
|
+
while (parent && !(parent instanceof HelixHeadlessToggleGroup)) {
|
|
217
|
+
parent = parent.parentElement;
|
|
218
|
+
}
|
|
219
|
+
return parent;
|
|
220
|
+
}
|
|
@@ -2,13 +2,18 @@ import { HelixElement, HostClickController } from '@fusion.dev/helix-core';
|
|
|
2
2
|
import { type PropertyValues, type TemplateResult } from 'lit';
|
|
3
3
|
declare const HelixHeadlessToggle_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinSelectableItemInterface) & typeof HelixElement;
|
|
4
4
|
/**
|
|
5
|
-
* @summary One option in a toggle group.
|
|
5
|
+
* @summary One option in a toggle group, or a toggle button on its own.
|
|
6
6
|
*
|
|
7
7
|
* @description
|
|
8
8
|
* Renders a `<button role="radio">` for use as a form control. In a group with
|
|
9
9
|
* `selection-mode="multiple"` it is a `<button>` that says whether it is
|
|
10
10
|
* pressed with `aria-pressed`.
|
|
11
11
|
*
|
|
12
|
+
* **On its own, outside a `Toggle.Group`, it is a toggle button**, as React
|
|
13
|
+
* Aria's `ToggleButton`: a `<button>` with `aria-pressed`. A press, Enter or
|
|
14
|
+
* Space sends `helix-select`, which you can cancel, then flips `selected` and
|
|
15
|
+
* sends `change`. Write `selected` in the markup to start it pressed.
|
|
16
|
+
*
|
|
12
17
|
* Do not use it for navigation. For navigation, use links marked with
|
|
13
18
|
* `aria-current` and styled by the `data-navigation-item` recipe in the base
|
|
14
19
|
* layer. The recipe has the same look, and links are not announced as choices.
|
|
@@ -21,6 +26,7 @@ declare const HelixHeadlessToggle_base: (new (...args: any[]) => import("@fusion
|
|
|
21
26
|
*
|
|
22
27
|
* @slot - The item's label.
|
|
23
28
|
*
|
|
29
|
+
* @event {Event} change - Fired after a toggle button on its own is pressed or released.
|
|
24
30
|
* @event {HelixSelectEvent} helix-select - Fired when the item is chosen. Cancelable: call `preventDefault()` to keep the current choice.
|
|
25
31
|
*
|
|
26
32
|
* @csspart item - The radio button.
|
|
@@ -61,8 +67,8 @@ export declare class HelixHeadlessToggle extends HelixHeadlessToggle_base {
|
|
|
61
67
|
get defaultSelected(): boolean;
|
|
62
68
|
set defaultSelected(next: boolean);
|
|
63
69
|
/**
|
|
64
|
-
* Whether the item holds the group's tab stop. Unset,
|
|
65
|
-
* it.
|
|
70
|
+
* Whether the item holds the group's tab stop. Unset, no group holds the
|
|
71
|
+
* item, and it is a toggle button with a tab stop of its own.
|
|
66
72
|
*
|
|
67
73
|
* Written by the group on every item: `true` for the chosen item, or for the
|
|
68
74
|
* first item that can be chosen while there is no selection to carry the
|
|
@@ -79,6 +85,18 @@ export declare class HelixHeadlessToggle extends HelixHeadlessToggle_base {
|
|
|
79
85
|
* Written by the group, like `selected`, so no attribute.
|
|
80
86
|
*/
|
|
81
87
|
pressed?: boolean | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Whether no `Toggle.Group` holds the item, which makes it a toggle button
|
|
90
|
+
* of its own. A group writes `focusable` on every item it holds. A server,
|
|
91
|
+
* and the render that hydrates its markup, cannot tell, and draw the item a
|
|
92
|
+
* group holds.
|
|
93
|
+
*/
|
|
94
|
+
private get alone();
|
|
95
|
+
/**
|
|
96
|
+
* Chooses the item in its group, or, on its own, flips `selected` once
|
|
97
|
+
* `helix-select` stands and sends `change`.
|
|
98
|
+
*/
|
|
99
|
+
select(): boolean;
|
|
82
100
|
protected willUpdate(changed: PropertyValues): void;
|
|
83
101
|
/**
|
|
84
102
|
* Asks for the render that follows a hydration, so the stop stops being every
|
|
@@ -18,23 +18,40 @@ let HelixHeadlessToggle = class HelixHeadlessToggle extends MixinSelectableItem(
|
|
|
18
18
|
set defaultSelected(next) {
|
|
19
19
|
this.toggleAttribute('selected', next);
|
|
20
20
|
}
|
|
21
|
+
get alone() {
|
|
22
|
+
return this.focusable === undefined && this.childrenKnown;
|
|
23
|
+
}
|
|
24
|
+
select() {
|
|
25
|
+
if (!this.alone) {
|
|
26
|
+
return super.select();
|
|
27
|
+
}
|
|
28
|
+
if (!super.select()) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
this.selected = !this.selected;
|
|
32
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
21
35
|
willUpdate(changed) {
|
|
22
36
|
super.willUpdate(changed);
|
|
23
|
-
this.toggleState('pressed', this.pressed === true);
|
|
37
|
+
this.toggleState('pressed', this.alone ? this.selected : this.pressed === true);
|
|
24
38
|
}
|
|
25
39
|
firstUpdated(changed) {
|
|
26
40
|
super.firstUpdated(changed);
|
|
27
41
|
this.finishHydration();
|
|
28
42
|
}
|
|
29
43
|
render() {
|
|
44
|
+
const alone = this.alone;
|
|
45
|
+
const radio = !alone && this.pressed === undefined;
|
|
46
|
+
const pressed = alone ? this.selected : this.pressed;
|
|
30
47
|
return html `<button
|
|
31
48
|
part="item"
|
|
32
49
|
type="button"
|
|
33
|
-
role=${
|
|
34
|
-
aria-checked=${
|
|
35
|
-
aria-pressed=${
|
|
50
|
+
role=${radio ? 'radio' : nothing}
|
|
51
|
+
aria-checked=${radio ? String(this.selected) : nothing}
|
|
52
|
+
aria-pressed=${radio || pressed === undefined ? nothing : String(pressed)}
|
|
36
53
|
?disabled=${this.disabled}
|
|
37
|
-
tabindex=${(this.focusable ?? this.selected) || !this.childrenKnown ? '0' : '-1'}
|
|
54
|
+
tabindex=${alone || (this.focusable ?? this.selected) || !this.childrenKnown ? '0' : '-1'}
|
|
38
55
|
@click=${() => this.select()}
|
|
39
56
|
><slot></slot></button>`;
|
|
40
57
|
}
|
|
@@ -316,6 +316,12 @@ export declare class HelixHeadlessVideo extends HelixElement {
|
|
|
316
316
|
* Whether the video starts again at the end.
|
|
317
317
|
*/
|
|
318
318
|
loop: boolean;
|
|
319
|
+
/**
|
|
320
|
+
* The title the device's own media controls show while the video plays,
|
|
321
|
+
* such as on a lock screen. Empty, the default, shows `label`. A playlist
|
|
322
|
+
* writes the current entry's name here.
|
|
323
|
+
*/
|
|
324
|
+
mediaTitle: string;
|
|
319
325
|
/**
|
|
320
326
|
* What the button that opens `overflow-controls` is called.
|
|
321
327
|
*/
|
|
@@ -47,6 +47,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
|
|
|
47
47
|
this.label = '';
|
|
48
48
|
this.loadingLabel = 'Loading';
|
|
49
49
|
this.loop = false;
|
|
50
|
+
this.mediaTitle = '';
|
|
50
51
|
this.moreLabel = 'More controls';
|
|
51
52
|
this.muteLabel = 'Mute';
|
|
52
53
|
this.nextLabel = 'Next';
|
|
@@ -351,7 +352,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
|
|
|
351
352
|
}
|
|
352
353
|
claimSession() {
|
|
353
354
|
const skips = writtenList(`${this.extraControls} ${this.overflowControls}`);
|
|
354
|
-
this.session.claim({ artwork: this.poster, title: this.label }, {
|
|
355
|
+
this.session.claim({ artwork: this.poster, title: this.mediaTitle || this.label }, {
|
|
355
356
|
next: skips.includes('next') ? () => this.skip(false) : null,
|
|
356
357
|
previous: skips.includes('previous') ? () => this.skip(true) : null,
|
|
357
358
|
seek: (seconds) => this.seek(seconds),
|
|
@@ -954,6 +955,9 @@ __decorate([
|
|
|
954
955
|
__decorate([
|
|
955
956
|
property({ reflect: true, type: Boolean })
|
|
956
957
|
], HelixHeadlessVideo.prototype, "loop", void 0);
|
|
958
|
+
__decorate([
|
|
959
|
+
property({ attribute: 'media-title' })
|
|
960
|
+
], HelixHeadlessVideo.prototype, "mediaTitle", void 0);
|
|
957
961
|
__decorate([
|
|
958
962
|
words,
|
|
959
963
|
property({ attribute: 'more-label' })
|
|
@@ -30,6 +30,10 @@ declare const HelixHeadlessVideoPlaylist_base: (new (...args: any[]) => import("
|
|
|
30
30
|
* sends `helix-end`. `shuffle` plays the entries in a random order, starting
|
|
31
31
|
* from the one that is current, and `previous` retraces that order.
|
|
32
32
|
*
|
|
33
|
+
* **The player is named by the current entry**, and the device's own media
|
|
34
|
+
* controls show the entry as the title. A player you gave a `label` keeps
|
|
35
|
+
* your name, and only the title follows the entry.
|
|
36
|
+
*
|
|
33
37
|
* **Keyboard.** Each entry is a button in the tab order, and Enter or Space
|
|
34
38
|
* plays it. The current entry carries `aria-current`. While focus is on the
|
|
35
39
|
* player, the player's own keys work, and `Video` lists them. Its previous
|
|
@@ -125,7 +125,10 @@ let HelixHeadlessVideoPlaylist = class HelixHeadlessVideoPlaylist extends MixinC
|
|
|
125
125
|
if (player.src !== current.src) {
|
|
126
126
|
player.src = current.src;
|
|
127
127
|
player.poster = current.poster;
|
|
128
|
-
player.
|
|
128
|
+
player.mediaTitle = current.text;
|
|
129
|
+
if (!player.hasAttribute('label')) {
|
|
130
|
+
player.label = current.text;
|
|
131
|
+
}
|
|
129
132
|
if (this.chased) {
|
|
130
133
|
void player.updateComplete?.then(() => player.play()).catch(() => { });
|
|
131
134
|
}
|