@fluid-topics/ft-combobox 1.3.35 → 1.3.37

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.
@@ -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>): (query: 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
- const list = values
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
- if (q.length === 0) {
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) {
@@ -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;
@@ -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
- // Dispatch change event when clearing the field after having a value
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.closeListbox(true);
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="${index === this.activeIndex}"
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
- const isNewValue = suggestion.kind === "new" && !!this.suggestionsHelper;
301
- const classes = {
302
- "ft-combobox-suggestion": true,
303
- "ft-combobox-suggestion--with-helper": isNewValue,
304
- };
305
- return html `
306
- <div class="${classMap(classes)}" tabindex="-1">
307
- <ft-ripple></ft-ripple>
308
- ${isNewValue ? html `
309
- <ft-typography aria-label="${this.suggestionsHelper}," class="ft-combobox-suggestion--helper-text"
310
- variant="${FtTypographyVariants.caption}">
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.filter = value;
451
- this._value = value;
452
- this.input.value = value;
453
- this.input.setSelectionRange(value.length, value.length);
454
- this.updateSuggestions();
455
- this.dispatchChangeEvent();
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: this._value,
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.setVisualFocusCombobox();
481
- this.closeListbox(true);
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);