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

IgniteUI / igniteui-angular / 23852606264

01 Apr 2026 02:02PM UTC coverage: 89.266% (-0.002%) from 89.268%
23852606264

push

github

web-flow
fix(pivot): Add container div for measuring in pivot vertical containers. (#17010)

14401 of 16964 branches covered (84.89%)

Branch coverage included in aggregate %.

29060 of 31723 relevant lines covered (91.61%)

34476.16 hits per line

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

94.8
/projects/igniteui-angular/combo/src/combo/combo.component.ts
1
import { NgClass, NgTemplateOutlet } from '@angular/common';
2
import { AfterViewInit, Component, OnInit, OnDestroy, ViewChild, Input, Output, EventEmitter, HostListener, DoCheck, booleanAttribute } from '@angular/core';
3

4
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
5

6
import {
7
    IBaseEventArgs,
8
    IBaseCancelableEventArgs,
9
    CancelableEventArgs,
10
    EditorProvider
11
} from 'igniteui-angular/core';
12
import { IgxForOfDirective } from 'igniteui-angular/directives';
13
import { IgxRippleDirective } from 'igniteui-angular/directives';
14
import { IgxButtonDirective } from 'igniteui-angular/directives';
15
import { IgxComboItemComponent } from './combo-item.component';
16
import { IgxComboDropDownComponent } from './combo-dropdown.component';
17
import { IgxComboFilteringPipe, IgxComboGroupingPipe } from './combo.pipes';
18
import { IGX_COMBO_COMPONENT, IgxComboBaseDirective } from './combo.common';
19
import { IgxComboAddItemComponent } from './combo-add-item.component';
20
import { IgxComboAPIService } from './combo.api';
21
import { IgxInputGroupComponent, IgxInputDirective, IgxReadOnlyInputDirective, IgxSuffixDirective } from 'igniteui-angular/input-group';
22
import { IgxIconComponent } from 'igniteui-angular/icon';
23
import { IgxDropDownItemNavigationDirective } from 'igniteui-angular/drop-down';
24

25
/** Event emitted when an igx-combo's selection is changing */
26
export interface IComboSelectionChangingEventArgs extends IBaseCancelableEventArgs {
27
    /** An array containing the values that are currently selected */
28
    oldValue: any[];
29
    /** An array containing the values that will be selected after this event */
30
    newValue: any[];
31
    /** An array containing the items that are currently selected */
32
    oldSelection: any[];
33
    /** An array containing the items that will be selected after this event */
34
    newSelection: any[];
35
    /** An array containing the items that will be added to the selection (if any) */
36
    added: any[];
37
    /** An array containing the items that will be removed from the selection (if any) */
38
    removed: any[];
39
    /** The text that will be displayed in the combo text box */
40
    displayText: string;
41
    /** The user interaction that triggered the selection change */
42
    event?: Event;
43
}
44

45
/** Event emitted when the igx-combo's search input changes */
46
export interface IComboSearchInputEventArgs extends IBaseCancelableEventArgs {
47
    /** The text that has been typed into the search input */
48
    searchText: string;
49
}
50

51
export interface IComboItemAdditionEvent extends IBaseEventArgs, CancelableEventArgs {
52
    oldCollection: any[];
53
    addedItem: any;
54
    newCollection: any[];
55
}
56

57
/**
58
 * When called with sets A & B, returns A - B (as array);
59
 *
60
 * @hidden
61
 */
62
const diffInSets = (set1: Set<any>, set2: Set<any>): any[] => {
3✔
63
    const results = [];
239✔
64
    set1.forEach(entry => {
239✔
65
        if (!set2.has(entry)) {
627✔
66
            results.push(entry);
371✔
67
        }
68
    });
69
    return results;
239✔
70
};
71

72
/**
73
 *  Represents a drop-down list that provides editable functionalities, allowing users to choose an option from a predefined list.
74
 *
75
 * @igxModule IgxComboModule
76
 * @igxTheme igx-combo-theme
77
 * @igxKeywords combobox, combo selection
78
 * @igxGroup Grids & Lists
79
 *
80
 * @remarks
81
 * It provides the ability to filter items as well as perform selection with the provided data.
82
 * Additionally, it exposes keyboard navigation and custom styling capabilities.
83
 * @example
84
 * ```html
85
 * <igx-combo [itemsMaxHeight]="250" [data]="locationData"
86
 *  [displayKey]="'field'" [valueKey]="'field'"
87
 *  placeholder="Location(s)" searchPlaceholder="Search...">
88
 * </igx-combo>
89
 * ```
90
 */
91
@Component({
92
    selector: 'igx-combo',
93
    templateUrl: 'combo.component.html',
94
    providers: [
95
        IgxComboAPIService,
96
        { provide: IGX_COMBO_COMPONENT, useExisting: IgxComboComponent },
97
        { provide: NG_VALUE_ACCESSOR, useExisting: IgxComboComponent, multi: true }
98
    ],
99
    imports: [
100
        NgTemplateOutlet,
101
        NgClass,
102
        FormsModule,
103
        IgxInputGroupComponent,
104
        IgxInputDirective,
105
        IgxSuffixDirective,
106
        IgxIconComponent,
107
        IgxComboDropDownComponent,
108
        IgxDropDownItemNavigationDirective,
109
        IgxForOfDirective,
110
        IgxComboItemComponent,
111
        IgxComboAddItemComponent,
112
        IgxButtonDirective,
113
        IgxRippleDirective,
114
        IgxReadOnlyInputDirective,
115
        IgxComboFilteringPipe,
116
        IgxComboGroupingPipe
117
    ]
118
})
119
export class IgxComboComponent extends IgxComboBaseDirective implements AfterViewInit, ControlValueAccessor, OnInit,
3✔
120
    OnDestroy, DoCheck, EditorProvider {
121
    /**
122
     * Whether the combo's search box should be focused after the dropdown is opened.
123
     * When `false`, the combo's list item container will be focused instead
124
     */
125
    @Input({ transform: booleanAttribute })
126
    public autoFocusSearch = true;
308✔
127

128

129
    /**
130
     * Defines the placeholder value for the combo dropdown search field
131
     *
132
     * @deprecated in version 18.2.0. Replaced with values in the localization resource strings.
133
     *
134
     * ```typescript
135
     * // get
136
     * let myComboSearchPlaceholder = this.combo.searchPlaceholder;
137
     * ```
138
     *
139
     * ```html
140
     * <!--set-->
141
     * <igx-combo [searchPlaceholder]='newPlaceHolder'></igx-combo>
142
     * ```
143
     */
144
    @Input()
145
    public searchPlaceholder: string;
146

147
    /**
148
     * Emitted when item selection is changing, before the selection completes
149
     *
150
     * ```html
151
     * <igx-combo (selectionChanging)='handleSelection()'></igx-combo>
152
     * ```
153
     */
154
    @Output()
155
    public selectionChanging = new EventEmitter<IComboSelectionChangingEventArgs>();
308✔
156

157
    /** @hidden @internal */
158
    @ViewChild(IgxComboDropDownComponent, { static: true })
159
    public dropdown: IgxComboDropDownComponent;
160

161
    /** @hidden @internal */
162
    public get filteredData(): any[] | null {
163
        return this.disableFiltering ? this.data : this._filteredData;
9,412✔
164
    }
165
    /** @hidden @internal */
166
    public set filteredData(val: any[] | null) {
167
        this._filteredData = this.groupKey ? (val || []).filter((e) => e.isHeader !== true) : val;
9,925!
168
        this.checkMatch();
914✔
169
    }
170

171
    protected _prevInputValue = '';
308✔
172

173
    private _displayText: string;
174

175
    constructor() {
176
        super();
308✔
177
        this.comboAPI.register(this);
308✔
178
    }
179

180
    @HostListener('keydown.ArrowDown', ['$event'])
181
    @HostListener('keydown.Alt.ArrowDown', ['$event'])
182
    public onArrowDown(event: Event) {
183
        event.preventDefault();
2✔
184
        event.stopPropagation();
2✔
185
        this.open();
2✔
186
    }
187

188
    @HostListener('keydown.Escape', ['$event'])
189
    public onEscape(event: Event) {
190
        event.stopPropagation();
3✔
191
        if (this.collapsed) {
3✔
192
            this.deselectAllItems(true, event);
2✔
193
        }
194
    }
195

196
    /** @hidden @internal */
197
    public get displaySearchInput(): boolean {
198
        return !this.disableFiltering || this.allowCustomValues;
7,740✔
199
    }
200

201
    /**
202
     * @hidden @internal
203
     */
204
    public handleKeyUp(event: KeyboardEvent): void {
205
        // TODO: use PlatformUtil for keyboard navigation
206
        if (event.key === 'ArrowDown' || event.key === 'Down') {
12✔
207
            this.dropdown.focusedItem = this.dropdown.items[0];
6✔
208
            this.dropdownContainer.nativeElement.focus();
6✔
209
        } else if (event.key === 'Escape' || event.key === 'Esc') {
6✔
210
            this.toggle();
2✔
211
        }
212
    }
213

214
    /**
215
     * @hidden @internal
216
     */
217
    public handleSelectAll(evt) {
218
        if (evt.checked) {
2✔
219
            this.selectAllItems();
1✔
220
        } else {
221
            this.deselectAllItems();
1✔
222
        }
223
    }
224

225
    /**
226
     * @hidden @internal
227
     */
228
    public writeValue(value: any[]): void {
229
        const selection = Array.isArray(value) ? value.filter(x => x !== undefined) : [];
1,107✔
230
        const oldSelection = this.selection;
548✔
231
        this.selectionService.select_items(this.id, selection, true);
548✔
232
        this.cdr.markForCheck();
548✔
233
        this._displayValue = this.createDisplayText(this.selection, oldSelection);
548✔
234
        this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
1,078✔
235
    }
236

237
    /** @hidden @internal */
238
    public ngDoCheck(): void {
239
        if (this.data?.length && this.selection.length) {
3,779✔
240
            this._displayValue = this._displayText || this.createDisplayText(this.selection, []);
1,206✔
241
            this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
5,201✔
242
        }
243
    }
244

245
    /**
246
     * @hidden
247
     */
248
    public getEditElement(): HTMLElement {
249
        return this.comboInput.nativeElement;
4✔
250
    }
251

252
    /**
253
     * @hidden @internal
254
     */
255
    public get context(): any {
256
        return {
×
257
            $implicit: this
258
        };
259
    }
260

261
    /**
262
     * @hidden @internal
263
     */
264
    public handleClearItems(event: Event): void {
265
        if (this.disabled) {
9✔
266
            return;
1✔
267
        }
268
        this.deselectAllItems(true, event);
8✔
269
        if (this.collapsed) {
8✔
270
            this.getEditElement().focus();
4✔
271
        } else {
272
            this.focusSearchInput(true);
4✔
273
        }
274
        event.stopPropagation();
8✔
275
    }
276

277
    /**
278
     * Select defined items
279
     *
280
     * @param newItems new items to be selected
281
     * @param clearCurrentSelection if true clear previous selected items
282
     * ```typescript
283
     * this.combo.select(["New York", "New Jersey"]);
284
     * ```
285
     */
286
    public select(newItems: Array<any>, clearCurrentSelection?: boolean, event?: Event) {
287
        if (newItems) {
81✔
288
            const newSelection = this.selectionService.add_items(this.id, newItems, clearCurrentSelection);
81✔
289
            this.setSelection(newSelection, event);
81✔
290
        }
291
    }
292

293
    /**
294
     * Deselect defined items
295
     *
296
     * @param items items to deselected
297
     * ```typescript
298
     * this.combo.deselect(["New York", "New Jersey"]);
299
     * ```
300
     */
301
    public deselect(items: Array<any>, event?: Event) {
302
        if (items) {
19✔
303
            const newSelection = this.selectionService.delete_items(this.id, items);
19✔
304
            this.setSelection(newSelection, event);
19✔
305
        }
306
    }
307

308
    /**
309
     * Select all (filtered) items
310
     *
311
     * @param ignoreFilter if set to true, selects all items, otherwise selects only the filtered ones.
312
     * ```typescript
313
     * this.combo.selectAllItems();
314
     * ```
315
     */
316
    public selectAllItems(ignoreFilter?: boolean, event?: Event) {
317
        const allVisible = this.selectionService.get_all_ids(ignoreFilter ? this.data : this.filteredData, this.valueKey);
5✔
318
        const newSelection = this.selectionService.add_items(this.id, allVisible);
5✔
319
        this.setSelection(newSelection, event);
5✔
320
    }
321

322
    /**
323
     * Deselect all (filtered) items
324
     *
325
     * @param ignoreFilter if set to true, deselects all items, otherwise deselects only the filtered ones.
326
     * ```typescript
327
     * this.combo.deselectAllItems();
328
     * ```
329
     */
330
    public deselectAllItems(ignoreFilter?: boolean, event?: Event): void {
331
        let newSelection = this.selectionService.get_empty();
14✔
332
        if (this.filteredData.length !== this.data.length && !ignoreFilter) {
14✔
333
            newSelection = this.selectionService.delete_items(this.id, this.selectionService.get_all_ids(this.filteredData, this.valueKey));
1✔
334
        }
335
        this.setSelection(newSelection, event);
14✔
336
    }
337

338
    /**
339
     * Selects/Deselects a single item
340
     *
341
     * @param itemID the itemID of the specific item
342
     * @param select If the item should be selected (true) or deselected (false)
343
     *
344
     * Without specified valueKey;
345
     * ```typescript
346
     * this.combo.valueKey = null;
347
     * const items: { field: string, region: string}[] = data;
348
     * this.combo.setSelectedItem(items[0], true);
349
     * ```
350
     * With specified valueKey;
351
     * ```typescript
352
     * this.combo.valueKey = 'field';
353
     * const items: { field: string, region: string}[] = data;
354
     * this.combo.setSelectedItem('Connecticut', true);
355
     * ```
356
     */
357
    public setSelectedItem(itemID: any, select = true, event?: Event): void {
×
358
        if (itemID === undefined) {
7!
359
            return;
×
360
        }
361
        if (select) {
7✔
362
            this.select([itemID], false, event);
4✔
363
        } else {
364
            this.deselect([itemID], event);
3✔
365
        }
366
    }
367

368
    /** @hidden @internal */
369
    public handleOpened() {
370
        this.triggerCheck();
59✔
371

372
        // Disabling focus of the search input should happen only when drop down opens.
373
        // During keyboard navigation input should receive focus, even the autoFocusSearch is disabled.
374
        // That is why in such cases focusing of the dropdownContainer happens outside focusSearchInput method.
375
        if (this.autoFocusSearch) {
59✔
376
            this.focusSearchInput(true);
56✔
377
        } else {
378
            this.dropdownContainer.nativeElement.focus();
3✔
379
        }
380
        this.opened.emit({ owner: this });
59✔
381
    }
382

383
    /** @hidden @internal */
384
    public focusSearchInput(opening?: boolean): void {
385
        if (this.displaySearchInput && this.searchInput) {
61✔
386
            this.searchInput.nativeElement.focus();
60✔
387
        } else {
388
            if (opening) {
1!
389
                this.dropdownContainer.nativeElement.focus();
1✔
390
            } else {
391
                this.comboInput.nativeElement.focus();
×
392
                this.toggle();
×
393
            }
394
        }
395
    }
396

397
    protected setSelection(selection: Set<any>, event?: Event): void {
398
        const currentSelection = this.selectionService.get(this.id);
119✔
399
        const removed = this.convertKeysToItems(diffInSets(currentSelection, selection));
119✔
400
        const added = this.convertKeysToItems(diffInSets(selection, currentSelection));
119✔
401
        const newValue = Array.from(selection);
119✔
402
        const oldValue = Array.from(currentSelection || []);
119!
403
        const newSelection = this.convertKeysToItems(newValue);
119✔
404
        const oldSelection = this.convertKeysToItems(oldValue);
119✔
405
        const displayText = this.createDisplayText(this.convertKeysToItems(newValue), oldValue);
119✔
406
        const args: IComboSelectionChangingEventArgs = {
119✔
407
            newValue,
408
            oldValue,
409
            newSelection,
410
            oldSelection,
411
            added,
412
            removed,
413
            event,
414
            owner: this,
415
            displayText,
416
            cancel: false
417
        };
418
        this.selectionChanging.emit(args);
119✔
419
        if (!args.cancel) {
119✔
420
            this.selectionService.select_items(this.id, args.newValue, true);
117✔
421
            this._value = args.newValue;
117✔
422
            if (displayText !== args.displayText) {
117✔
423
                this._displayValue = this._displayText = args.displayText;
2✔
424
            } else {
425
                this._displayValue = this.createDisplayText(this.selection, args.oldSelection);
115✔
426
            }
427
            this._onChangeCallback(args.newValue);
117✔
428
        } else if (this.isRemote) {
2✔
429
            this.registerRemoteEntries(diffInSets(selection, currentSelection), false);
1✔
430
        }
431
    }
432

433
    protected createDisplayText(newSelection: any[], oldSelection: any[]) {
434
        const selection = this.valueKey ? newSelection.map(item => item[this.valueKey]) : newSelection;
6,843✔
435
        return this.isRemote
1,987✔
436
            ? this.getRemoteSelection(selection, oldSelection)
437
            : this.concatDisplayText(newSelection);
438
    }
439

440
    protected getSearchPlaceholderText(): string {
441
        return this.searchPlaceholder ||
7,661✔
442
            (this.disableFiltering ? this.resourceStrings.igx_combo_addCustomValues_placeholder : this.resourceStrings.igx_combo_filter_search_placeholder);
1,290✔
443
    }
444

445
    /** Returns a string that should be populated in the combo's text box */
446
    private concatDisplayText(selection: any[]): string {
447
        const value = this.displayKey !== null && this.displayKey !== undefined ?
1,946✔
448
            selection.map(entry => entry[this.displayKey]).join(', ') :
6,847✔
449
            selection.join(', ');
450
        return value;
1,946✔
451
    }
452
}
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