@fusion.dev/helix-headless 0.149.0 → 0.150.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 +471 -45
- package/dist/button/root/button-root.d.ts +5 -0
- package/dist/button/root/button-root.js +3 -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/date-input/root/date-input-root.d.ts +1 -0
- package/dist/element-events.generated.js +1 -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/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/slider/root/slider-root.d.ts +8 -0
- package/dist/slider/root/slider-root.js +6 -3
- 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/lint.json +1 -1
- package/package.json +7 -7
- package/types/jsx.d.ts +160 -58
- package/types/vue.d.ts +28 -16
|
@@ -222,6 +222,11 @@ export declare class HelixHeadlessButton extends HelixFormElement {
|
|
|
222
222
|
* inconsistently.
|
|
223
223
|
*/
|
|
224
224
|
private sayPending;
|
|
225
|
+
/**
|
|
226
|
+
* @internal Says `words` in the button's live region, for a subclass with
|
|
227
|
+
* news of its own, as `CopyButton` says the result of a copy.
|
|
228
|
+
*/
|
|
229
|
+
protected sayStatus(words: string): void;
|
|
225
230
|
/**
|
|
226
231
|
* @internal The live region a pending button speaks in, for a subclass that
|
|
227
232
|
* replaces the template, as `CopyButton` does.
|
|
@@ -74,6 +74,9 @@ let HelixHeadlessButton = class HelixHeadlessButton extends HelixFormElement {
|
|
|
74
74
|
}
|
|
75
75
|
this.status.say(this.pending && focused ? this.pendingLabel : '', { clear: false });
|
|
76
76
|
}
|
|
77
|
+
sayStatus(words) {
|
|
78
|
+
this.status.say(words);
|
|
79
|
+
}
|
|
77
80
|
renderLive() {
|
|
78
81
|
return drawStatus(this.status);
|
|
79
82
|
}
|
|
@@ -45,6 +45,7 @@ declare const HelixHeadlessColorInput_base: (new (...args: any[]) => import("@fu
|
|
|
45
45
|
* @slot end - Content at the trailing end, before the trigger.
|
|
46
46
|
* @slot feedback - The message that says what is wrong with the value.
|
|
47
47
|
* @slot hint - Help text under the field.
|
|
48
|
+
* @slot label - The label, for content richer than the `label` text.
|
|
48
49
|
* @slot panel-footer - Content in the panel under the picker, such as a note or an action.
|
|
49
50
|
* @slot panel-header - Content in the panel above the picker, such as a heading or a shortcut.
|
|
50
51
|
* @slot start - Content at the leading end of the field, inside its border.
|
|
@@ -503,6 +503,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
|
|
|
503
503
|
min="0"
|
|
504
504
|
max="100"
|
|
505
505
|
tabindex="-1"
|
|
506
|
+
style="writing-mode: vertical-lr; direction: rtl"
|
|
506
507
|
aria-label=${axisName(this.brightnessLabel)}
|
|
507
508
|
aria-describedby=${this.feedback === '' ? nothing : 'feedback'}
|
|
508
509
|
aria-valuetext=${reversed}
|
|
@@ -31,6 +31,9 @@ declare const HelixHeadlessCombobox_base: (new (...args: any[]) => import("@fusi
|
|
|
31
31
|
* | Escape | Closes the list and puts the chosen words back. On a closed field, a second Escape clears a single choice. |
|
|
32
32
|
* | Tab | Closes the list and moves on. |
|
|
33
33
|
* | Backspace | With `multiple`, on an empty field, removes the last tag. |
|
|
34
|
+
* | Left | With `multiple`, with the caret at the start of the text, moves to the last tag's remove button. Right on a right-to-left page. |
|
|
35
|
+
* | Left, Right on a tag | Move between the tags, and past the last back to the text. |
|
|
36
|
+
* | Delete, Backspace on a tag | Removes the tag. |
|
|
34
37
|
* | Space, Home, End, letters | Edit the text. |
|
|
35
38
|
*
|
|
36
39
|
* **ARIA.** The input is the `combobox`, named by the label, with
|
|
@@ -46,7 +49,8 @@ declare const HelixHeadlessCombobox_base: (new (...args: any[]) => import("@fusi
|
|
|
46
49
|
* Set `autocomplete` to let a browser's autofill choose an option.
|
|
47
50
|
*
|
|
48
51
|
* With `multiple`, each chosen option is a tag in the field with its own remove
|
|
49
|
-
* control. The
|
|
52
|
+
* control. The text is the field's one tab stop, and the arrow keys reach the
|
|
53
|
+
* tags from it. The panel stays open and the filter clears after each choice, so the
|
|
50
54
|
* next choice starts from the whole list.
|
|
51
55
|
*
|
|
52
56
|
* With `creatable`, the combobox offers a create row and fires `helix-create`.
|
|
@@ -584,6 +588,13 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
|
|
|
584
588
|
* tag does.
|
|
585
589
|
*/
|
|
586
590
|
protected removeLastToken(): void;
|
|
591
|
+
/** The tags, which the arrow keys reach from the text. */
|
|
592
|
+
protected tokenElements(): HTMLElement[];
|
|
593
|
+
/**
|
|
594
|
+
* Focus goes to the text field, the one tab stop, and never to a tag's remove
|
|
595
|
+
* button ahead of it, where Enter or Space would take a choice away.
|
|
596
|
+
*/
|
|
597
|
+
protected get focusTarget(): HTMLElement | null;
|
|
587
598
|
/** One tag asking to be taken out of the set. */
|
|
588
599
|
private readonly handleDismiss;
|
|
589
600
|
/**
|
|
@@ -446,6 +446,12 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
|
|
|
446
446
|
this.drop(last);
|
|
447
447
|
}
|
|
448
448
|
}
|
|
449
|
+
tokenElements() {
|
|
450
|
+
return [...this.renderRoot.querySelectorAll('[part="tag"]')];
|
|
451
|
+
}
|
|
452
|
+
get focusTarget() {
|
|
453
|
+
return this.partNamed('control') ?? super.focusTarget;
|
|
454
|
+
}
|
|
449
455
|
sync() {
|
|
450
456
|
if (this.multiple) {
|
|
451
457
|
reportValues(this.internals, this.name, this.value === '' ? [] : this.value.split(','));
|
|
@@ -574,6 +580,7 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
|
|
|
574
580
|
data-value=${option.value}
|
|
575
581
|
dismiss-label=${formatWords(this, 'removeLabel', { name: option.text })}
|
|
576
582
|
?dismissible=${!this.isDisabled && !this.readOnly}
|
|
583
|
+
.dismissTabStop=${false}
|
|
577
584
|
@helix-dismiss=${this.handleDismiss}
|
|
578
585
|
>${option.text}</${this.tag('tag')}
|
|
579
586
|
>`;
|
|
@@ -619,6 +626,7 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
|
|
|
619
626
|
aria-expanded=${this.open ? 'true' : 'false'}
|
|
620
627
|
aria-describedby=${this.describedBy ?? nothing}
|
|
621
628
|
aria-invalid=${this.invalidity ?? nothing}
|
|
629
|
+
aria-required=${this.required ? 'true' : nothing}
|
|
622
630
|
placeholder=${(this.chosen.length > 0 || this.unresolved ? '' : this.placeholder) || nothing}
|
|
623
631
|
?disabled=${this.isDisabled}
|
|
624
632
|
?readonly=${this.readOnly}
|
|
@@ -29,8 +29,9 @@ import '../../tooltip/index.js';
|
|
|
29
29
|
* **Keyboard.** Enter and Space copy, as they press a native button.
|
|
30
30
|
*
|
|
31
31
|
* The button is named `label`, or the host's `aria-label`, and its name
|
|
32
|
-
* changes to `copied-label` or `error-label` for the moment after a press
|
|
33
|
-
*
|
|
32
|
+
* changes to `copied-label` or `error-label` for the moment after a press. The
|
|
33
|
+
* same words are said in a live region, so a screen reader hears the result,
|
|
34
|
+
* and the tooltip shows them.
|
|
34
35
|
*
|
|
35
36
|
* @since 0.1.0
|
|
36
37
|
* @status stable
|
|
@@ -46,7 +47,7 @@ import '../../tooltip/index.js';
|
|
|
46
47
|
* @event {HelixCopyErrorEvent} helix-copy-error - Fired when a press put nothing on the clipboard: the browser did not allow the write, or there was nothing to copy.
|
|
47
48
|
*
|
|
48
49
|
* @csspart button - The inner button.
|
|
49
|
-
* @csspart live - The live region that says `pending-label` when the button becomes pending under focus. It is rendered from the first time
|
|
50
|
+
* @csspart live - The live region that says `copied-label` or `error-label` after a press, and `pending-label` when the button becomes pending under focus. It is rendered from the first time it says anything.
|
|
50
51
|
* @csspart mark - The clipboard icon, and the confirmation icon that replaces it.
|
|
51
52
|
* @csspart spinner - The ring a pending button turns, an empty element the styled button draws over the icon. It is rendered only while the button is pending.
|
|
52
53
|
* @csspart tooltip - The tooltip bubble, forwarded from the tooltip inside the button.
|
|
@@ -37,7 +37,11 @@ let HelixHeadlessCopyButton = class HelixHeadlessCopyButton extends HelixHeadles
|
|
|
37
37
|
const copying = this.clipboard.copy(this.text);
|
|
38
38
|
this.announcePress(event);
|
|
39
39
|
const wrote = await copying;
|
|
40
|
-
if (!
|
|
40
|
+
if (!this.isConnected) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.sayStatus(wrote ? this.copiedLabel : this.errorLabel);
|
|
44
|
+
if (!wrote) {
|
|
41
45
|
this.dispatchEvent(new HelixCopyErrorEvent());
|
|
42
46
|
}
|
|
43
47
|
};
|
|
@@ -35,6 +35,7 @@ declare const HelixHeadlessDateInput_base: (new (...args: any[]) => import("@fus
|
|
|
35
35
|
*
|
|
36
36
|
* @slot feedback - The error message under the field.
|
|
37
37
|
* @slot hint - The help text under the field.
|
|
38
|
+
* @slot label - The label, for content richer than the `label` text.
|
|
38
39
|
* @slot next-icon - The icon on the calendar's next month control.
|
|
39
40
|
* @slot panel-footer - Content in the panel under the calendar, such as a note or an action.
|
|
40
41
|
* @slot panel-header - Content in the panel above the calendar, such as a heading or a shortcut.
|
|
@@ -262,7 +262,7 @@ registerElementEvents({
|
|
|
262
262
|
'helix-headless-time-picker': ['change', 'helix-before-change', 'input'],
|
|
263
263
|
'helix-headless-toast': ['helix-dismiss'],
|
|
264
264
|
'helix-headless-toast-item': ['helix-dismiss'],
|
|
265
|
-
'helix-headless-toggle': ['helix-select'],
|
|
265
|
+
'helix-headless-toggle': ['change', 'helix-select'],
|
|
266
266
|
'helix-headless-toggle-group': ['change', 'helix-before-change', 'helix-select', 'input'],
|
|
267
267
|
'helix-headless-tooltip': ['helix-before-close', 'helix-before-open', 'toggle'],
|
|
268
268
|
'helix-headless-tour': [
|
|
@@ -29,7 +29,8 @@ declare const HelixHeadlessFilterChip_base: (new (...args: any[]) => import("@fu
|
|
|
29
29
|
*
|
|
30
30
|
* **Keyboard.** The remove button is a tab stop of its own, and Enter or Space
|
|
31
31
|
* on it asks to remove the filter. In a `FilterInput`, Backspace in the empty
|
|
32
|
-
* field asks to remove the last chip
|
|
32
|
+
* field asks to remove the last chip, the arrow keys move between the field
|
|
33
|
+
* and the chips, and Delete or Backspace on a chip asks to remove it.
|
|
33
34
|
*
|
|
34
35
|
* The remove button is named `dismiss-label` followed by the chip's words,
|
|
35
36
|
* such as "Remove filter Status is not open", so a row of chips says which
|
|
@@ -43,6 +43,9 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
43
43
|
* | Home, End | Highlights the first or the last suggestion while the panel is open. |
|
|
44
44
|
* | Escape | Closes the panel and stops there, so a dialog around the field stays open. |
|
|
45
45
|
* | Backspace | In an empty field, asks the last token to go. |
|
|
46
|
+
* | Left | With the caret at the start of the text, moves to the last token's remove button. Right on a right-to-left page. |
|
|
47
|
+
* | Left, Right on a token | Move between the tokens, and past the last back to the text. |
|
|
48
|
+
* | Delete, Backspace on a token | Asks the token to go. |
|
|
46
49
|
*
|
|
47
50
|
* **Typed filters.** The field parses only the fields you declare. Write
|
|
48
51
|
* `FilterField` elements, usually inside the `FilterMenu` slotted into `start`,
|
|
@@ -425,6 +428,8 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
425
428
|
private readonly handleLeading;
|
|
426
429
|
/** The row again, whenever what is in it changes. */
|
|
427
430
|
private readonly handleTokens;
|
|
431
|
+
/** The tokens slotted in `tags`, which the arrow keys reach from the text. */
|
|
432
|
+
protected tokenElements(): HTMLElement[];
|
|
428
433
|
/**
|
|
429
434
|
* Asks the last token to go, rather than taking it.
|
|
430
435
|
*
|
|
@@ -84,6 +84,9 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
86
|
this.handleKeydown = (event) => {
|
|
87
|
+
if (event.isComposing) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
87
90
|
if (this.tokenKeydown(event, this.value)) {
|
|
88
91
|
return;
|
|
89
92
|
}
|
|
@@ -310,6 +313,9 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
310
313
|
this.suggesting = options.length > 0;
|
|
311
314
|
this.activeAt = this.activeAt < options.length ? this.activeAt : -1;
|
|
312
315
|
}
|
|
316
|
+
tokenElements() {
|
|
317
|
+
return (this.tokens?.assignedElements({ flatten: true }) ?? []);
|
|
318
|
+
}
|
|
313
319
|
removeLastToken() {
|
|
314
320
|
const last = this.tokens
|
|
315
321
|
?.assignedElements({ flatten: true })
|
|
@@ -368,6 +374,7 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
368
374
|
const active = this.open ? options[this.activeAt] : undefined;
|
|
369
375
|
for (const [at, option] of options.entries()) {
|
|
370
376
|
option.active = this.open && at === this.activeAt;
|
|
377
|
+
option.selectionFollowsActive = true;
|
|
371
378
|
}
|
|
372
379
|
const control = this.control;
|
|
373
380
|
if (control && 'ariaActiveDescendantElement' in control) {
|
|
@@ -381,6 +381,8 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
|
|
|
381
381
|
* saving on `change` kept 150 while the form submitted 100.
|
|
382
382
|
*/
|
|
383
383
|
private readonly handleChange;
|
|
384
|
+
/** The value Enter settled and announced, until the field's `change` or the next keystroke. */
|
|
385
|
+
private enterCommit;
|
|
384
386
|
/**
|
|
385
387
|
* The four keys a spinner answers, which `Input` does not bind.
|
|
386
388
|
*
|
|
@@ -423,6 +425,10 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
|
|
|
423
425
|
*
|
|
424
426
|
* On `keypress`, where the platform runs its own implicit submission, so a
|
|
425
427
|
* consumer who prevents the `keydown` for Enter stops this too.
|
|
428
|
+
*
|
|
429
|
+
* **Settled first**, as blur settles it. The typed text is read as a number,
|
|
430
|
+
* `change` announces it, and the form value is written before the form is
|
|
431
|
+
* submitted, so a field showing `$2,500.50` submits 2500.5.
|
|
426
432
|
*/
|
|
427
433
|
private readonly handleKeypress;
|
|
428
434
|
/** @internal A single line of text counts against implicit submission. */
|
|
@@ -96,6 +96,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
96
96
|
};
|
|
97
97
|
this.handleInput = (event) => {
|
|
98
98
|
event.stopPropagation();
|
|
99
|
+
this.enterCommit = undefined;
|
|
99
100
|
const control = event.target;
|
|
100
101
|
this.requestValue(control.value);
|
|
101
102
|
restoreValue(control, this.shown);
|
|
@@ -109,7 +110,11 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
109
110
|
this.handleChange = (event) => {
|
|
110
111
|
event.stopPropagation();
|
|
111
112
|
this.settle();
|
|
112
|
-
this.
|
|
113
|
+
const announced = this.enterCommit === this.value;
|
|
114
|
+
this.enterCommit = undefined;
|
|
115
|
+
if (!announced) {
|
|
116
|
+
this.announceChange();
|
|
117
|
+
}
|
|
113
118
|
};
|
|
114
119
|
this.handleKeydown = (event) => {
|
|
115
120
|
if (event.key === 'ArrowUp') {
|
|
@@ -142,9 +147,15 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
142
147
|
event.preventDefault();
|
|
143
148
|
};
|
|
144
149
|
this.handleKeypress = (event) => {
|
|
145
|
-
if (event.key
|
|
146
|
-
|
|
150
|
+
if (event.key !== 'Enter' || event.defaultPrevented) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (this.settle()) {
|
|
154
|
+
this.sync();
|
|
155
|
+
this.enterCommit = this.value;
|
|
156
|
+
this.announceChange();
|
|
147
157
|
}
|
|
158
|
+
submitImplicitly(this.form);
|
|
148
159
|
};
|
|
149
160
|
}
|
|
150
161
|
static { this.words = 'helix-number-input'; }
|
|
@@ -44,7 +44,9 @@ export declare const optionsAmong: (elements: readonly Element[]) => HelixHeadle
|
|
|
44
44
|
* @documentation https://fusion.dev/helix/components/option
|
|
45
45
|
*
|
|
46
46
|
* @slot - The option's label.
|
|
47
|
+
* @slot description - A second line under the label, for content richer than the `description` text.
|
|
47
48
|
* @slot end - Content after the label, such as a shortcut or a count.
|
|
49
|
+
* @slot indicator - An icon shown while the option is chosen, in place of the check the styled option draws.
|
|
48
50
|
* @slot start - Content before the label, such as an icon or an avatar.
|
|
49
51
|
*
|
|
50
52
|
* @event {HelixSelectEvent} helix-select - The option was chosen. Cancelable: canceling it leaves the selection as it was, and keeps the browser from following an option's `href`.
|
|
@@ -57,6 +59,8 @@ export declare const optionsAmong: (elements: readonly Element[]) => HelixHeadle
|
|
|
57
59
|
* @csspart text - The label and the description under it, as one column.
|
|
58
60
|
*
|
|
59
61
|
* @cssstate active - While the keyboard is on the option, which is the option `aria-activedescendant` names.
|
|
62
|
+
* @cssstate disabled - While the option cannot be chosen.
|
|
63
|
+
* @cssstate has-indicator - An icon is slotted into `indicator`.
|
|
60
64
|
* @cssstate selected - While the option is chosen.
|
|
61
65
|
*/
|
|
62
66
|
export declare class HelixHeadlessOption extends HelixElement {
|
|
@@ -136,14 +140,10 @@ export declare class HelixHeadlessOption extends HelixElement {
|
|
|
136
140
|
* ends. Matching still reads the label, so what somebody types is matched
|
|
137
141
|
* against the name rather than against prose about it.
|
|
138
142
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* filled, so every option without a caption would carry an empty line. The
|
|
144
|
-
* detection that would avoid it does not exist, `:host(:has())` being invalid
|
|
145
|
-
* and the server having no children to inspect. A property is known at render
|
|
146
|
-
* time, so the component can decline to draw the box at all.
|
|
143
|
+
* For a caption richer than a string, slot it into `description`. The
|
|
144
|
+
* property is that slot's fallback inside the `description` part. With no
|
|
145
|
+
* property the slot is drawn bare, which takes no box while it is empty, so
|
|
146
|
+
* an option without a caption carries no empty line.
|
|
147
147
|
*
|
|
148
148
|
* It does not reflect, per the rule that payload state stays off the host: a
|
|
149
149
|
* string is not a fact a selector needs.
|
|
@@ -198,6 +198,15 @@ export declare class HelixHeadlessOption extends HelixElement {
|
|
|
198
198
|
* Written by the listbox.
|
|
199
199
|
*/
|
|
200
200
|
selected: boolean;
|
|
201
|
+
/**
|
|
202
|
+
* Whether the option says it is selected while the keyboard is on it.
|
|
203
|
+
*
|
|
204
|
+
* Written by a control whose list holds suggestions, such as `FilterInput`:
|
|
205
|
+
* nothing there stays chosen, so the row the keyboard is on is the
|
|
206
|
+
* selection, as the APG combobox's list with selection that follows focus
|
|
207
|
+
* says. No attribute: it is coordination between elements.
|
|
208
|
+
*/
|
|
209
|
+
selectionFollowsActive: boolean;
|
|
201
210
|
/**
|
|
202
211
|
* Where the link opens, such as `_blank`, for an option with an `href`.
|
|
203
212
|
*/
|
|
@@ -4,8 +4,8 @@ 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, followLink, HelixElement, HelixSelectEvent, } from '@fusion.dev/helix-core';
|
|
8
|
-
import { html
|
|
7
|
+
import { customElement, followLink, HelixElement, HelixSelectEvent, slotFilled, } from '@fusion.dev/helix-core';
|
|
8
|
+
import { html } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
export const optionsAmong = (elements) => {
|
|
11
11
|
const isOption = (element) => 'text' in element && 'value' in element && 'selected' in element;
|
|
@@ -23,6 +23,7 @@ let HelixHeadlessOption = class HelixHeadlessOption extends HelixElement {
|
|
|
23
23
|
this.label = '';
|
|
24
24
|
this.rel = '';
|
|
25
25
|
this.selected = false;
|
|
26
|
+
this.selectionFollowsActive = false;
|
|
26
27
|
this.target = '';
|
|
27
28
|
this.textValue = '';
|
|
28
29
|
this.value = '';
|
|
@@ -59,10 +60,14 @@ let HelixHeadlessOption = class HelixHeadlessOption extends HelixElement {
|
|
|
59
60
|
super.willUpdate(changed);
|
|
60
61
|
this.toggleState('selected', this.selected);
|
|
61
62
|
this.toggleState('active', this.active);
|
|
63
|
+
this.toggleState('disabled', this.disabled);
|
|
62
64
|
}
|
|
63
65
|
render() {
|
|
64
66
|
this.internals.role = 'option';
|
|
65
|
-
this.internals.ariaSelected =
|
|
67
|
+
this.internals.ariaSelected =
|
|
68
|
+
this.href === ''
|
|
69
|
+
? String(this.selected || (this.selectionFollowsActive && this.active))
|
|
70
|
+
: null;
|
|
66
71
|
this.internals.ariaDisabled = this.disabled ? 'true' : null;
|
|
67
72
|
this.internals.ariaDescription =
|
|
68
73
|
this.alternate !== '' && this.alternateLabel !== '' ? this.alternateLabel : null;
|
|
@@ -73,9 +78,15 @@ let HelixHeadlessOption = class HelixHeadlessOption extends HelixElement {
|
|
|
73
78
|
<slot name="start"></slot>
|
|
74
79
|
<span part="text">
|
|
75
80
|
<span part="label"><slot>${this.label}</slot></span>
|
|
76
|
-
${this.description
|
|
81
|
+
${this.description
|
|
82
|
+
? html `<span part="description"><slot name="description">${this.description}</slot></span>`
|
|
83
|
+
: html `<slot name="description"></slot>`}
|
|
77
84
|
</span>
|
|
78
85
|
<slot name="end"></slot>
|
|
86
|
+
<slot
|
|
87
|
+
name="indicator"
|
|
88
|
+
@slotchange=${(event) => this.toggleState('has-indicator', slotFilled(event.target))}
|
|
89
|
+
></slot>
|
|
79
90
|
</div>`;
|
|
80
91
|
}
|
|
81
92
|
};
|
|
@@ -109,6 +120,9 @@ __decorate([
|
|
|
109
120
|
__decorate([
|
|
110
121
|
property({ type: Boolean })
|
|
111
122
|
], HelixHeadlessOption.prototype, "selected", void 0);
|
|
123
|
+
__decorate([
|
|
124
|
+
property({ attribute: false })
|
|
125
|
+
], HelixHeadlessOption.prototype, "selectionFollowsActive", void 0);
|
|
112
126
|
__decorate([
|
|
113
127
|
property()
|
|
114
128
|
], HelixHeadlessOption.prototype, "target", void 0);
|
|
@@ -114,8 +114,15 @@ export declare class HelixHeadlessRadioGroup extends HelixHeadlessRadioGroup_bas
|
|
|
114
114
|
valueMissingMessage: string;
|
|
115
115
|
protected readonly keys: KeyboardNavigationController;
|
|
116
116
|
protected readonly cascade: DisabledCascadeController;
|
|
117
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* The radios, in light-DOM order. Client only: the server has no tree.
|
|
119
|
+
*
|
|
120
|
+
* Descendants, as `Checkbox.Group` reads its boxes, so a radio inside a
|
|
121
|
+
* layout wrapper of yours is one of the group's. A radio held by a group
|
|
122
|
+
* nested inside this one is that group's.
|
|
123
|
+
*/
|
|
118
124
|
get items(): HelixHeadlessRadio[];
|
|
125
|
+
private observer;
|
|
119
126
|
/**
|
|
120
127
|
* Announces before it applies, so a listener can refuse the change.
|
|
121
128
|
*
|
|
@@ -42,7 +42,10 @@ let HelixHeadlessRadioGroup = class HelixHeadlessRadioGroup extends MixinControl
|
|
|
42
42
|
}
|
|
43
43
|
static { this.words = 'helix-radio-group'; }
|
|
44
44
|
get items() {
|
|
45
|
-
|
|
45
|
+
if (this.children === undefined) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return [...this.querySelectorAll('*')].filter((element) => element instanceof HelixHeadlessRadio && nearestGroup(element) === this);
|
|
46
49
|
}
|
|
47
50
|
get answer() {
|
|
48
51
|
return this.items.find((item) => item.value === this.value);
|
|
@@ -91,10 +94,14 @@ let HelixHeadlessRadioGroup = class HelixHeadlessRadioGroup extends MixinControl
|
|
|
91
94
|
connectedCallback() {
|
|
92
95
|
super.connectedCallback();
|
|
93
96
|
this.addEventListener('helix-select', this.handleSelection);
|
|
97
|
+
this.observer = new MutationObserver(() => this.requestUpdate());
|
|
98
|
+
this.observer.observe(this, { childList: true, subtree: true });
|
|
94
99
|
}
|
|
95
100
|
disconnectedCallback() {
|
|
96
101
|
super.disconnectedCallback();
|
|
97
102
|
this.removeEventListener('helix-select', this.handleSelection);
|
|
103
|
+
this.observer?.disconnect();
|
|
104
|
+
this.observer = undefined;
|
|
98
105
|
}
|
|
99
106
|
willUpdate(changed) {
|
|
100
107
|
super.willUpdate(changed);
|
|
@@ -134,3 +141,10 @@ HelixHeadlessRadioGroup = __decorate([
|
|
|
134
141
|
customElement('helix-headless-radio-group')
|
|
135
142
|
], HelixHeadlessRadioGroup);
|
|
136
143
|
export { HelixHeadlessRadioGroup };
|
|
144
|
+
function nearestGroup(radio) {
|
|
145
|
+
let parent = radio.parentElement;
|
|
146
|
+
while (parent && !(parent instanceof HelixHeadlessRadioGroup)) {
|
|
147
|
+
parent = parent.parentElement;
|
|
148
|
+
}
|
|
149
|
+
return parent;
|
|
150
|
+
}
|
|
@@ -290,6 +290,14 @@ export declare class HelixHeadlessSlider extends HelixHeadlessSlider_base {
|
|
|
290
290
|
* from the origin to the thumb as `--slider-from` and `--slider-to`.
|
|
291
291
|
*/
|
|
292
292
|
private get fillStyle();
|
|
293
|
+
/**
|
|
294
|
+
* The range's axis, written on it, so a vertical slider is vertical to the
|
|
295
|
+
* accessibility tree with no stylesheet: Chromium reads a range's
|
|
296
|
+
* orientation off its writing mode and ignores `aria-orientation` there.
|
|
297
|
+
* `direction: rtl` puts the maximum at the top, which `vertical-lr` alone
|
|
298
|
+
* puts at the foot.
|
|
299
|
+
*/
|
|
300
|
+
private get axis();
|
|
293
301
|
/**
|
|
294
302
|
* A value snapped to the step, counted from `min` the way a range counts it.
|
|
295
303
|
*
|
|
@@ -112,10 +112,13 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
|
|
|
112
112
|
get fillStyle() {
|
|
113
113
|
const fraction = this.fraction;
|
|
114
114
|
if (!this.origin) {
|
|
115
|
-
return `--slider-fraction: ${fraction}`;
|
|
115
|
+
return `--slider-fraction: ${fraction}${this.axis}`;
|
|
116
116
|
}
|
|
117
117
|
const at = { center: 0.5, end: 1, start: 0 }[this.origin] ?? 0;
|
|
118
|
-
return `--slider-fraction: ${fraction}; --slider-from: ${Math.min(at, fraction)}; --slider-to: ${Math.max(at, fraction)}`;
|
|
118
|
+
return `--slider-fraction: ${fraction}; --slider-from: ${Math.min(at, fraction)}; --slider-to: ${Math.max(at, fraction)}${this.axis}`;
|
|
119
|
+
}
|
|
120
|
+
get axis() {
|
|
121
|
+
return this.orientation === 'vertical' ? '; writing-mode: vertical-lr; direction: rtl' : '';
|
|
119
122
|
}
|
|
120
123
|
snapped(at) {
|
|
121
124
|
if (!(this.step > 0)) {
|
|
@@ -208,7 +211,7 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
|
|
|
208
211
|
id=${which === 'start' ? 'control' : 'control-end'}
|
|
209
212
|
data-thumb=${which}
|
|
210
213
|
type="range"
|
|
211
|
-
style="--slider-from: ${this.along(start)}; --slider-to: ${this.along(end)}"
|
|
214
|
+
style="--slider-from: ${this.along(start)}; --slider-to: ${this.along(end)}${this.axis}"
|
|
212
215
|
min=${this.min}
|
|
213
216
|
max=${this.max}
|
|
214
217
|
step=${this.step}
|
|
@@ -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']);
|