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

IgniteUI / igniteui-angular / 16026043880

02 Jul 2025 01:10PM UTC coverage: 91.488% (+0.08%) from 91.409%
16026043880

Pull #15982

github

web-flow
Merge 693a735da into 7e40671a1
Pull Request #15982: Address grids column headers accessibility issues - active descendant, what is announced by SRs

13459 of 15781 branches covered (85.29%)

38 of 39 new or added lines in 8 files covered. (97.44%)

131 existing lines in 3 files now uncovered.

27161 of 29688 relevant lines covered (91.49%)

39508.7 hits per line

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

91.78
/projects/igniteui-angular/src/lib/grids/cell.component.ts
1
import {
2
    ChangeDetectionStrategy,
3
    ChangeDetectorRef,
4
    Component,
5
    ElementRef,
6
    HostBinding,
7
    HostListener,
8
    Input,
9
    TemplateRef,
10
    ViewChild,
11
    NgZone,
12
    OnInit,
13
    OnDestroy,
14
    OnChanges,
15
    SimpleChanges,
16
    Inject,
17
    ViewChildren,
18
    QueryList,
19
    AfterViewInit,
20
    booleanAttribute
21
} from '@angular/core';
22
import { formatPercent, NgClass, NgTemplateOutlet, DecimalPipe, PercentPipe, CurrencyPipe, DatePipe, getLocaleCurrencyCode, getCurrencySymbol } from '@angular/common';
23
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
24

25
import { first, takeUntil, takeWhile } from 'rxjs/operators';
26
import { Subject } from 'rxjs';
27

28
import { IgxTextHighlightDirective } from '../directives/text-highlight/text-highlight.directive';
29
import { formatCurrency, formatDate, PlatformUtil } from '../core/utils';
30
import { IgxGridSelectionService } from './selection/selection.service';
31
import { HammerGesturesManager } from '../core/touch';
32
import { GridSelectionMode } from './common/enums';
33
import { CellType, ColumnType, GridType, IgxCellTemplateContext, IGX_GRID_BASE, RowType } from './common/grid.interface';
34
import { GridColumnDataType } from '../data-operations/data-util';
35
import { IgxRowDirective } from './row.directive';
36
import { ISearchInfo } from './common/events';
37
import { IgxGridCell } from './grid-public-cell';
38
import { ISelectionNode } from './common/types';
39
import { AutoPositionStrategy, HorizontalAlignment, IgxOverlayService } from '../services/public_api';
40
import { IgxIconComponent } from '../icon/icon.component';
41
import { IgxGridCellImageAltPipe, IgxStringReplacePipe, IgxColumnFormatterPipe } from './common/pipes';
42
import { IgxTooltipDirective } from '../directives/tooltip/tooltip.directive';
43
import { IgxTooltipTargetDirective } from '../directives/tooltip/tooltip-target.directive';
44
import { IgxSuffixDirective } from '../directives/suffix/suffix.directive';
45
import { IgxPrefixDirective } from '../directives/prefix/prefix.directive';
46
import { IgxDateTimeEditorDirective } from '../directives/date-time-editor/date-time-editor.directive';
47
import { IgxTimePickerComponent } from '../time-picker/time-picker.component';
48
import { IgxDatePickerComponent } from '../date-picker/date-picker.component';
49
import { IgxCheckboxComponent } from '../checkbox/checkbox.component';
50
import { IgxTextSelectionDirective } from '../directives/text-selection/text-selection.directive';
51
import { IgxFocusDirective } from '../directives/focus/focus.directive';
52
import { IgxInputDirective } from '../directives/input/input.directive';
53
import { IgxInputGroupComponent } from '../input-group/input-group.component';
54
import { IgxChipComponent } from '../chips/chip.component';
55

56
/**
57
 * Providing reference to `IgxGridCellComponent`:
58
 * ```typescript
59
 * @ViewChild('grid', { read: IgxGridComponent })
60
 *  public grid: IgxGridComponent;
61
 * ```
62
 * ```typescript
63
 *  let column = this.grid.columnList.first;
64
 * ```
65
 * ```typescript
66
 *  let cell = column.cells[0];
67
 * ```
68
 */
