@fusion.dev/helix-headless 0.170.0 → 0.171.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 +610 -93
- package/dist/chat/composer/chat-composer.d.ts +2 -1
- package/dist/chat/composer/chat-composer.js +7 -1
- package/dist/chat/root/chat-root.d.ts +36 -6
- package/dist/chat/root/chat-root.js +53 -5
- package/dist/code-block/root/code-block-root.d.ts +20 -2
- package/dist/code-block/root/code-block-root.js +29 -8
- package/dist/color-picker/root/color-picker-root.d.ts +50 -7
- package/dist/color-picker/root/color-picker-root.js +62 -1
- package/dist/combobox/root/combobox-root.d.ts +16 -6
- package/dist/combobox/root/combobox-root.js +9 -2
- package/dist/command/root/command-root.d.ts +16 -1
- package/dist/command/root/command-root.js +8 -0
- package/dist/filter-input/chip/filter-chip.d.ts +13 -3
- package/dist/filter-input/chip/filter-chip.js +5 -0
- package/dist/filter-input/menu/filter-menu.js +0 -1
- package/dist/filter-input/root/filter-input-root.d.ts +71 -4
- package/dist/filter-input/root/filter-input-root.js +87 -2
- package/dist/format-date/root/format-date-root.d.ts +3 -1
- package/dist/input/root/input-root.d.ts +6 -0
- package/dist/input/root/input-root.js +13 -0
- package/dist/listbox/root/listbox-root.d.ts +6 -0
- package/dist/listbox/root/listbox-root.js +6 -1
- package/dist/number-input/root/number-input-root.d.ts +16 -6
- package/dist/number-input/root/number-input-root.js +15 -4
- package/dist/select/root/select-root.js +2 -0
- package/dist/steps/root/steps-root.d.ts +32 -8
- package/dist/steps/root/steps-root.js +70 -9
- package/dist/steps/step/step.d.ts +9 -1
- package/dist/steps/step/step.js +5 -0
- package/dist/video-playlist/root/video-playlist-root.d.ts +19 -1
- package/dist/video-playlist/root/video-playlist-root.js +14 -3
- package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
- package/dist/virtual-list/root/virtual-list-root.js +7 -2
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +490 -116
- package/types/vue.d.ts +108 -25
- package/types/words.d.ts +12 -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, afterLinkFollowed, countLabel, customElement, DelayController, drawAutofill, drawStatus, formatWords, HelixCreateEvent, HelixFilterEvent, HelixFormElement, handedLocale, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, onApple, PopoverController, reportValues, StatusController, submitImplicitly, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { AriaForwardController, afterLinkFollowed, chosenValues, countLabel, customElement, DelayController, drawAutofill, drawStatus, formatWords, HelixCreateEvent, HelixFilterEvent, HelixFormElement, handedLocale, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, onApple, PopoverController, reportValues, StatusController, submitImplicitly, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { caretDown } from '@fusion.dev/helix-icons';
|
|
9
9
|
import { nothing } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
@@ -69,6 +69,7 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
|
|
|
69
69
|
placement: 'bottom-start',
|
|
70
70
|
surface: () => this.partNamed('surface'),
|
|
71
71
|
});
|
|
72
|
+
this.created = new Set();
|
|
72
73
|
this.handleInput = (event) => {
|
|
73
74
|
event.stopPropagation();
|
|
74
75
|
const typed = event.target.value;
|
|
@@ -252,7 +253,11 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
|
|
|
252
253
|
return this.partNamed('listbox');
|
|
253
254
|
}
|
|
254
255
|
get values() {
|
|
255
|
-
|
|
256
|
+
const known = new Set(this.created);
|
|
257
|
+
for (const option of optionsAmong(this.knownChildren)) {
|
|
258
|
+
known.add(option.value);
|
|
259
|
+
}
|
|
260
|
+
return chosenValues(this.value, known, this.multiple);
|
|
256
261
|
}
|
|
257
262
|
get chosen() {
|
|
258
263
|
const options = optionsAmong(this.knownChildren);
|
|
@@ -296,6 +301,7 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
|
|
|
296
301
|
return;
|
|
297
302
|
}
|
|
298
303
|
const next = this.multiple ? [...this.values, wanted].join(',') : wanted;
|
|
304
|
+
this.created.add(wanted);
|
|
299
305
|
if (!this.requestValue(next)) {
|
|
300
306
|
return;
|
|
301
307
|
}
|
|
@@ -343,6 +349,7 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
|
|
|
343
349
|
}
|
|
344
350
|
const named = (this.listbox?.options ?? []).find((option) => option.text.trim().toLowerCase() === typed.toLowerCase());
|
|
345
351
|
const answer = named?.value ?? typed;
|
|
352
|
+
this.created.add(answer);
|
|
346
353
|
if (this.multiple) {
|
|
347
354
|
if (!this.values.includes(answer)) {
|
|
348
355
|
this.requestValue([...this.values, answer].join(','));
|
|
@@ -55,7 +55,8 @@ declare const HelixHeadlessCommand_base: (new (...args: any[]) => import("@fusio
|
|
|
55
55
|
* | `hotkey`, Command or Control with K by default | Opens the palette from anywhere on the page, or closes it. With `inline`, puts focus in its filter field. |
|
|
56
56
|
* | Down, Up | Moves the highlight to the next or the previous command, round from the last to the first. |
|
|
57
57
|
* | Page Down, Page Up | Moves the highlight by as many commands as the list shows at once. |
|
|
58
|
-
* | Home, End
|
|
58
|
+
* | Home, End | Move the caret in the filter field. With `home-end="list"`, move the highlight to the first or the last command. |
|
|
59
|
+
* | Left, Right | Move the caret in the filter field. |
|
|
59
60
|
* | Enter | Runs the highlighted command, or follows it where it is a link, and closes the palette. On a command with `opens`, shows its page. |
|
|
60
61
|
* | Command or Control with Enter | Takes the highlighted command's `alternate` destination, where it has one. |
|
|
61
62
|
* | Backspace | In an empty filter on a page, goes back to the page before. |
|
|
@@ -144,6 +145,14 @@ export declare class HelixHeadlessCommand extends HelixHeadlessCommand_base {
|
|
|
144
145
|
* property only, there being no way to write a function in markup.
|
|
145
146
|
*/
|
|
146
147
|
filterMatch?: ((text: string, filter: string) => boolean) | undefined;
|
|
148
|
+
/**
|
|
149
|
+
* Where Home and End go from the filter field.
|
|
150
|
+
*
|
|
151
|
+
* Absent, they move the caret, as APG's combobox has them. `list` moves the
|
|
152
|
+
* highlight to the first or the last command, as React Aria's Autocomplete
|
|
153
|
+
* does, and Shift with either still selects the text.
|
|
154
|
+
*/
|
|
155
|
+
homeEnd?: 'list';
|
|
147
156
|
/**
|
|
148
157
|
* The key that opens the palette, written as `mod+k`, `shift+p`, or `k`,
|
|
149
158
|
* in Kbd's chord spelling: `mod` is Command or Control, `ctrl`, `cmd`,
|
|
@@ -178,6 +187,12 @@ export declare class HelixHeadlessCommand extends HelixHeadlessCommand_base {
|
|
|
178
187
|
* Accessible name for the overlay, which has no visible heading.
|
|
179
188
|
*/
|
|
180
189
|
label: string;
|
|
190
|
+
/**
|
|
191
|
+
* How the commands are laid out, as Listbox's `layout`: `grid` draws them in
|
|
192
|
+
* rows and columns, for a palette of icons or emoji, and the arrow keys
|
|
193
|
+
* follow the rows and the columns. Absent is one command under another.
|
|
194
|
+
*/
|
|
195
|
+
layout?: 'grid' | 'stack';
|
|
181
196
|
/**
|
|
182
197
|
* What the list says when its commands could not be loaded, such as after a
|
|
183
198
|
* request to a server fails. Empty, null or no attribute is no error, so
|
|
@@ -246,6 +246,8 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
|
|
|
246
246
|
results-label=${this.resultsLabel}
|
|
247
247
|
?loading=${this.loading}
|
|
248
248
|
.items=${this.items}
|
|
249
|
+
.fieldHomeEnd=${this.homeEnd === 'list'}
|
|
250
|
+
layout=${this.layout ?? nothing}
|
|
249
251
|
filterable
|
|
250
252
|
@helix-before-change=${this.handleChange}
|
|
251
253
|
@helix-filter=${this.handleFilter}
|
|
@@ -299,6 +301,9 @@ __decorate([
|
|
|
299
301
|
__decorate([
|
|
300
302
|
property({ attribute: false })
|
|
301
303
|
], HelixHeadlessCommand.prototype, "filterMatch", void 0);
|
|
304
|
+
__decorate([
|
|
305
|
+
property({ attribute: 'home-end' })
|
|
306
|
+
], HelixHeadlessCommand.prototype, "homeEnd", void 0);
|
|
302
307
|
__decorate([
|
|
303
308
|
property()
|
|
304
309
|
], HelixHeadlessCommand.prototype, "hotkey", void 0);
|
|
@@ -312,6 +317,9 @@ __decorate([
|
|
|
312
317
|
words,
|
|
313
318
|
property()
|
|
314
319
|
], HelixHeadlessCommand.prototype, "label", void 0);
|
|
320
|
+
__decorate([
|
|
321
|
+
property({ reflect: true })
|
|
322
|
+
], HelixHeadlessCommand.prototype, "layout", void 0);
|
|
315
323
|
__decorate([
|
|
316
324
|
property({ attribute: 'load-error' })
|
|
317
325
|
], HelixHeadlessCommand.prototype, "loadError", void 0);
|
|
@@ -28,9 +28,10 @@ declare const HelixHeadlessFilterChip_base: (new (...args: any[]) => import("@fu
|
|
|
28
28
|
* press. `requestChange` documents the pattern.
|
|
29
29
|
*
|
|
30
30
|
* **Keyboard.** The remove button is a tab stop of its own, and Enter or Space
|
|
31
|
-
* on it asks to remove the filter. In a `FilterInput`,
|
|
32
|
-
* field asks to remove the last chip, the
|
|
33
|
-
* and the chips, and Delete or Backspace on
|
|
31
|
+
* on it asks to remove the filter. In a `FilterInput`, the field's text is the
|
|
32
|
+
* one tab stop: Backspace in the empty field asks to remove the last chip, the
|
|
33
|
+
* arrow keys move between the field and the chips, and Delete or Backspace on
|
|
34
|
+
* a chip asks to remove it.
|
|
34
35
|
*
|
|
35
36
|
* The remove button is named `dismiss-label` followed by the chip's words,
|
|
36
37
|
* such as "Remove filter Status is not open", so a row of chips says which
|
|
@@ -78,6 +79,15 @@ export declare class HelixHeadlessFilterChip extends HelixHeadlessFilterChip_bas
|
|
|
78
79
|
* closing a message.
|
|
79
80
|
*/
|
|
80
81
|
dismissLabel: string;
|
|
82
|
+
/**
|
|
83
|
+
* Whether the remove button is a tab stop of its own.
|
|
84
|
+
*
|
|
85
|
+
* `FilterInput` writes `false` on the chips in its `tags` slot: the field's
|
|
86
|
+
* text is its one tab stop, and the arrow keys move from the text to the
|
|
87
|
+
* chips, as in a `multiple` Combobox. No attribute: it is coordination
|
|
88
|
+
* between elements, as `Tag.dismissTabStop` is.
|
|
89
|
+
*/
|
|
90
|
+
dismissTabStop: boolean;
|
|
81
91
|
/**
|
|
82
92
|
* The name of the field the filter is on, as shown on the chip.
|
|
83
93
|
*
|
|
@@ -13,6 +13,7 @@ let HelixHeadlessFilterChip = class HelixHeadlessFilterChip extends MixinDismiss
|
|
|
13
13
|
constructor() {
|
|
14
14
|
super(...arguments);
|
|
15
15
|
this.dismissLabel = 'Remove filter';
|
|
16
|
+
this.dismissTabStop = true;
|
|
16
17
|
this.label = '';
|
|
17
18
|
this.operatorLabel = '';
|
|
18
19
|
}
|
|
@@ -43,6 +44,7 @@ let HelixHeadlessFilterChip = class HelixHeadlessFilterChip extends MixinDismiss
|
|
|
43
44
|
part="dismiss"
|
|
44
45
|
type="button"
|
|
45
46
|
aria-label=${this.removeName}
|
|
47
|
+
tabindex=${this.dismissTabStop ? nothing : '-1'}
|
|
46
48
|
@click=${() => this.dismiss()}
|
|
47
49
|
>
|
|
48
50
|
<svg part="dismiss-mark" fill="currentColor" viewBox=${x.viewBox} aria-hidden="true">
|
|
@@ -56,6 +58,9 @@ __decorate([
|
|
|
56
58
|
words,
|
|
57
59
|
property({ attribute: 'dismiss-label' })
|
|
58
60
|
], HelixHeadlessFilterChip.prototype, "dismissLabel", void 0);
|
|
61
|
+
__decorate([
|
|
62
|
+
property({ attribute: false })
|
|
63
|
+
], HelixHeadlessFilterChip.prototype, "dismissTabStop", void 0);
|
|
59
64
|
__decorate([
|
|
60
65
|
property()
|
|
61
66
|
], HelixHeadlessFilterChip.prototype, "label", void 0);
|
|
@@ -180,7 +180,6 @@ let HelixHeadlessFilterMenu = class HelixHeadlessFilterMenu extends HelixElement
|
|
|
180
180
|
part="divider"
|
|
181
181
|
exportparts="content: divider-content, line: divider-line"
|
|
182
182
|
slot="submenu"
|
|
183
|
-
announced
|
|
184
183
|
></${divider}>`
|
|
185
184
|
: nothing}
|
|
186
185
|
${offered.map((option) => html `<${item}
|
|
@@ -33,8 +33,9 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
33
33
|
* first colon, reads a leading `-` as the negation, and reads a word in
|
|
34
34
|
* brackets before the colon as the comparison.
|
|
35
35
|
*
|
|
36
|
-
* **Keyboard.** Focus stays in the text
|
|
37
|
-
* suggestion as its `aria-activedescendant
|
|
36
|
+
* **Keyboard.** Focus stays in the text, which is the field's one tab stop. The
|
|
37
|
+
* text names the highlighted suggestion as its `aria-activedescendant`, and the
|
|
38
|
+
* arrow keys reach the tokens.
|
|
38
39
|
*
|
|
39
40
|
* | Key | What it does |
|
|
40
41
|
* |---|---|
|
|
@@ -92,7 +93,8 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
92
93
|
* and removing the chip alone would leave its value ticked in the hidden
|
|
93
94
|
* menu. Its dismissal is treated as the field being filtered by nothing, so
|
|
94
95
|
* it fires `helix-facet` like every other change, and the chip goes when the
|
|
95
|
-
* filter goes.
|
|
96
|
+
* filter goes. Set `chips` and the field adds, updates, and removes those
|
|
97
|
+
* chips itself, from every `helix-facet` you do not cancel.
|
|
96
98
|
*
|
|
97
99
|
* @since 0.1.0
|
|
98
100
|
* @status stable
|
|
@@ -105,6 +107,8 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
105
107
|
* @slot feedback - The message that says what is wrong, for content richer than a string. It shows while there is a message.
|
|
106
108
|
* @slot hint - Help under the control, for content richer than the `hint` text.
|
|
107
109
|
* @slot label - The label, for content richer than the `label` text.
|
|
110
|
+
* @slot loading - What the panel shows while `loading` is set, for content richer than `loading-text`.
|
|
111
|
+
* @slot no-results - What the panel shows when `filter-suggestions` hides every suggestion, for content richer than `no-results-text`.
|
|
108
112
|
* @slot start - Content before the text, such as a search icon.
|
|
109
113
|
* @slot suggestions - Suggestions for what to type next, as `Option`s and `Option.Group`s.
|
|
110
114
|
* @slot tags - The active filters, as `FilterChip`s or `Tag`s.
|
|
@@ -121,17 +125,21 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
121
125
|
*
|
|
122
126
|
* @csspart clear - The button that empties the field.
|
|
123
127
|
* @csspart control - The input itself.
|
|
128
|
+
* @csspart empty - Shown in the panel when `filter-suggestions` hides every suggestion.
|
|
124
129
|
* @csspart feedback - The error message under the field.
|
|
125
130
|
* @csspart field - The box that holds everything.
|
|
126
131
|
* @csspart hint - The help text under the field, shown while there is no error.
|
|
127
132
|
* @csspart label - The label above the field.
|
|
128
133
|
* @csspart lead - The box before the text that holds the `start` slot.
|
|
129
134
|
* @csspart live - The live region that says how many suggestions the open panel offers. It is rendered from the first time the panel opens with suggestions.
|
|
135
|
+
* @csspart loading - Shown in the panel while the suggestions load.
|
|
130
136
|
* @csspart rest - The count of tokens that did not fit. It is a `<button>` with `expandable` and a `<span>` without it.
|
|
131
137
|
* @csspart surface - The floating panel the suggestions sit in.
|
|
132
138
|
* @csspart tags - The row the tokens sit in.
|
|
133
139
|
*
|
|
134
140
|
* @cssstate invalid - The field has an error to show, from the browser's validation or from your `feedback`.
|
|
141
|
+
* @cssstate loading - While `loading` is set.
|
|
142
|
+
* @cssstate no-results - `filter-suggestions` hid every suggestion, and `no-results-text` or the `no-results` slot says so.
|
|
135
143
|
* @cssstate open - The suggestions are open.
|
|
136
144
|
*/
|
|
137
145
|
export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_base {
|
|
@@ -143,6 +151,20 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
143
151
|
protected readonly aria: AriaForwardController;
|
|
144
152
|
/** Typed, so every keystroke is announced. See `announcesInput`. */
|
|
145
153
|
protected readonly announcesInput = true;
|
|
154
|
+
/**
|
|
155
|
+
* Whether the field keeps a `FilterChip` for each filter in its `tags` slot,
|
|
156
|
+
* from the `helix-facet` events it hears.
|
|
157
|
+
*
|
|
158
|
+
* Off, you build the chips, which is what you do when your own model holds
|
|
159
|
+
* the filters. On, a filter typed, picked, or changed adds or updates the
|
|
160
|
+
* chip for its field, one with no values takes it away (unless its
|
|
161
|
+
* comparison takes none, such as "is empty"), and removing the chip clears
|
|
162
|
+
* the filter. A `helix-facet` you cancel changes no chip.
|
|
163
|
+
*
|
|
164
|
+
* The chips are elements the field adds to its own children, so leave this
|
|
165
|
+
* off where a framework renders those children and would remove them.
|
|
166
|
+
*/
|
|
167
|
+
chips: boolean;
|
|
146
168
|
/**
|
|
147
169
|
* Shows a button that empties the field.
|
|
148
170
|
*
|
|
@@ -163,6 +185,27 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
163
185
|
* what you slot and you narrow it.
|
|
164
186
|
*/
|
|
165
187
|
filterSuggestions: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Whether the suggestions are still loading, such as while a request for the
|
|
190
|
+
* values of a typed key is out.
|
|
191
|
+
*
|
|
192
|
+
* The panel opens on `loading-text`, or the `loading` slot, while the field
|
|
193
|
+
* has focus, and a screen reader hears it. Slot the options and clear this
|
|
194
|
+
* when they arrive.
|
|
195
|
+
*/
|
|
196
|
+
loading: boolean;
|
|
197
|
+
/**
|
|
198
|
+
* Text the panel shows while the suggestions load.
|
|
199
|
+
*/
|
|
200
|
+
loadingText: string;
|
|
201
|
+
/**
|
|
202
|
+
* Text the panel shows when `filter-suggestions` hides every suggestion, such
|
|
203
|
+
* as "No matching filters".
|
|
204
|
+
*
|
|
205
|
+
* Empty, or no `no-results` slot, and the panel closes instead, as it does
|
|
206
|
+
* when it has nothing to offer.
|
|
207
|
+
*/
|
|
208
|
+
noResultsText: string;
|
|
166
209
|
/**
|
|
167
210
|
* The text shown while the field is empty.
|
|
168
211
|
*
|
|
@@ -341,6 +384,16 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
341
384
|
* else could have said it.
|
|
342
385
|
*/
|
|
343
386
|
private announceFacet;
|
|
387
|
+
/**
|
|
388
|
+
* Keeps the chip for a filter in step with it, under `chips`.
|
|
389
|
+
*
|
|
390
|
+
* After every listener has had its turn, so a consumer's `preventDefault()`
|
|
391
|
+
* on the same event, on this element or above it, is read: `dispatchEvent`
|
|
392
|
+
* runs them all before the microtask does.
|
|
393
|
+
*/
|
|
394
|
+
private readonly handleFacet;
|
|
395
|
+
/** Adds, updates or removes the chip for one filter. See `chips`. */
|
|
396
|
+
private drawChip;
|
|
344
397
|
/**
|
|
345
398
|
* A chip asking to go, answered as the filter going.
|
|
346
399
|
*
|
|
@@ -413,6 +466,12 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
413
466
|
* is why `settleOpen` is not called from here.
|
|
414
467
|
*/
|
|
415
468
|
private refreshSuggesting;
|
|
469
|
+
/**
|
|
470
|
+
* Whether `filter-suggestions` has hidden every suggestion there is, where a
|
|
471
|
+
* `no-results-text` or a `no-results` slot says so. The panel stays open on
|
|
472
|
+
* it, and `:state(no-results)` is on the host.
|
|
473
|
+
*/
|
|
474
|
+
private get unmatched();
|
|
416
475
|
/** The panel again, whenever what is in it changes. */
|
|
417
476
|
private readonly handleSuggestions;
|
|
418
477
|
/** Focus arriving in the field is a reader asking what they can type. */
|
|
@@ -426,7 +485,15 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
426
485
|
private readonly handleFocusOut;
|
|
427
486
|
/** The leading box again, whenever what is in front of the text changes. */
|
|
428
487
|
private readonly handleLeading;
|
|
429
|
-
/**
|
|
488
|
+
/**
|
|
489
|
+
* The row again, whenever what is in it changes.
|
|
490
|
+
*
|
|
491
|
+
* **And the text is the field's one tab stop.** Each token's remove button
|
|
492
|
+
* was a stop of its own, so Tab through a field with five filters took six
|
|
493
|
+
* presses. A `Tag` or `FilterChip` takes `dismissTabStop` for this, as the
|
|
494
|
+
* tags of a `multiple` Combobox do, and the arrow keys still reach every one.
|
|
495
|
+
* A token not yet defined is told once it is.
|
|
496
|
+
*/
|
|
430
497
|
private readonly handleTokens;
|
|
431
498
|
/** The tokens slotted in `tags`, which the arrow keys reach from the text. */
|
|
432
499
|
protected tokenElements(): HTMLElement[];
|
|
@@ -21,9 +21,13 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
21
21
|
target: () => this.partNamed('control'),
|
|
22
22
|
});
|
|
23
23
|
this.announcesInput = true;
|
|
24
|
+
this.chips = false;
|
|
24
25
|
this.clearable = false;
|
|
25
26
|
this.clearLabel = 'Clear';
|
|
26
27
|
this.filterSuggestions = false;
|
|
28
|
+
this.loading = false;
|
|
29
|
+
this.loadingText = 'Loading';
|
|
30
|
+
this.noResultsText = '';
|
|
27
31
|
this.placeholder = '';
|
|
28
32
|
this.suggestionsLabel = '{count, plural, one {# suggestion} other {# suggestions}}';
|
|
29
33
|
this.status = new StatusController(this);
|
|
@@ -59,6 +63,17 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
59
63
|
event.stopPropagation();
|
|
60
64
|
this.announceChange();
|
|
61
65
|
};
|
|
66
|
+
this.handleFacet = (event) => {
|
|
67
|
+
if (!this.chips) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const facet = event.detail;
|
|
71
|
+
queueMicrotask(() => {
|
|
72
|
+
if (!event.defaultPrevented) {
|
|
73
|
+
this.drawChip(facet);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
};
|
|
62
77
|
this.handleDismiss = (event) => {
|
|
63
78
|
const chip = event.composedPath()[0];
|
|
64
79
|
if (!(chip instanceof HTMLElement) || chip.slot !== 'tags') {
|
|
@@ -161,6 +176,20 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
161
176
|
};
|
|
162
177
|
this.handleTokens = () => {
|
|
163
178
|
this.over = this.hideOverflow(this.tokens);
|
|
179
|
+
for (const token of this.tokenElements()) {
|
|
180
|
+
const quiet = () => {
|
|
181
|
+
if ('dismissTabStop' in token) {
|
|
182
|
+
;
|
|
183
|
+
token.dismissTabStop = false;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
if (token.localName.includes('-') && !customElements.get(token.localName)) {
|
|
187
|
+
void customElements.whenDefined(token.localName).then(quiet);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
quiet();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
164
193
|
};
|
|
165
194
|
}
|
|
166
195
|
static { this.words = 'helix-filter-input'; }
|
|
@@ -181,14 +210,22 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
181
210
|
}
|
|
182
211
|
}
|
|
183
212
|
sayCount() {
|
|
184
|
-
const count = this.open && this.suggesting ? this.suggestions.length : -1;
|
|
213
|
+
const count = this.open && this.suggesting ? (this.loading ? -2 : this.suggestions.length) : -1;
|
|
185
214
|
if (count === this.saidCount) {
|
|
186
215
|
return;
|
|
187
216
|
}
|
|
188
217
|
this.saidCount = count;
|
|
218
|
+
if (count === -2) {
|
|
219
|
+
this.status.sayNow(this.loadingText);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
189
222
|
if (count < 0) {
|
|
190
223
|
return;
|
|
191
224
|
}
|
|
225
|
+
if (count === 0 && this.noResultsText !== '') {
|
|
226
|
+
this.status.sayNow(this.noResultsText);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
192
229
|
this.status.sayNow(this.suggestionLabel
|
|
193
230
|
? countLabel(count, this.suggestionLabel, this.suggestionsLabel, wordsLocale(this))
|
|
194
231
|
: formatWords(this, 'suggestionsLabel', { count }));
|
|
@@ -259,6 +296,25 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
259
296
|
? menu.apply(facet.field, facet.values, facet.operator)
|
|
260
297
|
: this.dispatchEvent(new HelixFacetEvent(facet));
|
|
261
298
|
}
|
|
299
|
+
drawChip(facet) {
|
|
300
|
+
const tag = `${this.constructor.tier}filter-chip`;
|
|
301
|
+
const held = [...this.children].find((child) => child.localName === tag && child.getAttribute('name') === facet.field);
|
|
302
|
+
if (facet.values.length === 0 && facet.arity !== 'none') {
|
|
303
|
+
held?.remove();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const chip = held ?? document.createElement(tag);
|
|
307
|
+
chip.slot = 'tags';
|
|
308
|
+
chip.dismissible = true;
|
|
309
|
+
chip.label = facet.label;
|
|
310
|
+
chip.setAttribute('name', facet.field);
|
|
311
|
+
chip.operator = facet.operator;
|
|
312
|
+
chip.operatorLabel = facet.operatorLabel;
|
|
313
|
+
chip.textContent = facet.values.join(', ');
|
|
314
|
+
if (!held) {
|
|
315
|
+
this.append(chip);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
262
318
|
readFilters() {
|
|
263
319
|
const fields = this.fields;
|
|
264
320
|
if (fields.length === 0) {
|
|
@@ -310,9 +366,13 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
310
366
|
refreshSuggesting() {
|
|
311
367
|
this.narrow();
|
|
312
368
|
const options = this.suggestions;
|
|
313
|
-
this.suggesting = options.length > 0;
|
|
369
|
+
this.suggesting = options.length > 0 || this.loading || this.unmatched;
|
|
314
370
|
this.activeAt = this.activeAt < options.length ? this.activeAt : -1;
|
|
315
371
|
}
|
|
372
|
+
get unmatched() {
|
|
373
|
+
const said = this.noResultsText !== '' || this.knownChildren.some((child) => child.slot === 'no-results');
|
|
374
|
+
return said && !this.loading && this.options.length > 0 && this.suggestions.length === 0;
|
|
375
|
+
}
|
|
316
376
|
tokenElements() {
|
|
317
377
|
return (this.tokens?.assignedElements({ flatten: true }) ?? []);
|
|
318
378
|
}
|
|
@@ -328,10 +388,12 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
328
388
|
connectedCallback() {
|
|
329
389
|
super.connectedCallback();
|
|
330
390
|
this.addEventListener('helix-dismiss', this.handleDismiss);
|
|
391
|
+
this.addEventListener(HelixFacetEvent.eventName, this.handleFacet);
|
|
331
392
|
}
|
|
332
393
|
disconnectedCallback() {
|
|
333
394
|
super.disconnectedCallback();
|
|
334
395
|
this.removeEventListener('helix-dismiss', this.handleDismiss);
|
|
396
|
+
this.removeEventListener(HelixFacetEvent.eventName, this.handleFacet);
|
|
335
397
|
}
|
|
336
398
|
willUpdate(changed) {
|
|
337
399
|
super.willUpdate(changed);
|
|
@@ -348,6 +410,8 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
348
410
|
}
|
|
349
411
|
this.handleTokens();
|
|
350
412
|
this.refreshSuggesting();
|
|
413
|
+
this.toggleState('loading', this.loading);
|
|
414
|
+
this.toggleState('no-results', this.unmatched);
|
|
351
415
|
}
|
|
352
416
|
firstUpdated(changed) {
|
|
353
417
|
super.firstUpdated(changed);
|
|
@@ -443,11 +507,22 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
443
507
|
?hidden=${!this.open}
|
|
444
508
|
>
|
|
445
509
|
<slot name="suggestions" @slotchange=${this.handleSuggestions}></slot>
|
|
510
|
+
<!-- Hidden from the tree, which a listbox keeps for its options: the
|
|
511
|
+
live region says these. -->
|
|
512
|
+
<div part="loading" aria-hidden="true" ?hidden=${!this.loading}>
|
|
513
|
+
<slot name="loading">${this.loadingText}</slot>
|
|
514
|
+
</div>
|
|
515
|
+
<div part="empty" aria-hidden="true" ?hidden=${!this.unmatched}>
|
|
516
|
+
<slot name="no-results">${this.noResultsText}</slot>
|
|
517
|
+
</div>
|
|
446
518
|
</div>
|
|
447
519
|
${drawStatus(this.status)}
|
|
448
520
|
${this.renderHint()}${this.renderFeedback()}`;
|
|
449
521
|
}
|
|
450
522
|
};
|
|
523
|
+
__decorate([
|
|
524
|
+
property({ type: Boolean })
|
|
525
|
+
], HelixHeadlessFilterInput.prototype, "chips", void 0);
|
|
451
526
|
__decorate([
|
|
452
527
|
property({ type: Boolean })
|
|
453
528
|
], HelixHeadlessFilterInput.prototype, "clearable", void 0);
|
|
@@ -458,6 +533,16 @@ __decorate([
|
|
|
458
533
|
__decorate([
|
|
459
534
|
property({ attribute: 'filter-suggestions', type: Boolean })
|
|
460
535
|
], HelixHeadlessFilterInput.prototype, "filterSuggestions", void 0);
|
|
536
|
+
__decorate([
|
|
537
|
+
property({ reflect: true, type: Boolean })
|
|
538
|
+
], HelixHeadlessFilterInput.prototype, "loading", void 0);
|
|
539
|
+
__decorate([
|
|
540
|
+
words,
|
|
541
|
+
property({ attribute: 'loading-text' })
|
|
542
|
+
], HelixHeadlessFilterInput.prototype, "loadingText", void 0);
|
|
543
|
+
__decorate([
|
|
544
|
+
property({ attribute: 'no-results-text' })
|
|
545
|
+
], HelixHeadlessFilterInput.prototype, "noResultsText", void 0);
|
|
461
546
|
__decorate([
|
|
462
547
|
property()
|
|
463
548
|
], HelixHeadlessFilterInput.prototype, "placeholder", void 0);
|
|
@@ -15,7 +15,9 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
|
|
|
15
15
|
* **`relative` shows the same reading in words**, such as "3 hours ago" at
|
|
16
16
|
* `long`, "3 hr. ago" at `short` and "3h ago" at `narrow`, the styles
|
|
17
17
|
* `sl-relative-time` offers. `numeric="always"` says "1 day ago" where the
|
|
18
|
-
* default says "yesterday".
|
|
18
|
+
* default says "yesterday". Both readings round the count, as
|
|
19
|
+
* `sl-relative-time` does: 100 seconds ago is "2 minutes ago", and 59.6
|
|
20
|
+
* minutes is "1 hour".
|
|
19
21
|
*
|
|
20
22
|
* **`date-style` and `time-style` take each locale's own pattern**, such as
|
|
21
23
|
* "Tuesday, August 4, 2026" at `full`, and stand in for the separate fields,
|
|
@@ -209,6 +209,12 @@ export declare class HelixHeadlessInput extends HelixHeadlessInput_base {
|
|
|
209
209
|
/** Where the selection starts, as an offset into the value. */
|
|
210
210
|
get selectionStart(): number;
|
|
211
211
|
set selectionStart(at: number);
|
|
212
|
+
/**
|
|
213
|
+
* Replaces a range of the text, as the native method does: the selection
|
|
214
|
+
* where `start` and `end` are left out, and `selectMode` says what is
|
|
215
|
+
* selected after. It sends no event, as the native method sends none.
|
|
216
|
+
*/
|
|
217
|
+
setRangeText(replacement: string, start?: number, end?: number, selectMode?: SelectionMode): void;
|
|
212
218
|
/**
|
|
213
219
|
* Selects the range from `start` to `end` in one call, which setting
|
|
214
220
|
* `selectionStart` and `selectionEnd` one at a time cannot always do.
|
|
@@ -97,6 +97,19 @@ let HelixHeadlessInput = class HelixHeadlessInput extends MixinControllerValue(M
|
|
|
97
97
|
control.selectionStart = at;
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
+
setRangeText(replacement, start, end, selectMode) {
|
|
101
|
+
const control = this.control;
|
|
102
|
+
if (!control) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (start === undefined || end === undefined) {
|
|
106
|
+
control.setRangeText(replacement);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
control.setRangeText(replacement, start, end, selectMode);
|
|
110
|
+
}
|
|
111
|
+
this.value = control.value;
|
|
112
|
+
}
|
|
100
113
|
setSelectionRange(start, end, direction) {
|
|
101
114
|
this.control?.setSelectionRange(start, end, direction);
|
|
102
115
|
}
|
|
@@ -154,6 +154,12 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
|
|
|
154
154
|
emptyText: string;
|
|
155
155
|
/** What the list is narrowed to, whoever set it. */
|
|
156
156
|
private filterState;
|
|
157
|
+
/**
|
|
158
|
+
* @internal Set by `Command` under `home-end="list"`: whether Home and End
|
|
159
|
+
* in the filter field move the highlight, without Shift, rather than the
|
|
160
|
+
* caret. A property, as coordination between elements.
|
|
161
|
+
*/
|
|
162
|
+
fieldHomeEnd: boolean;
|
|
157
163
|
/**
|
|
158
164
|
* The text the list is filtered by, if any.
|
|
159
165
|
*
|
|
@@ -23,6 +23,7 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
|
|
|
23
23
|
this.defaultFilter = '';
|
|
24
24
|
this.emptyText = 'No matches';
|
|
25
25
|
this.filterState = undefined;
|
|
26
|
+
this.fieldHomeEnd = false;
|
|
26
27
|
this.filterable = false;
|
|
27
28
|
this.filterDelay = 0;
|
|
28
29
|
this.filterLabel = 'Filter';
|
|
@@ -127,7 +128,8 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
|
|
|
127
128
|
(event.key === 'Home' || event.key === 'End');
|
|
128
129
|
const grid = this.layout === 'grid' && !this.data.records;
|
|
129
130
|
const along = grid && !typing ? inlineStep(this, event.key) : 0;
|
|
130
|
-
|
|
131
|
+
const fieldMoves = this.fieldHomeEnd && !event.shiftKey ? MOVES : FIELD_MOVES;
|
|
132
|
+
if (!fieldRange && !along && !(typing ? fieldMoves : MOVES).has(event.key)) {
|
|
131
133
|
return;
|
|
132
134
|
}
|
|
133
135
|
event.preventDefault();
|
|
@@ -673,6 +675,9 @@ __decorate([
|
|
|
673
675
|
words,
|
|
674
676
|
property({ attribute: 'empty-text' })
|
|
675
677
|
], HelixHeadlessListbox.prototype, "emptyText", void 0);
|
|
678
|
+
__decorate([
|
|
679
|
+
property({ attribute: false })
|
|
680
|
+
], HelixHeadlessListbox.prototype, "fieldHomeEnd", void 0);
|
|
676
681
|
__decorate([
|
|
677
682
|
property()
|
|
678
683
|
], HelixHeadlessListbox.prototype, "filter", null);
|
|
@@ -124,7 +124,9 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
|
|
|
124
124
|
*/
|
|
125
125
|
badInputMessage: string;
|
|
126
126
|
/**
|
|
127
|
-
* The name of the step down button, read by screen readers.
|
|
127
|
+
* The name of the step down button, read by screen readers. The field's
|
|
128
|
+
* label follows it, so a reader hears "Decrease Quantity", as React Aria
|
|
129
|
+
* names its steppers.
|
|
128
130
|
*/
|
|
129
131
|
decrementLabel: string;
|
|
130
132
|
/**
|
|
@@ -138,7 +140,9 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
|
|
|
138
140
|
*/
|
|
139
141
|
formatOptions?: Intl.NumberFormatOptions;
|
|
140
142
|
/**
|
|
141
|
-
* The name of the step up button, read by screen readers.
|
|
143
|
+
* The name of the step up button, read by screen readers. The field's
|
|
144
|
+
* label follows it, so a reader hears "Increase Quantity", as React Aria
|
|
145
|
+
* names its steppers.
|
|
142
146
|
*/
|
|
143
147
|
incrementLabel: string;
|
|
144
148
|
/**
|
|
@@ -321,16 +325,22 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
|
|
|
321
325
|
* refused step, or one pinned at a bound, finished nothing.
|
|
322
326
|
*/
|
|
323
327
|
private land;
|
|
324
|
-
/** Steps the value up, by `largeStep` steps when `large` is set. */
|
|
325
|
-
stepUp(large?: boolean): void;
|
|
326
328
|
/**
|
|
327
|
-
* Steps the value
|
|
329
|
+
* Steps the value up by `step`, `count` times, as the native method and
|
|
330
|
+
* `Input.stepUp` do. `true` takes `largeStep` steps, as `PageUp` does.
|
|
331
|
+
*/
|
|
332
|
+
stepUp(count?: number | boolean): void;
|
|
333
|
+
/**
|
|
334
|
+
* Steps the value down by `step`, `count` times. `true` takes `largeStep`
|
|
335
|
+
* steps, as `PageDown` does.
|
|
328
336
|
*
|
|
329
337
|
* The same path a press of the down arrow takes, so a consumer driving this
|
|
330
338
|
* from their own control gets the clamping and the announcement rather than
|
|
331
339
|
* having to repeat both.
|
|
332
340
|
*/
|
|
333
|
-
stepDown(
|
|
341
|
+
stepDown(count?: number | boolean): void;
|
|
342
|
+
/** How many steps a `stepUp` argument asks for: a count, or `largeStep` for `true`. */
|
|
343
|
+
private stepsOf;
|
|
334
344
|
/** The timer of a held stepper button. */
|
|
335
345
|
private repeat;
|
|
336
346
|
/** Whether a pointer press stepped already, so the click that ends it does not step again. */
|