@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.
Files changed (39) hide show
  1. package/custom-elements.json +610 -93
  2. package/dist/chat/composer/chat-composer.d.ts +2 -1
  3. package/dist/chat/composer/chat-composer.js +7 -1
  4. package/dist/chat/root/chat-root.d.ts +36 -6
  5. package/dist/chat/root/chat-root.js +53 -5
  6. package/dist/code-block/root/code-block-root.d.ts +20 -2
  7. package/dist/code-block/root/code-block-root.js +29 -8
  8. package/dist/color-picker/root/color-picker-root.d.ts +50 -7
  9. package/dist/color-picker/root/color-picker-root.js +62 -1
  10. package/dist/combobox/root/combobox-root.d.ts +16 -6
  11. package/dist/combobox/root/combobox-root.js +9 -2
  12. package/dist/command/root/command-root.d.ts +16 -1
  13. package/dist/command/root/command-root.js +8 -0
  14. package/dist/filter-input/chip/filter-chip.d.ts +13 -3
  15. package/dist/filter-input/chip/filter-chip.js +5 -0
  16. package/dist/filter-input/menu/filter-menu.js +0 -1
  17. package/dist/filter-input/root/filter-input-root.d.ts +71 -4
  18. package/dist/filter-input/root/filter-input-root.js +87 -2
  19. package/dist/format-date/root/format-date-root.d.ts +3 -1
  20. package/dist/input/root/input-root.d.ts +6 -0
  21. package/dist/input/root/input-root.js +13 -0
  22. package/dist/listbox/root/listbox-root.d.ts +6 -0
  23. package/dist/listbox/root/listbox-root.js +6 -1
  24. package/dist/number-input/root/number-input-root.d.ts +16 -6
  25. package/dist/number-input/root/number-input-root.js +15 -4
  26. package/dist/select/root/select-root.js +2 -0
  27. package/dist/steps/root/steps-root.d.ts +32 -8
  28. package/dist/steps/root/steps-root.js +70 -9
  29. package/dist/steps/step/step.d.ts +9 -1
  30. package/dist/steps/step/step.js +5 -0
  31. package/dist/video-playlist/root/video-playlist-root.d.ts +19 -1
  32. package/dist/video-playlist/root/video-playlist-root.js +14 -3
  33. package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
  34. package/dist/virtual-list/root/virtual-list-root.js +7 -2
  35. package/lint.json +1 -1
  36. package/package.json +5 -5
  37. package/types/jsx.d.ts +490 -116
  38. package/types/vue.d.ts +108 -25
  39. 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
- return this.value === '' ? [] : this.value.split(',');
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, Left, Right | Move the caret in the filter field. |
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`, Backspace in the empty
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.
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. The text names the highlighted
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
- /** The row again, whenever what is in it changes. */
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
- if (!fieldRange && !along && !(typing ? FIELD_MOVES : MOVES).has(event.key)) {
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 down, by `largeStep` steps when `large` is set.
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(large?: boolean): void;
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. */