• 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

92.9
/projects/igniteui-angular/src/lib/grids/filtering/base/grid-filtering-row.component.ts
1
import {
2
    AfterViewInit,
3
    ChangeDetectorRef,
4
    Component,
5
    Input,
6
    TemplateRef,
7
    ViewChild,
8
    ViewChildren,
9
    QueryList,
10
    ElementRef,
11
    HostBinding,
12
    ChangeDetectionStrategy,
13
    ViewRef,
14
    HostListener,
15
    OnDestroy
16
} from '@angular/core';
17
import { GridColumnDataType, DataUtil } from '../../../data-operations/data-util';
18
import { IgxDropDownComponent } from '../../../drop-down/drop-down.component';
19
import { IFilteringOperation } from '../../../data-operations/filtering-condition';
20
import { FilteringLogic, IFilteringExpression } from '../../../data-operations/filtering-expression.interface';
21
import { HorizontalAlignment, VerticalAlignment, OverlaySettings } from '../../../services/overlay/utilities';
22
import { ConnectedPositioningStrategy } from '../../../services/overlay/position/connected-positioning-strategy';
23
import { IgxDropDownItemComponent } from '../../../drop-down/drop-down-item.component';
24
import { ISelectionEventArgs } from '../../../drop-down/drop-down.common';
25
import { IgxFilteringService } from '../grid-filtering.service';
26
import { AbsoluteScrollStrategy } from '../../../services/overlay/scroll';
27
import { IgxDatePickerComponent } from '../../../date-picker/date-picker.component';
28
import { IgxTimePickerComponent } from '../../../time-picker/time-picker.component';
29
import { isEqual, PlatformUtil } from '../../../core/utils';
30
import { Subject } from 'rxjs';
31
import { takeUntil } from 'rxjs/operators';
32
import { ExpressionUI } from '../excel-style/common';
33
import { ColumnType } from '../../common/grid.interface';
34
import { IgxRippleDirective } from '../../../directives/ripple/ripple.directive';
35
import { IgxChipComponent, IBaseChipEventArgs } from '../../../chips/chip.component';
36
import { IgxChipsAreaComponent } from '../../../chips/chips-area.component';
37
import { IgxButtonDirective } from '../../../directives/button/button.directive';
38
import { IgxDateTimeEditorDirective } from '../../../directives/date-time-editor/date-time-editor.directive';
39
import { IgxPickerToggleComponent, IgxPickerClearComponent } from '../../../date-common/picker-icons.common';
40
import { IgxSuffixDirective } from '../../../directives/suffix/suffix.directive';
41
import { IgxInputDirective } from '../../../directives/input/input.directive';
42
import { IgxDropDownItemNavigationDirective } from '../../../drop-down/drop-down-navigation.directive';
43
import { IgxPrefixDirective } from '../../../directives/prefix/prefix.directive';
44
import { IgxInputGroupComponent } from '../../../input-group/input-group.component';
45
import { IgxIconComponent } from '../../../icon/icon.component';
46
import { NgTemplateOutlet, NgClass } from '@angular/common';
47
import { IgxIconButtonDirective } from '../../../directives/button/icon-button.directive';
48
import { Size } from '../../common/enums';
49

50
/**
51
 * @hidden
52
 */
