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

IgniteUI / igniteui-angular / 23354069329

20 Mar 2026 05:11PM UTC coverage: 9.846% (-79.6%) from 89.462%
23354069329

Pull #17071

github

web-flow
Merge 742ad5a5c into 0018afe92
Pull Request #17071: fix(IgxGrid): Do not apply width constraint to groups.

905 of 16404 branches covered (5.52%)

Branch coverage included in aggregate %.

1 of 3 new or added lines in 1 file covered. (33.33%)

24265 existing lines in 328 files now uncovered.

3714 of 30509 relevant lines covered (12.17%)

6.21 hits per line

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

1.14
/projects/igniteui-angular/src/lib/combo/combo.component.ts
1
import { NgClass, NgTemplateOutlet } from '@angular/common';
2
import {
3
    AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnInit, OnDestroy, DOCUMENT,
4
    Optional, Inject, Injector, ViewChild, Input, Output, EventEmitter, HostListener, DoCheck, booleanAttribute,
5
} from '@angular/core';
6

7
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
8

9
import { IgxSelectionAPIService } from '../core/selection';
10
import { IBaseEventArgs, IBaseCancelableEventArgs, CancelableEventArgs } from '../core/utils';
11
import { IgxForOfDirective } from '../directives/for-of/for_of.directive';
12
import { IgxIconService } from '../icon/icon.service';
13
import { IgxRippleDirective } from '../directives/ripple/ripple.directive';
14
import { IgxButtonDirective } from '../directives/button/button.directive';
15
import { IgxInputGroupComponent } from '../input-group/input-group.component';
16
import { IgxComboItemComponent } from './combo-item.component';
17
import { IgxComboDropDownComponent } from './combo-dropdown.component';
18
import { IgxComboFilteringPipe, IgxComboGroupingPipe } from './combo.pipes';
19
import { IGX_COMBO_COMPONENT, IgxComboBaseDirective } from './combo.common';
20
import { IgxComboAddItemComponent } from './combo-add-item.component';
21
import { IgxComboAPIService } from './combo.api';
22
import { EditorProvider } from '../core/edit-provider';
23
import { IgxInputGroupType, IGX_INPUT_GROUP_TYPE } from '../input-group/public_api';
24
import { IgxDropDownItemNavigationDirective } from '../drop-down/drop-down-navigation.directive';
25
import { IgxIconComponent } from '../icon/icon.component';
26
import { IgxSuffixDirective } from '../directives/suffix/suffix.directive';
27
import { IgxInputDirective } from '../directives/input/input.directive';
28
import { IgxReadOnlyInputDirective } from '../directives/input/read-only-input.directive';
29

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

50
/** Event emitted when the igx-combo's search input changes */
51
export interface IComboSearchInputEventArgs extends IBaseCancelableEventArgs {
52
    /** The text that has been typed into the search input */
53
    searchText: string;
54
}
55

56
export interface IComboItemAdditionEvent extends IBaseEventArgs, CancelableEventArgs {
57
    oldCollection: any[];
58
    addedItem: any;
59
    newCollection: any[];
60
}
61

62
/**
63
 * When called with sets A & B, returns A - B (as array);
64
 *
65
 * @hidden
66
 */
67
const diffInSets = (set1: Set<any>, set2: Set<any>): any[] => {
3✔
UNCOV
68
    const results = [];
×
UNCOV
69
    set1.forEach(entry => {
×
UNCOV
70
        if (!set2.has(entry)) {
×
UNCOV
71
            results.push(entry);
×
72
        }
73
    });
UNCOV
74
    return results;
×
75
};
76

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

133

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

152
    /**
153
     * Emitted when item selection is changing, before the selection completes
154
     *
155
     * ```html
156
     * <igx-combo (selectionChanging)='handleSelection()'></igx-combo>
157
     * ```
158
     */
159
    @Output()
UNCOV
160
    public selectionChanging = new EventEmitter<IComboSelectionChangingEventArgs>();
×
161

162
    /** @hidden @internal */
163
    @ViewChild(IgxComboDropDownComponent, { static: true })
164
    public dropdown: IgxComboDropDownComponent;
165

166
    /** @hidden @internal */
167
    public get filteredData(): any[] | null {
UNCOV
168
        return this.disableFiltering ? this.data : this._filteredData;
×
169
    }
170
    /** @hidden @internal */
171
    public set filteredData(val: any[] | null) {
UNCOV
172
        this._filteredData = this.groupKey ? (val || []).filter((e) => e.isHeader !== true) : val;
×
UNCOV
173
        this.checkMatch();
×
174
    }
175

UNCOV
176
    protected _prevInputValue = '';
×
177

178
    private _displayText: string;
179

180
    constructor(
181
        elementRef: ElementRef,
182
        cdr: ChangeDetectorRef,
183
        selectionService: IgxSelectionAPIService,
184
        comboAPI: IgxComboAPIService,
185
        @Inject(DOCUMENT) document: any,
186
        @Optional() @Inject(IGX_INPUT_GROUP_TYPE) _inputGroupType: IgxInputGroupType,
187
        @Optional() _injector: Injector,
188
        @Optional() @Inject(IgxIconService) _iconService?: IgxIconService,
189
    ) {
UNCOV
190
        super(elementRef, cdr, selectionService, comboAPI, document, _inputGroupType, _injector, _iconService);
×
UNCOV
191
        this.comboAPI.register(this);
×
192
    }
193

194
    @HostListener('keydown.ArrowDown', ['$event'])
195
    @HostListener('keydown.Alt.ArrowDown', ['$event'])
196
    public onArrowDown(event: Event) {
UNCOV
197
        event.preventDefault();
×
UNCOV
198
        event.stopPropagation();
×
UNCOV
199
        this.open();
×
200
    }
201

202
    /** @hidden @internal */
203
    public get displaySearchInput(): boolean {
UNCOV
204
        return !this.disableFiltering || this.allowCustomValues;
×
205
    }
206

207
    /**
208
     * @hidden @internal
209
     */
210
    public handleKeyUp(event: KeyboardEvent): void {
211
        // TODO: use PlatformUtil for keyboard navigation
UNCOV
212
        if (event.key === 'ArrowDown' || event.key === 'Down') {
×
UNCOV
213
            this.dropdown.focusedItem = this.dropdown.items[0];
×
UNCOV
214
            this.dropdownContainer.nativeElement.focus();
×
UNCOV
215
        } else if (event.key === 'Escape' || event.key === 'Esc') {
×
UNCOV
216
            this.toggle();
×
217
        }
218
    }
219

220
    /**
221
     * @hidden @internal
222
     */
223
    public handleSelectAll(evt) {
UNCOV
224
        if (evt.checked) {
×
UNCOV
225
            this.selectAllItems();
×
226
        } else {
UNCOV
227
            this.deselectAllItems();
×
228
        }
229
    }
230

231
    /**
232
     * @hidden @internal
233
     */
234
    public writeValue(value: any[]): void {
UNCOV
235
        const selection = Array.isArray(value) ? value.filter(x => x !== undefined) : [];
×
UNCOV
236
        const oldSelection = this.selection;
×
UNCOV
237
        this.selectionService.select_items(this.id, selection, true);
×
UNCOV
238
        this.cdr.markForCheck();
×
UNCOV
239
        this._displayValue = this.createDisplayText(this.selection, oldSelection);
×
UNCOV
240
        this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
×
241
    }
242

243
    /** @hidden @internal */
244
    public ngDoCheck(): void {
UNCOV
245
        if (this.data?.length && this.selection.length) {
×
UNCOV
246
            this._displayValue = this._displayText || this.createDisplayText(this.selection, []);
×
UNCOV
247
            this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
×
248
        }
249
    }
250

251
    /**
252
     * @hidden
253
     */
254
    public getEditElement(): HTMLElement {
UNCOV
255
        return this.comboInput.nativeElement;
×
256
    }
257

258
    /**
259
     * @hidden @internal
260
     */
261
    public get context(): any {
262
        return {
×
263
            $implicit: this
264
        };
265
    }
266

267
    /**
268
     * @hidden @internal
269
     */
270
    public clearInput(event: Event): void {
UNCOV
271
        this.deselectAllItems(true, event);
×
UNCOV
272
        if (this.collapsed) {
×
UNCOV
273
            this.getEditElement().focus();
×
274
        } else {
UNCOV
275
            this.focusSearchInput(true);
×
276
        }
UNCOV
277
        event.stopPropagation();
×
278
    }