69
@Component({
70
    changeDetection: ChangeDetectionStrategy.OnPush,
71
    selector: 'igx-grid-cell',
72
    templateUrl: './cell.component.html',
73
    providers: [HammerGesturesManager],
74
    imports: [
75
        NgClass,
76
        NgTemplateOutlet,
77
        DecimalPipe,
78
        PercentPipe,
79
        CurrencyPipe,
80
        DatePipe,
81
        ReactiveFormsModule,
82
        IgxChipComponent,
83
        IgxTextHighlightDirective,
84
        IgxIconComponent,
85
        IgxInputGroupComponent,
86
        IgxInputDirective,
87
        IgxFocusDirective,
88
        IgxTextSelectionDirective,
89
        IgxCheckboxComponent,
90
        IgxDatePickerComponent,
91
        IgxTimePickerComponent,
92
        IgxDateTimeEditorDirective,
93
        IgxPrefixDirective,
94
        IgxSuffixDirective,
95
        IgxTooltipTargetDirective,
96
        IgxTooltipDirective,
97
        IgxGridCellImageAltPipe,
98
        IgxStringReplacePipe,
99
        IgxColumnFormatterPipe
100
    ]
101
})
102
export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellType, AfterViewInit {
3✔
103
    private _destroy$ = new Subject<void>();
152,254✔
104
    /**
105
     * @hidden
106
     * @internal
107
     */
108
    @HostBinding('class.igx-grid__td--new')
109
    public get isEmptyAddRowCell() {
110
        return this.intRow.addRowUI && (this.value === undefined || this.value === null);
1,213,577✔
111
    }
112

113
    /**
114
     * @hidden
115
     * @internal
116
     */
117
    @ViewChildren('error', { read: IgxTooltipDirective })
118
    public errorTooltip: QueryList<IgxTooltipDirective>;
119

120
    /**
121
     * @hidden
122
     * @internal
123
     */
124
    @ViewChild('errorIcon', { read: IgxIconComponent, static: false })
125
    public errorIcon: IgxIconComponent;
126

127
    /**
128
     * Gets the default error template.
129
     * @hidden @internal
130
     */
131
    @ViewChild('defaultError', { read: TemplateRef, static: true })
132
    public defaultErrorTemplate: TemplateRef<any>;
133

134
    /**
135
     * Gets the column of the cell.
136
     * ```typescript
137
     *  let cellColumn = this.cell.column;
138
     * ```
139
     *
140
     * @memberof IgxGridCellComponent
141
     */
142
    @Input()
143
    public column: ColumnType;
144

145

146
    /**
147
     * @hidden
148
     * @internal
149
     */
150
    protected get formGroup(): FormGroup {
151
        return this.grid.validation.getFormGroup(this.intRow.key);
5,827,194✔
152
    }
153

154
    /**
155
     * @hidden
156
     * @internal
157
     */
158
    @Input()
159
    public intRow: IgxRowDirective;
160

161
    /**
162
     * Gets the row of the cell.
163
     * ```typescript
164
     * let cellRow = this.cell.row;
165
     * ```
166
     *
167
     * @memberof IgxGridCellComponent
168
     */
169
    @Input()
170
    public get row(): RowType {
171
        return this.grid.createRow(this.intRow.index);
1,794✔
172
    }
173

174
    /**
175
     * Gets the data of the row of the cell.
176
     * ```typescript
177
     * let rowData = this.cell.rowData;
178
     * ```
179
     *
180
     * @memberof IgxGridCellComponent
181
     */
182
    @Input()
183
    public rowData: any;
184

185
    /**
186
     * @hidden
187
     * @internal
188
     */
189
    @Input()
190
    public columnData: any;
191

192
    /**
193
     * Sets/gets the template of the cell.
194
     * ```html
195
     * <ng-template #cellTemplate igxCell let-value>
196
     *   <div style="font-style: oblique; color:blueviolet; background:red">
197
     *       <span>{{value}}</span>
198
     *   </div>
199
     * </ng-template>
200
     * ```
201
     * ```typescript
202
     * @ViewChild('cellTemplate',{read: TemplateRef})
203
     * cellTemplate: TemplateRef<any>;
204
     * ```
205
     * ```typescript
206
     * this.cell.cellTemplate = this.cellTemplate;
207
     * ```
208
     * ```typescript
209
     * let template =  this.cell.cellTemplate;
210
     * ```
211
     *
212
     * @memberof IgxGridCellComponent
213
     */
214
    @Input()
215
    public cellTemplate: TemplateRef<any>;
216

217
    @Input()
218
    public cellValidationErrorTemplate: TemplateRef<any>;
219

220
    @Input()
221
    public pinnedIndicator: TemplateRef<any>;
222

223
    /**
224
     * Sets/gets the cell value.
225
     * ```typescript
226
     * this.cell.value = "Cell Value";
227
     * ```
228
     * ```typescript
229
     * let cellValue = this.cell.value;
230
     * ```
231
     *
232
     * @memberof IgxGridCellComponent
233
     */
234
    @Input()
235
    public value: any;
236

237
    /**
238
     * Gets the cell formatter.
239
     * ```typescript
240
     * let cellForamatter = this.cell.formatter;
241
     * ```
242
     *
243
     * @memberof IgxGridCellComponent
244
     */
245
    @Input()
246
    public formatter: (value: any, rowData?: any, columnData?: any) => any;
247

248
    /**
249
     * Gets the cell template context object.
250
     * ```typescript
251
     *  let context = this.cell.context();
252
     * ```
253
     *
254
     * @memberof IgxGridCellComponent
255
     */
256
    public get context(): IgxCellTemplateContext {
257
        const getCellType = () => this.getCellType(true);
647,910✔
258
        const ctx: IgxCellTemplateContext = {
647,910✔
259
            $implicit: this.value,
260
            additionalTemplateContext: this.column.additionalTemplateContext,
261
            get cell() {
262
                /* Turns the `cell` property from the template context object into lazy-evaluated one.
263
                 * Otherwise on each detection cycle the cell template is recreating N cell instances where
264
                 * N = number of visible cells in the grid, leading to massive performance degradation in large grids.
265
                 */
266
                return getCellType();
1,998✔
267
            }
268
        };
269
        if (this.editMode) {
647,910✔
270
            ctx.formControl = this.formControl;
2,449✔
271
        }
272
        if (this.isInvalid) {
647,910✔
273
            ctx.defaultErrorTemplate = this.defaultErrorTemplate;
414✔
274
        }
275
        return ctx;
647,910✔
276
    }
277

278
    /**
279
     * Gets the cell template.
280
     * ```typescript
281
     * let template = this.cell.template;
282
     * ```
283
     *
284
     * @memberof IgxGridCellComponent
285
     */
286
    public get template(): TemplateRef<any> {
287
        if (this.editMode && this.formGroup) {
323,910✔
288
            const inlineEditorTemplate = this.column.inlineEditorTemplate;
1,223✔
289
            return inlineEditorTemplate ? inlineEditorTemplate : this.inlineEditorTemplate;
1,223✔
290
        }
291
        if (this.cellTemplate) {
322,687✔
292
            return this.cellTemplate;
693✔
293
        }
294
        if (this.grid.rowEditable && this.intRow.addRowUI) {
321,994✔
295
            return this.addRowCellTemplate;
1,918✔
296
        }
297
        return this.defaultCellTemplate;
320,076✔
298
    }
299

300
    /**
301
     * Gets the pinned indicator template.
302
     * ```typescript
303
     * let template = this.cell.pinnedIndicatorTemplate;
304
     * ```
305
     *
306
     * @memberof IgxGridCellComponent
307
     */
308
    public get pinnedIndicatorTemplate() {
309
        if (this.pinnedIndicator) {
323,862!
310
            return this.pinnedIndicator;
×
311
        }
312
        return this.defaultPinnedIndicator;
323,862✔
313
    }
314

315
    /**
316
     * Gets the `id` of the grid in which the cell is stored.
317
     * ```typescript
318
     * let gridId = this.cell.gridID;
319
     * ```
320
     *
321
     * @memberof IgxGridCellComponent
322
     */
323
    public get gridID(): any {
324
        return this.intRow.gridID;
302,942✔
325
    }
326

327

328
    /**
329
     * Gets the `index` of the row where the cell is stored.
330
     * ```typescript
331
     * let rowIndex = this.cell.rowIndex;
332
     * ```
333
     *
334
     * @memberof IgxGridCellComponent
335
     */
336
    @HostBinding('attr.data-rowIndex')
337
    public get rowIndex(): number {
338
        return this.intRow.index;
6,063,929✔
339
    }
340

341
    /**
342
     * Gets the `index` of the cell column.
343
     * ```typescript
344
     * let columnIndex = this.cell.columnIndex;
345
     * ```
346
     *
347
     * @memberof IgxGridCellComponent
348
     */
349
    public get columnIndex(): number {
350
        return this.column.index;
17,840✔
351
    }
352

353
    /**
354
     * Returns the column visible index.
355
     * ```typescript
356
     * let visibleColumnIndex = this.cell.visibleColumnIndex;
357
     * ```
358
     *
359
     * @memberof IgxGridCellComponent
360
     */
361
    @HostBinding('attr.data-visibleIndex')
362
    @Input()
363
    public get visibleColumnIndex() {
364
        return this.column.columnLayoutChild ? this.column.visibleIndex : this._vIndex;
4,854,894✔
365
    }
366

367
    public set visibleColumnIndex(val) {
368
        this._vIndex = val;
158,812✔
369
    }
370

371
    /**
372
     * Gets the ID of the cell.
373
     * ```typescript
374
     * let cellID = this.cell.cellID;
375
     * ```
376
     *
377
     * @memberof IgxGridCellComponent
378
     */
379
    public get cellID() {
380
        const primaryKey = this.grid.primaryKey;
459✔
381
        const rowID = primaryKey ? this.rowData[primaryKey] : this.rowData;
459✔
382
        return { rowID, columnID: this.columnIndex, rowIndex: this.rowIndex };
459✔
383
    }
384

385
    @HostBinding('attr.id')
386
    public get attrCellID() {
387
        return `${this.intRow.gridID}_${this.rowIndex}_${this.visibleColumnIndex}`;
1,211,706✔
388
    }
389

390
    @HostBinding('attr.title')
391
    public get title() {
392
        if (this.editMode || this.cellTemplate || this.errorShowing) {
1,211,719✔
393
            return '';
5,215✔
394
        }
395

396
        if (this.formatter) {
1,206,504✔
397
            return this.formatter(this.value, this.rowData, this.columnData);
21,889✔
398
        }
399

400
        const args = this.column.pipeArgs;
1,184,615✔
401
        const locale = this.grid.locale;
1,184,615✔
402

403
        switch (this.column.dataType) {
1,184,615✔
404
            case GridColumnDataType.Percent:
405
                return formatPercent(this.value, locale, args.digitsInfo);
427✔
406
            case GridColumnDataType.Currency:
407
                return formatCurrency(this.value, this.currencyCode, args.display, args.digitsInfo, locale);
20,400✔
408
            case GridColumnDataType.Date:
409
            case GridColumnDataType.DateTime:
410
            case GridColumnDataType.Time:
411
                return formatDate(this.value, args.format, locale, args.timezone);
169,989✔
412
        }
413
        return this.value;
993,799✔
414
    }
415

416
    @HostBinding('class.igx-grid__td--bool-true')
417
    public get booleanClass() {
418
        return this.column.dataType === 'boolean' && this.value;
1,211,706✔
419
    }
420

421
    /**
422
     * Returns a reference to the nativeElement of the cell.
423
     * ```typescript
424
     * let cellNativeElement = this.cell.nativeElement;
425
     * ```
426
     *
427
     * @memberof IgxGridCellComponent
428
     */
429
    public get nativeElement(): HTMLElement {
430
        return this.element.nativeElement;
1,273,716✔
431
    }
432

433
    /**
434
     * @hidden
435
     * @internal
436
     */
437
    @Input()
438
    public get cellSelectionMode() {
439
        return this._cellSelection;
305,525✔
440
    }
441

442
    public set cellSelectionMode(value) {
443
        if (this._cellSelection === value) {
152,622✔
444
            return;
151,650✔
445
        }
446
        this.zone.runOutsideAngular(() => {
972✔
447
            if (value === GridSelectionMode.multiple) {
972✔
448
                this.addPointerListeners(value);
60✔
449
            } else {
450
                this.removePointerListeners(this._cellSelection);
912✔
451
            }
452
        });
453
        this._cellSelection = value;
972✔
454
    }
455

456
    /**
457
     * @hidden
458
     * @internal
459
     */
460
    @Input()
461
    public set lastSearchInfo(value: ISearchInfo) {
462
        this._lastSearchInfo = value;
157,554✔
463
        this.highlightText(this._lastSearchInfo.searchText, this._lastSearchInfo.caseSensitive, this._lastSearchInfo.exactMatch);
157,554✔
464
    }
465

466
    /**
467
     * @hidden
468
     * @internal
469
     */
470
    @Input()
471
    @HostBinding('class.igx-grid__td--pinned-last')
472
    public lastPinned = false;
152,254✔
473

474
    /**
475
     * @hidden
476
     * @internal
477
     */
478
    @Input()
479
    @HostBinding('class.igx-grid__td--pinned-first')
480
    public firstPinned = false;
152,254✔
481

482
    /**
483
     * Returns whether the cell is in edit mode.
484
     */
485
    @Input({ transform: booleanAttribute })
486
    @HostBinding('class.igx-grid__td--editing')
487
    public editMode = false;
152,254✔
488

489
    /**
490
     * Sets/get the `role` property of the cell.
491
     * Default value is `"gridcell"`.
492
     * ```typescript
493
     * this.cell.role = 'grid-cell';
494
     * ```
495
     * ```typescript
496
     * let cellRole = this.cell.role;
497
     * ```
498
     *
499
     * @memberof IgxGridCellComponent
500
     */
501
    @HostBinding('attr.role')
502
    public role = 'gridcell';
152,254✔
503

504
    /**
505
     * Gets whether the cell is editable.
506
     * ```typescript
507
     * let isCellReadonly = this.cell.readonly;
508
     * ```
509
     *
510
     * @memberof IgxGridCellComponent
511
     */
512
    @HostBinding('attr.aria-readonly')
513
    public get readonly(): boolean {
514
        return !this.editable;
1,211,707✔
515
    }
516

517
    /** @hidden @internal */
518
    @HostBinding('attr.aria-describedby')
519
    public get ariaDescribeBy() {
520
        let describeBy = this.grid.headerGroupsList
1,212,783✔
521
                        .find(hg => hg.column.field === this.column.field)?.headerID || '';
4,388,510✔
522
        if (this.isInvalid) {
1,212,783✔
523
            describeBy += ' ' + this.ariaErrorMessage;
107✔
524
        }
525
        return describeBy;
1,212,783✔
526
    }
527

528
    /** @hidden @internal */
529
    public get ariaErrorMessage() {
530
        return this.grid.id + '_' + this.column.field + '_' + this.intRow.index + '_error';
245✔
531
    }
532

533
    /**
534
     * @hidden
535
     * @internal
536
     */
537
    @HostBinding('class.igx-grid__td--invalid')
538
    @HostBinding('attr.aria-invalid')
539
    public get isInvalid() {
540
        const isInvalid = this.formGroup?.get(this.column?.field)?.invalid && this.formGroup?.get(this.column?.field)?.touched;
4,610,108✔
541
        return !this.intRow.deleted && isInvalid;
4,610,108✔
542
    }
543

544
    /**
545
     * @hidden
546
     * @internal
547
     */
548
    @HostBinding('class.igx-grid__td--valid')
549
    public get isValidAfterEdit() {
550
        const formControl = this.formGroup?.get(this.column?.field);
1,211,706✔
551
        return this.editMode && formControl && !formControl.invalid && formControl.dirty;
1,211,706✔
552
    }
553

554
    /**
555
     * Gets the formControl responsible for value changes and validation for this cell.
556
     */
557
    protected get formControl(): FormControl {
558
        return this.grid.validation.getFormControl(this.intRow.key, this.column.field) as FormControl;
5,028✔
559
    }
560

561
    public get gridRowSpan(): number {
562
        return this.column.gridRowSpan;
198✔
563
    }
564

565
    public get gridColumnSpan(): number {
566
        return this.column.gridColumnSpan;
198✔
567
    }
568

569
    public get rowEnd(): number {
UNCOV
570
        return this.column.rowEnd;
×
571
    }
572

573
    public get colEnd(): number {
UNCOV
574
        return this.column.colEnd;
×
575
    }
576

577
    public get rowStart(): number {
UNCOV
578
        return this.column.rowStart;
×
579
    }
580

581
    public get colStart(): number {
UNCOV
582
        return this.column.colStart;
×
583
    }
584

585
    /**
586
     * Gets the width of the cell.
587
     * ```typescript
588
     * let cellWidth = this.cell.width;
589
     * ```
590
     *
591
     * @memberof IgxGridCellComponent
592
     */
593
    @Input()
594
    public width = '';
152,254✔
595

596
    /**
597
     * @hidden
598
     */
599
    @Input()
600
    @HostBinding('class.igx-grid__td--active')
601
    public active = false;
152,254✔
602

603
    @HostBinding('attr.aria-selected')
604
    public get ariaSelected() {
605
        return this.selected || this.column.selected || this.intRow.selected;
1,211,706✔
606
    }
607

608
    /**
609
     * Gets whether the cell is selected.
610
     * ```typescript
611
     * let isSelected = this.cell.selected;
612
     * ```
613
     *
614
     * @memberof IgxGridCellComponent
615
     */
616
    @HostBinding('class.igx-grid__td--selected')
617
    public get selected() {
618
        return this.selectionService.selected(this.selectionNode);
2,425,497✔
619
    }
620

621
    /**
622
     * Selects/deselects the cell.
623
     * ```typescript
624
     * this.cell.selected = true.
625
     * ```
626
     *
627
     * @memberof IgxGridCellComponent
628
     */
629
    public set selected(val: boolean) {
630
        const node = this.selectionNode;
1✔
631
        if (val) {
1!
632
            this.selectionService.add(node);
1✔
633
        } else {
UNCOV
634
            this.selectionService.remove(node);
×
635
        }
636
        this.grid.notifyChanges();
1✔
637
    }
638

639
    /**
640
     * Gets whether the cell column is selected.
641
     * ```typescript
642
     * let isCellColumnSelected = this.cell.columnSelected;
643
     * ```
644
     *
645
     * @memberof IgxGridCellComponent
646
     */
647
    @HostBinding('class.igx-grid__td--column-selected')
648
    public get columnSelected() {
649
        return this.selectionService.isColumnSelected(this.column.field);
1,211,706✔
650
    }
651

652
    /**
653
     * Sets the current edit value while a cell is in edit mode.
654
     * Only for cell editing mode.
655
     * ```typescript
656
     * this.cell.editValue = value;
657
     * ```
658
     *
659
     * @memberof IgxGridCellComponent
660
     */
661
    public set editValue(value) {
662
        if (this.grid.crudService.cellInEditMode) {
44✔
663
            this.grid.crudService.cell.editValue = value;
44✔
664
        }
665
    }
666

667
    /**
668
     * Gets the current edit value while a cell is in edit mode.
669
     * Only for cell editing mode.
670
     * ```typescript
671
     * let editValue = this.cell.editValue;
672
     * ```
673
     *
674
     * @memberof IgxGridCellComponent
675
     */
676
    public get editValue() {
677
        if (this.grid.crudService.cellInEditMode) {
163✔
678
            return this.grid.crudService.cell.editValue;
163✔
679
        }
680
    }
681

682
    /**
683
     * Returns whether the cell is editable.
684
     */
685
    public get editable(): boolean {
686
        return this.column.editable && !this.intRow.disabled;
1,212,972✔
687
    }
688

689
    /**
690
     * @hidden
691
     */
692
    @Input()
693
    @HostBinding('class.igx-grid__td--row-pinned-first')
694
    public displayPinnedChip = false;
152,254✔
695

696
    @HostBinding('style.min-height.px')
697
    protected get minHeight() {
698
        if ((this.grid as any).isCustomSetRowHeight) {
1,211,706✔
699
            return this.grid.renderedRowHeight;
464✔
700
        }
701
    }
702

703
    @HostBinding('attr.aria-rowindex')
704
    protected get ariaRowIndex(): number {
705
        // +2 because aria-rowindex is 1-based and the first row is the header
706
        return this.rowIndex + 2;
1,211,706✔
707
    }
708

709
    @HostBinding('attr.aria-colindex')
710
    protected get ariaColIndex(): number {
711
        return this.column.index + 1;
1,211,706✔
712
    }
713

714
    @ViewChild('defaultCell', { read: TemplateRef, static: true })
715
    protected defaultCellTemplate: TemplateRef<any>;
716

717
    @ViewChild('defaultPinnedIndicator', { read: TemplateRef, static: true })
718
    protected defaultPinnedIndicator: TemplateRef<any>;
719

720
    @ViewChild('inlineEditor', { read: TemplateRef, static: true })
721
    protected inlineEditorTemplate: TemplateRef<any>;
722

723
    @ViewChild('addRowCell', { read: TemplateRef, static: true })
724
    protected addRowCellTemplate: TemplateRef<any>;
725

726
    @ViewChild(IgxTextHighlightDirective, { read: IgxTextHighlightDirective })
727
    protected set highlight(value: IgxTextHighlightDirective) {
728
        this._highlight = value;
154,282✔
729

730
        if (this._highlight && this.grid.lastSearchInfo.searchText) {
154,282✔
731
            this._highlight.highlight(this.grid.lastSearchInfo.searchText,
399✔
732
                this.grid.lastSearchInfo.caseSensitive,
733
                this.grid.lastSearchInfo.exactMatch);
734
            this._highlight.activateIfNecessary();
399✔
735
        }
736
    }
737

738
    protected get highlight() {
739
        return this._highlight;
353,024✔
740
    }
741

742
    protected get selectionNode(): ISelectionNode {
743
        return {
2,427,374✔
744
            row: this.rowIndex,
745
            column: this.column.columnLayoutChild ? this.column.parent.visibleIndex : this.visibleColumnIndex,
2,427,374✔
746
            layout: this.column.columnLayoutChild ? {
2,427,374✔
747
                rowStart: this.column.rowStart,
748
                colStart: this.column.colStart,
749
                rowEnd: this.column.rowEnd,
750
                colEnd: this.column.colEnd,
751
                columnVisibleIndex: this.visibleColumnIndex
752
            } : null
753
        };
754
    }
755

756
    /**
757
     * Sets/gets the highlight class of the cell.
758
     * Default value is `"igx-highlight"`.
759
     * ```typescript
760
     * let highlightClass = this.cell.highlightClass;
761
     * ```
762
     * ```typescript
763
     * this.cell.highlightClass = 'igx-cell-highlight';
764
     * ```
765
     *
766
     * @memberof IgxGridCellComponent
767
     */
768
    public highlightClass = 'igx-highlight';
152,254✔
769

770
    /**
771
     * Sets/gets the active highlight class class of the cell.
772
     * Default value is `"igx-highlight__active"`.
773
     * ```typescript
774
     * let activeHighlightClass = this.cell.activeHighlightClass;
775
     * ```
776
     * ```typescript
777
     * this.cell.activeHighlightClass = 'igx-cell-highlight_active';
778
     * ```
779
     *
780
     * @memberof IgxGridCellComponent
781
     */
782
    public activeHighlightClass = 'igx-highlight__active';
152,254✔
783

784
    /** @hidden @internal */
785
    public get step(): number {
786
        const digitsInfo = this.column.pipeArgs.digitsInfo;
214✔
787
        if (!digitsInfo) {
214!
UNCOV
788
            return 1;
×
789
        }
790
        const step = +digitsInfo.substr(digitsInfo.indexOf('.') + 1, 1);
214✔
791
        return 1 / (Math.pow(10, step));
214✔
792
    }
793

794
    /** @hidden @internal */
795
    public get currencyCode(): string {
796
        return this.column.pipeArgs.currencyCode ?
57,432✔
797
            this.column.pipeArgs.currencyCode : getLocaleCurrencyCode(this.grid.locale);
798
    }
799

800
    /** @hidden @internal */
801
    public get currencyCodeSymbol(): string {
802
        return getCurrencySymbol(this.currencyCode, 'wide', this.grid.locale);
4✔
803
    }
804

805
    protected _lastSearchInfo: ISearchInfo;
806
    private _highlight: IgxTextHighlightDirective;
807
    private _cellSelection: GridSelectionMode = GridSelectionMode.multiple;
152,254✔
808
    private _vIndex = -1;
152,254✔
809

810
    constructor(
811
        protected selectionService: IgxGridSelectionService,
152,254✔
812
        @Inject(IGX_GRID_BASE) public grid: GridType,
152,254✔
813
        @Inject(IgxOverlayService) protected overlayService: IgxOverlayService,
152,254✔
814
        public cdr: ChangeDetectorRef,
152,254✔
815
        private element: ElementRef<HTMLElement>,
152,254✔
816
        protected zone: NgZone,
152,254✔
817
        private touchManager: HammerGesturesManager,
152,254✔
818
        protected platformUtil: PlatformUtil
152,254✔
819
    ) { }
820

821
    /**
822
     * @hidden
823
     * @internal
824
     */
825
    @HostListener('dblclick', ['$event'])
826
    public onDoubleClick = (event: MouseEvent) => {
152,254✔
827
        if (event.type === 'doubletap') {
150✔
828
            // prevent double-tap to zoom on iOS
829
            event.preventDefault();
1✔
830
        }
831
        if (this.editable && !this.editMode && !this.intRow.deleted && !this.grid.crudService.rowEditingBlocked) {
150✔
832
            this.grid.crudService.enterEditMode(this, event as Event);
144✔
833
        }
834

835
        this.grid.doubleClick.emit({
150✔
836
            cell: this.getCellType(),
837
            event
838
        });
839
    };
840

841
    /**
842
     * @hidden
843
     * @internal
844
     */
845
    @HostListener('click', ['$event'])
846
    public onClick(event: MouseEvent) {
847
        this.grid.cellClick.emit({
260✔
848
            cell: this.getCellType(),
849
            event
850
        });
851
    }
852

853
    /**
854
     * @hidden
855
     * @internal
856
     */
857
    public ngOnInit() {
858
        this.zone.runOutsideAngular(() => {
152,254✔
859
            this.nativeElement.addEventListener('pointerdown', this.pointerdown);
152,254✔
860
            this.addPointerListeners(this.cellSelectionMode);
152,254✔
861
        });
862
        if (this.platformUtil.isIOS) {
152,254✔
863
            this.touchManager.addEventListener(this.nativeElement, 'doubletap', this.onDoubleClick, {
40✔
864
                cssProps: {} /* don't disable user-select, etc */
865
            });
866
        }
867

868
    }
869

870
    public ngAfterViewInit() {
871
        this.errorTooltip.changes.pipe(takeUntil(this._destroy$)).subscribe(() => {
152,254✔
872
            if (this.errorTooltip.length > 0 && this.active) {
43✔
873
                // error ocurred
874
                this.cdr.detectChanges();
24✔
875
                this.openErrorTooltip();
24✔
876
            }
877
        });
878
    }
879

880
    /**
881
     * @hidden
882
     * @internal
883
     */
884
    public errorShowing = false;
152,254✔
885

886
    private openErrorTooltip() {
887
        const tooltip = this.errorTooltip.first;
25✔
888
        tooltip.open(
25✔
889
            {
890
                target: this.errorIcon.el.nativeElement,
891
                closeOnOutsideClick: true,
892
                excludeFromOutsideClick: [this.nativeElement],
893
                closeOnEscape: false,
894
                outlet: this.grid.outlet,
895
                modal: false,
896
                positionStrategy: new AutoPositionStrategy({
897
                    horizontalStartPoint: HorizontalAlignment.Center,
898
                    horizontalDirection: HorizontalAlignment.Center
899
                })
900
            }
901
        );
902
    }
903

904
    /**
905
     * @hidden
906
     * @internal
907
     */
908
    public ngOnDestroy() {
909
        this.zone.runOutsideAngular(() => {
150,909✔
910
            this.nativeElement.removeEventListener('pointerdown', this.pointerdown);
150,909✔
911
            this.removePointerListeners(this.cellSelectionMode);
150,909✔
912
        });
913
        this.touchManager.destroy();
150,909✔
914
        this._destroy$.next();
150,909✔
915
        this._destroy$.complete();
150,909✔
916
    }
917

918
    /**
919
     * @hidden
920
     * @internal
921
     */
922
    public ngOnChanges(changes: SimpleChanges): void {
923
        if (changes.editMode && changes.editMode.currentValue && this.formControl) {
290,521✔
924
            // ensure when values change, form control is forced to be marked as touche.
925
            this.formControl.valueChanges.pipe(takeWhile(() => this.editMode)).subscribe(() => this.formControl.markAsTouched());
397✔
926
            // while in edit mode subscribe to value changes on the current form control and set to editValue
927
            this.formControl.statusChanges.pipe(takeWhile(() => this.editMode)).subscribe(status => {
397✔
928
                if (status === 'INVALID' && this.errorTooltip.length > 0) {
189!
929
                    this.cdr.detectChanges();
×
930
                    const tooltip = this.errorTooltip.first;
×
UNCOV
931
                    this.resizeAndRepositionOverlayById(tooltip.overlayId, this.errorTooltip.first.element.offsetWidth);
×
932
                }
933
            });
934
        }
935
        if (changes.value && !changes.value.firstChange) {
290,521✔
936
            if (this.highlight) {
47,786✔
937
                this.highlight.lastSearchInfo.searchText = this.grid.lastSearchInfo.searchText;
45,394✔
938
                this.highlight.lastSearchInfo.caseSensitive = this.grid.lastSearchInfo.caseSensitive;
45,394✔
939
                this.highlight.lastSearchInfo.exactMatch = this.grid.lastSearchInfo.exactMatch;
45,394✔
940
            }
941
        }
942
    }
943

944

945

946
    /**
947
     * @hidden @internal
948
     */
949
    private resizeAndRepositionOverlayById(overlayId: string, newSize: number) {
950
        const overlay = this.overlayService.getOverlayById(overlayId);
×
951
        if (!overlay) return;
×
952
        overlay.initialSize.width = newSize;
×
953
        overlay.elementRef.nativeElement.parentElement.style.width = newSize + 'px';
×
UNCOV
954
        this.overlayService.reposition(overlayId);
×
955
    }
956

957
    /**
958
     * Starts/ends edit mode for the cell.
959
     *
960
     * ```typescript
961
     * cell.setEditMode(true);
962
     * ```
963
     */
964
    public setEditMode(value: boolean): void {
965
        if (this.intRow.deleted) {
30!
UNCOV
966
            return;
×
967
        }
968
        if (this.editable && value) {
30✔
969
            if (this.grid.crudService.cellInEditMode) {
26✔
970
                this.grid.gridAPI.update_cell(this.grid.crudService.cell);
5✔
971
                this.grid.crudService.endCellEdit();
5✔
972
            }
973
            this.grid.crudService.enterEditMode(this);
26✔
974
        } else {
975
            this.grid.crudService.endCellEdit();
4✔
976
        }
977
        this.grid.notifyChanges();
30✔
978
    }
979

980
    /**
981
     * Sets new value to the cell.
982
     * ```typescript
983
     * this.cell.update('New Value');
984
     * ```
985
     *
986
     * @memberof IgxGridCellComponent
987
     */
988
    // TODO: Refactor
989
    public update(val: any) {
990
        if (this.intRow.deleted) {
33!
UNCOV
991
            return;
×
992
        }
993

994
        let cell = this.grid.crudService.cell;
33✔
995
        if (!cell) {
33✔
996
            cell = this.grid.crudService.createCell(this);
14✔
997
        }
998
        cell.editValue = val;
33✔
999
        this.grid.gridAPI.update_cell(cell);
33✔
1000
        this.grid.crudService.endCellEdit();
33✔
1001
        this.cdr.markForCheck();
33✔
1002
    }
1003

1004
    /**
1005
     *
1006
     * @hidden
1007
     * @internal
1008
     */
1009
    public pointerdown = (event: PointerEvent) => {
152,254✔
1010
        if (this.cellSelectionMode !== GridSelectionMode.multiple) {
424✔
1011
            this.activate(event);
23✔
1012
            return;
23✔
1013
        }
1014
        if (!this.platformUtil.isLeftClick(event)) {
401✔
1015
            event.preventDefault();
4✔
1016
            this.grid.navigation.setActiveNode({ rowIndex: this.rowIndex, colIndex: this.visibleColumnIndex });
4✔
1017
            this.selectionService.addKeyboardRange();
4✔
1018
            this.selectionService.initKeyboardState();
4✔
1019
            this.selectionService.primaryButton = false;
4✔
1020
            // Ensure RMB Click on edited cell does not end cell editing
1021
            if (!this.selected) {
4✔
1022
                this.grid.crudService.updateCell(true, event);
2✔
1023
            }
1024
            return;
4✔
1025
        } else {
1026
            this.selectionService.primaryButton = true;
397✔
1027
        }
1028
        this.selectionService.pointerDown(this.selectionNode, event.shiftKey, event.ctrlKey);
397✔
1029
        this.activate(event);
397✔
1030
    };
1031

1032
    /**
1033
     *
1034
     * @hidden
1035
     * @internal
1036
     */
1037
    public pointerenter = (event: PointerEvent) => {
152,254✔
1038
        const isHierarchicalGrid = this.grid.type === 'hierarchical';
118✔
1039
        if (isHierarchicalGrid && (!this.grid.navigation?.activeNode?.gridID || this.grid.navigation.activeNode.gridID !== this.gridID)) {
118✔
1040
            return;
3✔
1041
        }
1042
        const dragMode = this.selectionService.pointerEnter(this.selectionNode, event);
115✔
1043
        if (dragMode) {
115✔
1044
            this.grid.cdr.detectChanges();
114✔
1045
        }
1046
    };
1047

1048
    /**
1049
     * @hidden
1050
     * @internal
1051
     */
1052
    public focusout = () => {
152,254✔
1053
        this.closeErrorTooltip();
48✔
1054
    }
1055

1056
    private closeErrorTooltip() {
1057
        const tooltip = this.errorTooltip.first;
48✔
1058
        if (tooltip) {
48!
UNCOV
1059
            tooltip.close();
×
1060
        }
1061
    }
1062

1063
    /**
1064
     * @hidden
1065
     * @internal
1066
     */
1067
    public pointerup = (event: PointerEvent) => {
152,254✔
1068
        const isHierarchicalGrid = this.grid.type === 'hierarchical';
398✔
1069
        if (!this.platformUtil.isLeftClick(event) || (isHierarchicalGrid && (!this.grid.navigation?.activeNode?.gridID ||
398✔
1070
            this.grid.navigation.activeNode.gridID !== this.gridID))) {
1071
            return;
4✔
1072
        }
1073
        if (this.selectionService.pointerUp(this.selectionNode, this.grid.rangeSelected)) {
394✔
1074
            this.grid.cdr.detectChanges();
49✔
1075
        }
1076
    };
1077

1078
    /**
1079
     * @hidden
1080
     * @internal
1081
     */
1082
    public activate(event: FocusEvent | KeyboardEvent) {
1083
        const node = this.selectionNode;
970✔
1084
        let shouldEmitSelection = !this.selectionService.isActiveNode(node);
970✔
1085

1086
        if (this.selectionService.primaryButton) {
970!
1087
            const currentActive = this.selectionService.activeElement;
970✔
1088
            if (this.cellSelectionMode === GridSelectionMode.single && (event as any)?.ctrlKey && this.selected) {
970✔
1089
                this.selectionService.activeElement = null;
1✔
1090
                shouldEmitSelection = true;
1✔
1091
            } else {
1092
                this.selectionService.activeElement = node;
969✔
1093
            }
1094
            const cancel = this._updateCRUDStatus(event);
970✔
1095
            if (cancel) {
970✔
1096
                this.selectionService.activeElement = currentActive;
2✔
1097
                return;
2✔
1098
            }
1099

1100
            const activeElement = this.selectionService.activeElement;
968✔
1101
            const row = activeElement ? this.grid.gridAPI.get_row_by_index(activeElement.row) : null;
968✔
1102
            if (this.grid.crudService.rowEditingBlocked && row && this.intRow.key !== row.key) {
968!
UNCOV
1103
                return;
×
1104
            }
1105

1106
        } else {
1107
            this.selectionService.activeElement = null;
×
1108
            if (this.grid.crudService.cellInEditMode && !this.editMode) {
×
UNCOV
1109
                this.grid.crudService.updateCell(true, event);
×
1110
            }
1111
        }
1112

1113
        this.grid.navigation.setActiveNode({ row: this.rowIndex, column: this.visibleColumnIndex });
968✔
1114

1115
        const isTargetErrorIcon = event && event.target && event.target === this.errorIcon?.el.nativeElement
968✔
1116
        if (this.isInvalid && !isTargetErrorIcon) {
968✔
1117
            this.cdr.detectChanges();
1✔
1118
            this.openErrorTooltip();
1✔
1119
            this.grid.activeNodeChange.pipe(first()).subscribe(() => {
1✔
UNCOV
1120
                this.closeErrorTooltip();
×
1121
            });
1122
        }
1123
        this.selectionService.primaryButton = true;
968✔
1124
        if (this.cellSelectionMode === GridSelectionMode.multiple && this.selectionService.activeElement) {
968✔
1125
            if (this.selectionService.isInMap(this.selectionService.activeElement) && (event as any)?.ctrlKey && !(event as any)?.shiftKey) {
936✔
1126
                this.selectionService.remove(this.selectionService.activeElement);
3✔
1127
                shouldEmitSelection = true;
3✔
1128
            } else {
1129
                this.selectionService.add(this.selectionService.activeElement, false); // pointer events handle range generation
933✔
1130
                this.selectionService.keyboardStateOnFocus(node, this.grid.rangeSelected, this.nativeElement);
933✔
1131
            }
1132
        }
1133
        if (this.grid.isCellSelectable && shouldEmitSelection) {
968✔
1134
            this.zone.run(() => this.grid.selected.emit({ cell: this.getCellType(), event }));
941✔
1135
        }
1136
    }
1137

1138
    /**
1139
     * If the provided string matches the text in the cell, the text gets highlighted.
1140
     * ```typescript
1141
     * this.cell.highlightText('Cell Value', true);
1142
     * ```
1143
     *
1144
     * @memberof IgxGridCellComponent
1145
     */
1146
    public highlightText(text: string, caseSensitive?: boolean, exactMatch?: boolean): number {
1147
        return this.highlight && this.column.searchable ? this.highlight.highlight(text, caseSensitive, exactMatch) : 0;
160,651✔
1148
    }
1149

1150
    /**
1151
     * Clears the highlight of the text in the cell.
1152
     * ```typescript
1153
     * this.cell.clearHighLight();
1154
     * ```
1155
     *
1156
     * @memberof IgxGridCellComponent
1157
     */
1158
    public clearHighlight() {
1159
        if (this.highlight && this.column.searchable) {
40✔
1160
            this.highlight.clearHighlight();
40✔
1161
        }
1162
    }
1163

1164
    /**
1165
     * @hidden
1166
     * @internal
1167
     */
1168
    public calculateSizeToFit(range: any): number {
1169
        return this.platformUtil.getNodeSizeViaRange(range, this.nativeElement);
214✔
1170
    }
1171

1172
    /**
1173
     * @hidden
1174
     * @internal
1175
     */
1176
    public get searchMetadata() {
1177
        const meta = new Map<string, any>();
302,801✔
1178
        meta.set('pinned', this.grid.isRecordPinnedByViewIndex(this.intRow.index));
302,801✔
1179
        return meta;
302,801✔
1180
    }
1181

1182
    /**
1183
     * @hidden
1184
     * @internal
1185
     */
1186
    private _updateCRUDStatus(event?: Event) {
1187
        if (this.editMode) {
970✔
1188
            return;
48✔
1189
        }
1190

1191
        let editableArgs;
1192
        const crud = this.grid.crudService;
922✔
1193
        const editableCell = this.grid.crudService.cell;
922✔
1194
        const editMode = !!(crud.row || crud.cell);
922✔
1195

1196
        if (this.editable && editMode && !this.intRow.deleted) {
922✔
1197
            if (editableCell) {
88✔
1198
                editableArgs = this.grid.crudService.updateCell(false, event);
77✔
1199

1200
                /* This check is related with the following issue #6517:
1201
                 * when edit cell that belongs to a column which is sorted and press tab,
1202
                 * the next cell in edit mode is with wrong value /its context is not updated/;
1203
                 * So we reapply sorting before the next cell enters edit mode.
1204
                 * Also we need to keep the notifyChanges below, because of the current
1205
                 * change detection cycle when we have editing with enabled transactions
1206
                 */
1207
                if (this.grid.sortingExpressions.length && this.grid.sortingExpressions.indexOf(editableCell.column.field)) {
77!
UNCOV
1208
                    this.grid.cdr.detectChanges();
×
1209
                }
1210

1211
                if (editableArgs && editableArgs.cancel) {
77✔
1212
                    return true;
2✔
1213
                }
1214

1215
                crud.exitCellEdit(event);
75✔
1216
            }
1217
            this.grid.tbody.nativeElement.focus({ preventScroll: true });
86✔
1218
            this.grid.notifyChanges();
86✔
1219
            crud.enterEditMode(this, event);
86✔
1220
            return false;
86✔
1221
        }
1222

1223
        if (editableCell && crud.sameRow(this.cellID.rowID)) {
834✔
1224
            this.grid.crudService.updateCell(true, event);
1✔
1225
        } else if (editMode && !crud.sameRow(this.cellID.rowID)) {
833✔
1226
            this.grid.crudService.endEdit(true, event);
3✔
1227
        }
1228
    }
1229

1230
    private addPointerListeners(selection) {
1231
        if (selection !== GridSelectionMode.multiple) {
152,314✔
1232
            return;
604✔
1233
        }
1234
        this.nativeElement.addEventListener('pointerenter', this.pointerenter);
151,710✔
1235
        this.nativeElement.addEventListener('pointerup', this.pointerup);
151,710✔
1236
        this.nativeElement.addEventListener('focusout', this.focusout);
151,710✔
1237
    }
1238

1239
    private removePointerListeners(selection) {
1240
        if (selection !== GridSelectionMode.multiple) {
151,821✔
1241
            return;
852✔
1242
        }
1243
        this.nativeElement.removeEventListener('pointerenter', this.pointerenter);
150,969✔
1244
        this.nativeElement.removeEventListener('pointerup', this.pointerup);
150,969✔
1245
        this.nativeElement.removeEventListener('focusout', this.focusout);
150,969✔
1246
    }
1247

1248
    private getCellType(useRow?: boolean): CellType {
1249
        const rowID = useRow ? this.grid.createRow(this.intRow.index, this.intRow.data) : this.intRow.index;
3,349✔
1250
        return new IgxGridCell(this.grid, rowID, this.column);
3,349✔
1251
    }
1252
}
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