• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-webcomponents / 18975232228

31 Oct 2025 02:14PM UTC coverage: 98.19% (+0.06%) from 98.129%
18975232228

Pull #1935

github

web-flow
Merge 3041f990d into 6ddeda7cc
Pull Request #1935: fix(textarea): update sass themes

5319 of 5598 branches covered (95.02%)

Branch coverage included in aggregate %.

35311 of 35781 relevant lines covered (98.69%)

1648.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.82
/src/components/combo/combo.ts
1
import {
10✔
2
  ComboResourceStringsEN,
10✔
3
  type IComboResourceStrings,
10✔
4
} from 'igniteui-i18n-core';
10✔
5
import { html, LitElement, nothing, type TemplateResult } from 'lit';
10✔
6
import { property, queryAssignedElements, state } from 'lit/decorators.js';
10✔
7
import { ifDefined } from 'lit/directives/if-defined.js';
10✔
8
import { createRef, ref } from 'lit/directives/ref.js';
10✔
9

10✔
10
import { addThemingController } from '../../theming/theming-controller.js';
10✔
11
import { addRootClickController } from '../common/controllers/root-click.js';
10✔
12
import { blazorAdditionalDependencies } from '../common/decorators/blazorAdditionalDependencies.js';
10✔
13
import { blazorIndirectRender } from '../common/decorators/blazorIndirectRender.js';
10✔
14
import { watch } from '../common/decorators/watch.js';
10✔
15
import { registerComponent } from '../common/definitions/register.js';
10✔
16
import { addI18nController } from '../common/i18n/i18n-controller.js';
10✔
17
import type { Constructor } from '../common/mixins/constructor.js';
10✔
18
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
10✔
19
import { FormAssociatedRequiredMixin } from '../common/mixins/forms/associated-required.js';
10✔
20
import { createFormValueState } from '../common/mixins/forms/form-value.js';
10✔
21
import { partMap } from '../common/part-map.js';
10✔
22
import {
10✔
23
  addSafeEventListener,
10✔
24
  asArray,
10✔
25
  equal,
10✔
26
  findElementFromEventPath,
10✔
27
  first,
10✔
28
  isEmpty,
10✔
29
} from '../common/util.js';
10✔
30
import IgcIconComponent from '../icon/icon.js';
10✔
31
import IgcInputComponent from '../input/input.js';
10✔
32
import IgcPopoverComponent from '../popover/popover.js';
10✔
33
import IgcValidationContainerComponent from '../validation-container/validation-container.js';
10✔
34
import IgcComboHeaderComponent from './combo-header.js';
10✔
35
import IgcComboItemComponent from './combo-item.js';
10✔
36
import IgcComboListComponent from './combo-list.js';
10✔
37
import { DataController } from './controllers/data.js';
10✔
38
import { ComboNavigationController } from './controllers/navigation.js';
10✔
39
import { SelectionController } from './controllers/selection.js';
10✔
40
import { styles } from './themes/combo.base.css.js';
10✔
41
import { styles as shared } from './themes/shared/combo.common.css.js';
10✔
42
import { all } from './themes/themes.js';
10✔
43
import type {
10✔
44
  ComboItemTemplate,
10✔
45
  ComboRecord,
10✔
46
  ComboRenderFunction,
10✔
47
  ComboValue,
10✔
48
  FilteringOptions,
10✔
49
  GroupingDirection,
10✔
50
  IgcComboComponentEventMap,
10✔
51
  Item,
10✔
52
  Keys,
10✔
53
} from './types.js';
10✔
54
import { comboValidators } from './validators.js';
10✔
55

10✔
56
/* blazorSupportsVisualChildren */
10✔
57
/**
10✔
58
 * The Combo component is similar to the Select component in that it provides a list of options from which the user can make a selection.
10✔
59
 * In contrast to the Select component, the Combo component displays all options in a virtualized list of items,
10✔
60
 * meaning the combo box can simultaneously show thousands of options, where one or more options can be selected.
10✔
61
 * Additionally, users can create custom item templates, allowing for robust data visualization.
10✔
62
 * The Combo component features case-sensitive filtering, grouping, complex data binding, dynamic addition of values and more.
10✔
63
 *
10✔
64
 * @element igc-combo
10✔
65
 *
10✔
66
 * @slot prefix - Renders content before the input of the combo.
10✔
67
 * @slot suffix - Renders content after the input of the combo.
10✔
68
 * @slot header - Renders a container before the list of options of the combo.
10✔
69
 * @slot footer - Renders a container after the list of options of the combo.
10✔
70
 * @slot empty - Renders content when the combo dropdown list has no items/data.
10✔
71
 * @slot helper-text - Renders content below the input of the combo.
10✔
72
 * @slot toggle-icon - Renders content inside the suffix container of the combo.
10✔
73
 * @slot clear-icon - Renders content inside the suffix container of the combo.
10✔
74
 * @slot value-missing - Renders content when the required validation fails.
10✔
75
 * @slot custom-error - Renders content when setCustomValidity(message) is set.
10✔
76
 * @slot invalid - Renders content when the component is in invalid state (validity.valid = false).
10✔
77
 *
10✔
78
 * @fires igcChange - Emitted when the control's selection has changed.
10✔
79
 * @fires igcOpening - Emitted just before the list of options is opened.
10✔
80
 * @fires igcOpened - Emitted after the list of options is opened.
10✔
81
 * @fires igcClosing - Emitter just before the list of options is closed.
10✔
82
 * @fires igcClosed - Emitted after the list of options is closed.
10✔
83
 *
10✔
84
 * @csspart label - The encapsulated text label of the combo.
10✔
85
 * @csspart input - The main input field of the combo.
10✔
86
 * @csspart native-input - The native input of the main input field of the combo.
10✔
87
 * @csspart prefix - The prefix wrapper of the combo.
10✔
88
 * @csspart suffix - The suffix wrapper of the combo.
10✔
89
 * @csspart toggle-icon - The toggle icon wrapper of the combo.
10✔
90
 * @csspart clear-icon - The clear icon wrapper of the combo.
10✔
91
 * @csspart case-icon - The case icon wrapper of the combo.
10✔
92
 * @csspart helper-text - The helper text wrapper of the combo.
10✔
93
 * @csspart search-input - The search input field of the combo.
10✔
94
 * @csspart list-wrapper - The list of options wrapper of the combo.
10✔
95
 * @csspart list - The list of options box of the combo.
10✔
96
 * @csspart item - Represents each item in the list of options of the combo.
10✔
97
 * @csspart group-header - Represents each header in the list of options of the combo.
10✔
98
 * @csspart active - Appended to the item parts list when the item is active of the combo.
10✔
99
 * @csspart selected - Appended to the item parts list when the item is selected of the combo.
10✔
100
 * @csspart checkbox - Represents each checkbox of each list item of the combo.
10✔
101
 * @csspart checkbox-indicator - Represents the checkbox indicator of each list item of the combo.
10✔
102
 * @csspart checked - Appended to checkbox parts list when checkbox is checked in the combo.
10✔
103
 * @csspart header - The container holding the header content of the combo.
10✔
104
 * @csspart footer - The container holding the footer content of the combo.
10✔
105
 * @csspart empty - The container holding the empty content of the combo.
10✔
106
 */
