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

IgniteUI / igniteui-angular / 13561607909

27 Feb 2025 08:03AM UTC coverage: 91.644% (+0.003%) from 91.641%
13561607909

push

github

web-flow
fix(grid): Update grid cell active state selector specificity (#15402)

13328 of 15596 branches covered (85.46%)

26882 of 29333 relevant lines covered (91.64%)

33708.8 hits per line

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

96.08
/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[] => {
2✔
67
    const results = [];
221✔
68
    set1.forEach(entry => {
221✔
69
        if (!set2.has(entry)) {
615✔
70
            results.push(entry);
365✔
71
        }
72
    });
73
    return results;
221✔
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,
2✔
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;
269✔
130

131
    /**
132
     * Enables/disables filtering in the list. The default is `false`.
133
     */
134
    @Input({ transform: booleanAttribute })
135
    public get disableFiltering(): boolean {
136
        return this._disableFiltering || this.filteringOptions.filterable === false;
17,381✔
137
    }
138
    public set disableFiltering(value: boolean) {
139
        this._disableFiltering = value;
101✔
140
    }
141

142
    /**
143
     * Defines the placeholder value for the combo dropdown search field
144
     *
145
     * @deprecated in version 18.2.0. Replaced with values in the localization resource strings.
146
     *
147
     * ```typescript
148
     * // get
149
     * let myComboSearchPlaceholder = this.combo.searchPlaceholder;
150
     * ```
151
     *
152
     * ```html
153
     * <!--set-->
154
     * <igx-combo [searchPlaceholder]='newPlaceHolder'></igx-combo>
155
     * ```
156
     */
157
    @Input()
158
    public searchPlaceholder: string;
159

160
    /**
161
     * Emitted when item selection is changing, before the selection completes
162
     *
163
     * ```html
164
     * <igx-combo (selectionChanging)='handleSelection()'></igx-combo>
165
     * ```
166
     */
167
    @Output()
168
    public selectionChanging = new EventEmitter<IComboSelectionChangingEventArgs>();
269✔
169

170
    /** @hidden @internal */
171
    @ViewChild(IgxComboDropDownComponent, { static: true })
172
    public dropdown: IgxComboDropDownComponent;
173

174
    /** @hidden @internal */
175
    public get filteredData(): any[] | null {
176
        return this.disableFiltering ? this.data : this._filteredData;
6,344✔
177
    }
178
    /** @hidden @internal */
179
    public set filteredData(val: any[] | null) {
180
        this._filteredData = this.groupKey ? (val || []).filter((e) => e.isHeader !== true) : val;
9,443!
181
        this.checkMatch();
697✔
182
    }
183

184
    protected _prevInputValue = '';
269✔
185

186
    private _displayText: string;
187
    private _disableFiltering = false;
269✔
188

189
    constructor(
190
        elementRef: ElementRef,
191
        cdr: ChangeDetectorRef,
192
        selectionService: IgxSelectionAPIService,
193
        comboAPI: IgxComboAPIService,
194
        @Inject(DOCUMENT) document: any,
195
        @Optional() @Inject(IGX_INPUT_GROUP_TYPE) _inputGroupType: IgxInputGroupType,
196
        @Optional() _injector: Injector,
197
        @Optional() @Inject(IgxIconService) _iconService?: IgxIconService,
198
    ) {
199
        super(elementRef, cdr, selectionService, comboAPI, document, _inputGroupType, _injector, _iconService);
269✔
200
        this.comboAPI.register(this);
269✔
201
    }
202

203
    @HostListener('keydown.ArrowDown', ['$event'])
204
    @HostListener('keydown.Alt.ArrowDown', ['$event'])
205
    public onArrowDown(event: Event) {
206
        event.preventDefault();
2✔
207
        event.stopPropagation();
2✔
208
        this.open();
2✔
209
    }
210

211
    /** @hidden @internal */
212
    public get displaySearchInput(): boolean {
213
        return !this.disableFiltering || this.allowCustomValues;
4,940✔
214
    }
215

216
    /**
217
     * @hidden @internal
218
     */
219
    public handleKeyUp(event: KeyboardEvent): void {
220
        // TODO: use PlatformUtil for keyboard navigation
221
        if (event.key === 'ArrowDown' || event.key === 'Down') {
12✔
222
            this.dropdown.focusedItem = this.dropdown.items[0];
6✔
223
            this.dropdownContainer.nativeElement.focus();
6✔
224
        } else if (event.key === 'Escape' || event.key === 'Esc') {
6✔
225
            this.toggle();
2✔
226
        }
227
    }
228

229
    /**
230
     * @hidden @internal
231
     */
232
    public handleSelectAll(evt) {
233
        if (evt.checked) {
2✔
234
            this.selectAllItems();
1✔
235
        } else {
236
            this.deselectAllItems();
1✔
237
        }
238
    }
239

240
    /**
241
     * @hidden @internal
242
     */
243
    public writeValue(value: any[]): void {
244
        const selection = Array.isArray(value) ? value.filter(x => x !== undefined) : [];
811✔
245
        const oldSelection = this.selection;
491✔
246
        this.selectionService.select_items(this.id, selection, true);
461✔
247
        this.cdr.markForCheck();
461✔
248
        this._displayValue = this.createDisplayText(this.selection, oldSelection);
461✔
249
        this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
686✔
250
    }
251

252
    /** @hidden @internal */
253
    public ngDoCheck(): void {
254
        if (this.data?.length && this.selection.length) {
2,389✔
255
            this._displayValue = this._displayText || this.createDisplayText(this.selection, []);
934✔
256
            this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
3,731✔
257
        }
258
    }
259

260
    /**
261
     * @hidden
262
     */
263
    public getEditElement(): HTMLElement {
264
        return this.comboInput.nativeElement;
4✔
265
    }
266

267
    /**
268
     * @hidden @internal
269
     */
270
    public get context(): any {
271
        return {
×
272
            $implicit: this
273
        };
274
    }
275

276
    /**
277
     * @hidden @internal
278
     */
279
    public handleClearItems(event: Event): void {
280
        if (this.disabled) {
9✔
281
            return;
1✔
282
        }
283
        this.deselectAllItems(true, event);
8✔
284
        if (this.collapsed) {
8✔
285
            this.getEditElement().focus();
4✔
286
        } else {
287
            this.focusSearchInput(true);
4✔
288
        }
289
        event.stopPropagation();
8✔
290
    }
291

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

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

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

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

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

383
    /** @hidden @internal */
384
    public handleOpened() {
385
        this.triggerCheck();
48✔
386

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

398
    /** @hidden @internal */
399
    public focusSearchInput(opening?: boolean): void {
400
        if (this.displaySearchInput && this.searchInput) {
50✔
401
            this.searchInput.nativeElement.focus();
49✔
402
        } else {
403
            if (opening) {
1!
404
                this.dropdownContainer.nativeElement.focus();
1✔
405
            } else {
406
                this.comboInput.nativeElement.focus();
×
407
                this.toggle();
×
408
            }
409
        }
410
    }
411

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

448
    protected createDisplayText(newSelection: any[], oldSelection: any[]) {
449
        const selection = this.valueKey ? newSelection.map(item => item[this.valueKey]) : newSelection;
4,965✔
450
        return this.isRemote
1,610✔
451
            ? this.getRemoteSelection(selection, oldSelection)
452
            : this.concatDisplayText(newSelection);
453
    }
454

455
    protected getSearchPlaceholderText(): string {
456
        return this.searchPlaceholder ||
4,872✔
457
            (this.disableFiltering ? this.resourceStrings.igx_combo_addCustomValues_placeholder : this.resourceStrings.igx_combo_filter_search_placeholder);
1,207✔
458
    }
459

460
    /** Returns a string that should be populated in the combo's text box */
461
    private concatDisplayText(selection: any[]): string {
462
        const value = this.displayKey !== null && this.displayKey !== undefined ?
1,569✔
463
            selection.map(entry => entry[this.displayKey]).join(', ') :
4,963✔
464
            selection.join(', ');
465
        return value;
1,569✔
466
    }
467
}
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