279

280
    /**
281
     * @hidden @internal
282
     */
283
    public handleClearItems(event: Event): void {
UNCOV
284
        if (this.disabled) {
×
UNCOV
285
            return;
×
286
        }
UNCOV
287
        this.clearInput(event);
×
288
    }
289

290
    /**
291
     * @hidden @internal
292
     */
293
    public handleClearKeyDown(eventArgs: KeyboardEvent) {
UNCOV
294
        if (eventArgs.key === 'Enter' || eventArgs.key === ' ') {
×
UNCOV
295
            eventArgs.preventDefault();
×
UNCOV
296
            this.clearInput(eventArgs);
×
297
        }
298
    }
299

300
    /**
301
     * Select defined items
302
     *
303
     * @param newItems new items to be selected
304
     * @param clearCurrentSelection if true clear previous selected items
305
     * ```typescript
306
     * this.combo.select(["New York", "New Jersey"]);
307
     * ```
308
     */
309
    public select(newItems: Array<any>, clearCurrentSelection?: boolean, event?: Event) {
UNCOV
310
        if (newItems) {
×
UNCOV
311
            const newSelection = this.selectionService.add_items(this.id, newItems, clearCurrentSelection);
×
UNCOV
312
            this.setSelection(newSelection, event);
×
313
        }
314
    }
315

316
    /**
317
     * Deselect defined items
318
     *
319
     * @param items items to deselected
320
     * ```typescript
321
     * this.combo.deselect(["New York", "New Jersey"]);
322
     * ```
323
     */
324
    public deselect(items: Array<any>, event?: Event) {
UNCOV
325
        if (items) {
×
UNCOV
326
            const newSelection = this.selectionService.delete_items(this.id, items);
×
UNCOV
327
            this.setSelection(newSelection, event);
×
328
        }
329
    }
330

331
    /**
332
     * Select all (filtered) items
333
     *
334
     * @param ignoreFilter if set to true, selects all items, otherwise selects only the filtered ones.
335
     * ```typescript
336
     * this.combo.selectAllItems();
337
     * ```
338
     */
339
    public selectAllItems(ignoreFilter?: boolean, event?: Event) {
UNCOV
340
        const allVisible = this.selectionService.get_all_ids(ignoreFilter ? this.data : this.filteredData, this.valueKey);
×
UNCOV
341
        const newSelection = this.selectionService.add_items(this.id, allVisible);
×
UNCOV
342
        this.setSelection(newSelection, event);
×
343
    }
344

345
    /**
346
     * Deselect all (filtered) items
347
     *
348
     * @param ignoreFilter if set to true, deselects all items, otherwise deselects only the filtered ones.
349
     * ```typescript
350
     * this.combo.deselectAllItems();
351
     * ```
352
     */
353
    public deselectAllItems(ignoreFilter?: boolean, event?: Event): void {
UNCOV
354
        let newSelection = this.selectionService.get_empty();
×
UNCOV
355
        if (this.filteredData.length !== this.data.length && !ignoreFilter) {
×
UNCOV
356
            newSelection = this.selectionService.delete_items(this.id, this.selectionService.get_all_ids(this.filteredData, this.valueKey));
×
357
        }
UNCOV
358
        this.setSelection(newSelection, event);
×
359
    }
360

361
    /**
362
     * Selects/Deselects a single item
363
     *
364
     * @param itemID the itemID of the specific item
365
     * @param select If the item should be selected (true) or deselected (false)
366
     *
367
     * Without specified valueKey;
368
     * ```typescript
369
     * this.combo.valueKey = null;
370
     * const items: { field: string, region: string}[] = data;
371
     * this.combo.setSelectedItem(items[0], true);
372
     * ```
373
     * With specified valueKey;
374
     * ```typescript
375
     * this.combo.valueKey = 'field';
376
     * const items: { field: string, region: string}[] = data;
377
     * this.combo.setSelectedItem('Connecticut', true);
378
     * ```
379
     */
380
    public setSelectedItem(itemID: any, select = true, event?: Event): void {
×
UNCOV
381
        if (itemID === undefined) {
×
382
            return;
×
383
        }
UNCOV
384
        if (select) {
×
UNCOV
385
            this.select([itemID], false, event);
×
386
        } else {
UNCOV
387
            this.deselect([itemID], event);
×
388
        }
389
    }