10✔
107
@blazorAdditionalDependencies('IgcIconComponent, IgcInputComponent')
10✔
108
@blazorIndirectRender
10✔
109
export default class IgcComboComponent<
10✔
110
  T extends object = any,
10✔
111
> extends FormAssociatedRequiredMixin(
10✔
112
  EventEmitterMixin<IgcComboComponentEventMap, Constructor<LitElement>>(
10✔
113
    LitElement
10✔
114
  )
10✔
115
) {
10✔
116
  public static readonly tagName = 'igc-combo';
10✔
117
  public static styles = [styles, shared];
10✔
118

10✔
119
  /* blazorSuppress */
10✔
120
  public static register() {
10✔
121
    registerComponent(
10✔
122
      IgcComboComponent,
10✔
123
      IgcIconComponent,
10✔
124
      IgcComboListComponent,
10✔
125
      IgcComboItemComponent,
10✔
126
      IgcComboHeaderComponent,
10✔
127
      IgcInputComponent,
10✔
128
      IgcPopoverComponent,
10✔
129
      IgcValidationContainerComponent
10✔
130
    );
10✔
131
  }
10✔
132

10✔
133
  protected override get __validators() {
10✔
134
    return comboValidators;
461✔
135
  }
461✔
136

10✔
137
  /** The primary input of the combo component. */
10✔
138
  private _inputRef = createRef<IgcInputComponent>();
10✔
139

10✔
140
  /** The search input of the combo component. */
10✔
141
  private _searchRef = createRef<IgcInputComponent>();
10✔
142

10✔
143
  /** The combo virtualized dropdown list. */
10✔
144
  private _listRef = createRef<IgcComboListComponent>();
10✔
145

10✔
146
  private readonly _rootClickController = addRootClickController(this, {
10✔
147
    onHide: async () => {
10✔
148
      if (!this.handleClosing()) {
9✔
149
        return;
9✔
150
      }
9✔
151
      this.open = false;
9✔
152

9✔
153
      await this.updateComplete;
9✔
154
      this.emitEvent('igcClosed');
9✔
155
    },
9✔
156
  });
10✔
157

10✔
158
  private readonly _i18nController = addI18nController<IComboResourceStrings>(
10✔
159
    this,
10✔
160
    {
10✔
161
      defaultEN: ComboResourceStringsEN,
10✔
162
    }
10✔
163
  );
10✔
164

10✔
165
  protected override readonly _formValue = createFormValueState<
10✔
166
    ComboValue<T>[]
10✔
167
  >(this, {
10✔
168
    initialValue: [],
10✔
169
    transformers: {
10✔
170
      setValue: asArray,
10✔
171
      setDefaultValue: asArray,
10✔
172
    },
10✔
173
  });
10✔
174

10✔
175
  private _data: T[] = [];
10✔
176

10✔
177
  private _valueKey?: Keys<T>;
10✔
178
  private _displayKey?: Keys<T>;
10✔
179
  private _groupKey?: Keys<T>;
10✔
180

10✔
181
  private _disableFiltering = false;
10✔
182
  private _singleSelect = false;
10✔
183

10✔
184
  private _groupSorting: GroupingDirection = 'asc';
10✔
185
  private _filteringOptions: FilteringOptions<T> = {
10✔
186
    filterKey: this.displayKey,
10✔
187
    caseSensitive: false,
10✔
188
    matchDiacritics: false,
10✔
189
  };
10✔
190

10✔
191
  @state()
10✔
192
  private _activeDescendant!: string;
10✔
193

10✔
194
  @state()
10✔
195
  private _displayValue = '';
10✔
196

10✔
197
  protected _state = new DataController<T>(this);
10✔
198
  protected _selection = new SelectionController<T>(this, this._state);
10✔
199
  protected _navigation = new ComboNavigationController(this, this._state, {
10✔
200
    input: this._inputRef,
10✔
201
    search: this._searchRef,
10✔
202
    list: this._listRef,
10✔
203
  });
10✔
204

10✔
205
  @queryAssignedElements({ slot: 'suffix' })
10✔
206
  protected inputSuffix!: HTMLElement[];
10✔
207

10✔
208
  @queryAssignedElements({ slot: 'prefix' })
10✔
209
  protected inputPrefix!: HTMLElement[];
10✔
210

10✔
211
  /** The data source used to generate the list of options. */
10✔
212
  /* treatAsRef */
10✔
213
  @property({ attribute: false })
10✔
214
  public set data(value: T[]) {
10✔
215
    if (this._data === value) {
103!
216
      return;
×
217
    }
×
218
    this._data = asArray(value);
103✔
219
    const pristine = this._pristine;
103✔
220
    this.value = asArray(this.value);
103✔
221
    this._pristine = pristine;
103✔
222
    this._state.runPipeline();
103✔
223
  }
103✔
224

10✔
225
  public get data() {
10✔
226
    return this._data;
1,394✔
227
  }
1,394✔
228

10✔
229
  /**
10✔
230
   * The outlined attribute of the control.
10✔
231
   * @attr outlined
10✔
232
   */
10✔
233
  @property({ type: Boolean, reflect: true })
10✔
234
  public outlined = false;
10✔
235

10✔
236
  /**
10✔
237
   * Enables single selection mode and moves item filtering to the main input.
10✔
238
   * @attr single-select
10✔
239
   * @default false
10✔
240
   */
10✔
241
  @property({ type: Boolean, reflect: true, attribute: 'single-select' })
10✔
242
  public set singleSelect(value: boolean) {
10✔
243
    if (this._singleSelect === Boolean(value)) {
26✔
244
      return;
2✔
245
    }
2✔
246

24✔
247
    this._singleSelect = Boolean(value);
24✔
248
    this._selection.clear();
24✔
249
    if (this.hasUpdated) {
26✔
250
      this.updateValue();
21✔
251
      this.resetSearchTerm();
21✔
252
      this._navigation.active = -1;
21✔
253
    }
21✔
254
  }
26✔
255

10✔
256
  public get singleSelect(): boolean {
10✔
257
    return this._singleSelect;
2,391✔
258
  }
2,391✔
259

10✔
260
  /**
10✔
261
   * The autofocus attribute of the control.
10✔
262
   * @attr autofocus
10✔
263
   */
10✔
264
  @property({ type: Boolean })
10✔
265
  public override autofocus!: boolean;
10✔
266

10✔
267
  /**
10✔
268
   * Focuses the list of options when the menu opens.
10✔
269
   * @attr autofocus-list
10✔
270
   */
10✔
271
  @property({ type: Boolean, attribute: 'autofocus-list' })
10✔
272
  public autofocusList = false;
10✔
273

10✔
274
  /**
10✔
275
   * Gets/Sets the locale used for formatting and displaying the dates in the component.
10✔
276
   * @attr locale
10✔
277
   */
10✔
278
  @property()
10✔
279
  public set locale(value: string) {
10✔
280
    this._i18nController.locale = value;
×
281
  }
×
282

10✔
283
  public get locale() {
10✔
284
    return this._i18nController.locale;
101✔
285
  }
101✔
286

10✔
287
  /**
10✔
288
   * The label attribute of the control.
10✔
289
   * @attr label
10✔
290
   */
10✔
291
  @property()
10✔
292
  public label!: string;
10✔
293

10✔
294
  /**
10✔
295
   * The placeholder attribute of the control.
10✔
296
   * @attr placeholder
10✔
297
   */
10✔
298
  @property()
10✔
299
  public placeholder!: string;
10✔
300

10✔
301
  /**
10✔
302
   * The placeholder attribute of the search input.
10✔
303
   * @attr placeholder-search
10✔
304
   */
10✔
305
  @property({ attribute: 'placeholder-search' })
10✔
306
  public set placeholderSearch(value: string) {
10✔
307
    this._placeholderSearch = value;
1✔
308
  }
1✔
309

10✔
310
  public get placeholderSearch() {
10✔
311
    return (
578✔
312
      this._placeholderSearch ??
578✔
313
      this.resourceStrings.combo_filter_search_placeholder ??
575!
314
      'Search'
×
315
    );
578✔
316
  }
578✔
317

10✔
318
  private _placeholderSearch: string | undefined;
10✔
319

10✔
320
  /**
10✔
321
   * Sets the open state of the component.
10✔
322
   * @attr open
10✔
323
   */
10✔
324
  @property({ type: Boolean, reflect: true })
10✔
325
  public open = false;
10✔
326

10✔
327
  /**
10✔
328
   * The resource strings for localization.
10✔
329
   */
10✔
330
  @property({ attribute: false })
10✔
331
  public set resourceStrings(value: IComboResourceStrings) {
10✔
332
    this._i18nController.resourceStrings = value;
×
333
  }
×
334

10✔
335
  public get resourceStrings(): IComboResourceStrings {
10✔
336
    return this._i18nController.resourceStrings;
1,163✔
337
  }
1,163✔
338

10✔
339
  /**
10✔
340
   * The key in the data source used when selecting items.
10✔
341
   * @attr value-key
10✔
342
   */
10✔
343
  @property({ attribute: 'value-key' })
10✔
344
  public set valueKey(value: Keys<T> | undefined) {
10✔
345
    this._valueKey = value;
101✔
346
    this._displayKey = this._displayKey ?? this._valueKey;
101✔
347
  }
101✔
348

10✔
349
  public get valueKey() {
10✔
350
    return this._valueKey;
1,791✔
351
  }
1,791✔
352

10✔
353
  /**
10✔
354
   * The key in the data source used to display items in the list.
10✔
355
   * @attr display-key
10✔
356
   */
10✔
357
  @property({ attribute: 'display-key' })
10✔
358
  public set displayKey(value: Keys<T> | undefined) {
10✔
359
    this._displayKey = value;
95✔
360
    if (!this.filteringOptions.filterKey) {
95✔
361
      this.filteringOptions = { filterKey: this.displayKey };
95✔
362
    }
95✔
363
  }
95✔
364

10✔
365
  public get displayKey() {
10✔
366
    return this._displayKey ?? this._valueKey;
1,888✔
367
  }
1,888✔
368

10✔
369
  /**
10✔
370
   * The key in the data source used to group items in the list.
10✔
371
   * @attr group-key
10✔
372
   */
10✔
373
  @property({ attribute: 'group-key' })
10✔
374
  public set groupKey(value: Keys<T> | undefined) {
10✔
375
    if (this._groupKey !== value) {
70✔
376
      this._groupKey = value;
70✔
377
      this._state.runPipeline();
70✔
378
    }
70✔
379
  }
70✔
380

10✔
381
  public get groupKey() {
10✔
382
    return this._groupKey;
1,445✔
383
  }
1,445✔
384

10✔
385
  /**
10✔
386
   * Sorts the items in each group by ascending or descending order.
10✔
387
   * @attr group-sorting
10✔
388
   * @default asc
10✔
389
   * @type {"asc" | "desc" | "none"}
10✔
390
   */
10✔
391
  @property({ attribute: 'group-sorting' })
10✔
392
  public set groupSorting(value: GroupingDirection) {
10✔
393
    if (this._groupSorting !== value) {
3✔
394
      this._groupSorting = value;
2✔
395
      this._state.runPipeline();
2✔
396
    }
2✔
397
  }
3✔
398

10✔
399
  public get groupSorting() {
10✔
400
    return this._groupSorting;
313✔
401
  }
313✔
402

10✔
403
  /**
10✔
404
   * An object that configures the filtering of the combo.
10✔
405
   * @attr filtering-options
10✔
406
   * @type {FilteringOptions<T>}
10✔
407
   * @param filterKey - The key in the data source used when filtering the list of options.
10✔
408
   * @param caseSensitive - Determines whether the filtering operation should be case sensitive.
10✔
409
   * @param matchDiacritics -If true, the filter distinguishes between accented letters and their base letters.
10✔
410
   */
10✔
411
  @property({ type: Object, attribute: 'filtering-options' })
10✔
412
  public set filteringOptions(value: Partial<FilteringOptions<T>>) {
10✔
413
    const options = { ...this._filteringOptions, ...value };
100✔
414
    if (!equal(options, this._filteringOptions)) {
100✔
415
      this._filteringOptions = options;
100✔
416
      this._state.runPipeline();
100✔
417
    }
100✔
418
  }
100✔
419

10✔
420
  public get filteringOptions(): FilteringOptions<T> {
10✔
421
    return this._filteringOptions;
1,084✔
422
  }
1,084✔
423

10✔
424
  /**
10✔
425
   * Enables the case sensitive search icon in the filtering input.
10✔
426
   * @attr case-sensitive-icon
10✔
427
   */
10✔
428
  @property({ type: Boolean, attribute: 'case-sensitive-icon' })
10✔
429
  public caseSensitiveIcon = false;
10✔
430

10✔
431
  /**
10✔
432
   * Disables the filtering of the list of options.
10✔
433
   * @attr disable-filtering
10✔
434
   * @default false
10✔
435
   */
10✔
436
  @property({ type: Boolean, attribute: 'disable-filtering' })
10✔
437
  public set disableFiltering(value: boolean) {
10✔
438
    this._disableFiltering = value;
2✔
439
    this.resetSearchTerm();
2✔
440
  }
2✔
441

10✔
442
  public get disableFiltering(): boolean {
10✔
443
    return this._disableFiltering;
579✔
444
  }
579✔
445

10✔
446
  /**
10✔
447
   * Hides the clear button.
10✔
448
   * @attr disable-clear
10✔
449
   * @default false
10✔
450
   */
10✔
451
  @property({ type: Boolean, attribute: 'disable-clear' })
10✔
452
  public disableClear = false;
10✔
453

10✔
454
  /* blazorSuppress */
10✔
455
  /**
10✔
456
   * The template used for the content of each combo item.
10✔
457
   * @type {ComboItemTemplate<T>}
10✔
458
   */
10✔
459
  @property({ attribute: false })
10✔
460
  public itemTemplate: ComboItemTemplate<T> = ({ item }) =>
10✔
461
    html`${this.displayKey ? item[this.displayKey] : item}`;
497!
462

10✔
463
  /* blazorSuppress */
10✔
464
  /**
10✔
465
   * The template used for the content of each combo group header.
10✔
466
   * @type {ComboItemTemplate<T>}
10✔
467
   */
10✔
468
  @property({ attribute: false })
10✔
469
  public groupHeaderTemplate: ComboItemTemplate<T> = ({ item }) =>
10✔
470
    html`${this.groupKey && item[this.groupKey]}`;
179✔
471

10✔
472
  /**
10✔
473
   * Sets the value (selected items). The passed value must be a valid JSON array.
10✔
474
   * If the data source is an array of complex objects, the `valueKey` attribute must be set.
10✔
475
   * Note that when `displayKey` is not explicitly set, it will fall back to the value of `valueKey`.
10✔
476
   *
10✔
477
   * @attr value
10✔
478
   *
10✔
479
   * @example
10✔
480
   * ```tsx
10✔
481
   * <igc-combo
10✔
482
   *  .data=${[
10✔
483
   *    {
10✔
484
   *      id: 'BG01',
10✔
485
   *      name: 'Sofia'
10✔
486
   *    },
10✔
487
   *    {
10✔
488
   *      id: 'BG02',
10✔
489
   *      name: 'Plovdiv'
10✔
490
   *    }
10✔
491
   *  ]}
10✔
492
   *  display-key='name'
10✔
493
   *  value-key='id'
10✔
494
   *  value='["BG01", "BG02"]'>
10✔
495
   *  </igc-combo>
10✔
496
   * ```
10✔
497
   */
10✔
498
  /* blazorPrimitiveValue */
10✔
499
  /* blazorByValueArray */
10✔
500
  /* blazorGenericType */
10✔
501
  /* @tsTwoWayProperty (true, "Change", "Detail.NewValue", false) */
10✔
502
  @property({ type: Array })
10✔
503
  public set value(items: ComboValue<T>[]) {
10✔
504
    this._formValue.value = items;
145✔
505
    if (this.hasUpdated) {
145✔
506
      this._updateSelection();
42✔
507
      this.updateValue();
42✔
508
    }
42✔
509
  }
145✔
510

10✔
511
  /**
10✔
512
   * Returns the current selection as a list of comma separated values,
10✔
513
   * represented by the value key, when provided.
10✔
514
   */
10✔
515
  public get value(): ComboValue<T>[] {
10✔
516
    return this._formValue.value;
1,925✔
517
  }
1,925✔
518

10✔
519
  protected _updateSelection() {
10✔
520
    this._selection.deselect();
149✔
521
    if (!isEmpty(this.value)) {
149✔
522
      this._selection.select(this.value);
40✔
523
    }
40✔
524
  }
149✔
525

10✔
526
  @watch('open')
10✔
527
  protected toggleDirectiveChange() {
10✔
528
    this._rootClickController.update();
168✔
529
  }
168✔
530

10✔
531
  constructor() {
10✔
532
    super();
101✔
533

101✔
534
    addThemingController(this, all);
101✔
535
    addSafeEventListener(this, 'blur', this._handleBlur);
101✔
536
    addSafeEventListener(this, 'focusin', this._handleFocusIn);
101✔
537
  }
101✔
538

10✔
539
  protected override async firstUpdated() {
10✔
540
    await this.updateComplete;
101✔
541

101✔
542
    this._updateSelection();
101✔
543
    this.updateValue(this.hasUpdated);
101✔
544
    this._pristine = true;
101✔
545
    this._state.runPipeline();
101✔
546
  }
101✔
547

10✔
548
  protected override _restoreDefaultValue(): void {
10✔
549
    this._formValue.value = this._formValue.defaultValue;
6✔
550
    this._updateSelection();
6✔
551
    this.updateValue(true);
6✔
552
    this._validate();
6✔
553
  }
6✔
554

10✔
555
  protected override _setDefaultValue(current: string | null): void {
10✔
556
    this.defaultValue = JSON.parse(current ?? '[]');
16!
557
  }
16✔
558

10✔
559
  protected override _setFormValue(): void {
10✔
560
    if (isEmpty(this.value)) {
210✔
561
      super._setFormValue(null);
133✔
562
      return;
133✔
563
    }
133✔
564

77✔
565
    if (this.singleSelect) {
210✔
566
      super._setFormValue(`${first(this.value)}`);
25✔
567
      return;
25✔
568
    }
25✔
569

52✔
570
    if (this.name) {
210✔
571
      const value = new FormData();
26✔
572
      for (const item of this.value) {
26✔
573
        value.append(this.name, `${item}`);
52✔
574
      }
52✔
575
      super._setFormValue(value);
26✔
576
    }
26✔
577
  }
210✔
578

10✔
579
  protected resetSearchTerm() {
10✔
580
    this._state.searchTerm = '';
48✔
581
  }
48✔
582

10✔
583
  protected updateValue(initial = false) {
10✔
584
    if (isEmpty(this.data)) {
221✔
585
      return;
11✔
586
    }
11✔
587
    this._formValue.value = this._selection.getSelectedValuesByKey(
210✔
588
      this.valueKey
210✔
589
    );
210✔
590
    this._displayValue = this._selection
210✔
591
      .getSelectedValuesByKey(this.displayKey)
210✔
592
      .join(', ');
210✔
593

210✔
594
    this._setFormValue();
210✔
595

210✔
596
    if (!initial) {
221✔
597
      this._validate();
110✔
598
      this._listRef.value!.requestUpdate();
110✔
599
    }
110✔
600
  }
221✔
601

10✔
602
  /* alternateName: focusComponent */
10✔
603
  /** Sets focus on the component. */
10✔
604
  public override focus(options?: FocusOptions) {
10✔
605
    this._inputRef.value!.focus(options);
5✔
606
  }
5✔
607

10✔
608
  /* alternateName: blurComponent */
10✔
609
  /** Removes focus from the component. */
10✔
610
  public override blur() {
10✔
611
    this._inputRef.value!.blur();
1✔
612
  }
1✔
613

10✔
614
  /**
10✔
615
   * Returns the current selection as an array of objects as provided in the `data` source.
10✔
616
   */
10✔
617
  public get selection(): T[] {
10✔
618
    return this._selection.asArray;
9✔
619
  }
9✔
620

10✔
621
  /**
10✔
622
   * Selects option(s) in the list by either reference or valueKey.
10✔
623
   * If not argument is provided all items will be selected.
10✔
624
   * @param { Item<T> | Items<T> } items - One or more items to be selected. Multiple items should be passed as an array.
10✔
625
   * When valueKey is specified, the corresponding value should be used in place of the item reference.
10✔
626
   * @example
10✔
627
   * ```typescript
10✔
628
   * const combo<IgcComboComponent<T>> = document.querySelector('igc-combo');
10✔
629
   *
10✔
630
   * // Select one item at a time by reference when valueKey is not specified.
10✔
631
   * combo.select(combo.data[0]);
10✔
632
   *
10✔
633
   * // Select multiple items at a time by reference when valueKey is not specified.
10✔
634
   * combo.select([combo.data[0], combo.data[1]]);
10✔
635
   *
10✔
636
   * // Select one item at a time when valueKey is specified.
10✔
637
   * combo.select('BG01');
10✔
638
   *
10✔
639
   * // Select multiple items at a time when valueKey is specified.
10✔
640
   * combo.select(['BG01', 'BG02']);
10✔
641
   * ```
10✔
642
   */
10✔
643
  public select(items?: Item<T> | Item<T>[]) {
10✔
644
    this._selection.select(items);
26✔
645
    this.updateValue();
26✔
646
  }
26✔
647

10✔
648
  /**
10✔
649
   * Deselects option(s) in the list by either reference or valueKey.
10✔
650
   * If not argument is provided all items will be deselected.
10✔
651
   * @param { Item<T> | Items<T> } items - One or more items to be deselected. Multiple items should be passed as an array.
10✔
652
   * When valueKey is specified, the corresponding value should be used in place of the item reference.
10✔
653
   * @example
10✔
654
   * ```typescript
10✔
655
   * const combo<IgcComboComponent<T>> = document.querySelector('igc-combo');
10✔
656
   *
10✔
657
   * // Deselect one item at a time by reference when valueKey is not specified.
10✔
658
   * combo.deselect(combo.data[0]);
10✔
659
   *
10✔
660
   * // Deselect multiple items at a time by reference when valueKey is not specified.
10✔
661
   * combo.deselect([combo.data[0], combo.data[1]]);
10✔
662
   *
10✔
663
   * // Deselect one item at a time when valueKey is specified.
10✔
664
   * combo.deselect('BG01');
10✔
665
   *
10✔
666
   * // Deselect multiple items at a time when valueKey is specified.
10✔
667
   * combo.deselect(['BG01', 'BG02']);
10✔
668
   * ```
10✔
669
   */
10✔
670
  public deselect(items?: Item<T> | Item<T>[]) {
10✔
671
    this._selection.deselect(items);
9✔
672
    this.updateValue();
9✔
673
  }
9✔
674

10✔
675
  protected async handleMainInput({ detail }: CustomEvent<string>) {
10✔
676
    this._setTouchedState();
11✔
677
    this._show();
11✔
678
    this._state.searchTerm = detail;
11✔
679

11✔
680
    // wait for the dataState to update after filtering
11✔
681
    await this.updateComplete;
11✔
682

11✔
683
    const matchIndex = this._state.dataState.findIndex((i) => !i.header);
11✔
684
    this._navigation.active = detail ? matchIndex : -1;
11!
685

11✔
686
    // update the list after changing the active item
11✔
687
    this._listRef.value!.requestUpdate();
11✔
688

11✔
689
    // clear the selection upon typing
11✔
690
    this.clearSingleSelection();
11✔
691
  }
11✔
692

10✔
693
  protected _handleFocusIn() {
10✔
694
    this._setTouchedState();
37✔
695
  }
37✔
696

10✔
697
  protected override _handleBlur() {
10✔
698
    if (this._selection.isEmpty) {
37✔
699
      this._displayValue = '';
24✔
700
      this.resetSearchTerm();
24✔
701
    }
24✔
702
    super._handleBlur();
37✔
703
  }
37✔
704

10✔
705
  protected handleSearchInput({ detail }: CustomEvent<string>) {
10✔
706
    this._state.searchTerm = detail;
4✔
707
  }
4✔
708

10✔
709
  protected handleOpening() {
10✔
710
    return this.emitEvent('igcOpening', { cancelable: true });
5✔
711
  }
5✔
712

10✔
713
  protected handleClosing(): boolean {
10✔
714
    return this.emitEvent('igcClosing', { cancelable: true });
17✔
715
  }
17✔
716

10✔
717
  protected async _show(emitEvent = true) {
10✔
718
    if (this.open || (emitEvent && !this.handleOpening())) {
51✔
719
      return false;
11✔
720
    }
11✔
721

40✔
722
    this.open = true;
40✔
723
    await this.updateComplete;
40✔
724

40✔
725
    if (emitEvent) {
51✔
726
      this.emitEvent('igcOpened');
4✔
727
    }
4✔
728

40✔
729
    if (!this.singleSelect) {
51✔
730
      this._listRef.value!.focus();
28✔
731
    }
28✔
732

40✔
733
    if (!this.autofocusList) {
51✔
734
      this._searchRef.value!.focus();
35✔
735
    }
35✔
736

40✔
737
    return true;
40✔
738
  }
51✔
739

10✔
740
  /** Shows the list of options. */
10✔
741
  public async show(): Promise<boolean> {
10✔
742
    return await this._show(false);
35✔
743
  }
35✔
744

10✔
745
  protected async _hide(emitEvent = true) {
10✔
746
    if (!this.open || (emitEvent && !this.handleClosing())) {
21✔
747
      return false;
4✔
748
    }
4✔
749

17✔
750
    this.open = false;
17✔
751
    await this.updateComplete;
17✔
752

17✔
753
    if (emitEvent) {
21✔
754
      this.emitEvent('igcClosed');
16✔
755
    }
16✔
756
    this._navigation.active = -1;
17✔
757
    return true;
17✔
758
  }
21✔
759

10✔
760
  /** Hides the list of options. */
10✔
761
  public async hide(): Promise<boolean> {
10✔
762
    return await this._hide(false);
1✔
763
  }
1✔
764

10✔
765
  protected _toggle(emit = true) {
10✔
766
    return this.open ? this._hide(emit) : this._show(emit);
6✔
767
  }
6✔
768

10✔
769
  /** Toggles the list of options. */
10✔
770
  public async toggle(): Promise<boolean> {
10✔
771
    return await this._toggle(false);
2✔
772
  }
2✔
773

10✔
774
  private _getActiveDescendantId(index: number) {
10✔
775
    const position = index + 1;
488✔
776
    const id = this.id ? `${this.id}-item-${position}` : `item-${position}`;
488!
777

488✔
778
    return { id, position };
488✔
779
  }
488✔
780

10✔
781
  protected itemRenderer: ComboRenderFunction<T> = (
10✔
782
    item: ComboRecord<T>,
719✔
783
    index: number
719✔
784
  ) => {
719✔
785
    if (!item) {
719✔
786
      return html`${nothing}`;
61✔
787
    }
61✔
788

667✔
789
    if (this.groupKey && item.header) {
719✔
790
      return html`
179✔
791
        <igc-combo-header part="group-header">
179✔
792
          ${this.groupHeaderTemplate({ item: item.value })}
179✔
793
        </igc-combo-header>
179✔
794
      `;
179✔
795
    }
179✔
796

497✔
797
    const { id, position } = this._getActiveDescendantId(index);
497✔
798
    const active = this._navigation.active === index;
497✔
799
    const selected = this._selection.has(this.data.at(item.dataIndex));
497✔
800

497✔
801
    if (active) {
719✔
802
      this._activeDescendant = id;
45✔
803
    }
45✔
804

497✔
805
    return html`
497✔
806
      <igc-combo-item
497✔
807
        id=${id}
497✔
808
        part=${partMap({ item: true, selected, active })}
497✔
809
        aria-setsize=${this._state.dataState.length}
497✔
810
        aria-posinset=${position}
497✔
811
        exportparts="checkbox, checkbox-indicator, checked"
497✔
812
        .index=${index}
497✔
813
        ?active=${active}
497✔
814
        ?selected=${selected}
497✔
815
        ?hide-checkbox=${this.singleSelect}
497✔
816
      >
497✔
817
        ${this.itemTemplate({ item: item.value })}
497✔
818
      </igc-combo-item>
719✔
819
    `;
719✔
820
  };
719✔
821

10✔
822
  protected itemClickHandler(event: PointerEvent) {
10✔
823
    this._setTouchedState();
5✔
824
    const target = findElementFromEventPath<IgcComboItemComponent>(
5✔
825
      IgcComboItemComponent.tagName,
5✔
826
      event
5✔
827
    );
5✔
828

5✔
829
    if (!target) {
5!
830
      return;
×
831
    }
×
832

5✔
833
    this.toggleSelect(target.index);
5✔
834

5✔
835
    if (this.singleSelect) {
5!
836
      this._inputRef.value!.focus();
×
837
      this._hide();
×
838
    } else {
5✔
839
      this._searchRef.value!.focus();
5✔
840
    }
5✔
841
  }
5✔
842

10✔
843
  protected toggleSelect(index: number) {
10✔
844
    const { dataIndex } = this._state.dataState.at(index)!;
9✔
845

9✔
846
    this._selection.changeSelection(dataIndex);
9✔
847
    this._navigation.active = index;
9✔
848
    this.updateValue();
9✔
849
  }
9✔
850

10✔
851
  protected selectByIndex(index: number) {
10✔
852
    const { dataIndex } = this._state.dataState.at(index)!;
4✔
853

4✔
854
    this._selection.selectByIndex(dataIndex);
4✔
855
    this._navigation.active = index;
4✔
856
    this.updateValue();
4✔
857
  }
4✔
858

10✔
859
  /** @hidden @internal */
10✔
860
  public clearSelection() {
10✔
861
    if (this.singleSelect) {
3✔
862
      this.resetSearchTerm();
1✔
863
      this.clearSingleSelection();
1✔
864
    } else {
3✔
865
      this._selection.deselect([], true);
2✔
866
    }
2✔
867
    this.updateValue();
3✔
868
  }
3✔
869

10✔
870
  protected clearSingleSelection() {
10✔
871
    const _selection = this._selection.asArray;
12✔
872
    const selection = first(_selection);
12✔
873

12✔
874
    if (selection) {
12✔
875
      const item = this.valueKey ? selection[this.valueKey] : selection;
3!
876
      this._selection.deselect(item, !isEmpty(_selection));
3✔
877
      this._formValue.value = [];
3✔
878
    }
3✔
879
  }
12✔
880

10✔
881
  protected handleClearIconClick(e: PointerEvent) {
10✔
882
    e.stopPropagation();
1✔
883
    this.clearSelection();
1✔
884
    this._navigation.active = -1;
1✔
885
  }
1✔
886

10✔
887
  protected toggleCaseSensitivity() {
10✔
888
    this.filteringOptions = {
2✔
889
      caseSensitive: !this.filteringOptions.caseSensitive,
2✔
890
    };
2✔
891
  }
2✔
892

10✔
893
  private _stopPropagation(e: Event) {
10✔
894
    e.stopPropagation();
×
895
  }
×
896

10✔
897
  private renderToggleIcon() {
10✔
898
    return html`
473✔
899
      <span
473✔
900
        slot="suffix"
473✔
901
        part=${partMap({
473✔
902
          'toggle-icon': true,
473✔
903
          filled: !isEmpty(this.value),
473✔
904
        })}
473✔
905
      >
473✔
906
        <slot name="toggle-icon">
473✔
907
          <igc-icon
473✔
908
            name=${this.open ? 'input_collapse' : 'input_expand'}
473✔
909
            collection="default"
473✔
910
            aria-hidden="true"
473✔
911
          ></igc-icon>
473✔
912
        </slot>
473✔
913
      </span>
473✔
914
    `;
473✔
915
  }
473✔
916

10✔
917
  private renderClearIcon() {
10✔
918
    return html`
473✔
919
      <span
473✔
920
        slot="suffix"
473✔
921
        part="clear-icon"
473✔
922
        @click=${this.handleClearIconClick}
473✔
923
        ?hidden=${this.disableClear || this._selection.isEmpty}
473✔
924
      >
473✔
925
        <slot name="clear-icon">
473✔
926
          <igc-icon
473✔
927
            name="input_clear"
473✔
928
            collection="default"
473✔
929
            aria-hidden="true"
473✔
930
          ></igc-icon>
473✔
931
        </slot>
473✔
932
      </span>
473✔
933
    `;
473✔
934
  }
473✔
935

10✔
936
  private renderMainInput() {
10✔
937
    return html`
473✔
938
      <igc-input
473✔
939
        ${ref(this._inputRef)}
473✔
940
        id="target"
473✔
941
        slot="anchor"
473✔
942
        role="combobox"
473✔
943
        aria-controls="dropdown"
473✔
944
        aria-owns="dropdown"
473✔
945
        aria-expanded=${this.open}
473✔
946
        aria-describedby="combo-helper-text"
473✔
947
        aria-disabled=${this.disabled}
473✔
948
        exportparts="container: input, input: native-input, label, prefix, suffix"
473✔
949
        @click=${this._toggle}
473✔
950
        placeholder=${ifDefined(this.placeholder)}
473✔
951
        label=${ifDefined(this.label)}
473✔
952
        @igcChange=${this._stopPropagation}
473✔
953
        @igcInput=${this.handleMainInput}
473✔
954
        .value=${this._displayValue}
473✔
955
        .disabled=${this.disabled}
473✔
956
        .required=${this.required}
473✔
957
        .invalid=${this.invalid}
473✔
958
        .outlined=${this.outlined}
473✔
959
        .autofocus=${this.autofocus}
473✔
960
        ?readonly=${!this.singleSelect}
473✔
961
      >
473✔
962
        <span slot=${!isEmpty(this.inputPrefix) && 'prefix'}>
473!
963
          <slot name="prefix"></slot>
473✔
964
        </span>
473✔
965
        ${this.renderClearIcon()}
473✔
966
        <span slot=${!isEmpty(this.inputSuffix) && 'suffix'}>
473!
967
          <slot name="suffix"></slot>
473✔
968
        </span>
473✔
969
        ${this.renderToggleIcon()}
473✔
970
      </igc-input>
473✔
971
    `;
473✔
972
  }
473✔
973

10✔
974
  private renderSearchInput() {
10✔
975
    return html`
473✔
976
      <div
473✔
977
        part="filter-input"
473✔
978
        ?hidden=${this.disableFiltering || this.singleSelect}
473✔
979
      >
473✔
980
        <igc-input
473✔
981
          ${ref(this._searchRef)}
473✔
982
          .value=${this._state.searchTerm}
473✔
983
          part="search-input"
473✔
984
          placeholder=${this.placeholderSearch}
473✔
985
          exportparts="input: search-input"
473✔
986
          @igcInput=${this.handleSearchInput}
473✔
987
        >
473✔
988
          <igc-icon
473✔
989
            slot=${this.caseSensitiveIcon && 'suffix'}
473!
990
            name="case_sensitive"
473✔
991
            collection="default"
473✔
992
            part=${partMap({
473✔
993
              'case-icon': true,
473✔
994
              active: this.filteringOptions.caseSensitive ?? false,
473!
995
            })}
473✔
996
            @click=${this.toggleCaseSensitivity}
473✔
997
          ></igc-icon>
473✔
998
        </igc-input>
473✔
999
      </div>
473✔
1000
    `;
473✔
1001
  }
473✔
1002

10✔
1003
  private renderEmptyTemplate() {
10✔
1004
    return html`
473✔
1005
      <div part="empty" ?hidden=${!isEmpty(this._state.dataState)}>
473✔
1006
        <slot name="empty">${this.resourceStrings.combo_empty_message}</slot>
473✔
1007
      </div>
473✔
1008
    `;
473✔
1009
  }
473✔
1010

10✔
1011
  private renderList() {
10✔
1012
    return html`
473✔
1013
      <div .inert=${!this.open} part="list-wrapper">
473✔
1014
        ${this.renderSearchInput()}
473✔
1015
        <div part="header">
473✔
1016
          <slot name="header"></slot>
473✔
1017
        </div>
473✔
1018
        <igc-combo-list
473✔
1019
          ${ref(this._listRef)}
473✔
1020
          aria-multiselectable=${!this.singleSelect}
473✔
1021
          id="dropdown"
473✔
1022
          part="list"
473✔
1023
          role="listbox"
473✔
1024
          tabindex="0"
473✔
1025
          aria-labelledby="target"
473✔
1026
          aria-activedescendant=${ifDefined(this._activeDescendant)}
473✔
1027
          .items=${this._state.dataState}
473✔
1028
          .renderItem=${this.itemRenderer}
473✔
1029
          ?hidden=${isEmpty(this._state.dataState)}
473✔
1030
          @click=${this.itemClickHandler}
473✔
1031
        >
473✔
1032
        </igc-combo-list>
473✔
1033
        ${this.renderEmptyTemplate()}
473✔
1034
        <div part="footer">
473✔
1035
          <slot name="footer"></slot>
473✔
1036
        </div>
473✔
1037
      </div>
473✔
1038
    `;
473✔
1039
  }
473✔
1040

10✔
1041
  private renderHelperText(): TemplateResult {
10✔
1042
    return IgcValidationContainerComponent.create(this, {
473✔
1043
      id: 'combo-helper-text',
473✔
1044
      hasHelperText: true,
473✔
1045
    });
473✔
1046
  }
473✔
1047

10✔
1048
  protected override render() {
10✔
1049
    return html`
473✔
1050
      <igc-popover ?open=${this.open} flip shift same-width>
473✔
1051
        ${this.renderMainInput()} ${this.renderList()}
473✔
1052
      </igc-popover>
473✔
1053
      ${this.renderHelperText()}
473✔
1054
    `;
473✔
1055
  }
473✔
1056
}
10✔
1057

10✔
1058
declare global {
10✔
1059
  interface HTMLElementTagNameMap {
10✔
1060
    'igc-combo': IgcComboComponent<object>;
10✔
1061
  }
10✔
1062
}
10✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc