@fluid-topics/ft-combobox 1.3.34 → 1.3.36
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/build/ComboboxMessages.d.ts +4 -0
- package/build/ComboboxMessages.js +5 -0
- package/build/ComboboxSuggestionProvider.d.ts +1 -1
- package/build/ComboboxSuggestionProvider.js +4 -7
- package/build/ft-combobox.d.ts +10 -0
- package/build/ft-combobox.js +146 -37
- package/build/ft-combobox.light.js +1458 -305
- package/build/ft-combobox.min.js +1460 -307
- package/build/ft-combobox.properties.d.ts +3 -0
- package/build/ft-combobox.styles.d.ts +3 -1
- package/build/ft-combobox.styles.js +20 -4
- package/package.json +4 -4
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { DefaultI18nMessages, I18nMessageContext, I18nMessages } from "@fluid-topics/ft-i18n";
|
|
2
2
|
export interface FtComboboxMessages extends I18nMessages {
|
|
3
3
|
ariaLiveOptions(count: number): string;
|
|
4
|
+
valueAdded(value: string): string;
|
|
5
|
+
valueRemoved(value: string): string;
|
|
6
|
+
maxValuesReached(max: number): string;
|
|
7
|
+
removeValueLabel(value: string): string;
|
|
4
8
|
}
|
|
5
9
|
export declare const comboboxContext: I18nMessageContext<FtComboboxMessages>;
|
|
6
10
|
export declare const defaultComboboxMessages: DefaultI18nMessages<FtComboboxMessages>;
|
|
@@ -3,4 +3,9 @@ export const comboboxContext = I18nMessageContext.build("ftCombobox");
|
|
|
3
3
|
export const defaultComboboxMessages = {
|
|
4
4
|
"ariaLiveOptions": "{0} options",
|
|
5
5
|
"ariaLiveOptions[\\=1]": "1 option",
|
|
6
|
+
"valueAdded": "{0} added",
|
|
7
|
+
"valueRemoved": "{0} removed",
|
|
8
|
+
"maxValuesReached": "Maximum {0} values allowed",
|
|
9
|
+
"maxValuesReached[\\=1]": "Maximum 1 value allowed",
|
|
10
|
+
"removeValueLabel": "Remove {0}",
|
|
6
11
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { FtComboboxSuggestionDefinition } from "./models";
|
|
2
|
-
export declare function basicSuggestionsProvider(values: Array<string | FtComboboxSuggestionDefinition
|
|
2
|
+
export declare function basicSuggestionsProvider(values: Array<string | FtComboboxSuggestionDefinition>, maxSuggest?: number): (query: string) => FtComboboxSuggestionDefinition[];
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
export function basicSuggestionsProvider(values) {
|
|
1
|
+
export function basicSuggestionsProvider(values, maxSuggest = 20) {
|
|
2
2
|
return (query) => {
|
|
3
3
|
const q = (query || "").toLowerCase();
|
|
4
|
-
|
|
4
|
+
return values
|
|
5
5
|
.map((value) => (typeof value === "string" ? { value, label: value } : value))
|
|
6
6
|
.filter((v) => v.label.toLowerCase().includes(q))
|
|
7
|
-
.sort((a, b) => alphabeticalSortWithPriorToStartWithQuery(a.label, b.label, q))
|
|
8
|
-
|
|
9
|
-
return list;
|
|
10
|
-
}
|
|
11
|
-
return list;
|
|
7
|
+
.sort((a, b) => alphabeticalSortWithPriorToStartWithQuery(a.label, b.label, q))
|
|
8
|
+
.slice(0, maxSuggest);
|
|
12
9
|
};
|
|
13
10
|
}
|
|
14
11
|
function alphabeticalSortWithPriorToStartWithQuery(a, b, query) {
|
package/build/ft-combobox.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export declare class FtCombobox extends FtCombobox_base implements FtComboboxPro
|
|
|
23
23
|
get value(): string;
|
|
24
24
|
canOfferNewValue: boolean;
|
|
25
25
|
set value(value: string);
|
|
26
|
+
multiValued: boolean;
|
|
27
|
+
maxValues?: number;
|
|
28
|
+
maxLength?: number;
|
|
29
|
+
labels: string[];
|
|
30
|
+
private labelsToValues;
|
|
26
31
|
private filter;
|
|
27
32
|
private isOpen;
|
|
28
33
|
private comboboxHasVisualFocus;
|
|
@@ -30,6 +35,7 @@ export declare class FtCombobox extends FtCombobox_base implements FtComboboxPro
|
|
|
30
35
|
private providedSuggestions;
|
|
31
36
|
private visibleSuggestions;
|
|
32
37
|
private activeIndex;
|
|
38
|
+
private ariaLiveMessage;
|
|
33
39
|
private input;
|
|
34
40
|
private listbox;
|
|
35
41
|
constructor();
|
|
@@ -58,6 +64,10 @@ export declare class FtCombobox extends FtCombobox_base implements FtComboboxPro
|
|
|
58
64
|
private removeVisualFocusAll;
|
|
59
65
|
private setValue;
|
|
60
66
|
private dispatchChangeEvent;
|
|
67
|
+
private addToSelection;
|
|
68
|
+
private clearInput;
|
|
69
|
+
private removeFromSelection;
|
|
70
|
+
private dispatchMultiValueChangeEvent;
|
|
61
71
|
private isPrintableCharacter;
|
|
62
72
|
private commitSelectionFromEnter;
|
|
63
73
|
private moveFocusToNextSuggestion;
|
package/build/ft-combobox.js
CHANGED
|
@@ -16,6 +16,7 @@ import { FtInputLabel } from "@fluid-topics/ft-input-label";
|
|
|
16
16
|
import { FtRipple } from "@fluid-topics/ft-ripple";
|
|
17
17
|
import { FtTypography, FtTypographyBody1, FtTypographyVariants } from "@fluid-topics/ft-typography";
|
|
18
18
|
import { FtIcon } from "@fluid-topics/ft-icon";
|
|
19
|
+
import { FtChip } from "@fluid-topics/ft-chip";
|
|
19
20
|
import { withI18n } from "@fluid-topics/ft-i18n";
|
|
20
21
|
import { comboboxContext, defaultComboboxMessages } from "./ComboboxMessages";
|
|
21
22
|
class FtCombobox extends withI18n(FtLitElement) {
|
|
@@ -37,6 +38,9 @@ class FtCombobox extends withI18n(FtLitElement) {
|
|
|
37
38
|
this.suggestionsProvider = () => [];
|
|
38
39
|
this._value = "";
|
|
39
40
|
this.canOfferNewValue = true;
|
|
41
|
+
this.multiValued = false;
|
|
42
|
+
this.labels = [];
|
|
43
|
+
this.labelsToValues = new Map();
|
|
40
44
|
this.filter = "";
|
|
41
45
|
this.isOpen = false;
|
|
42
46
|
this.comboboxHasVisualFocus = false;
|
|
@@ -44,16 +48,18 @@ class FtCombobox extends withI18n(FtLitElement) {
|
|
|
44
48
|
this.providedSuggestions = [];
|
|
45
49
|
this.visibleSuggestions = [];
|
|
46
50
|
this.activeIndex = -1;
|
|
51
|
+
this.ariaLiveMessage = "";
|
|
47
52
|
this.onComboboxInput = () => {
|
|
48
53
|
const newValue = this.input.value;
|
|
49
|
-
const hadValue = this._value !== "";
|
|
54
|
+
const hadValue = this._value !== "" || (this.multiValued && this.labels.length > 0);
|
|
50
55
|
this.filter = newValue;
|
|
51
56
|
this.activeIndex = -1;
|
|
52
|
-
//
|
|
53
|
-
if (hadValue && newValue === "") {
|
|
57
|
+
// Single-value mode
|
|
58
|
+
if (!this.multiValued && hadValue && newValue === "") {
|
|
54
59
|
this._value = "";
|
|
55
60
|
this.dispatchChangeEvent();
|
|
56
61
|
}
|
|
62
|
+
this.ariaLiveMessage = "";
|
|
57
63
|
this.updateSuggestions();
|
|
58
64
|
const total = this.getTotalSuggestionsCount();
|
|
59
65
|
if (total > 0) {
|
|
@@ -183,7 +189,9 @@ class FtCombobox extends withI18n(FtLitElement) {
|
|
|
183
189
|
else {
|
|
184
190
|
this.setValue(suggestion.label);
|
|
185
191
|
}
|
|
186
|
-
this.
|
|
192
|
+
if (!this.multiValued) {
|
|
193
|
+
this.closeListbox(true);
|
|
194
|
+
}
|
|
187
195
|
};
|
|
188
196
|
this.addI18nContext(comboboxContext, defaultComboboxMessages);
|
|
189
197
|
}
|
|
@@ -241,6 +249,7 @@ class FtCombobox extends withI18n(FtLitElement) {
|
|
|
241
249
|
aria-activedescendant="${this.getActiveDescendantId()}"
|
|
242
250
|
aria-label="${ifDefined(this.label)}"
|
|
243
251
|
name="${ifDefined(this.name)}"
|
|
252
|
+
maxlength="${ifDefined(this.maxLength)}"
|
|
244
253
|
?disabled=${this.disabled}
|
|
245
254
|
.value=${this.value}
|
|
246
255
|
@input=${this.onComboboxInput}
|
|
@@ -252,13 +261,14 @@ class FtCombobox extends withI18n(FtLitElement) {
|
|
|
252
261
|
${(this.renderIcon())}
|
|
253
262
|
</div>
|
|
254
263
|
<div class="sr-only" aria-live="polite" aria-atomic="true">
|
|
255
|
-
${this.renderLiveText()}
|
|
264
|
+
${this.ariaLiveMessage || this.renderLiveText()}
|
|
256
265
|
</div>
|
|
257
266
|
${when(this.visibleSuggestions.length > 0, () => html `
|
|
258
267
|
<ul
|
|
259
268
|
id="combobox-listbox"
|
|
260
269
|
class="${classMap(listboxClasses)}"
|
|
261
270
|
role="listbox"
|
|
271
|
+
aria-multiselectable="${ifDefined(this.multiValued)}"
|
|
262
272
|
aria-label="${this.label || "Options"}"
|
|
263
273
|
data-visible="${this.isOpen}"
|
|
264
274
|
>
|
|
@@ -271,6 +281,20 @@ class FtCombobox extends withI18n(FtLitElement) {
|
|
|
271
281
|
${this.helper}
|
|
272
282
|
</ft-typography>
|
|
273
283
|
` : nothing}
|
|
284
|
+
${when(this.multiValued && this.labels.length > 0, () => html `
|
|
285
|
+
<div class="ft-combobox--chips-container" role="group" aria-label="Selected values">
|
|
286
|
+
${repeat(this.labels, (value) => value, (value) => html `
|
|
287
|
+
<ft-chip removable
|
|
288
|
+
dense
|
|
289
|
+
hideIconTooltip
|
|
290
|
+
label="${value}"
|
|
291
|
+
iconLabel="${comboboxContext.messages.removeValueLabel(value)}"
|
|
292
|
+
@icon-click=${() => this.removeFromSelection(value)}>
|
|
293
|
+
${value}
|
|
294
|
+
</ft-chip>
|
|
295
|
+
`)}
|
|
296
|
+
</div>
|
|
297
|
+
`)}
|
|
274
298
|
</div>
|
|
275
299
|
`;
|
|
276
300
|
}
|
|
@@ -287,36 +311,36 @@ class FtCombobox extends withI18n(FtLitElement) {
|
|
|
287
311
|
` : nothing;
|
|
288
312
|
}
|
|
289
313
|
renderSuggestion(suggestion, index) {
|
|
314
|
+
const isNewValue = suggestion.kind === "new" && !!this.suggestionsHelper;
|
|
315
|
+
const isSelected = this.multiValued && this.labels.includes(suggestion.label);
|
|
316
|
+
const isActive = index === this.activeIndex;
|
|
317
|
+
const classes = {
|
|
318
|
+
"ft-combobox-suggestion": true,
|
|
319
|
+
"ft-combobox-suggestion--with-helper": isNewValue,
|
|
320
|
+
};
|
|
290
321
|
return html `
|
|
291
322
|
<li
|
|
292
323
|
id="${suggestion.kind === "new" ? "suggestion-new-value" : `option-${index}`}"
|
|
293
324
|
class="ft-combobox--option ${suggestion.kind === "new" ? "ft-combobox--option-new-value" : ""}"
|
|
294
325
|
role="option"
|
|
295
|
-
aria-selected="${
|
|
326
|
+
aria-selected="${this.multiValued ? isSelected : isActive}"
|
|
327
|
+
data-active="${isActive}"
|
|
328
|
+
data-selected="${isSelected}"
|
|
296
329
|
@pointerdown=${(e) => e.preventDefault()}
|
|
297
330
|
@click=${() => this.onSuggestionClick(suggestion)}
|
|
298
331
|
>
|
|
299
|
-
${(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
${this.suggestionsHelper}
|
|
312
|
-
</ft-typography>
|
|
313
|
-
` : nothing}
|
|
314
|
-
<ft-typography part="label" variant="${FtTypographyVariants.body1}" class="ft-combobox-suggestion--label">
|
|
315
|
-
${suggestion.label}
|
|
316
|
-
</ft-typography>
|
|
317
|
-
</div>
|
|
318
|
-
`;
|
|
319
|
-
})()}
|
|
332
|
+
<div class="${classMap(classes)}" tabindex="-1">
|
|
333
|
+
<ft-ripple ?primary=${isSelected} ?activated=${isSelected}></ft-ripple>
|
|
334
|
+
${isNewValue ? html `
|
|
335
|
+
<ft-typography aria-label="${this.suggestionsHelper}," class="ft-combobox-suggestion--helper-text"
|
|
336
|
+
variant="${FtTypographyVariants.caption}">
|
|
337
|
+
${this.suggestionsHelper}
|
|
338
|
+
</ft-typography>
|
|
339
|
+
` : nothing}
|
|
340
|
+
<ft-typography part="label" variant="${FtTypographyVariants.body1}" class="ft-combobox-suggestion--label">
|
|
341
|
+
${suggestion.label}
|
|
342
|
+
</ft-typography>
|
|
343
|
+
</div>
|
|
320
344
|
</li>
|
|
321
345
|
`;
|
|
322
346
|
}
|
|
@@ -328,8 +352,10 @@ class FtCombobox extends withI18n(FtLitElement) {
|
|
|
328
352
|
shouldOfferNew() {
|
|
329
353
|
var _a;
|
|
330
354
|
const currentValue = ((_a = this.input) === null || _a === void 0 ? void 0 : _a.value) || this.value || "";
|
|
355
|
+
const isSelected = this.multiValued && this.labels.includes(currentValue);
|
|
331
356
|
return currentValue !== ""
|
|
332
357
|
&& !this.providedSuggestions.some((o) => o.label === currentValue)
|
|
358
|
+
&& !isSelected
|
|
333
359
|
&& this.canOfferNewValue;
|
|
334
360
|
}
|
|
335
361
|
getTotalSuggestionsCount() {
|
|
@@ -380,7 +406,7 @@ class FtCombobox extends withI18n(FtLitElement) {
|
|
|
380
406
|
if (this.suggestionsProvider) {
|
|
381
407
|
this.providedSuggestions = await this.suggestionsProvider(this.filter);
|
|
382
408
|
const filteredSuggestion = (this.filterSuggestions
|
|
383
|
-
? this.providedSuggestions.filter((suggestion) => suggestion.label.toLowerCase().includes(this.filter))
|
|
409
|
+
? this.providedSuggestions.filter((suggestion) => suggestion.label.toLowerCase().includes(this.filter.toLowerCase()))
|
|
384
410
|
: this.providedSuggestions).map((o, i) => ({ id: `regular-${i}`, label: o.label, kind: "regular" }));
|
|
385
411
|
this.visibleSuggestions = [
|
|
386
412
|
...filteredSuggestion,
|
|
@@ -447,16 +473,74 @@ class FtCombobox extends withI18n(FtLitElement) {
|
|
|
447
473
|
this.listboxHasVisualFocus = false;
|
|
448
474
|
}
|
|
449
475
|
setValue(value) {
|
|
450
|
-
this.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
476
|
+
if (this.multiValued) {
|
|
477
|
+
this.addToSelection(value);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
this.filter = value;
|
|
481
|
+
this._value = value;
|
|
482
|
+
this.input.value = value;
|
|
483
|
+
this.input.setSelectionRange(value.length, value.length);
|
|
484
|
+
this.updateSuggestions();
|
|
485
|
+
this.dispatchChangeEvent();
|
|
486
|
+
}
|
|
456
487
|
}
|
|
457
488
|
dispatchChangeEvent() {
|
|
489
|
+
var _a, _b;
|
|
490
|
+
const resolvedSuggestion = (_b = (_a = this.providedSuggestions.find((o) => this._value === o.label)) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : this._value;
|
|
491
|
+
this.dispatchEvent(new CustomEvent("change", {
|
|
492
|
+
detail: resolvedSuggestion,
|
|
493
|
+
bubbles: true,
|
|
494
|
+
composed: true,
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
addToSelection(label) {
|
|
498
|
+
var _a, _b;
|
|
499
|
+
const isBlank = label.trim().length === 0;
|
|
500
|
+
if (isBlank) {
|
|
501
|
+
this.clearInput();
|
|
502
|
+
this.closeListbox(true);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const isSelected = this.labels.includes(label);
|
|
506
|
+
if (isSelected) {
|
|
507
|
+
this.removeFromSelection(label);
|
|
508
|
+
this.closeListbox(true);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const maxValuesReached = this.maxValues && this.labels.length >= this.maxValues;
|
|
512
|
+
if (maxValuesReached) {
|
|
513
|
+
this.ariaLiveMessage = comboboxContext.messages.maxValuesReached((_a = this.maxValues) !== null && _a !== void 0 ? _a : 0);
|
|
514
|
+
this.closeListbox(true);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
this.labels = [...this.labels, label];
|
|
518
|
+
const suggestion = this.providedSuggestions.find((o) => o.label === label);
|
|
519
|
+
this.labelsToValues.set(label, (_b = suggestion === null || suggestion === void 0 ? void 0 : suggestion.value) !== null && _b !== void 0 ? _b : label);
|
|
520
|
+
this.ariaLiveMessage = comboboxContext.messages.valueAdded(label);
|
|
521
|
+
this.dispatchMultiValueChangeEvent();
|
|
522
|
+
this.clearInput();
|
|
523
|
+
this.closeListbox(true);
|
|
524
|
+
}
|
|
525
|
+
clearInput() {
|
|
526
|
+
this.filter = "";
|
|
527
|
+
this.input.value = "";
|
|
528
|
+
this.input.setSelectionRange(0, 0);
|
|
529
|
+
this.updateSuggestions();
|
|
530
|
+
this.input.focus();
|
|
531
|
+
}
|
|
532
|
+
removeFromSelection(label) {
|
|
533
|
+
this.labels = this.labels.filter((v) => v !== label);
|
|
534
|
+
this.labelsToValues.delete(label);
|
|
535
|
+
this.updateSuggestions();
|
|
536
|
+
this.ariaLiveMessage = comboboxContext.messages.valueRemoved(label);
|
|
537
|
+
this.dispatchMultiValueChangeEvent();
|
|
538
|
+
this.input.focus();
|
|
539
|
+
}
|
|
540
|
+
dispatchMultiValueChangeEvent() {
|
|
541
|
+
const resolvedSuggestions = this.labels.map((l) => { var _a; return (_a = this.labelsToValues.get(l)) !== null && _a !== void 0 ? _a : l; });
|
|
458
542
|
this.dispatchEvent(new CustomEvent("change", {
|
|
459
|
-
detail:
|
|
543
|
+
detail: resolvedSuggestions,
|
|
460
544
|
bubbles: true,
|
|
461
545
|
composed: true,
|
|
462
546
|
}));
|
|
@@ -477,8 +561,14 @@ class FtCombobox extends withI18n(FtLitElement) {
|
|
|
477
561
|
this.setValue(this.input.value);
|
|
478
562
|
}
|
|
479
563
|
}
|
|
480
|
-
this.
|
|
481
|
-
|
|
564
|
+
if (!this.multiValued) {
|
|
565
|
+
this.setVisualFocusCombobox();
|
|
566
|
+
this.closeListbox(true);
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
// In multi-valued mode, keep input focused but reset active index
|
|
570
|
+
this.activeIndex = -1;
|
|
571
|
+
}
|
|
482
572
|
}
|
|
483
573
|
moveFocusToNextSuggestion(event) {
|
|
484
574
|
const totalCount = this.getTotalSuggestionsCount();
|
|
@@ -569,6 +659,7 @@ FtCombobox.elementDefinitions = {
|
|
|
569
659
|
"ft-ripple": FtRipple,
|
|
570
660
|
"ft-typography": FtTypography,
|
|
571
661
|
"ft-icon": FtIcon,
|
|
662
|
+
"ft-chip": FtChip,
|
|
572
663
|
};
|
|
573
664
|
__decorate([
|
|
574
665
|
property()
|
|
@@ -615,6 +706,21 @@ __decorate([
|
|
|
615
706
|
__decorate([
|
|
616
707
|
property({ type: Boolean })
|
|
617
708
|
], FtCombobox.prototype, "canOfferNewValue", void 0);
|
|
709
|
+
__decorate([
|
|
710
|
+
property({ type: Boolean })
|
|
711
|
+
], FtCombobox.prototype, "multiValued", void 0);
|
|
712
|
+
__decorate([
|
|
713
|
+
numberProperty()
|
|
714
|
+
], FtCombobox.prototype, "maxValues", void 0);
|
|
715
|
+
__decorate([
|
|
716
|
+
numberProperty()
|
|
717
|
+
], FtCombobox.prototype, "maxLength", void 0);
|
|
718
|
+
__decorate([
|
|
719
|
+
state()
|
|
720
|
+
], FtCombobox.prototype, "labels", void 0);
|
|
721
|
+
__decorate([
|
|
722
|
+
state()
|
|
723
|
+
], FtCombobox.prototype, "labelsToValues", void 0);
|
|
618
724
|
__decorate([
|
|
619
725
|
state()
|
|
620
726
|
], FtCombobox.prototype, "filter", void 0);
|
|
@@ -636,6 +742,9 @@ __decorate([
|
|
|
636
742
|
__decorate([
|
|
637
743
|
state()
|
|
638
744
|
], FtCombobox.prototype, "activeIndex", void 0);
|
|
745
|
+
__decorate([
|
|
746
|
+
state()
|
|
747
|
+
], FtCombobox.prototype, "ariaLiveMessage", void 0);
|
|
639
748
|
__decorate([
|
|
640
749
|
query(".ft-combobox--input")
|
|
641
750
|
], FtCombobox.prototype, "input", void 0);
|