390

391
    /** @hidden @internal */
392
    public handleOpened() {
UNCOV
393
        this.triggerCheck();
×
394

395
        // Disabling focus of the search input should happen only when drop down opens.
396
        // During keyboard navigation input should receive focus, even the autoFocusSearch is disabled.
397
        // That is why in such cases focusing of the dropdownContainer happens outside focusSearchInput method.
UNCOV
398
        if (this.autoFocusSearch) {
×
UNCOV
399
            this.focusSearchInput(true);
×
400
        } else {
UNCOV
401
            this.dropdownContainer.nativeElement.focus();
×
402
        }
UNCOV
403
        this.opened.emit({ owner: this });
×
404
    }
405

406
    /** @hidden @internal */
407
    public focusSearchInput(opening?: boolean): void {
UNCOV
408
        if (this.displaySearchInput && this.searchInput) {
×
UNCOV
409
            this.searchInput.nativeElement.focus();
×
410
        } else {
UNCOV
411
            if (opening) {
×
UNCOV
412
                this.dropdownContainer.nativeElement.focus();
×
413
            } else {
414
                this.comboInput.nativeElement.focus();
×
415
                this.toggle();
×
416
            }
417
        }
418
    }
419

420
    protected setSelection(selection: Set<any>, event?: Event): void {
UNCOV
421
        const currentSelection = this.selectionService.get(this.id);
×
UNCOV
422
        const removed = this.convertKeysToItems(diffInSets(currentSelection, selection));
×
UNCOV
423
        const added = this.convertKeysToItems(diffInSets(selection, currentSelection));
×
UNCOV
424
        const newValue = Array.from(selection);
×
UNCOV
425
        const oldValue = Array.from(currentSelection || []);
×
UNCOV
426
        const newSelection = this.convertKeysToItems(newValue);
×
UNCOV
427
        const oldSelection = this.convertKeysToItems(oldValue);
×
UNCOV
428
        const displayText = this.createDisplayText(this.convertKeysToItems(newValue), oldValue);
×
UNCOV
429
        const args: IComboSelectionChangingEventArgs = {
×
430
            newValue,
431
            oldValue,
432
            newSelection,
433
            oldSelection,
434
            added,
435
            removed,
436
            event,
437
            owner: this,
438
            displayText,
439
            cancel: false
440
        };
UNCOV
441
        this.selectionChanging.emit(args);
×
UNCOV
442
        if (!args.cancel) {
×
UNCOV
443
            this.selectionService.select_items(this.id, args.newValue, true);
×
UNCOV
444
            this._value = args.newValue;
×
UNCOV
445
            if (displayText !== args.displayText) {
×
UNCOV
446
                this._displayValue = this._displayText = args.displayText;
×
447
            } else {
UNCOV
448
                this._displayValue = this.createDisplayText(this.selection, args.oldSelection);
×
449
            }
UNCOV
450
            this._onChangeCallback(args.newValue);
×
UNCOV
451
        } else if (this.isRemote) {
×
UNCOV
452
            this.registerRemoteEntries(diffInSets(selection, currentSelection), false);
×
453
        }
454
    }
455

456
    protected createDisplayText(newSelection: any[], oldSelection: any[]) {
UNCOV
457
        const selection = this.valueKey ? newSelection.map(item => item[this.valueKey]) : newSelection;
×
UNCOV
458
        return this.isRemote
×
459
            ? this.getRemoteSelection(selection, oldSelection)
460
            : this.concatDisplayText(newSelection);
461
    }
462

463
    protected getSearchPlaceholderText(): string {
UNCOV
464
        return this.searchPlaceholder ||
×
465
            (this.disableFiltering ? this.resourceStrings.igx_combo_addCustomValues_placeholder : this.resourceStrings.igx_combo_filter_search_placeholder);
×
466
    }
467

468
    /** Returns a string that should be populated in the combo's text box */
469
    private concatDisplayText(selection: any[]): string {
UNCOV
470
        const value = this.displayKey !== null && this.displayKey !== undefined ?
×
UNCOV
471
            selection.map(entry => entry[this.displayKey]).join(', ') :
×
472
            selection.join(', ');
UNCOV
473
        return value;
×
474
    }
475
}
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