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

IgniteUI / igniteui-angular / 16414944874

21 Jul 2025 10:48AM UTC coverage: 91.647% (+0.01%) from 91.637%
16414944874

push

github

web-flow
Merge pull request #15922 from IgniteUI/mkirkova/feat-combo-toggle-button-19.2.x

Make toggle button focusable and add aria label to the combo input - 19.2.x

13477 of 15775 branches covered (85.43%)

4 of 4 new or added lines in 1 file covered. (100.0%)

7 existing lines in 2 files now uncovered.

27111 of 29582 relevant lines covered (91.65%)

37034.14 hits per line

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

96.12
/projects/igniteui-angular/src/lib/combo/combo.component.ts
1
import { DOCUMENT, NgClass, NgTemplateOutlet } from '@angular/common';
2
import {
3
    AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnInit, OnDestroy,
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

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

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

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

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

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

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

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

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

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

173
    protected _prevInputValue = '';
293✔
174

175
    private _displayText: string;
176

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

191
    @HostListener('keydown.ArrowDown', ['$event'])
192
    @HostListener('keydown.Alt.ArrowDown', ['$event'])
193
    public onArrowDown(event: Event) {
194
        event.preventDefault();
2✔
195
        event.stopPropagation();
2✔
196
        this.open();
2✔
197
    }
198

199
    /** @hidden @internal */
200
    public get displaySearchInput(): boolean {
201
        return !this.disableFiltering || this.allowCustomValues;
7,650✔
202
    }
203

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

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

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

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

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

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

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

277
    /**
278
     * @hidden @internal
279
     */
280
    public handleClearItems(event: Event): void {
281
        if (this.disabled) {
9✔
282
            return;
1✔
283
        }
284
        this.clearInput(event);
8✔
285
    }
286

287
    /**
288
     * @hidden @internal
289
     */
290
    public handleClearKeyDown(eventArgs: KeyboardEvent) {
291
        if (eventArgs.key === 'Enter' || eventArgs.key === ' ') {
1!
292
            eventArgs.preventDefault();
1✔
293
            this.clearInput(eventArgs);
1✔
294
        }
295
    }
296

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

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

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

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

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

388
    /** @hidden @internal */
389
    public handleOpened() {
390
        this.triggerCheck();
52✔
391

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

403
    /** @hidden @internal */
404
    public focusSearchInput(opening?: boolean): void {
405
        if (this.displaySearchInput && this.searchInput) {
55✔
406
            this.searchInput.nativeElement.focus();
54✔
407
        } else {
408
            if (opening) {
1!
409
                this.dropdownContainer.nativeElement.focus();
1✔
410
            } else {
UNCOV
411
                this.comboInput.nativeElement.focus();
×
UNCOV
412
                this.toggle();
×
413
            }
414
        }
415
    }
416

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

453
    protected createDisplayText(newSelection: any[], oldSelection: any[]) {
454
        const selection = this.valueKey ? newSelection.map(item => item[this.valueKey]) : newSelection;
6,839✔
455
        return this.isRemote
1,969✔
456
            ? this.getRemoteSelection(selection, oldSelection)
457
            : this.concatDisplayText(newSelection);
458
    }
459

460
    protected getSearchPlaceholderText(): string {
461
        return this.searchPlaceholder ||
7,577✔
462
            (this.disableFiltering ? this.resourceStrings.igx_combo_addCustomValues_placeholder : this.resourceStrings.igx_combo_filter_search_placeholder);
1,228✔
463
    }
464

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