@fusion.dev/helix-headless 0.177.1 → 0.178.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 +557 -53
- package/dist/color-input/root/color-input-root.d.ts +0 -13
- package/dist/color-input/root/color-input-root.js +1 -19
- package/dist/date-input/root/date-input-root.d.ts +7 -1
- package/dist/date-input/root/date-input-root.js +6 -2
- package/dist/date-picker/root/date-picker-root.d.ts +6 -2
- package/dist/date-picker/root/date-picker-root.js +6 -2
- package/dist/filter-input/index.d.ts +1 -0
- package/dist/filter-input/root/filter-input-root.d.ts +92 -6
- package/dist/filter-input/root/filter-input-root.js +182 -10
- package/dist/filter-input/suggest.d.ts +47 -0
- package/dist/filter-input/suggest.js +17 -0
- package/dist/graph/root/graph-root.d.ts +101 -3
- package/dist/graph/root/graph-root.js +188 -9
- package/dist/time-picker/root/time-picker-root.d.ts +8 -3
- package/dist/time-picker/root/time-picker-root.js +2 -1
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +324 -54
- package/types/vue.d.ts +79 -16
- package/types/words.d.ts +23 -4
|
@@ -306,19 +306,6 @@ export declare class HelixHeadlessColorInput extends HelixHeadlessColorInput_bas
|
|
|
306
306
|
*/
|
|
307
307
|
protected sync(): void;
|
|
308
308
|
protected updated(changed: PropertyValues): void;
|
|
309
|
-
/** Whether the trigger's press opened the panel that is open now. */
|
|
310
|
-
private pressedOpen;
|
|
311
|
-
/**
|
|
312
|
-
* The trigger's press, which notes that it opened the panel, so Escape hands
|
|
313
|
-
* focus back to the trigger.
|
|
314
|
-
*/
|
|
315
|
-
private readonly handlePress;
|
|
316
|
-
/**
|
|
317
|
-
* Hands focus back to the control that opened the panel: the trigger after
|
|
318
|
-
* its press, which is the APG date picker dialog's rule, and the text after
|
|
319
|
-
* ArrowDown from it.
|
|
320
|
-
*/
|
|
321
|
-
protected returnFocus(): void;
|
|
322
309
|
/**
|
|
323
310
|
* The picker the field opens, behind a seam the styled tier can move.
|
|
324
311
|
*
|
|
@@ -55,12 +55,6 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
|
|
|
55
55
|
this.handleSampleEnd = () => {
|
|
56
56
|
this.requestOpen(true);
|
|
57
57
|
};
|
|
58
|
-
this.pressedOpen = false;
|
|
59
|
-
this.handlePress = () => {
|
|
60
|
-
const opening = !this.open;
|
|
61
|
-
this.handleTriggerClick();
|
|
62
|
-
this.pressedOpen = opening && this.open;
|
|
63
|
-
};
|
|
64
58
|
}
|
|
65
59
|
static { this.words = 'helix-color-input'; }
|
|
66
60
|
requestValue(next) {
|
|
@@ -123,18 +117,6 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
|
|
|
123
117
|
if (changed.has('value') && parseColor(this.value)) {
|
|
124
118
|
this.settled = this.value;
|
|
125
119
|
}
|
|
126
|
-
if (changed.has('open') && !this.open) {
|
|
127
|
-
this.pressedOpen = false;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
returnFocus() {
|
|
131
|
-
const trigger = this.pressedOpen ? this.partNamed('trigger') : null;
|
|
132
|
-
this.pressedOpen = false;
|
|
133
|
-
if (trigger) {
|
|
134
|
-
trigger.focus();
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
super.returnFocus();
|
|
138
120
|
}
|
|
139
121
|
renderPicker() {
|
|
140
122
|
return html `<${this.tag('color-picker')}
|
|
@@ -222,7 +204,7 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
|
|
|
222
204
|
: 'swatch-value'
|
|
223
205
|
: nothing}
|
|
224
206
|
?disabled=${this.isDisabled}
|
|
225
|
-
@click=${this.
|
|
207
|
+
@click=${this.handleTriggerClick}
|
|
226
208
|
>
|
|
227
209
|
<span
|
|
228
210
|
part="preview"
|
|
@@ -486,7 +486,13 @@ export declare class HelixHeadlessDateInput extends HelixHeadlessDateInput_base
|
|
|
486
486
|
unavailableMessage: string;
|
|
487
487
|
/**
|
|
488
488
|
* The message for text that is no date, where `bad-input-message` is not
|
|
489
|
-
* set. `{mode}` is `single`, `range`, or `multiple
|
|
489
|
+
* set. `{mode}` is `single`, `range`, or `multiple`, and `{example}` is a
|
|
490
|
+
* date written as the field writes one, `August 24, 2026` in `en-US` and
|
|
491
|
+
* `24. August 2026` in `de-DE` at the default `date-style`.
|
|
492
|
+
*
|
|
493
|
+
* **The default names a date the field reads back.** It said "Enter a date
|
|
494
|
+
* as YYYY-MM-DD." to a reader whose field showed `August 4, 2026`, and a
|
|
495
|
+
* pattern token is words for a developer. Time Input says the same.
|
|
490
496
|
*/
|
|
491
497
|
unreadableMessage: string;
|
|
492
498
|
/**
|
|
@@ -12,6 +12,7 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
|
12
12
|
import { html } from 'lit/static-html.js';
|
|
13
13
|
import '../../date-picker/index.js';
|
|
14
14
|
import '../../tag/root/index.js';
|
|
15
|
+
const EXAMPLE = '2026-08-24';
|
|
15
16
|
const dates = (value) => value
|
|
16
17
|
.replace(/\s+[–-]\s+/g, ',')
|
|
17
18
|
.split(',')
|
|
@@ -119,7 +120,7 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinSegmented
|
|
|
119
120
|
}
|
|
120
121
|
};
|
|
121
122
|
this.unavailableMessage = 'Choose a date that is available.';
|
|
122
|
-
this.unreadableMessage = '{mode, select, single {Enter a date as
|
|
123
|
+
this.unreadableMessage = '{mode, select, single {Enter a date such as {example}.} other {Enter dates such as {example}, separated by commas.}}';
|
|
123
124
|
this.yearViewLabel = '{heading}. Choose a year';
|
|
124
125
|
this.handleIconChange = () => {
|
|
125
126
|
this.requestUpdate();
|
|
@@ -232,7 +233,10 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinSegmented
|
|
|
232
233
|
const held = dates(this.value);
|
|
233
234
|
if (held.length > 0 && !held.every(isDateValue)) {
|
|
234
235
|
return (this.badInputMessage ||
|
|
235
|
-
formatWords(this, 'unreadableMessage', {
|
|
236
|
+
formatWords(this, 'unreadableMessage', {
|
|
237
|
+
example: this.written(EXAMPLE),
|
|
238
|
+
mode: this.mode ?? 'single',
|
|
239
|
+
}));
|
|
236
240
|
}
|
|
237
241
|
if (this.min !== '' && held.some((one) => one < this.min)) {
|
|
238
242
|
return [
|
|
@@ -117,8 +117,12 @@ export declare class HelixHeadlessDatePicker extends HelixHeadlessDatePicker_bas
|
|
|
117
117
|
* **Every sentence a control raises in the browser's bubble is a property**,
|
|
118
118
|
* named for the validity flag it reports, so a form in another language
|
|
119
119
|
* speaks it. Unset is the sentence registered for the page's language, then
|
|
120
|
-
* the English. `{
|
|
121
|
-
*
|
|
120
|
+
* the English. `{example}` is a date written as the calendar names one,
|
|
121
|
+
* `August 24, 2026` in `en-US` and `24. August 2026` in `de-DE`, and `{mode}`
|
|
122
|
+
* is `single`, `range` or `multiple`, so each can have its own sentence.
|
|
123
|
+
*
|
|
124
|
+
* **The default names a date in words.** It said "Enter a date as
|
|
125
|
+
* YYYY-MM-DD.", and a pattern token is words for a developer.
|
|
122
126
|
*/
|
|
123
127
|
badInputMessage: string;
|
|
124
128
|
/**
|
|
@@ -9,6 +9,7 @@ import { caretLeft, caretRight } from '@fusion.dev/helix-icons';
|
|
|
9
9
|
import { html, nothing } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
11
11
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
12
|
+
const EXAMPLE = '2026-08-24';
|
|
12
13
|
const KEYS = new Set([
|
|
13
14
|
'ArrowDown',
|
|
14
15
|
'ArrowLeft',
|
|
@@ -25,7 +26,7 @@ const firstOf = (year, month) => addMonths(asDate('2000-01-01'), (year - 2000) *
|
|
|
25
26
|
let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerFeedback(MixinControllerValue(HelixFormElement)) {
|
|
26
27
|
constructor() {
|
|
27
28
|
super(...arguments);
|
|
28
|
-
this.badInputMessage = '{mode, select, single {Enter a date as
|
|
29
|
+
this.badInputMessage = '{mode, select, single {Enter a date such as {example}.} other {Enter dates such as {example}, separated by commas.}}';
|
|
29
30
|
this.defaultMonth = '';
|
|
30
31
|
this.disableDates = '';
|
|
31
32
|
this.disableDays = '';
|
|
@@ -491,7 +492,10 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
|
|
|
491
492
|
if (held.length > 0 && held.some((one) => asDate(one) === null)) {
|
|
492
493
|
return [
|
|
493
494
|
{ badInput: true },
|
|
494
|
-
formatWords(this, 'badInputMessage', {
|
|
495
|
+
formatWords(this, 'badInputMessage', {
|
|
496
|
+
example: this.spoken(EXAMPLE),
|
|
497
|
+
mode: this.mode ?? 'single',
|
|
498
|
+
}),
|
|
495
499
|
];
|
|
496
500
|
}
|
|
497
501
|
for (const one of held) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AriaForwardController, HelixFormElement, PopoverController } from '@fusion.dev/helix-core';
|
|
2
2
|
import { type PropertyValues, type TemplateResult } from 'lit';
|
|
3
3
|
import { HelixHeadlessFilterField } from '../field/filter-field.js';
|
|
4
|
+
import { type FilterSource } from '../suggest.js';
|
|
4
5
|
declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinTokensInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
|
|
5
6
|
/**
|
|
6
7
|
* @summary A text field for filters, with the active filters shown in it as tokens.
|
|
@@ -67,9 +68,15 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
67
68
|
*
|
|
68
69
|
* **The picker has an entry of its own.** `FilterMenu` draws its pages with
|
|
69
70
|
* `Menu`, which a field without a picker does not need, so this family's entry
|
|
70
|
-
* leaves it out. A page that writes
|
|
71
|
-
*
|
|
72
|
-
*
|
|
71
|
+
* leaves it out. A page that writes a `FilterMenu` imports both entries:
|
|
72
|
+
*
|
|
73
|
+
* ```js
|
|
74
|
+
* import '@fusion.dev/helix/filter-input'
|
|
75
|
+
* import '@fusion.dev/helix/filter-input/menu'
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* A picker written without the second is reported once the page has loaded,
|
|
79
|
+
* with the entry to add.
|
|
73
80
|
*
|
|
74
81
|
* **Suggestions.** Slot options into `suggestions` and the field shows them,
|
|
75
82
|
* moves through them with the arrow keys, and presses one on Enter. The option
|
|
@@ -85,6 +92,24 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
85
92
|
* completion back and put the caret after it, or the next parse reads the wrong
|
|
86
93
|
* term.
|
|
87
94
|
*
|
|
95
|
+
* **Suggestions from a source.** Set `source` to a function, and the field asks
|
|
96
|
+
* it for suggestions as the text changes and draws them as options in the
|
|
97
|
+
* panel. It is given the term at the caret, the field the term is a value of,
|
|
98
|
+
* the whole text, and an `AbortSignal`, and returns the suggestions or a
|
|
99
|
+
* promise of them. `filter-delay` waits for typing to rest before asking, and
|
|
100
|
+
* a later keystroke aborts a request still out. While a promise is pending, the
|
|
101
|
+
* panel shows `loading-text` and a screen reader hears it, then the count.
|
|
102
|
+
* Choosing a suggestion writes its value over the term at the caret, and a
|
|
103
|
+
* finished `field:value` is read into a chip as Enter would. Cancel its
|
|
104
|
+
* `helix-select` to write it yourself.
|
|
105
|
+
*
|
|
106
|
+
* ```js
|
|
107
|
+
* filter.source = async ({ field, term, signal }) => {
|
|
108
|
+
* const response = await fetch(`/values?field=${field}&q=${term}`, { signal })
|
|
109
|
+
* return response.json()
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
88
113
|
* **State.** Three things have three different owners:
|
|
89
114
|
*
|
|
90
115
|
* - `value` is the text. The user types, the element applies it, and `input`
|
|
@@ -126,7 +151,7 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
126
151
|
* @event {HelixBeforeOpenEvent} helix-before-open - Fired before the suggestions open. Cancelable: call `preventDefault()` to keep them closed.
|
|
127
152
|
* @event {HelixDismissEvent} helix-dismiss - Fired when the user removes a token with its remove button or with Backspace in an empty field. A `FilterChip` that names a declared field sends `helix-facet` instead. Cancelable: call `preventDefault()` to keep the token.
|
|
128
153
|
* @event {HelixFacetEvent} helix-facet - Fired when a filter changes, whether it was typed, picked, or removed. Cancelable: call `preventDefault()` to leave the text and the chip as they were.
|
|
129
|
-
* @event {HelixSelectEvent} helix-select - Fired by an option in `suggestions` when it is chosen, and bubbles out of the field. Cancelable: call `preventDefault()` to stop other listeners acting on it. The field takes no action on
|
|
154
|
+
* @event {HelixSelectEvent} helix-select - Fired by an option in `suggestions` when it is chosen, and bubbles out of the field. Cancelable: call `preventDefault()` to stop other listeners acting on it. The field takes no action on an option you slotted, and you decide what it does. A suggestion from `source` is written over the term at the caret unless you cancel it.
|
|
130
155
|
* @event {Event} input - Fired when the text changes.
|
|
131
156
|
* @event {ToggleEvent} toggle - Fired when the suggestions panel opens or closes, with `newState` and `oldState`.
|
|
132
157
|
*
|
|
@@ -139,13 +164,13 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
139
164
|
* @csspart label - The label above the field.
|
|
140
165
|
* @csspart lead - The box before the text that holds the `start` slot.
|
|
141
166
|
* @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.
|
|
142
|
-
* @csspart loading - Shown in the panel while the suggestions load.
|
|
167
|
+
* @csspart loading - Shown in the panel while the suggestions load: while `loading` is set, or a request to `source` is out.
|
|
143
168
|
* @csspart rest - The count of tokens that did not fit. It is a `<button>` with `expandable` and a `<span>` without it.
|
|
144
169
|
* @csspart surface - The floating panel the suggestions sit in.
|
|
145
170
|
* @csspart tags - The row the tokens sit in.
|
|
146
171
|
*
|
|
147
172
|
* @cssstate invalid - The field has an error to show, from the browser's validation or from your `feedback`.
|
|
148
|
-
* @cssstate loading - While `loading` is set.
|
|
173
|
+
* @cssstate loading - While `loading` is set, or a request to `source` is out.
|
|
149
174
|
* @cssstate no-results - `filter-suggestions` hid every suggestion, and `no-results-text` or the `no-results` slot says so.
|
|
150
175
|
* @cssstate open - The suggestions are open.
|
|
151
176
|
*/
|
|
@@ -186,6 +211,12 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
186
211
|
* an icon.
|
|
187
212
|
*/
|
|
188
213
|
clearLabel: string;
|
|
214
|
+
/**
|
|
215
|
+
* How long typing rests before `source` is asked, in milliseconds. A delay
|
|
216
|
+
* sends one request for a word in place of one for each letter. The arrow
|
|
217
|
+
* keys and Enter ask at once. `0` asks on every keystroke.
|
|
218
|
+
*/
|
|
219
|
+
filterDelay: number;
|
|
189
220
|
/**
|
|
190
221
|
* Whether the panel narrows itself to the suggestions that hold the term
|
|
191
222
|
* being typed, as React Aria's Autocomplete filters. Off, the panel shows
|
|
@@ -219,6 +250,15 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
219
250
|
* Not a label: it goes when typing starts.
|
|
220
251
|
*/
|
|
221
252
|
placeholder: string;
|
|
253
|
+
/**
|
|
254
|
+
* Where the suggestions come from: a function given the term at the caret,
|
|
255
|
+
* the field it is a value of, the whole text, and an `AbortSignal`, which
|
|
256
|
+
* returns the suggestions or a promise of them. Each is a string, or a
|
|
257
|
+
* `value` with a `label` and `disabled`. The field draws them as options in
|
|
258
|
+
* the panel, beside any you slot. A property only, there being no way to
|
|
259
|
+
* write a function in markup. See the class description.
|
|
260
|
+
*/
|
|
261
|
+
source?: FilterSource | undefined;
|
|
222
262
|
/**
|
|
223
263
|
* What a reader hears for one suggestion, where `suggestions-label` should
|
|
224
264
|
* not say it. `{count}` is the count.
|
|
@@ -465,6 +505,52 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
465
505
|
* would only be a copy of that one going stale.
|
|
466
506
|
*/
|
|
467
507
|
private settleOpen;
|
|
508
|
+
/** Whether a request to `source` is out, which the panel shows as loading. */
|
|
509
|
+
private fetching;
|
|
510
|
+
/** Whether the suggestions are loading: `loading`, or a request to `source` out. */
|
|
511
|
+
private get busy();
|
|
512
|
+
/** The request to `source` still out, which a later one aborts. */
|
|
513
|
+
private request;
|
|
514
|
+
/** The text `source` was last asked about, so focus alone does not ask again. */
|
|
515
|
+
private asked;
|
|
516
|
+
/** The source the suggestions drawn now came from. */
|
|
517
|
+
private used;
|
|
518
|
+
/** The options this element drew from what `source` gave, which the next answer replaces. */
|
|
519
|
+
private sourced;
|
|
520
|
+
/** The request waiting out `filterDelay`, if any. */
|
|
521
|
+
private readonly asking;
|
|
522
|
+
/**
|
|
523
|
+
* A new source, or none: what the old one gave goes, and the new one is
|
|
524
|
+
* asked where the reader is in the field.
|
|
525
|
+
*/
|
|
526
|
+
private resetSource;
|
|
527
|
+
/** Asks `source` once typing rests for `filterDelay`, or at once without one. */
|
|
528
|
+
private askSoon;
|
|
529
|
+
/**
|
|
530
|
+
* Asks `source` for suggestions for the term at the caret, and draws what
|
|
531
|
+
* it gives.
|
|
532
|
+
*
|
|
533
|
+
* **A later request aborts this one, and an answer that arrives late is
|
|
534
|
+
* dropped**, whether or not the source heeds the signal: the panel shows
|
|
535
|
+
* what the text says now.
|
|
536
|
+
*/
|
|
537
|
+
private ask;
|
|
538
|
+
/**
|
|
539
|
+
* Draws what `source` gave as options in the `suggestions` slot, in place of
|
|
540
|
+
* the ones it gave before, and leaves the ones you slotted alone. The
|
|
541
|
+
* options are this tier's `Option`, which a page on the headless tier
|
|
542
|
+
* imports; one not registered is reported.
|
|
543
|
+
*/
|
|
544
|
+
private drawSourced;
|
|
545
|
+
/**
|
|
546
|
+
* A suggestion `source` gave, chosen: its value written over the term at the
|
|
547
|
+
* caret, and a finished `field:value` read into a chip, as Enter reads it.
|
|
548
|
+
*
|
|
549
|
+
* **After every listener has had its turn**, as a chip under `chips` is
|
|
550
|
+
* drawn, so a `preventDefault()` on the `helix-select` is read and leaves the
|
|
551
|
+
* text to you.
|
|
552
|
+
*/
|
|
553
|
+
private readonly handleSourcedSelect;
|
|
468
554
|
/** Moves the highlight, wrapping, and opens the panel if it was closed. */
|
|
469
555
|
private move;
|
|
470
556
|
private readonly handleKeydown;
|
|
@@ -4,13 +4,13 @@ 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, countLabel, customElement, drawIcon, drawStatus, formatWords, HelixFacetEvent, HelixFormElement, holdCaret, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, PopoverController, reportRuntime, restoreValue, StatusController, scrollItemIntoView, words, wordsLocale, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { AriaForwardController, countLabel, customElement, DelayController, drawIcon, drawStatus, formatWords, HelixFacetEvent, HelixFormElement, holdCaret, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, PopoverController, reportRuntime, restoreValue, StatusController, scrollItemIntoView, words, wordsLocale, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { x } from '@fusion.dev/helix-icons';
|
|
9
9
|
import { html, nothing } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
11
11
|
import { HelixHeadlessFilterField } from '../field/filter-field.js';
|
|
12
12
|
import { readFilterText } from '../grammar.js';
|
|
13
|
-
import { currentTerm, suggests, tokenEntry } from '../suggest.js';
|
|
13
|
+
import { completeTerm, currentField, currentTerm, suggests, tokenEntry, } from '../suggest.js';
|
|
14
14
|
const picks = (element) => typeof element.apply === 'function' &&
|
|
15
15
|
Array.isArray(element.fields);
|
|
16
16
|
const asksToGo = (node) => typeof node?.dismiss === 'function';
|
|
@@ -25,6 +25,7 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
25
25
|
this.chips = false;
|
|
26
26
|
this.clearable = false;
|
|
27
27
|
this.clearLabel = 'Clear';
|
|
28
|
+
this.filterDelay = 0;
|
|
28
29
|
this.filterSuggestions = false;
|
|
29
30
|
this.loading = false;
|
|
30
31
|
this.loadingText = 'Loading';
|
|
@@ -59,6 +60,7 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
59
60
|
restoreValue(control, this.value);
|
|
60
61
|
this.dismissed = false;
|
|
61
62
|
this.settleOpen();
|
|
63
|
+
this.askSoon();
|
|
62
64
|
if (event.inputType === 'insertFromPaste') {
|
|
63
65
|
this.readFilters();
|
|
64
66
|
}
|
|
@@ -84,13 +86,27 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
84
86
|
return;
|
|
85
87
|
}
|
|
86
88
|
const field = chip.getAttribute('name') ?? '';
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
89
|
+
const declared = field === '' ? undefined : this.fields.find((one) => (one.name ?? '') === field);
|
|
90
|
+
if (!declared) {
|
|
89
91
|
return;
|
|
90
92
|
}
|
|
91
93
|
event.preventDefault();
|
|
92
94
|
event.stopPropagation();
|
|
93
|
-
menu.
|
|
95
|
+
const menu = this.menuFor(field);
|
|
96
|
+
if (menu) {
|
|
97
|
+
menu.apply(field, []);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const { label, operators } = declared.vocabulary;
|
|
101
|
+
const usual = operators.find((operator) => operator.default) ?? operators[0];
|
|
102
|
+
this.announceFacet({
|
|
103
|
+
arity: usual?.arity ?? 'many',
|
|
104
|
+
field,
|
|
105
|
+
label,
|
|
106
|
+
operator: usual?.name ?? '',
|
|
107
|
+
operatorLabel: '',
|
|
108
|
+
values: [],
|
|
109
|
+
});
|
|
94
110
|
};
|
|
95
111
|
this.handleClear = () => {
|
|
96
112
|
if (this.value !== '' && this.requestValue('')) {
|
|
@@ -102,6 +118,36 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
102
118
|
control.focus();
|
|
103
119
|
}
|
|
104
120
|
};
|
|
121
|
+
this.fetching = false;
|
|
122
|
+
this.sourced = [];
|
|
123
|
+
this.asking = new DelayController(this, { renders: false });
|
|
124
|
+
this.handleSourcedSelect = (event) => {
|
|
125
|
+
const option = event.composedPath()[0];
|
|
126
|
+
if (!this.sourced.includes(option)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
queueMicrotask(() => {
|
|
130
|
+
if (event.defaultPrevented || !this.sourced.includes(option)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const value = option.value;
|
|
134
|
+
const written = completeTerm(this.value, this.control?.selectionStart ?? this.value.length, value);
|
|
135
|
+
if (!this.requestValue(written.text)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const control = this.control;
|
|
139
|
+
if (control) {
|
|
140
|
+
restoreValue(control, this.value);
|
|
141
|
+
control.setSelectionRange(written.caret, written.caret);
|
|
142
|
+
}
|
|
143
|
+
this.activeAt = -1;
|
|
144
|
+
this.dismissed = false;
|
|
145
|
+
if (currentField(written.text, written.caret - 1) !== '') {
|
|
146
|
+
this.readFilters();
|
|
147
|
+
}
|
|
148
|
+
this.ask();
|
|
149
|
+
});
|
|
150
|
+
};
|
|
105
151
|
this.handleKeydown = (event) => {
|
|
106
152
|
if (event.isComposing) {
|
|
107
153
|
return;
|
|
@@ -109,6 +155,9 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
109
155
|
if (this.tokenKeydown(event, this.value)) {
|
|
110
156
|
return;
|
|
111
157
|
}
|
|
158
|
+
if (['ArrowDown', 'ArrowUp', 'Enter'].includes(event.key)) {
|
|
159
|
+
this.asking.flush();
|
|
160
|
+
}
|
|
112
161
|
if (event.key === 'Enter' && !(this.open && this.suggestions[this.activeAt])) {
|
|
113
162
|
if (this.readFilters() > 0) {
|
|
114
163
|
event.preventDefault();
|
|
@@ -172,6 +221,9 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
172
221
|
};
|
|
173
222
|
this.handleFocusIn = () => {
|
|
174
223
|
this.settleOpen();
|
|
224
|
+
if (this.source && this.asked !== this.value) {
|
|
225
|
+
this.ask();
|
|
226
|
+
}
|
|
175
227
|
};
|
|
176
228
|
this.handleFocusOut = (event) => {
|
|
177
229
|
const next = event.relatedTarget;
|
|
@@ -220,7 +272,7 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
220
272
|
}
|
|
221
273
|
}
|
|
222
274
|
sayCount() {
|
|
223
|
-
const count = this.open && this.suggesting ? (this.
|
|
275
|
+
const count = this.open && this.suggesting ? (this.busy ? -2 : this.suggestions.length) : -1;
|
|
224
276
|
if (count === this.saidCount) {
|
|
225
277
|
return;
|
|
226
278
|
}
|
|
@@ -360,6 +412,109 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
360
412
|
const focused = this.matches(':focus-within') || this.contains(document.activeElement);
|
|
361
413
|
this.requestOpen(this.suggesting && !this.dismissed && !this.isDisabled && focused);
|
|
362
414
|
}
|
|
415
|
+
get busy() {
|
|
416
|
+
return this.loading || this.fetching;
|
|
417
|
+
}
|
|
418
|
+
resetSource() {
|
|
419
|
+
if (this.source === this.used) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
this.used = this.source;
|
|
423
|
+
this.request?.abort();
|
|
424
|
+
this.request = undefined;
|
|
425
|
+
this.fetching = false;
|
|
426
|
+
this.asked = undefined;
|
|
427
|
+
if (this.sourced.length > 0) {
|
|
428
|
+
this.drawSourced([]);
|
|
429
|
+
}
|
|
430
|
+
if (this.source && (this.matches(':focus-within') || this.contains(document.activeElement))) {
|
|
431
|
+
this.ask();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
askSoon() {
|
|
435
|
+
if (!this.source) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
this.asking.cancel();
|
|
439
|
+
if (this.filterDelay > 0) {
|
|
440
|
+
this.asking.start(this.filterDelay, () => this.ask());
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
this.ask();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
ask() {
|
|
447
|
+
this.asking.cancel();
|
|
448
|
+
const source = this.source;
|
|
449
|
+
if (!source) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
this.used = source;
|
|
453
|
+
this.request?.abort();
|
|
454
|
+
const request = new AbortController();
|
|
455
|
+
this.request = request;
|
|
456
|
+
const text = this.value;
|
|
457
|
+
const caret = this.control?.selectionStart ?? text.length;
|
|
458
|
+
this.asked = text;
|
|
459
|
+
const take = (suggestions) => {
|
|
460
|
+
if (this.request !== request) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
this.request = undefined;
|
|
464
|
+
this.fetching = false;
|
|
465
|
+
this.drawSourced(Array.isArray(suggestions) ? suggestions : []);
|
|
466
|
+
};
|
|
467
|
+
let answer;
|
|
468
|
+
try {
|
|
469
|
+
answer = source({
|
|
470
|
+
field: currentField(text, caret),
|
|
471
|
+
signal: request.signal,
|
|
472
|
+
term: currentTerm(text, caret),
|
|
473
|
+
text,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
take([]);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (typeof answer?.then !== 'function') {
|
|
481
|
+
take(answer);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
this.fetching = true;
|
|
485
|
+
answer.then(take, () => take([]));
|
|
486
|
+
}
|
|
487
|
+
drawSourced(suggestions) {
|
|
488
|
+
for (const option of this.sourced) {
|
|
489
|
+
option.remove();
|
|
490
|
+
}
|
|
491
|
+
const tag = `${this.constructor.tier}option`;
|
|
492
|
+
if (suggestions.length > 0 && !customElements.get(tag)) {
|
|
493
|
+
const entry = tag.startsWith('helix-headless-')
|
|
494
|
+
? '@fusion.dev/helix-headless'
|
|
495
|
+
: '@fusion.dev/helix';
|
|
496
|
+
reportRuntime({
|
|
497
|
+
kind: 'unregistered-part',
|
|
498
|
+
message: `<${tag}> is not registered, so the suggestions \`source\` gave cannot be chosen. Import '${entry}/option'.`,
|
|
499
|
+
tag: this.localName,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
this.sourced = suggestions.map((suggestion) => {
|
|
503
|
+
const { disabled, label, value } = typeof suggestion === 'string'
|
|
504
|
+
? { disabled: false, label: undefined, value: suggestion }
|
|
505
|
+
: suggestion;
|
|
506
|
+
const option = document.createElement(tag);
|
|
507
|
+
option.slot = 'suggestions';
|
|
508
|
+
option.setAttribute('value', value);
|
|
509
|
+
option.textContent = label ?? value;
|
|
510
|
+
if (disabled) {
|
|
511
|
+
option.setAttribute('disabled', '');
|
|
512
|
+
}
|
|
513
|
+
return option;
|
|
514
|
+
});
|
|
515
|
+
this.append(...this.sourced);
|
|
516
|
+
this.handleSuggestions();
|
|
517
|
+
}
|
|
363
518
|
move(by) {
|
|
364
519
|
const options = this.suggestions;
|
|
365
520
|
if (options.length === 0) {
|
|
@@ -376,12 +531,12 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
376
531
|
refreshSuggesting() {
|
|
377
532
|
this.narrow();
|
|
378
533
|
const options = this.suggestions;
|
|
379
|
-
this.suggesting = options.length > 0 || this.
|
|
534
|
+
this.suggesting = options.length > 0 || this.busy || this.unmatched;
|
|
380
535
|
this.activeAt = this.activeAt < options.length ? this.activeAt : -1;
|
|
381
536
|
}
|
|
382
537
|
get unmatched() {
|
|
383
538
|
const said = this.noResultsText !== '' || this.knownChildren.some((child) => child.slot === 'no-results');
|
|
384
|
-
return said && !this.
|
|
539
|
+
return said && !this.busy && this.options.length > 0 && this.suggestions.length === 0;
|
|
385
540
|
}
|
|
386
541
|
tokenElements() {
|
|
387
542
|
return (this.tokens?.assignedElements({ flatten: true }) ?? []);
|
|
@@ -399,11 +554,16 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
399
554
|
super.connectedCallback();
|
|
400
555
|
this.addEventListener('helix-dismiss', this.handleDismiss, { capture: true });
|
|
401
556
|
this.addEventListener(HelixFacetEvent.eventName, this.handleFacet);
|
|
557
|
+
this.addEventListener('helix-select', this.handleSourcedSelect);
|
|
402
558
|
}
|
|
403
559
|
disconnectedCallback() {
|
|
404
560
|
super.disconnectedCallback();
|
|
405
561
|
this.removeEventListener('helix-dismiss', this.handleDismiss, { capture: true });
|
|
406
562
|
this.removeEventListener(HelixFacetEvent.eventName, this.handleFacet);
|
|
563
|
+
this.removeEventListener('helix-select', this.handleSourcedSelect);
|
|
564
|
+
this.request?.abort();
|
|
565
|
+
this.request = undefined;
|
|
566
|
+
this.fetching = false;
|
|
407
567
|
}
|
|
408
568
|
willUpdate(changed) {
|
|
409
569
|
super.willUpdate(changed);
|
|
@@ -420,7 +580,7 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
420
580
|
}
|
|
421
581
|
this.handleTokens();
|
|
422
582
|
this.refreshSuggesting();
|
|
423
|
-
this.toggleState('loading', this.
|
|
583
|
+
this.toggleState('loading', this.busy);
|
|
424
584
|
this.toggleState('no-results', this.unmatched);
|
|
425
585
|
}
|
|
426
586
|
firstUpdated(changed) {
|
|
@@ -455,6 +615,9 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
455
615
|
}
|
|
456
616
|
updated(changed) {
|
|
457
617
|
super.updated(changed);
|
|
618
|
+
if (changed.has('source')) {
|
|
619
|
+
queueMicrotask(() => this.resetSource());
|
|
620
|
+
}
|
|
458
621
|
this.releaseCaret?.();
|
|
459
622
|
this.releaseCaret = undefined;
|
|
460
623
|
this.internals.setFormValue(this.formValue(), this.value);
|
|
@@ -543,7 +706,7 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
543
706
|
<slot name="suggestions" @slotchange=${this.handleSuggestions}></slot>
|
|
544
707
|
<!-- Hidden from the tree, which a listbox keeps for its options: the
|
|
545
708
|
live region says these. -->
|
|
546
|
-
<div part="loading" aria-hidden="true" ?hidden=${!this.
|
|
709
|
+
<div part="loading" aria-hidden="true" ?hidden=${!this.busy}>
|
|
547
710
|
<slot name="loading">${this.loadingText}</slot>
|
|
548
711
|
</div>
|
|
549
712
|
<div part="empty" aria-hidden="true" ?hidden=${!this.unmatched}>
|
|
@@ -564,6 +727,9 @@ __decorate([
|
|
|
564
727
|
words,
|
|
565
728
|
property({ attribute: 'clear-label' })
|
|
566
729
|
], HelixHeadlessFilterInput.prototype, "clearLabel", void 0);
|
|
730
|
+
__decorate([
|
|
731
|
+
property({ attribute: 'filter-delay', type: Number })
|
|
732
|
+
], HelixHeadlessFilterInput.prototype, "filterDelay", void 0);
|
|
567
733
|
__decorate([
|
|
568
734
|
property({ attribute: 'filter-suggestions', type: Boolean })
|
|
569
735
|
], HelixHeadlessFilterInput.prototype, "filterSuggestions", void 0);
|
|
@@ -580,6 +746,9 @@ __decorate([
|
|
|
580
746
|
__decorate([
|
|
581
747
|
property()
|
|
582
748
|
], HelixHeadlessFilterInput.prototype, "placeholder", void 0);
|
|
749
|
+
__decorate([
|
|
750
|
+
property({ attribute: false })
|
|
751
|
+
], HelixHeadlessFilterInput.prototype, "source", void 0);
|
|
583
752
|
__decorate([
|
|
584
753
|
property({ attribute: 'suggestion-label' })
|
|
585
754
|
], HelixHeadlessFilterInput.prototype, "suggestionLabel", void 0);
|
|
@@ -599,6 +768,9 @@ __decorate([
|
|
|
599
768
|
__decorate([
|
|
600
769
|
state()
|
|
601
770
|
], HelixHeadlessFilterInput.prototype, "leading", void 0);
|
|
771
|
+
__decorate([
|
|
772
|
+
state()
|
|
773
|
+
], HelixHeadlessFilterInput.prototype, "fetching", void 0);
|
|
602
774
|
HelixHeadlessFilterInput = __decorate([
|
|
603
775
|
customElement('helix-headless-filter-input')
|
|
604
776
|
], HelixHeadlessFilterInput);
|