@fusion.dev/helix-headless 0.170.0 → 0.172.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 +1045 -154
- package/dist/chat/composer/chat-composer.d.ts +30 -2
- package/dist/chat/composer/chat-composer.js +29 -4
- package/dist/chat/root/chat-root.d.ts +73 -7
- package/dist/chat/root/chat-root.js +90 -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-input/root/color-input-root.d.ts +15 -1
- package/dist/color-input/root/color-input-root.js +19 -1
- package/dist/color-picker/root/color-picker-root.d.ts +54 -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/data-table/root/data-table-root.d.ts +5 -1
- package/dist/data-table/root/data-table-root.js +1 -0
- package/dist/dialog/root/dialog-root.js +1 -0
- package/dist/drawer/root/drawer-root.d.ts +4 -0
- package/dist/drawer/root/drawer-root.js +1 -0
- package/dist/element-events.generated.js +1 -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/lightbox/root/lightbox-root.js +1 -0
- package/dist/listbox/root/listbox-root.d.ts +8 -1
- package/dist/listbox/root/listbox-root.js +9 -3
- package/dist/menu/item/menu-item.d.ts +2 -0
- package/dist/menu/item/menu-item.js +2 -0
- package/dist/menu/root/menu-root.d.ts +73 -12
- package/dist/menu/root/menu-root.js +103 -7
- 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/popover/root/popover-root.d.ts +59 -6
- package/dist/popover/root/popover-root.js +51 -3
- package/dist/select/root/select-root.d.ts +2 -2
- package/dist/select/root/select-root.js +4 -2
- 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/textarea/root/textarea-root.d.ts +8 -0
- package/dist/textarea/root/textarea-root.js +16 -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 +800 -144
- package/types/vue.d.ts +165 -31
- package/types/words.d.ts +20 -2
|
@@ -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
|
*
|
|
@@ -388,7 +394,8 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
|
|
|
388
394
|
* sounded the same. Said when the filter changes and the count with it, so
|
|
389
395
|
* a keystroke that leaves the same options says nothing new. An empty filter
|
|
390
396
|
* says nothing, being the list as it was given; no matches is `empty-text`,
|
|
391
|
-
* which the region says in place of this.
|
|
397
|
+
* which the region says in place of this. A load that ends says the count
|
|
398
|
+
* too, whatever the filter.
|
|
392
399
|
*/
|
|
393
400
|
private tell;
|
|
394
401
|
/**
|
|
@@ -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();
|
|
@@ -258,13 +260,14 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
|
|
|
258
260
|
if (!this.hasUpdated) {
|
|
259
261
|
return;
|
|
260
262
|
}
|
|
261
|
-
|
|
263
|
+
const loaded = changed.get('loading') === true && !this.loading;
|
|
264
|
+
if (!changed.has('filter') && !loaded) {
|
|
262
265
|
this.tellActive(changed);
|
|
263
266
|
return;
|
|
264
267
|
}
|
|
265
268
|
const count = this.shown.length;
|
|
266
269
|
this.announced =
|
|
267
|
-
this.filter.trim() && count
|
|
270
|
+
(this.filter.trim() || loaded) && count
|
|
268
271
|
? this.resultLabel
|
|
269
272
|
? countLabel(count, this.resultLabel, this.resultsLabel, wordsLocale(this))
|
|
270
273
|
: formatWords(this, 'resultsLabel', { count })
|
|
@@ -673,6 +676,9 @@ __decorate([
|
|
|
673
676
|
words,
|
|
674
677
|
property({ attribute: 'empty-text' })
|
|
675
678
|
], HelixHeadlessListbox.prototype, "emptyText", void 0);
|
|
679
|
+
__decorate([
|
|
680
|
+
property({ attribute: false })
|
|
681
|
+
], HelixHeadlessListbox.prototype, "fieldHomeEnd", void 0);
|
|
676
682
|
__decorate([
|
|
677
683
|
property()
|
|
678
684
|
], HelixHeadlessListbox.prototype, "filter", null);
|
|
@@ -81,6 +81,8 @@ import '../../kbd/index.js';
|
|
|
81
81
|
* @csspart surface - The panel a submenu opens in beside the row, under the menu's `submenus="flyout"`.
|
|
82
82
|
*
|
|
83
83
|
* @cssstate checked - The row is checked.
|
|
84
|
+
* @cssstate disabled - The row is disabled.
|
|
85
|
+
* @cssstate expanded - Under `submenus="flyout"`, the row's submenu is open beside it.
|
|
84
86
|
* @cssstate highlighted - In a menu with `filterable`, the arrow keys or the pointer reached the row while focus stays in the filter field.
|
|
85
87
|
* @cssstate submenu-mark - Under `submenus="flyout"`, a row in this row's submenu can be ticked or opens a submenu, so the submenu keeps its trailing column.
|
|
86
88
|
* @cssstate submenu-start - Under `submenus="flyout"`, a row in this row's submenu has content before its label, so the submenu keeps its leading column.
|
|
@@ -93,6 +93,8 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
|
|
|
93
93
|
super.willUpdate(changed);
|
|
94
94
|
this.toggleState('checked', this.checked);
|
|
95
95
|
this.toggleState('highlighted', this.virtual && this.highlighted);
|
|
96
|
+
this.toggleState('disabled', this.disabled);
|
|
97
|
+
this.toggleState('expanded', this.expanded);
|
|
96
98
|
if (!this.childrenKnown) {
|
|
97
99
|
return;
|
|
98
100
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AriaForwardController, HelixElement, KeyboardNavigationController, type Placement } from '@fusion.dev/helix-core';
|
|
1
|
+
import { AriaForwardController, HelixElement, KeyboardNavigationController, OpenPhaseController, type Placement } from '@fusion.dev/helix-core';
|
|
2
2
|
import { type PropertyValues, type TemplateResult } from 'lit';
|
|
3
3
|
declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.dev/helix-core").CollectionElement) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & typeof HelixElement;
|
|
4
4
|
/**
|
|
@@ -18,7 +18,9 @@ declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.d
|
|
|
18
18
|
*
|
|
19
19
|
* Submenus drill down by default. A submenu replaces the panel's content, and a
|
|
20
20
|
* back row returns to the previous page. One panel stays inside a narrow
|
|
21
|
-
* window, and works on touch screens, which have no hover.
|
|
21
|
+
* window, and works on touch screens, which have no hover. Focus follows a
|
|
22
|
+
* mouse resting on a row in either mode, and a drilled page is a `menu` named
|
|
23
|
+
* by the row that opened it.
|
|
22
24
|
*
|
|
23
25
|
* **`submenus="flyout"` opens each submenu in a panel of its own beside its
|
|
24
26
|
* row**, as React Aria's `SubmenuTrigger` and Radix's `Sub` do. Resting the
|
|
@@ -52,6 +54,11 @@ declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.d
|
|
|
52
54
|
* trigger's width, for a menu as wide as its trigger:
|
|
53
55
|
* `helix-menu::part(surface) { min-inline-size: var(--floating-anchor-inline-size) }`.
|
|
54
56
|
*
|
|
57
|
+
* **The choices in one place.** `checkedValues` holds the values of the rows
|
|
58
|
+
* that are checked, anywhere in the menu, and setting it checks exactly
|
|
59
|
+
* those rows, as React Aria's `selectedKeys` does. `change` is sent on the
|
|
60
|
+
* menu when a reader's choice changes it.
|
|
61
|
+
*
|
|
55
62
|
* With `context`, the trigger is an area and the menu is its context menu: a
|
|
56
63
|
* right-click, a long press, Shift+F10 or the menu key over the area opens it
|
|
57
64
|
* at the pointer, or at the area's corner from the keyboard, as Radix's context
|
|
@@ -74,7 +81,8 @@ declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.d
|
|
|
74
81
|
* field: Up and Down Arrow move the highlight through the rows that are left,
|
|
75
82
|
* Enter chooses the highlighted row, Right Arrow opens its submenu, Left Arrow
|
|
76
83
|
* on a submenu goes back, and Escape clears the filter and then closes the
|
|
77
|
-
* menu.
|
|
84
|
+
* menu. A screen reader hears how many rows the filter keeps
|
|
85
|
+
* (`results-label`). Home, End and the other keys move the caret and type. The field names
|
|
78
86
|
* the highlighted row with `aria-activedescendant`, and the rows are a `menu`
|
|
79
87
|
* after the field. Cancel `helix-filter` to keep every row
|
|
80
88
|
* showing and narrow the rows yourself, such as by replacing them with rows
|
|
@@ -87,9 +95,12 @@ declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.d
|
|
|
87
95
|
* @documentation https://fusion.dev/helix/components/menu
|
|
88
96
|
*
|
|
89
97
|
* @slot - The items.
|
|
98
|
+
* @slot empty - What the menu shows when it has no items, for content richer than `empty-text`.
|
|
99
|
+
* @slot loading - What the menu shows while its items load, for content richer than `loading-text`.
|
|
90
100
|
* @slot no-results - What the menu shows when the filter keeps no rows, for content richer than `no-results-text`.
|
|
91
101
|
* @slot trigger - The control that opens the menu, or with `context` the area a context menu opens over.
|
|
92
102
|
*
|
|
103
|
+
* @event {Event} change - A reader's choice checked or unchecked rows. Read `checkedValues` for the rows that are checked now.
|
|
93
104
|
* @event {HelixBeforeCloseEvent} helix-before-close - The menu is about to close. Cancelable: cancel it to keep it open.
|
|
94
105
|
* @event {HelixBeforeOpenEvent} helix-before-open - The menu is about to open. Cancelable: cancel it to keep it closed.
|
|
95
106
|
* @event {HelixFilterEvent} helix-filter - The text in the filter field changed, with the new text in `detail.value`. Cancelable: cancel it to keep every row showing and narrow the rows yourself.
|
|
@@ -110,6 +121,8 @@ declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.d
|
|
|
110
121
|
* @cssproperty --floating-available-block-size - The room between the panel and the edge of the screen it opens toward, written on the panel while it is open. Read it; a value set here is written over.
|
|
111
122
|
*
|
|
112
123
|
* @cssstate drilled - A submenu's page is showing, so content that is not a row can hide with the top page.
|
|
124
|
+
* @cssstate entering - From the moment the panel shows until its entrance has finished.
|
|
125
|
+
* @cssstate exiting - From the moment the panel hides until its exit has finished.
|
|
113
126
|
* @cssstate mark - A row on the current page has a checkbox or a link, so every row keeps the trailing column.
|
|
114
127
|
* @cssstate open - While the menu is open.
|
|
115
128
|
* @cssstate placed-bottom - While the open panel sits below its trigger.
|
|
@@ -124,6 +137,7 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
|
|
|
124
137
|
* `registerWords('de', { 'helix-menu': { backLabel: 'Zurück' } })`.
|
|
125
138
|
*/
|
|
126
139
|
static words: string;
|
|
140
|
+
protected statusSlots: boolean;
|
|
127
141
|
/**
|
|
128
142
|
* The text of the row that returns from a submenu.
|
|
129
143
|
*/
|
|
@@ -204,6 +218,12 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
|
|
|
204
218
|
* Space between the trigger and the surface, in pixels.
|
|
205
219
|
*/
|
|
206
220
|
offset: number;
|
|
221
|
+
/**
|
|
222
|
+
* What opens the menu from its trigger. `long-press` leaves a press to the
|
|
223
|
+
* trigger's own action and opens the menu on a press held for half a second,
|
|
224
|
+
* or on Alt+Down or Alt+Up.
|
|
225
|
+
*/
|
|
226
|
+
openOn: 'click' | 'long-press';
|
|
207
227
|
/** Whether the trigger opens the menu only on a held press. */
|
|
208
228
|
private get pressHeld();
|
|
209
229
|
/**
|
|
@@ -213,18 +233,18 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
|
|
|
213
233
|
private readonly longPress;
|
|
214
234
|
/** Where a held press on a context menu's area is, to open the menu there. */
|
|
215
235
|
private heldAt;
|
|
216
|
-
/**
|
|
217
|
-
* What opens the menu from its trigger. `long-press` leaves a press to the
|
|
218
|
-
* trigger's own action and opens the menu on a press held for half a second,
|
|
219
|
-
* or on Alt+Down or Alt+Up.
|
|
220
|
-
*/
|
|
221
|
-
openOn: 'click' | 'long-press';
|
|
222
236
|
/**
|
|
223
237
|
* The side the menu opens on, when there is room.
|
|
224
238
|
*
|
|
225
239
|
* It moves if that side does not fit.
|
|
226
240
|
*/
|
|
227
241
|
placement: Placement;
|
|
242
|
+
/**
|
|
243
|
+
* What a screen reader hears when the filter narrows the rows. `{count}` is
|
|
244
|
+
* how many rows are left, written in the locale, and each language writes
|
|
245
|
+
* the forms it has, as Listbox's `results-label` does.
|
|
246
|
+
*/
|
|
247
|
+
resultsLabel: string;
|
|
228
248
|
/**
|
|
229
249
|
* How a submenu opens. `drill` shows it in place of the page showing, with
|
|
230
250
|
* a way back. `flyout` opens it in a panel of its own beside its row.
|
|
@@ -257,6 +277,7 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
|
|
|
257
277
|
* menu that drills, so those keys are spoken for here rather than unhandled.
|
|
258
278
|
*/
|
|
259
279
|
protected readonly keys: KeyboardNavigationController;
|
|
280
|
+
protected readonly phases: OpenPhaseController;
|
|
260
281
|
/** Carries a host `aria-label` and `aria-labelledby` to the surface. */
|
|
261
282
|
protected readonly aria: AriaForwardController;
|
|
262
283
|
/** Where a context menu was asked for, in the viewport, while it is open. */
|
|
@@ -310,6 +331,21 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
|
|
|
310
331
|
* it, the one direction a reference crosses.
|
|
311
332
|
*/
|
|
312
333
|
private reach;
|
|
334
|
+
/** Watches for rows taken away while the menu is open. */
|
|
335
|
+
private rowWatch;
|
|
336
|
+
/**
|
|
337
|
+
* Moves the reader to a neighbor when the page takes away the row they were
|
|
338
|
+
* on: the row that took its place, or the one before it where it was the
|
|
339
|
+
* last. A menu with no rows left closes and gives focus to its trigger.
|
|
340
|
+
*
|
|
341
|
+
* **Focus went to the body**, and the menu then ignored every key, its
|
|
342
|
+
* listener being on the host: measured in Chromium, removing the focused
|
|
343
|
+
* row and pressing ArrowUp did nothing. List and Virtual List move focus to
|
|
344
|
+
* a neighbor the same way, as React Aria's collections do. With `filterable`,
|
|
345
|
+
* focus stays in the field and the highlight moves. Focus a reader put
|
|
346
|
+
* anywhere but the body is left there.
|
|
347
|
+
*/
|
|
348
|
+
private readonly followRemoved;
|
|
313
349
|
/** Opens the menu. */
|
|
314
350
|
show(): void;
|
|
315
351
|
/** Opens the menu with focus on its first or its last row. */
|
|
@@ -327,6 +363,23 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
|
|
|
327
363
|
private enter;
|
|
328
364
|
/** Returns to the page above, or closes when there is none. */
|
|
329
365
|
private leave;
|
|
366
|
+
/** The choice rows anywhere in the menu, in document order. */
|
|
367
|
+
private get choiceRows();
|
|
368
|
+
/**
|
|
369
|
+
* The values of the choice rows that are checked, anywhere in the menu, in
|
|
370
|
+
* document order.
|
|
371
|
+
*
|
|
372
|
+
* Set it to check exactly those rows and uncheck every other choice row, as
|
|
373
|
+
* React Aria's `selectedKeys` controls a menu's selection from one place.
|
|
374
|
+
* Each row keeps its own `checked` too, which this reads and writes. `change`
|
|
375
|
+
* is sent on the menu when a reader's choice changes it.
|
|
376
|
+
*/
|
|
377
|
+
get checkedValues(): string[];
|
|
378
|
+
set checkedValues(values: readonly string[]);
|
|
379
|
+
/** The checked values `change` last said, joined, so a choice that moved nothing says nothing. */
|
|
380
|
+
private toldChecked;
|
|
381
|
+
/** Sends `change` when a reader's choice has changed which rows are checked. */
|
|
382
|
+
private tellChecked;
|
|
330
383
|
private readonly handleSelect;
|
|
331
384
|
/**
|
|
332
385
|
* Keeps whatever has focus in sight, however it got there.
|
|
@@ -390,9 +443,14 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
|
|
|
390
443
|
private grace;
|
|
391
444
|
private stopHover;
|
|
392
445
|
/**
|
|
393
|
-
* A mouse resting on a row
|
|
394
|
-
* moment the row's submenu opens, or the submenus below the row's own page
|
|
446
|
+
* A mouse resting on a row: focus follows it, and under `flyout`, after a
|
|
447
|
+
* moment, the row's submenu opens, or the submenus below the row's own page
|
|
395
448
|
* close. Not while the pointer is crossing toward an open submenu.
|
|
449
|
+
*
|
|
450
|
+
* **In every mode**, as the APG's menus, React Aria's and Radix's have it.
|
|
451
|
+
* Under `drill` the pointer did not move focus, so the row under the
|
|
452
|
+
* pointer and the row the keys moved from were two rows, measured in
|
|
453
|
+
* Chromium: hovering Delete left focus on Cut.
|
|
396
454
|
*/
|
|
397
455
|
private readonly handleHover;
|
|
398
456
|
/** The pointer leaving a row whose submenu is open, which starts the grace area. */
|
|
@@ -402,8 +460,11 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
|
|
|
402
460
|
connectedCallback(): void;
|
|
403
461
|
disconnectedCallback(): void;
|
|
404
462
|
protected updated(changed: PropertyValues): void;
|
|
463
|
+
/** Whether the surface is named by a submenu's page, which `nameSurface` undoes at the top. */
|
|
464
|
+
private namedByPage;
|
|
405
465
|
/**
|
|
406
|
-
* Names the surface by its trigger, where nothing else names it
|
|
466
|
+
* Names the surface by its trigger, where nothing else names it, and a
|
|
467
|
+
* submenu's page by its title.
|
|
407
468
|
*
|
|
408
469
|
* By element reference, which is the one direction that crosses: the surface
|
|
409
470
|
* is in this shadow root and the trigger is in the tree around it. A host
|