53
@Component({
54
    changeDetection: ChangeDetectionStrategy.OnPush,
55
    selector: 'igx-grid-filtering-row',
56
    templateUrl: './grid-filtering-row.component.html',
57
    imports: [IgxDropDownComponent, IgxDropDownItemComponent, IgxChipsAreaComponent, IgxChipComponent, IgxIconComponent, IgxInputGroupComponent, IgxPrefixDirective, IgxDropDownItemNavigationDirective, IgxInputDirective, IgxSuffixDirective, IgxDatePickerComponent, IgxPickerToggleComponent, IgxPickerClearComponent, IgxTimePickerComponent, IgxDateTimeEditorDirective, NgTemplateOutlet, IgxButtonDirective, NgClass, IgxRippleDirective, IgxIconButtonDirective]
58
})
59
export class IgxGridFilteringRowComponent implements AfterViewInit, OnDestroy {
2✔
60
    @Input()
61
    public get column(): ColumnType {
62
        return this._column;
25,855✔
63
    }
64

65
    public set column(val) {
66
        if (this._column) {
107✔
67
            this.expressionsList.forEach(exp => exp.isSelected = false);
6✔
68
        }
69
        if (val) {
107✔
70
            this._column = val;
107✔
71

72
            this.expressionsList = this.filteringService.getExpressions(this._column.field);
107✔
73
            this.resetExpression();
107✔
74

75
            this.chipAreaScrollOffset = 0;
107✔
76
            this.transform(this.chipAreaScrollOffset);
107✔
77
        }
78
    }
79

80
    @Input()
81
    public get value(): any {
82
        return this._value;
2,007✔
83
    }
84

85
    public set value(val) {
86
        if (!val && val !== 0 && (this.expression.searchVal || this.expression.searchVal === 0)) {
73✔
87
            this.expression.searchVal = null;
15✔
88
            this._value = null;
15✔
89
            const index = this.expressionsList.findIndex(item => item.expression === this.expression);
15✔
90
            if (index === 0 && this.expressionsList.length === 1 && !this.expression.condition.isUnary) {
15✔
91
                this.filteringService.clearFilter(this.column.field);
14✔
92
            }
93
        } else {
94
            if (val === '') {
58!
95
                return;
×
96
            }
97
            const oldValue = this.expression.searchVal;
58✔
98
            if (isEqual(oldValue, val)) {
58✔
99
                return;
26✔
100
            }
101

102
            this._value = val;
32✔
103
            this.expression.searchVal = DataUtil.parseValue(this.column.dataType, val);
32✔
104
            if (this.expressionsList.find(item => item.expression === this.expression) === undefined) {
32✔
105
                this.addExpression(true);
27✔
106
            }
107
            this.filter();
32✔
108
        }
109
    }
110

111
    protected get filteringElementsSize(): Size {
112
        // needed because we want the size of the chips to be either Medium or Small
113
        return this.column.grid.gridSize === Size.Large ? Size.Medium : this.column.grid.gridSize;
3,192!
114
    }
115

116
    @HostBinding('class.igx-grid__filtering-row')
117
    public defaultCSSClass = true;
101✔
118

119
    @ViewChild('defaultFilterUI', { read: TemplateRef, static: true })
120
    protected defaultFilterUI: TemplateRef<any>;
121

122
    @ViewChild('defaultDateUI', { read: TemplateRef, static: true })
123
    protected defaultDateUI: TemplateRef<any>;
124

125
    @ViewChild('defaultTimeUI', { read: TemplateRef, static: true })
126
    protected defaultTimeUI: TemplateRef<any>;
127

128
    @ViewChild('defaultDateTimeUI', { read: TemplateRef, static: true })
129
    protected defaultDateTimeUI: TemplateRef<any>;
130

131
    @ViewChild('input', { read: ElementRef })
132
    protected input: ElementRef<HTMLInputElement>;
133

134
    @ViewChild('inputGroupConditions', { read: IgxDropDownComponent, static: true })
135
    protected dropDownConditions: IgxDropDownComponent;
136

137
    @ViewChild('chipsArea', { read: IgxChipsAreaComponent, static: true })
138
    protected chipsArea: IgxChipsAreaComponent;
139

140
    @ViewChildren('operators', { read: IgxDropDownComponent })
141
    protected dropDownOperators: QueryList<IgxDropDownComponent>;
142

143
    @ViewChild('inputGroup', { read: ElementRef })
144
    protected inputGroup: ElementRef<HTMLElement>;
145

146
    @ViewChild('picker')
147
    protected picker: IgxDatePickerComponent | IgxTimePickerComponent;
148

149
    @ViewChild('inputGroupPrefix', { read: ElementRef })
150
    protected inputGroupPrefix: ElementRef<HTMLElement>;
151

152
    @ViewChild('container', { static: true })
153
    protected container: ElementRef<HTMLElement>;
154

155
    @ViewChild('operand')
156
    protected operand: ElementRef<HTMLElement>;
157

158
    @ViewChild('closeButton', { static: true })
159
    protected closeButton: ElementRef<HTMLElement>;
160

161
    public get nativeElement() {
162
        return this.ref.nativeElement;
1,984✔
163
    }
164

165
    public showArrows: boolean;
166
    public expression: IFilteringExpression;
167
    public expressionsList: Array<ExpressionUI>;
168

169
    private _positionSettings = {
101✔
170
        horizontalStartPoint: HorizontalAlignment.Left,
171
        verticalStartPoint: VerticalAlignment.Bottom
172
    };
173

174
    private _conditionsOverlaySettings: OverlaySettings = {
101✔
175
        closeOnOutsideClick: true,
176
        modal: false,
177
        scrollStrategy: new AbsoluteScrollStrategy(),
178
        positionStrategy: new ConnectedPositioningStrategy(this._positionSettings)
179
    };
180

181
    private _operatorsOverlaySettings: OverlaySettings = {
101✔
182
        closeOnOutsideClick: true,
183
        modal: false,
184
        scrollStrategy: new AbsoluteScrollStrategy(),
185
        positionStrategy: new ConnectedPositioningStrategy(this._positionSettings)
186
    };
187

188
    private chipsAreaWidth: number;
189
    private chipAreaScrollOffset = 0;
101✔
190
    private _column = null;
101✔
191
    private isKeyPressed = false;
101✔
192
    private isComposing = false;
101✔
193
    private _cancelChipClick = false;
101✔
194
    private _value = null;
101✔
195

196
    /** switch to icon buttons when width is below 432px */
197
    private readonly NARROW_WIDTH_THRESHOLD = 432;
101✔
198

199
    private $destroyer = new Subject<void>();
101✔
200

201
    constructor(
202
        public filteringService: IgxFilteringService,
101✔
203
        public ref: ElementRef<HTMLElement>,
101✔
204
        public cdr: ChangeDetectorRef,
101✔
205
        protected platform: PlatformUtil,
101✔
206
    ) { }
207

208
    @HostListener('keydown', ['$event'])
209
    public onKeydownHandler(evt: KeyboardEvent) {
210
        if (this.platform.isFilteringKeyCombo(evt)) {
4✔
211
                evt.preventDefault();
2✔
212
                evt.stopPropagation();
2✔
213
                this.close();
2✔
214
        }
215
    }
216

217
    public ngAfterViewInit() {
218
        this._conditionsOverlaySettings.outlet = this.column.grid.outlet;
101✔
219
        this._operatorsOverlaySettings.outlet = this.column.grid.outlet;
101✔
220

221
        const selectedItem = this.expressionsList.find(expr => expr.isSelected === true);
101✔
222
        if (selectedItem) {
101✔
223
            this.expression = selectedItem.expression;
27✔
224
            this._value = this.expression.searchVal;
27✔
225
        }
226

227
        this.filteringService.grid.localeChange
101✔
228
        .pipe(takeUntil(this.$destroyer))
229
        .subscribe(() => {
230
            this.cdr.markForCheck();
×
231
        });
232

233
        requestAnimationFrame(() => this.focusEditElement());
101✔
234
    }
235

236
    public get disabled(): boolean {
237
        return !(this.column.filteringExpressionsTree && this.column.filteringExpressionsTree.filteringOperands.length > 0);
1,322✔
238
    }
239

240
    public get template(): TemplateRef<any> {
241
        if (this.column.dataType === GridColumnDataType.Date) {
661✔
242
            return this.defaultDateUI;
82✔
243
        }
244
        if (this.column.dataType === GridColumnDataType.Time) {
579✔
245
            return this.defaultTimeUI;
5✔
246
        }
247
        if (this.column.dataType === GridColumnDataType.DateTime) {
574✔
248
            return this.defaultDateTimeUI;
28✔
249
        }
250
        return this.defaultFilterUI;
546✔
251
    }
252

253
    public get type() {
254
        switch (this.column.dataType) {
546✔
255
            case GridColumnDataType.String:
256
            case GridColumnDataType.Boolean:
257
                return 'text';
458✔
258
            case GridColumnDataType.Number:
259
            case GridColumnDataType.Currency:
260
                return 'number';
88✔
261
        }
262
    }
263

264
    public get conditions(): any {
265
        return this.column.filters.conditionList();
821✔
266
    }
267

268
    public get isUnaryCondition(): boolean {
269
        if (this.expression.condition) {
574✔
270
            return this.expression.condition.isUnary;
553✔
271
        } else {
272
            return true;
21✔
273
        }
274
    }
275

276
    public get placeholder(): string {
277
        if (this.expression.condition && this.expression.condition.isUnary) {
661✔
278
            return this.filteringService.getChipLabel(this.expression);
88✔
279
        } else if (this.column.dataType === GridColumnDataType.Date) {
573✔
280
            return this.filteringService.grid.resourceStrings.igx_grid_filter_row_date_placeholder;
51✔
281
        } else if (this.column.dataType === GridColumnDataType.Boolean) {
522✔
282
            return this.filteringService.grid.resourceStrings.igx_grid_filter_row_boolean_placeholder;
21✔
283
        } else {
284
            return this.filteringService.grid.resourceStrings.igx_grid_filter_row_placeholder;
501✔
285
        }
286
    }
287

288
    /**
289
     * Event handler for keydown on the input group's prefix.
290
     */
291
    public onPrefixKeyDown(event: KeyboardEvent) {
292
        if (this.platform.isActivationKey(event) && this.dropDownConditions.collapsed) {
4!
293
            this.toggleConditionsDropDown(this.inputGroupPrefix.nativeElement);
×
294
            event.stopImmediatePropagation();
×
295
        } else if (event.key === this.platform.KEYMAP.TAB && !this.dropDownConditions.collapsed) {
4✔
296
            this.toggleConditionsDropDown(this.inputGroupPrefix.nativeElement);
2✔
297
        }
298
    }
299

300
    /**
301
     * Event handler for keydown on the input.
302
     */
303
    public onInputKeyDown(event: KeyboardEvent) {
304
        this.isKeyPressed = true;
50✔
305
        event.stopPropagation();
50✔
306
        if (this.column.dataType === GridColumnDataType.Boolean) {
50✔
307
            if (this.platform.isActivationKey(event)) {
2✔
308
                this.inputGroupPrefix.nativeElement.focus();
2✔
309
                this.toggleConditionsDropDown(this.inputGroupPrefix.nativeElement);
2✔
310
                return;
2✔
311
            }
312
        }
313
        if (event.key === this.platform.KEYMAP.ENTER) {
48✔
314
            if (this.isComposing) {
12!
315
                return;
×
316
            }
317
            this.commitInput();
12✔
318
        } else if (event.altKey && (event.key === this.platform.KEYMAP.ARROW_DOWN)) {
36✔
319
            this.inputGroupPrefix.nativeElement.focus();
1✔
320
            this.toggleConditionsDropDown(this.inputGroupPrefix.nativeElement);
1✔
321
        } else if (this.platform.isFilteringKeyCombo(event)) {
35✔
322
            event.preventDefault();
1✔
323
            this.close();
1✔
324
        }
325
    }
326

327
    /**
328
     * Event handler for keyup on the input.
329
     */
330
    public onInputKeyUp() {
331
        this.isKeyPressed = false;
33✔
332
    }
333

334
    /**
335
     * Event handler for input on the input.
336
     */
337
    public onInput(eventArgs) {
338
        if (!eventArgs) {
33✔
339
            return;
1✔
340
        }
341

342
        // The 'iskeyPressed' flag is needed for a case in IE, because the input event is fired on focus and for some reason,
343
        // when you have a japanese character as a placeholder, on init the value here is empty string .
344
        const target = eventArgs.target;
32✔
345
        if (this.column.dataType === GridColumnDataType.DateTime) {
32!
346
            this.value = eventArgs;
×
347
            return;
×
348
        }
349
        if (this.platform.isEdge && target.type !== 'number'
32!
350
            || this.isKeyPressed || target.value || target.checkValidity()) {
351
            this.value = target.value;
32✔
352
        }
353
    }
354

355
    /**
356
     * Event handler for compositionstart on the input.
357
     */
358
    public onCompositionStart() {
359
        this.isComposing = true;
×
360
    }
361

362
    /**
363
     * Event handler for compositionend on the input.
364
     */
365
    public onCompositionEnd() {
366
        this.isComposing = false;
×
367
    }
368

369
    /**
370
     * Event handler for input click event.
371
     */
372
    public onInputClick() {
373
        if (this.column.dataType === GridColumnDataType.Boolean && this.dropDownConditions.collapsed) {
1✔
374
            this.inputGroupPrefix.nativeElement.focus();
1✔
375
            this.toggleConditionsDropDown(this.inputGroupPrefix.nativeElement);
1✔
376
        }
377
    }
378

379
    /**
380
     * Returns the filtering operation condition for a given value.
381
     */
382
    public getCondition(value: string): IFilteringOperation {
383
        return this.column.filters.condition(value);
14,377✔
384
    }
385

386
    /**
387
     * Returns the translated condition name for a given value.
388
     */
389
    public translateCondition(value: string): string {
390
        return this.filteringService.grid.resourceStrings[`igx_grid_filter_${this.getCondition(value).name}`] || value;
7,087✔
391
    }
392

393
    /**
394
     * Returns the icon name of the current condition.
395
     */
396
    public getIconName(): string {
397
        if (this.column.dataType === GridColumnDataType.Boolean && this.expression.condition === null) {
574✔
398
            return this.getCondition(this.conditions[0]).iconName;
21✔
399
        } else {
400
            return this.expression.condition.iconName;
553✔
401
        }
402
    }
403

404
    /**
405
     * Returns whether a given condition is selected in dropdown.
406
     */
407
    public isConditionSelected(conditionName: string): boolean {
408
        if (this.expression.condition) {
7,087✔
409
            return this.expression.condition.name === conditionName;
6,940✔
410
        } else {
411
            return false;
147✔
412
        }
413
    }
414

415
    /**
416
     * Clears the current filtering.
417
     */
418
    public clearFiltering() {
419
        this.filteringService.clearFilter(this.column.field);
4✔
420
        this.resetExpression();
4✔
421
        if (this.input) {
4✔
422
            this.input.nativeElement.focus();
4✔
423
        }
424
        this.cdr.detectChanges();
4✔
425

426
        this.chipAreaScrollOffset = 0;
4✔
427
        this.transform(this.chipAreaScrollOffset);
4✔
428
    }
429

430
    /**
431
     * Commits the value of the input.
432
     */
433
    public commitInput() {
434
        const selectedItem = this.expressionsList.filter(ex => ex.isSelected === true);
35✔
435
        selectedItem.forEach(e => e.isSelected = false);
33✔
436

437
        let indexToDeselect = -1;
33✔
438
        for (let index = 0; index < this.expressionsList.length; index++) {
33✔
439
            const expression = this.expressionsList[index].expression;
35✔
440
            if (expression.searchVal === null && !expression.condition.isUnary) {
35✔
441
                indexToDeselect = index;
1✔
442
            }
443
        }
444
        if (indexToDeselect !== -1) {
33✔
445
            this.removeExpression(indexToDeselect, this.expression);
1✔
446
        }
447
        this.resetExpression();
33✔
448
        this._value = this.expression.searchVal;
33✔
449
        this.scrollChipsWhenAddingExpression();
33✔
450
    }
451

452
    /**
453
     * Clears the value of the input.
454
     */
455
    public clearInput(event?: MouseEvent) {
456
        event?.stopPropagation();
15✔
457
        this.value = null;
15✔
458
    }
459

460
    /**
461
     * Event handler for keydown on clear button.
462
     */
463
    public onClearKeyDown(eventArgs: KeyboardEvent) {
464
        if (this.platform.isActivationKey(eventArgs)) {
1✔
465
            eventArgs.preventDefault();
1✔
466
            this.clearInput();
1✔
467
            this.focusEditElement();
1✔
468
        }
469
    }
470

471
    /**
472
     * Event handler for click on clear button.
473
     */
474
    public onClearClick() {
475
        this.clearInput();
14✔
476
        this.focusEditElement();
14✔
477
    }
478

479
    /**
480
     * Event handler for keydown on commit button.
481
     */
482
    public onCommitKeyDown(eventArgs: KeyboardEvent) {
483
        if (this.platform.isActivationKey(eventArgs)) {
1✔
484
            eventArgs.preventDefault();
1✔
485
            this.commitInput();
1✔
486
            this.focusEditElement();
1✔
487
        }
488
    }
489

490
    /**
491
     * Event handler for click on commit button.
492
     */
493
    public onCommitClick(event?: MouseEvent) {
494
        event?.stopPropagation();
1✔
495
        this.commitInput();
1✔
496
        this.focusEditElement();
1✔
497
    }
498

499
    /**
500
     * Event handler for focusout on the input group.
501
     */
502
    public onInputGroupFocusout() {
503
        if (!this.value && this.value !== 0 &&
103✔
504
            this.expression.condition && !this.expression.condition.isUnary) {
505
            return;
29✔
506
        }
507
        requestAnimationFrame(() => {
74✔
508
            const focusedElement = this.column?.grid.document.activeElement;
73✔
509

510
            if (focusedElement.classList.contains('igx-chip__remove')) {
73!
511
                return;
×
512
            }
513

514
            if (!(focusedElement && this.editorFocused(focusedElement))
73✔
515
                && this.dropDownConditions.collapsed) {
516
                this.commitInput();
19✔
517
            }
518
        });
519
    }
520

521
    /**
522
     * Closes the filtering edit row.
523
     */
524
    public close() {
525
        if (this.expressionsList.length === 1 &&
23!
526
            this.expressionsList[0].expression.searchVal === null &&
527
            this.expressionsList[0].expression.condition.isUnary === false) {
528
            this.filteringService.getExpressions(this.column.field).pop();
×
529

530
            this.filter();
×
531
        } else {
532
            const condToRemove = this.expressionsList.filter(ex => ex.expression.searchVal === null && !ex.expression.condition.isUnary);
23✔
533
            if (condToRemove && condToRemove.length > 0) {
23!
534
                condToRemove.forEach(c => this.filteringService.removeExpression(this.column.field, this.expressionsList.indexOf(c)));
×
535
                this.filter();
×
536
            }
537
        }
538

539
        this.filteringService.isFilterRowVisible = false;
23✔
540
        this.filteringService.updateFilteringCell(this.column);
23✔
541
        this.filteringService.filteredColumn = null;
23✔
542
        this.filteringService.selectedExpression = null;
23✔
543
        this.filteringService.grid.theadRow.nativeElement.focus();
23✔
544

545
        this.chipAreaScrollOffset = 0;
23✔
546
        this.transform(this.chipAreaScrollOffset);
23✔
547
    }
548

549
    /**
550
     *  Event handler for date picker's selection.
551
     */
552
    public onDateSelected(value: Date) {
553
        this.value = value;
×
554
    }
555

556
    /** @hidden @internal */
557
    public inputGroupPrefixClick(event: MouseEvent) {
558
        event.stopPropagation();
46✔
559
        (event.currentTarget as HTMLElement).focus();
46✔
560
        this.toggleConditionsDropDown(event.currentTarget);
46✔
561
    }
562

563
    /**
564
     * Opens the conditions dropdown.
565
     */
566
    public toggleConditionsDropDown(target: any) {
567
        this._conditionsOverlaySettings.target = target;
52✔
568
        this._conditionsOverlaySettings.excludeFromOutsideClick = [target as HTMLElement];
52✔
569
        this.dropDownConditions.toggle(this._conditionsOverlaySettings);
52✔
570
    }
571

572
    /**
573
     * Opens the logic operators dropdown.
574
     */
575
    public toggleOperatorsDropDown(eventArgs, index) {
576
        this._operatorsOverlaySettings.target = eventArgs.target.parentElement;
1✔
577
        this._operatorsOverlaySettings.excludeFromOutsideClick = [eventArgs.target.parentElement as HTMLElement];
1✔
578
        this.dropDownOperators.toArray()[index].toggle(this._operatorsOverlaySettings);
1✔
579
    }
580

581
    /**
582
     * Event handler for change event in conditions dropdown.
583
     */
584
    public onConditionsChanged(eventArgs) {
585
        const value = (eventArgs.newSelection as IgxDropDownItemComponent).value;
43✔
586
        this.expression.condition = this.getCondition(value);
43✔
587
        if (this.expression.condition.isUnary) {
43✔
588
            // update grid's filtering on the next cycle to ensure the drop-down is closed
589
            // if the drop-down is not closed this event handler will be invoked multiple times
590
            requestAnimationFrame(() => this.unaryConditionChangedCallback());
26✔
591
        } else {
592
            requestAnimationFrame(() => this.conditionChangedCallback());
17✔
593
        }
594

595
        // Add requestAnimationFrame because of an issue in IE, where you are still able to write in the input,
596
        // if it has been focused and then set to readonly.
597
        requestAnimationFrame(() => this.focusEditElement());
43✔
598
    }
599

600

601
    public onChipPointerdown(args, chip: IgxChipComponent) {
602
        const activeElement = this.column?.grid.document.activeElement;
2✔
603
        this._cancelChipClick = chip.selected
2!
604
            && activeElement && this.editorFocused(activeElement);
605
    }
606

607
    public onChipClick(args, item: ExpressionUI) {
608
        if (this._cancelChipClick) {
9!
609
            this._cancelChipClick = false;
×
610
            return;
×
611
        }
612

613
        this.expressionsList.forEach(ex => ex.isSelected = false);
14✔
614

615
        this.toggleChip(item);
9✔
616
    }
617

618
    public toggleChip(item: ExpressionUI) {
619
        item.isSelected = !item.isSelected;
11✔
620
        if (item.isSelected) {
11✔
621
            this.expression = item.expression;
9✔
622
            this._value = this.expression.searchVal;
9✔
623
            this.focusEditElement();
9✔
624
        }
625
    }
626

627
    /**
628
     * Event handler for chip keydown event.
629
     */
630
    public onChipKeyDown(eventArgs: KeyboardEvent, item: ExpressionUI) {
631
        if (eventArgs.key === this.platform.KEYMAP.ENTER) {
2✔
632
            eventArgs.preventDefault();
2✔
633

634
            this.toggleChip(item);
2✔
635
        }
636
    }
637

638
    /**
639
     * Scrolls the first chip into view if the tab key is pressed on the left arrow.
640
     */
641
    public onLeftArrowKeyDown(event: KeyboardEvent) {
642
        if (event.key === this.platform.KEYMAP.TAB) {
1✔
643
            this.chipAreaScrollOffset = 0;
1✔
644
            this.transform(this.chipAreaScrollOffset);
1✔
645
        }
646
    }
647

648
    /**
649
     * Event handler for chip removed event.
650
     */
651
    public onChipRemoved(eventArgs: IBaseChipEventArgs, item: ExpressionUI) {
652
        const indexToRemove = this.expressionsList.indexOf(item);
8✔
653
        this.removeExpression(indexToRemove, item.expression);
8✔
654

655
        this.scrollChipsOnRemove();
8✔
656
    }
657

658
    /**
659
     * Event handler for logic operator changed event.
660
     */
661
    public onLogicOperatorChanged(eventArgs: ISelectionEventArgs, expression: ExpressionUI) {
662
        if (eventArgs.oldSelection) {
1✔
663
            expression.afterOperator = (eventArgs.newSelection as IgxDropDownItemComponent).value;
1✔
664
            this.expressionsList[this.expressionsList.indexOf(expression) + 1].beforeOperator = expression.afterOperator;
1✔
665

666
            // update grid's filtering on the next cycle to ensure the drop-down is closed
667
            // if the drop-down is not closed this event handler will be invoked multiple times
668
            requestAnimationFrame(() => this.filter());
1✔
669
        }
670
    }
671

672
    /**
673
     * Scrolls the chips into the chip area when left or right arrows are pressed.
674
     */
675
    public scrollChipsOnArrowPress(arrowPosition: string) {
676
        let count = 0;
4✔
677
        const chipAraeChildren = this.chipsArea.element.nativeElement.children;
4✔
678
        const containerRect = this.container.nativeElement.getBoundingClientRect();
4✔
679

680
        if (arrowPosition === 'right') {
4✔
681
            for (const chip of chipAraeChildren) {
2✔
682
                if (Math.ceil(chip.getBoundingClientRect().right) < Math.ceil(containerRect.right)) {
10✔
683
                    count++;
3✔
684
                }
685
            }
686

687
            if (count < chipAraeChildren.length) {
2✔
688
                this.chipAreaScrollOffset -= Math.ceil(chipAraeChildren[count].getBoundingClientRect().right) -
2✔
689
                    Math.ceil(containerRect.right) + 1;
690
                this.transform(this.chipAreaScrollOffset);
2✔
691
            }
692
        }
693

694
        if (arrowPosition === 'left') {
4✔
695
            for (const chip of chipAraeChildren) {
2✔
696
                if (Math.ceil(chip.getBoundingClientRect().left) < Math.ceil(containerRect.left)) {
10✔
697
                    count++;
1✔
698
                }
699
            }
700

701
            if (count > 0) {
2✔
702
                this.chipAreaScrollOffset += Math.ceil(containerRect.left) -
1✔
703
                    Math.ceil(chipAraeChildren[count - 1].getBoundingClientRect().left) + 1;
704
                this.transform(this.chipAreaScrollOffset);
1✔
705
            }
706
        }
707
    }
708

709
    /**
710
     * @hidden
711
     * Resets the chips area
712
     * @memberof IgxGridFilteringRowComponent
713
     */
714
    public resetChipsArea() {
715
        this.chipAreaScrollOffset = 0;
153✔
716
        this.transform(this.chipAreaScrollOffset);
153✔
717
        this.showHideArrowButtons();
153✔
718
    }
719

720
    /** @hidden @internal */
721
    public focusEditElement() {
722
        if (this.input) {
174✔
723
            this.input.nativeElement.focus();
143✔
724
        } else if (this.picker) {
31✔
725
            this.picker.getEditElement().focus();
31✔
726
        }
727
    }
728

729
    public ngOnDestroy() {
730
        this.$destroyer.next();
100✔
731
    }
732

733
    private showHideArrowButtons() {
734
        requestAnimationFrame(() => {
346✔
735
            if (this.filteringService.isFilterRowVisible) {
335✔
736
                const containerWidth = this.container.nativeElement.getBoundingClientRect().width;
317✔
737
                this.chipsAreaWidth = this.chipsArea.element.nativeElement.getBoundingClientRect().width;
317✔
738

739
                this.showArrows = this.chipsAreaWidth >= containerWidth && this.isColumnFiltered;
317✔
740

741
                // TODO: revise the cdr.detectChanges() usage here
742
                if (!(this.cdr as ViewRef).destroyed) {
317✔
743
                    this.cdr.detectChanges();
316✔
744
                }
745
            }
746
        });
747
    }
748

749
    private addExpression(isSelected: boolean) {
750
        const exprUI = new ExpressionUI();
42✔
751
        exprUI.expression = this.expression;
42✔
752
        exprUI.beforeOperator = this.expressionsList.length > 0 ? FilteringLogic.And : null;
42✔
753
        exprUI.isSelected = isSelected;
42✔
754

755
        this.expressionsList.push(exprUI);
42✔
756

757
        const length = this.expressionsList.length;
42✔
758
        if (this.expressionsList[length - 2]) {
42✔
759
            this.expressionsList[length - 2].afterOperator = this.expressionsList[length - 1].beforeOperator;
3✔
760
        }
761

762
        this.showHideArrowButtons();
42✔
763
    }
764

765
    private removeExpression(indexToRemove: number, expression: IFilteringExpression) {
766
        if (indexToRemove === 0 && this.expressionsList.length === 1) {
9✔
767
            this.clearFiltering();
3✔
768
            return;
3✔
769
        }
770

771
        this.filteringService.removeExpression(this.column.field, indexToRemove);
6✔
772

773
        this.filter();
6✔
774

775
        if (this.expression === expression) {
6✔
776
            this.resetExpression();
1✔
777
        }
778

779
        this.showHideArrowButtons();
6✔
780
    }
781

782
    private resetExpression(condition?: string) {
783
        this.expression = {
145✔
784
            fieldName: this.column.field,
785
            condition: null,
786
            conditionName: null,
787
            searchVal: null,
788
            ignoreCase: this.column.filteringIgnoreCase
789
        };
790

791
        if (this.column.dataType !== GridColumnDataType.Boolean) {
145✔
792
            this.expression.condition = this.getCondition(condition ?? this.conditions[0]);
139✔
793
        }
794

795
        if (this.column.dataType === GridColumnDataType.Date && this.input) {
145!
796
            this.input.nativeElement.value = null;
×
797
        }
798

799
        this.showHideArrowButtons();
145✔
800
    }
801

802
    private scrollChipsWhenAddingExpression() {
803
        const chipAraeChildren = this.chipsArea.element.nativeElement.children;
33✔
804
        if (!chipAraeChildren || chipAraeChildren.length === 0) {
33✔
805
            return;
2✔
806
        }
807

808
        const chipsContainerWidth = this.container.nativeElement.offsetWidth;
31✔
809
        const chipsAreaWidth = this.chipsArea.element.nativeElement.offsetWidth;
31✔
810

811
        if (chipsAreaWidth > chipsContainerWidth) {
31✔
812
            this.chipAreaScrollOffset = chipsContainerWidth - chipsAreaWidth;
1✔
813
            this.transform(this.chipAreaScrollOffset);
1✔
814
        }
815
    }
816

817
    private transform(offset: number) {
818
        requestAnimationFrame(() => {
300✔
819
            this.chipsArea.element.nativeElement.style.transform = `translate(${offset}px)`;
300✔
820
        });
821
    }
822

823
    private scrollChipsOnRemove() {
824
        let count = 0;
8✔
825
        const chipAraeChildren = this.chipsArea.element.nativeElement.children;
8✔
826
        const containerRect = this.container.nativeElement.getBoundingClientRect();
8✔
827

828
        for (const chip of chipAraeChildren) {
8✔
829
            if (Math.ceil(chip.getBoundingClientRect().right) < Math.ceil(containerRect.left)) {
28!
830
                count++;
×
831
            }
832
        }
833

834
        if (count <= 2) {
8!
835
            this.chipAreaScrollOffset = 0;
8✔
836
        } else {
837
            const dif = chipAraeChildren[count].id === 'chip' ? count - 2 : count - 1;
×
838
            this.chipAreaScrollOffset += Math.ceil(containerRect.left) - Math.ceil(chipAraeChildren[dif].getBoundingClientRect().left) + 1;
×
839
        }
840

841
        this.transform(this.chipAreaScrollOffset);
8✔
842
    }
843

844
    private conditionChangedCallback() {
845
        if (!!this.expression.searchVal || this.expression.searchVal === 0) {
17✔
846
            this.filter();
4✔
847
        } else if (this.value) {
13!
848
            this.value = null;
×
849
        }
850
    }
851

852
    private unaryConditionChangedCallback() {
853
        if (this.value) {
26✔
854
            this.value = null;
1✔
855
        }
856
        if (this.expressionsList.find(item => item.expression === this.expression) === undefined) {
26✔
857
            this.addExpression(true);
15✔
858
        }
859
        this.filter();
26✔
860
    }
861

862
    private filter() {
863
        this.filteringService.filterInternal(this.column.field);
69✔
864
    }
865

866
    private editorFocused(activeElement: Element): boolean {
867
        // if the first check is false and the second is undefined this will return undefined
868
        // make sure it always returns boolean
869
        return !!(this.inputGroup && this.inputGroup.nativeElement.contains(activeElement)
73✔
870
            || this.picker && this.picker.isFocused);
871
    }
872

873
    private get isColumnFiltered() {
874
        return this.column.filteringExpressionsTree && this.column.filteringExpressionsTree.filteringOperands.length > 0;
21✔
875
    }
876

877
    public get isNarrowWidth(): boolean {
878
        return this.nativeElement.offsetWidth < this.NARROW_WIDTH_THRESHOLD;
1,983✔
879
    }
880
}
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