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

IgniteUI / igniteui-angular / 11400942147

18 Oct 2024 09:14AM UTC coverage: 91.606% (-0.003%) from 91.609%
11400942147

push

github

web-flow
Merge pull request #14886 from IgniteUI/mkirkova/fix-14816

Make scroll buttons visual-only elements

12916 of 15130 branches covered (85.37%)

26212 of 28614 relevant lines covered (91.61%)

33826.23 hits per line

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

96.19
/projects/igniteui-angular/src/lib/combo/combo.component.ts
1
import { DOCUMENT, NgClass, NgIf, 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 { IgxStringFilteringOperand, IgxBooleanFilteringOperand } from '../data-operations/filtering-condition';
12
import { FilteringLogic } from '../data-operations/filtering-expression.interface';
13
import { IgxForOfDirective } from '../directives/for-of/for_of.directive';
14
import { IgxIconService } from '../icon/icon.service';
15
import { IgxRippleDirective } from '../directives/ripple/ripple.directive';
16
import { IgxButtonDirective } from '../directives/button/button.directive';
17
import { IgxInputGroupComponent } from '../input-group/input-group.component';
18
import { IgxComboItemComponent } from './combo-item.component';
19
import { IgxComboDropDownComponent } from './combo-dropdown.component';
20
import { IgxComboFilteringPipe, IgxComboGroupingPipe } from './combo.pipes';
21
import { IGX_COMBO_COMPONENT, IgxComboBaseDirective } from './combo.common';
22
import { IgxComboAddItemComponent } from './combo-add-item.component';
23
import { IgxComboAPIService } from './combo.api';
24
import { EditorProvider } from '../core/edit-provider';
25
import { IgxInputGroupType, IGX_INPUT_GROUP_TYPE } from '../input-group/public_api';
26
import { IgxDropDownItemNavigationDirective } from '../drop-down/drop-down-navigation.directive';
27
import { IgxIconComponent } from '../icon/icon.component';
28
import { IgxSuffixDirective } from '../directives/suffix/suffix.directive';
29
import { IgxInputDirective } from '../directives/input/input.directive';
30

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

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

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

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

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

135
    /**
136
     * Enables/disables filtering in the list. The default is `false`.
137
     */
138
    @Input({ transform: booleanAttribute })
139
    public get disableFiltering(): boolean {
140
        return this._disableFiltering || this.filteringOptions.filterable === false;
5,281✔
141
    }
142
    public set disableFiltering(value: boolean) {
143
        this._disableFiltering = value;
101✔
144
    }
145

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

164
    /**
165
     * Emitted when item selection is changing, before the selection completes
166
     *
167
     * ```html
168
     * <igx-combo (selectionChanging)='handleSelection()'></igx-combo>
169
     * ```
170
     */
171
    @Output()
172
    public selectionChanging = new EventEmitter<IComboSelectionChangingEventArgs>();
130✔
173

174
    /** @hidden @internal */
175
    @ViewChild(IgxComboDropDownComponent, { static: true })
176
    public dropdown: IgxComboDropDownComponent;
177

178
    /** @hidden @internal */
179
    public get filteredData(): any[] | null {
180
        return this.disableFiltering ? this.data : this._filteredData;
1,653✔
181
    }
182
    /** @hidden @internal */
183
    public set filteredData(val: any[] | null) {
184
        this._filteredData = this.groupKey ? (val || []).filter((e) => e.isHeader !== true) : val;
9,438!
185
        this.checkMatch();
301✔
186
    }
187

188
    /**
189
     * @hidden @internal
190
     */
191
    public filteringLogic = FilteringLogic.Or;
130✔
192

193
    protected stringFilters = IgxStringFilteringOperand;
130✔
194
    protected booleanFilters = IgxBooleanFilteringOperand;
130✔
195
    protected _prevInputValue = '';
130✔
196

197
    private _displayText: string;
198
    private _disableFiltering = false;
130✔
199

200
    constructor(
201
        elementRef: ElementRef,
202
        cdr: ChangeDetectorRef,
203
        selectionService: IgxSelectionAPIService,
204
        comboAPI: IgxComboAPIService,
205
        @Inject(DOCUMENT) document: any,
206
        @Optional() @Inject(IGX_INPUT_GROUP_TYPE) _inputGroupType: IgxInputGroupType,
207
        @Optional() _injector: Injector,
208
        @Optional() @Inject(IgxIconService) _iconService?: IgxIconService,
209
    ) {
210
        super(elementRef, cdr, selectionService, comboAPI, document, _inputGroupType, _injector, _iconService);
130✔
211
        this.comboAPI.register(this);
130✔
212
    }
213

214
    @HostListener('keydown.ArrowDown', ['$event'])
215
    @HostListener('keydown.Alt.ArrowDown', ['$event'])
216
    public onArrowDown(event: Event) {
217
        event.preventDefault();
2✔
218
        event.stopPropagation();
2✔
219
        this.open();
2✔
220
    }
221

222
    /** @hidden @internal */
223
    public get displaySearchInput(): boolean {
224
        return !this.disableFiltering || this.allowCustomValues;
1,246✔
225
    }
226

227
    /**
228
     * @hidden @internal
229
     */
230
    public handleKeyUp(event: KeyboardEvent): void {
231
        // TODO: use PlatformUtil for keyboard navigation
232
        if (event.key === 'ArrowDown' || event.key === 'Down') {
12✔
233
            this.dropdown.focusedItem = this.dropdown.items[0];
6✔
234
            this.dropdownContainer.nativeElement.focus();
6✔
235
        } else if (event.key === 'Escape' || event.key === 'Esc') {
6✔
236
            this.toggle();
2✔
237
        }
238
    }
239

240
    /**
241
     * @hidden @internal
242
     */
243
    public handleSelectAll(evt) {
244
        if (evt.checked) {
2✔
245
            this.selectAllItems();
1✔
246
        } else {
247
            this.deselectAllItems();
1✔
248
        }
249
    }
250

251
    /**
252
     * @hidden @internal
253
     */
254
    public writeValue(value: any[]): void {
255
        const selection = Array.isArray(value) ? value.filter(x => x !== undefined) : [];
50✔
256
        const oldSelection = this.selection;
50✔
257
        this.selectionService.select_items(this.id, selection, true);
47✔
258
        this.cdr.markForCheck();
47✔
259
        this._displayValue = this.createDisplayText(this.selection, oldSelection);
47✔
260
        this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
47✔
261
    }
262

263
    /** @hidden @internal */
264
    public ngDoCheck(): void {
265
        if (this.data?.length && this.selection.length) {
468✔
266
            this._displayValue = this._displayText || this.createDisplayText(this.selection, []);
104✔
267
            this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
303✔
268
        }
269
    }
270

271
    /**
272
     * @hidden
273
     */
274
    public getEditElement(): HTMLElement {
275
        return this.comboInput.nativeElement;
4✔
276
    }
277

278
    /**
279
     * @hidden @internal
280
     */
281
    public get context(): any {
282
        return {
×
283
            $implicit: this
284
        };
285
    }
286

287
    /**
288
     * @hidden @internal
289
     */
290
    public handleClearItems(event: Event): void {
291
        if (this.disabled) {
9✔
292
            return;
1✔
293
        }
294
        this.deselectAllItems(true, event);
8✔
295
        if (this.collapsed) {
8✔
296
            this.getEditElement().focus();
4✔
297
        } else {
298
            this.focusSearchInput(true);
4✔
299
        }
300
        event.stopPropagation();
8✔
301
    }
302

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

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

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

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

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

394
    /** @hidden @internal */
395
    public handleOpened() {
396
        this.triggerCheck();
43✔
397

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

409
    /** @hidden @internal */
410
    public focusSearchInput(opening?: boolean): void {
411
        if (this.displaySearchInput && this.searchInput) {
45✔
412
            this.searchInput.nativeElement.focus();
44✔
413
        } else {
414
            if (opening) {
1!
415
                this.dropdownContainer.nativeElement.focus();
1✔
416
            } else {
417
                this.comboInput.nativeElement.focus();
×
418
                this.toggle();
×
419
            }
420
        }
421
    }
422

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

459
    protected createDisplayText(newSelection: any[], oldSelection: any[]) {
460
        const selection = this.valueKey ? newSelection.map(item => item[this.valueKey]) : newSelection;
850✔
461
        return this.isRemote
360✔
462
            ? this.getRemoteSelection(selection, oldSelection)
463
            : this.concatDisplayText(newSelection);
464
    }
465

466
    protected getSearchPlaceholderText(): string {
467
        return this.searchPlaceholder ||
1,183✔
468
            (this.disableFiltering ? this.resourceStrings.igx_combo_addCustomValues_placeholder : this.resourceStrings.igx_combo_filter_search_placeholder);
1,181✔
469
    }
470

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