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

IgniteUI / igniteui-angular / 23353730325

20 Mar 2026 05:03PM UTC coverage: 9.784% (-79.5%) from 89.264%
23353730325

Pull #17069

github

web-flow
Merge cfa7e86d1 into a4dc50177
Pull Request #17069: fix(IgxGrid): Do not apply width constraint to groups.

921 of 16963 branches covered (5.43%)

Branch coverage included in aggregate %.

1 of 3 new or added lines in 1 file covered. (33.33%)

25213 existing lines in 340 files now uncovered.

3842 of 31721 relevant lines covered (12.11%)

6.13 hits per line

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

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

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

29
import {
30
    PlatformUtil,
31
    AutoPositionStrategy,
32
    HorizontalAlignment,
33
    IgxOverlayService,
34
    GridColumnDataType,
35
    ColumnType,
36
    IgxNumberFormatterPipe,
37
    IgxDateFormatterPipe,
38
    IgxCurrencyFormatterPipe,
39
    IgxPercentFormatterPipe
40
} from 'igniteui-angular/core';
41
import { IgxGridSelectionService } from './selection/selection.service';
42
import { HammerGesturesManager } from 'igniteui-angular/core';
43
import { GridSelectionMode } from './common/enums';
44
import { CellType, IgxCellTemplateContext, IGX_GRID_BASE, RowType } from './common/grid.interface';
45
import { IgxRowDirective } from './row.directive';
46
import { ISearchInfo } from './common/events';
47
import { IgxGridCell } from './grid-public-cell';
48
import { ISelectionNode } from './common/types';
49
import { IgxIconComponent } from 'igniteui-angular/icon';
50
import { IgxGridCellImageAltPipe, IgxStringReplacePipe, IgxColumnFormatterPipe } from './common/pipes';
51
import {
52
    IgxTooltipDirective,
53
    IgxTooltipTargetDirective,
54
    IgxDateTimeEditorDirective,
55
    IgxTextSelectionDirective,
56
    IgxFocusDirective,
57
    IgxTextHighlightDirective
58
 } from 'igniteui-angular/directives';
59
import { fadeOut, scaleInCenter } from 'igniteui-angular/animations';
60
import { IgxChipComponent } from 'igniteui-angular/chips';
61
import { IgxInputDirective, IgxInputGroupComponent, IgxPrefixDirective, IgxSuffixDirective } from 'igniteui-angular/input-group';
62
import { IgxCheckboxComponent } from 'igniteui-angular/checkbox';
63
import { IgxDatePickerComponent } from 'igniteui-angular/date-picker';
64
import { IgxTimePickerComponent } from 'igniteui-angular/time-picker';
65

66
/**
67
 * Providing reference to `IgxGridCellComponent`:
68
 * ```typescript
69
 * @ViewChild('grid', { read: IgxGridComponent })
70
 *  public grid: IgxGridComponent;
71
 * ```
72
 * ```typescript
73
 *  let column = this.grid.columnList.first;
74
 * ```
75
 * ```typescript
76
 *  let cell = column.cells[0];
77
 * ```
78
 */
79
@Component({
80
    changeDetection: ChangeDetectionStrategy.OnPush,
81
    selector: 'igx-grid-cell',
82
    templateUrl: './cell.component.html',
83
    providers: [HammerGesturesManager],
84
    imports: [
85
        NgClass,
86
        NgTemplateOutlet,
87
        IgxNumberFormatterPipe,
88
        IgxPercentFormatterPipe,
89
        IgxCurrencyFormatterPipe,
90
        IgxDateFormatterPipe,
91
        ReactiveFormsModule,
92
        IgxChipComponent,
93
        IgxTextHighlightDirective,
94
        IgxIconComponent,
95
        IgxInputGroupComponent,
96
        IgxInputDirective,
97
        IgxFocusDirective,
98
        IgxTextSelectionDirective,
99
        IgxCheckboxComponent,
100
        IgxDatePickerComponent,
101
        IgxTimePickerComponent,
102
        IgxDateTimeEditorDirective,
103
        IgxPrefixDirective,
104
        IgxSuffixDirective,
105
        IgxTooltipTargetDirective,
106
        IgxTooltipDirective,
107
        IgxGridCellImageAltPipe,
108
        IgxStringReplacePipe,
109
        IgxColumnFormatterPipe
110
    ]
111
})
112
export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellType, AfterViewInit {
3✔
113
    protected selectionService = inject(IgxGridSelectionService);
93✔
114
    public grid = inject(IGX_GRID_BASE);
93✔
115
    protected overlayService = inject(IgxOverlayService);
93✔
116
    public cdr = inject(ChangeDetectorRef);
93✔
117
    private element = inject(ElementRef<HTMLElement>);
93✔
118
    protected zone = inject(NgZone);
93✔
119
    private touchManager = inject(HammerGesturesManager);
93✔
120
    protected platformUtil = inject(PlatformUtil);
93✔
121

122
    private _destroy$ = new Subject<void>();
93✔
123
    /**
124
     * @hidden
125
     * @internal
126
     */
127
    @HostBinding('class.igx-grid__td--new')
128
    public get isEmptyAddRowCell() {
129
        return this.intRow.addRowUI && (this.value === undefined || this.value === null);
205!
130
    }
131

132
    /**
133
     * @hidden
134
     * @internal
135
     */
136
    @ViewChildren('error', { read: IgxTooltipDirective })
137
    public errorTooltip: QueryList<IgxTooltipDirective>;
138

139
    /**
140
     * @hidden
141
     * @internal
142
     */
143
    @ViewChild('errorIcon', { read: IgxIconComponent, static: false })
144
    public errorIcon: IgxIconComponent;
145

146
    /**
147
     * Gets the default error template.
148
     * @hidden @internal
149
     */
150
    @ViewChild('defaultError', { read: TemplateRef, static: true })
151
    public defaultErrorTemplate: TemplateRef<any>;
152

153
    /**
154
     * Gets the column of the cell.
155
     * ```typescript
156
     *  let cellColumn = this.cell.column;
157
     * ```
158
     *
159
     * @memberof IgxGridCellComponent
160
     */
161
    @Input()
162
    public column: ColumnType;
163

164
    /**
165
     * @hidden
166
     * @internal
167
     */
168
    @Input()
169
    public isPlaceholder: boolean;
170

171
    /**
172
        Gets whether this cell is a merged cell.
173
     */
174
    @Input()
175
    public isMerged: boolean;
176

177
    /**
178
     * @hidden
179
     * @internal
180
     */
181
    protected get formGroup(): FormGroup {
182
        return this.grid.validation.getFormGroup(this.intRow.key);
1,099✔
183
    }
184

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

192
    /**
193
     * Gets the row of the cell.
194
     * ```typescript
195
     * let cellRow = this.cell.row;
196
     * ```
197
     *
198
     * @memberof IgxGridCellComponent
199
     */
200
    @Input()
201
    public get row(): RowType {
UNCOV
202
        return this.grid.createRow(this.intRow.index);
×
203
    }
204

205
    /**
206
     * Gets the data of the row of the cell.
207
     * ```typescript
208
     * let rowData = this.cell.rowData;
209
     * ```
210
     *
211
     * @memberof IgxGridCellComponent
212
     */
213
    @Input()
214
    public rowData: any;
215

216
    /**
217
     * @hidden
218
     * @internal
219
     */
220
    @Input()
221
    public columnData: any;
222

223
    /**
224
     * Sets/gets the template of the cell.
225
     * ```html
226
     * <ng-template #cellTemplate igxCell let-value>
227
     *   <div style="font-style: oblique; color:blueviolet; background:red">
228
     *       <span>{{value}}</span>
229
     *   </div>
230
     * </ng-template>
231
     * ```
232
     * ```typescript
233
     * @ViewChild('cellTemplate',{read: TemplateRef})
234
     * cellTemplate: TemplateRef<any>;
235
     * ```
236
     * ```typescript
237
     * this.cell.cellTemplate = this.cellTemplate;
238
     * ```
239
     * ```typescript
240
     * let template =  this.cell.cellTemplate;
241
     * ```
242
     *
243
     * @memberof IgxGridCellComponent
244
     */
245
    @Input()
246
    public cellTemplate: TemplateRef<any>;
247

248
    @Input()
249
    public cellValidationErrorTemplate: TemplateRef<any>;
250

251
    @Input()
252
    public pinnedIndicator: TemplateRef<any>;
253

254
    /**
255
     * Sets/gets the cell value.
256
     * ```typescript
257
     * this.cell.value = "Cell Value";
258
     * ```
259
     * ```typescript
260
     * let cellValue = this.cell.value;
261
     * ```
262
     *
263
     * @memberof IgxGridCellComponent
264
     */
265
    @Input()
266
    public value: any;
267

268
    /**
269
     * Gets the cell formatter.
270
     * ```typescript
271
     * let cellForamatter = this.cell.formatter;
272
     * ```
273
     *
274
     * @memberof IgxGridCellComponent
275
     */
276
    @Input()
277
    public formatter: (value: any, rowData?: any, columnData?: any) => any;
278

279
    /**
280
     * Gets the cell template context object.
281
     * ```typescript
282
     *  let context = this.cell.context();
283
     * ```
284
     *
285
     * @memberof IgxGridCellComponent
286
     */
287
    public get context(): IgxCellTemplateContext {
288
        const getCellType = () => this.getCellType(true);
186✔
289
        const ctx: IgxCellTemplateContext = {
186✔
290
            $implicit: this.value,
291
            additionalTemplateContext: this.column.additionalTemplateContext,
292
            get cell() {
293
                /* Turns the `cell` property from the template context object into lazy-evaluated one.
294
                 * Otherwise on each detection cycle the cell template is recreating N cell instances where
295
                 * N = number of visible cells in the grid, leading to massive performance degradation in large grids.
296
                 */
UNCOV
297
                return getCellType();
×
298
            }
299
        };
300
        if (this.editMode) {
186!
UNCOV
301
            ctx.formControl = this.formControl;
×
302
        }
303
        if (this.isInvalid) {
186!
UNCOV
304
            ctx.defaultErrorTemplate = this.defaultErrorTemplate;
×
305
        }
306
        return ctx;
186✔
307
    }
308

309
    /**
310
     * Gets the cell template.
311
     * ```typescript
312
     * let template = this.cell.template;
313
     * ```
314
     *
315
     * @memberof IgxGridCellComponent
316
     */
317
    public get template(): TemplateRef<any> {
318
        if (this.isPlaceholder) {
93!
UNCOV
319
            return this.emptyCellTemplate;
×
320
        }
321
        if (this.editMode && this.formGroup) {
93!
UNCOV
322
            const inlineEditorTemplate = this.column.inlineEditorTemplate;
×
UNCOV
323
            return inlineEditorTemplate ? inlineEditorTemplate : this.inlineEditorTemplate;
×
324
        }
325
        if (this.cellTemplate) {
93!
UNCOV
326
            return this.cellTemplate;
×
327
        }
328
        if (this.grid.rowEditable && this.intRow.addRowUI) {
93!
UNCOV
329
            return this.addRowCellTemplate;
×
330
        }
331
        return this.defaultCellTemplate;
93✔
332
    }
333

334
    /**
335
     * Gets the pinned indicator template.
336
     * ```typescript
337
     * let template = this.cell.pinnedIndicatorTemplate;
338
     * ```
339
     *
340
     * @memberof IgxGridCellComponent
341
     */
342
    public get pinnedIndicatorTemplate() {
343
        if (this.pinnedIndicator) {
93!
344
            return this.pinnedIndicator;
×
345
        }
346
        return this.defaultPinnedIndicator;
93✔
347
    }
348

349
    /**
350
     * Gets the `id` of the grid in which the cell is stored.
351
     * ```typescript
352
     * let gridId = this.cell.gridID;
353
     * ```
354
     *
355
     * @memberof IgxGridCellComponent
356
     */
357
    public get gridID(): any {
358
        return this.intRow.gridID;
93✔
359
    }
360

361

362
    /**
363
     * Gets the `index` of the row where the cell is stored.
364
     * ```typescript
365
     * let rowIndex = this.cell.rowIndex;
366
     * ```
367
     *
368
     * @memberof IgxGridCellComponent
369
     */
370
    @HostBinding('attr.data-rowIndex')
371
    public get rowIndex(): number {
372
        return this.intRow.index;
1,025✔
373
    }
374

375
    /**
376
     * Gets the `index` of the cell column.
377
     * ```typescript
378
     * let columnIndex = this.cell.columnIndex;
379
     * ```
380
     *
381
     * @memberof IgxGridCellComponent
382
     */
383
    public get columnIndex(): number {
UNCOV
384
        return this.column.index;
×
385
    }
386

387
    /**
388
     * Returns the column visible index.
389
     * ```typescript
390
     * let visibleColumnIndex = this.cell.visibleColumnIndex;
391
     * ```
392
     *
393
     * @memberof IgxGridCellComponent
394
     */
395
    @HostBinding('attr.data-visibleIndex')
396
    @Input()
397
    public get visibleColumnIndex() {
398
        return this.column.columnLayoutChild ? this.column.visibleIndex : this._vIndex;
820!
399
    }
400

401
    public set visibleColumnIndex(val) {
402
        this._vIndex = val;
93✔
403
    }
404

405
    /**
406
     * Gets the ID of the cell.
407
     * ```typescript
408
     * let cellID = this.cell.cellID;
409
     * ```
410
     *
411
     * @memberof IgxGridCellComponent
412
     */
413
    public get cellID() {
UNCOV
414
        const primaryKey = this.grid.primaryKey;
×
UNCOV
415
        const rowID = primaryKey ? this.rowData[primaryKey] : this.rowData;
×
UNCOV
416
        return { rowID, columnID: this.columnIndex, rowIndex: this.rowIndex };
×
417
    }
418

419
    @HostBinding('attr.id')
420
    public get attrCellID() {
421
        return `${this.intRow.gridID}_${this.rowIndex}_${this.visibleColumnIndex}`;
205✔
422
    }
423

424
    @HostBinding('attr.title')
425
    public get title() {
426
        if (this.editMode || this.cellTemplate || this.errorShowing) {
205!
UNCOV
427
            return '';
×
428
        }
429

430
        if (this.formatter) {
205!
UNCOV
431
            return this.formatter(this.value, this.rowData, this.columnData);
×
432
        }
433

434
        const args = this.column.pipeArgs;
205✔
435
        const locale = this.grid.locale;
205✔
436
        const i18nFormatter = this.grid.i18nFormatter;
205✔
437

438
        switch (this.column.dataType) {
205!
439
            case GridColumnDataType.Percent:
UNCOV
440
                return i18nFormatter.formatPercent(this.value, locale, args.digitsInfo);
×
441
            case GridColumnDataType.Currency:
UNCOV
442
                return i18nFormatter.formatCurrency(this.value, locale, args.display, this.currencyCode, args.digitsInfo);
×
443
            case GridColumnDataType.Date:
444
            case GridColumnDataType.DateTime:
445
            case GridColumnDataType.Time:
UNCOV
446
                return i18nFormatter.formatDate(this.value, args.format, locale, args.timezone);
×
447
        }
448
        return this.value;
205✔
449
    }
450

451
    @HostBinding('class.igx-grid__td--bool-true')
452
    public get booleanClass() {
453
        return this.column.dataType === 'boolean' && this.value;
205!
454
    }
455

456
    /**
457
     * Returns a reference to the nativeElement of the cell.
458
     * ```typescript
459
     * let cellNativeElement = this.cell.nativeElement;
460
     * ```
461
     *
462
     * @memberof IgxGridCellComponent
463
     */
464
    public get nativeElement(): HTMLElement {
465
        return this.element.nativeElement;
768✔
466
    }
467

468
    /**
469
     * @hidden
470
     * @internal
471
     */
472
    @Input()
473
    public get cellSelectionMode() {
474
        return this._cellSelection;
186✔
475
    }
476

477
    public set cellSelectionMode(value) {
478
        if (this._cellSelection === value) {
93✔
479
            return;
93✔
480
        }
UNCOV
481
        this.zone.runOutsideAngular(() => {
×
UNCOV
482
            if (value === GridSelectionMode.multiple) {
×
UNCOV
483
                this.addPointerListeners(value);
×
484
            } else {
UNCOV
485
                this.removePointerListeners(this._cellSelection);
×
486
            }
487
        });
UNCOV
488
        this._cellSelection = value;
×
489
    }
490

491
    /**
492
     * @hidden
493
     * @internal
494
     */
495
    @Input()
496
    public set lastSearchInfo(value: ISearchInfo) {
497
        this._lastSearchInfo = value;
93✔
498
        this.highlightText(this._lastSearchInfo.searchText, this._lastSearchInfo.caseSensitive, this._lastSearchInfo.exactMatch);
93✔
499
    }
500

501
    /**
502
     * @hidden
503
     * @internal
504
     */
505
    @Input()
506
    @HostBinding('class.igx-grid__td--pinned-last')
507
    public lastPinned = false;
93✔
508

509
    /**
510
     * @hidden
511
     * @internal
512
     */
513
    @Input()
514
    @HostBinding('class.igx-grid__td--pinned-first')
515
    public firstPinned = false;
93✔
516

517
    /**
518
     * Returns whether the cell is in edit mode.
519
     */
520
    @Input({ transform: booleanAttribute })
521
    @HostBinding('class.igx-grid__td--editing')
522
    public editMode = false;
93✔
523

524
    /**
525
     * Sets/get the `role` property of the cell.
526
     * Default value is `"gridcell"`.
527
     * ```typescript
528
     * this.cell.role = 'grid-cell';
529
     * ```
530
     * ```typescript
531
     * let cellRole = this.cell.role;
532
     * ```
533
     *
534
     * @memberof IgxGridCellComponent
535
     */
536
    @HostBinding('attr.role')
537
    public role = 'gridcell';
93✔
538

539
    /**
540
     * Gets whether the cell is editable.
541
     * ```typescript
542
     * let isCellReadonly = this.cell.readonly;
543
     * ```
544
     *
545
     * @memberof IgxGridCellComponent
546
     */
547
    @HostBinding('attr.aria-readonly')
548
    public get readonly(): boolean {
549
        return !this.editable;
205✔
550
    }
551

552
    /** @hidden @internal */
553
    @HostBinding('attr.aria-describedby')
554
    public get ariaDescribeBy() {
555
        return this.isInvalid ? this.ariaErrorMessage : null;
205!
556
    }
557

558
    /** @hidden @internal */
559
    public get ariaErrorMessage() {
UNCOV
560
        return this.grid.id + '_' + this.column.field + '_' + this.intRow.index + '_error';
×
561
    }
562

563
    /**
564
     * @hidden
565
     * @internal
566
     */
567
    @HostBinding('class.igx-grid__td--invalid')
568
    @HostBinding('attr.aria-invalid')
569
    public get isInvalid() {
570
        if (this.formGroup) {
894!
UNCOV
571
            const isInvalid = this.grid.validation?.isFieldInvalid(this.formGroup, this.column?.field);
×
UNCOV
572
            return !this.intRow.deleted && isInvalid;
×
573
        }
574
        return false;
894✔
575
    }
576

577
    /**
578
     * @hidden
579
     * @internal
580
     */
581
    @HostBinding('class.igx-grid__td--valid')
582
    public get isValidAfterEdit() {
583
        if (this.formGroup) {
205!
UNCOV
584
            const isValidAfterEdit = this.grid.validation?.isFieldValidAfterEdit(this.formGroup, this.column?.field);
×
UNCOV
585
            return this.editMode && isValidAfterEdit;
×
586
        }
587
        return false;
205✔
588
    }
589

590
    /**
591
     * Gets the formControl responsible for value changes and validation for this cell.
592
     */
593
    protected get formControl(): FormControl {
UNCOV
594
        return this.grid.validation.getFormControl(this.intRow.key, this.column.field) as FormControl;
×
595
    }
596

597
    public get gridRowSpan(): number {
UNCOV
598
        return this.column.gridRowSpan;
×
599
    }
600

601
    public get gridColumnSpan(): number {
UNCOV
602
        return this.column.gridColumnSpan;
×
603
    }
604

605
    public get rowEnd(): number {
606
        return this.column.rowEnd;
×
607
    }
608

609
    public get colEnd(): number {
610
        return this.column.colEnd;
×
611
    }
612

613
    public get rowStart(): number {
614
        return this.column.rowStart;
×
615
    }
616

617
    public get colStart(): number {
618
        return this.column.colStart;
×
619
    }
620

621
    /**
622
     * Gets the width of the cell.
623
     * ```typescript
624
     * let cellWidth = this.cell.width;
625
     * ```
626
     *
627
     * @memberof IgxGridCellComponent
628
     */
629
    @Input()
630
    public width = '';
93✔
631

632
    /**
633
     * @hidden
634
     */
635
    @Input()
636
    @HostBinding('class.igx-grid__td--active')
637
    public active = false;
93✔
638

639
    @HostBinding('attr.aria-selected')
640
    public get ariaSelected() {
641
        return this.selected || this.column.selected || this.intRow.selected;
205✔
642
    }
643

644
    /**
645
     * Gets whether the cell is selected.
646
     * ```typescript
647
     * let isSelected = this.cell.selected;
648
     * ```
649
     *
650
     * @memberof IgxGridCellComponent
651
     */
652
    @HostBinding('class.igx-grid__td--selected')
653
    public get selected() {
654
        return this.selectionService.selected(this.selectionNode);
410✔
655
    }
656

657
    /**
658
     * Selects/deselects the cell.
659
     * ```typescript
660
     * this.cell.selected = true.
661
     * ```
662
     *
663
     * @memberof IgxGridCellComponent
664
     */
665
    public set selected(val: boolean) {
UNCOV
666
        const node = this.selectionNode;
×
UNCOV
667
        if (val) {
×
UNCOV
668
            this.selectionService.add(node);
×
669
        } else {
670
            this.selectionService.remove(node);
×
671
        }
UNCOV
672
        this.grid.notifyChanges();
×
673
    }
674

675
    /**
676
     * Gets whether the cell column is selected.
677
     * ```typescript
678
     * let isCellColumnSelected = this.cell.columnSelected;
679
     * ```
680
     *
681
     * @memberof IgxGridCellComponent
682
     */
683
    @HostBinding('class.igx-grid__td--column-selected')
684
    public get columnSelected() {
685
        return this.selectionService.isColumnSelected(this.column.field);
205✔
686
    }
687

688
    /**
689
     * Sets the current edit value while a cell is in edit mode.
690
     * Only for cell editing mode.
691
     * ```typescript
692
     * this.cell.editValue = value;
693
     * ```
694
     *
695
     * @memberof IgxGridCellComponent
696
     */
697
    public set editValue(value) {
UNCOV
698
        if (this.grid.crudService.cellInEditMode) {
×
UNCOV
699
            this.grid.crudService.cell.editValue = value;
×
700
        }
701
    }
702

703
    /**
704
     * Gets the current edit value while a cell is in edit mode.
705
     * Only for cell editing mode.
706
     * ```typescript
707
     * let editValue = this.cell.editValue;
708
     * ```
709
     *
710
     * @memberof IgxGridCellComponent
711
     */
712
    public get editValue() {
UNCOV
713
        if (this.grid.crudService.cellInEditMode) {
×
UNCOV
714
            return this.grid.crudService.cell.editValue;
×
715
        }
716
    }
717

718
    /**
719
     * Returns whether the cell is editable.
720
     */
721
    public get editable(): boolean {
722
        return this.column.editable && !this.intRow.disabled;
205!
723
    }
724

725
    /**
726
     * @hidden
727
     */
728
    @Input()
729
    @HostBinding('class.igx-grid__td--row-pinned-first')
730
    public displayPinnedChip = false;
93✔
731

732
    @HostBinding('style.min-height.px')
733
    protected get minHeight() {
734
        if ((this.grid as any).isCustomSetRowHeight) {
205!
UNCOV
735
            return this.grid.renderedRowHeight;
×
736
        }
737
    }
738

739
    @HostBinding('attr.aria-rowindex')
740
    protected get ariaRowIndex(): number {
741
        // +2 because aria-rowindex is 1-based and the first row is the header
742
        return this.rowIndex + 2;
205✔
743
    }
744

745
    @HostBinding('attr.aria-colindex')
746
    protected get ariaColIndex(): number {
747
        return this.column.index + 1;
205✔
748
    }
749

750
    @ViewChild('defaultCell', { read: TemplateRef, static: true })
751
    protected defaultCellTemplate: TemplateRef<any>;
752

753
    @ViewChild('emptyCell', { read: TemplateRef, static: true })
754
    protected emptyCellTemplate: TemplateRef<any>;
755

756
    @ViewChild('defaultPinnedIndicator', { read: TemplateRef, static: true })
757
    protected defaultPinnedIndicator: TemplateRef<any>;
758

759
    @ViewChild('inlineEditor', { read: TemplateRef, static: true })
760
    protected inlineEditorTemplate: TemplateRef<any>;
761

762
    @ViewChild('addRowCell', { read: TemplateRef, static: true })
763
    protected addRowCellTemplate: TemplateRef<any>;
764

765
    @ViewChild(IgxTextHighlightDirective, { read: IgxTextHighlightDirective })
766
    protected set highlight(value: IgxTextHighlightDirective) {
767
        this._highlight = value;
93✔
768

769
        if (this._highlight && this.grid.lastSearchInfo.searchText) {
93!
UNCOV
770
            this._highlight.highlight(this.grid.lastSearchInfo.searchText,
×
771
                this.grid.lastSearchInfo.caseSensitive,
772
                this.grid.lastSearchInfo.exactMatch);
UNCOV
773
            this._highlight.activateIfNecessary();
×
774
        }
775
    }
776

777
    protected get highlight() {
778
        return this._highlight;
93✔
779
    }
780

781
    protected get selectionNode(): ISelectionNode {
782
        return {
410✔
783
            row: this.rowIndex,
784
            column: this.column.columnLayoutChild ? this.column.parent.visibleIndex : this.visibleColumnIndex,
410!
785
            layout: this.column.columnLayoutChild ? {
410!
786
                rowStart: this.column.rowStart,
787
                colStart: this.column.colStart,
788
                rowEnd: this.column.rowEnd,
789
                colEnd: this.column.colEnd,
790
                columnVisibleIndex: this.visibleColumnIndex
791
            } : null
792
        };
793
    }
794

795
    /**
796
     * Sets/gets the highlight class of the cell.
797
     * Default value is `"igx-highlight"`.
798
     * ```typescript
799
     * let highlightClass = this.cell.highlightClass;
800
     * ```
801
     * ```typescript
802
     * this.cell.highlightClass = 'igx-cell-highlight';
803
     * ```
804
     *
805
     * @memberof IgxGridCellComponent
806
     */
807
    public highlightClass = 'igx-highlight';
93✔
808

809
    /**
810
     * Sets/gets the active highlight class class of the cell.
811
     * Default value is `"igx-highlight__active"`.
812
     * ```typescript
813
     * let activeHighlightClass = this.cell.activeHighlightClass;
814
     * ```
815
     * ```typescript
816
     * this.cell.activeHighlightClass = 'igx-cell-highlight_active';
817
     * ```
818
     *
819
     * @memberof IgxGridCellComponent
820
     */
821
    public activeHighlightClass = 'igx-highlight__active';
93✔
822

823
    /** @hidden @internal */
824
    public get step(): number {
UNCOV
825
        const digitsInfo = this.column.pipeArgs.digitsInfo;
×
UNCOV
826
        if (!digitsInfo) {
×
827
            return 1;
×
828
        }
UNCOV
829
        const step = +digitsInfo.substr(digitsInfo.indexOf('.') + 1, 1);
×
UNCOV
830
        return 1 / (Math.pow(10, step));
×
831
    }
832

833
    /** @hidden @internal */
834
    public get currencyCode(): string {
UNCOV
835
        return this.grid.i18nFormatter.getCurrencyCode(this.grid.locale, this.column.pipeArgs.currencyCode);
×
836
    }
837

838
    /** @hidden @internal */
839
    public get currencyCodeSymbol(): string {
UNCOV
840
        return this.grid.i18nFormatter.getCurrencySymbol(this.currencyCode, this.grid.locale);
×
841
    }
842

843
    protected _lastSearchInfo: ISearchInfo;
844
    private _highlight: IgxTextHighlightDirective;
845
    private _cellSelection: GridSelectionMode = GridSelectionMode.multiple;
93✔
846
    private _vIndex = -1;
93✔
847

848

849

850
    /**
851
     * @hidden
852
     * @internal
853
     */
854
    @HostListener('dblclick', ['$event'])
855
    public onDoubleClick = (event: MouseEvent) => {
93✔
UNCOV
856
        if (event.type === 'doubletap') {
×
857
            // prevent double-tap to zoom on iOS
UNCOV
858
            event.preventDefault();
×
859
        }
UNCOV
860
        if (this.editable && !this.editMode && !this.intRow.deleted && !this.grid.crudService.rowEditingBlocked) {
×
UNCOV
861
            this.grid.crudService.enterEditMode(this, event as Event);
×
862
        }
863

UNCOV
864
        this.grid.doubleClick.emit({
×
865
            cell: this.getCellType(),
866
            event
867
        });
868
    };
869

870
    /**
871
     * @hidden
872
     * @internal
873
     */
874
    @HostListener('click', ['$event'])
875
    public onClick(event: MouseEvent) {
UNCOV
876
        this.grid.cellClick.emit({
×
877
            cell: this.getCellType(),
878
            event
879
        });
880
    }
881

882
    /**
883
     * @hidden
884
     * @internal
885
     */
886
    public ngOnInit() {
887
        this.zone.runOutsideAngular(() => {
93✔
888
            this.nativeElement.addEventListener('pointerdown', this.pointerdown);
93✔
889
            this.addPointerListeners(this.cellSelectionMode);
93✔
890
        });
891
        if (this.platformUtil.isIOS) {
93!
UNCOV
892
            this.touchManager.addEventListener(this.nativeElement, 'doubletap', this.onDoubleClick, {
×
893
                cssProps: {} /* don't disable user-select, etc */
894
            });
895
        }
896

897
    }
898

899
    public ngAfterViewInit() {
900
        this.errorTooltip.changes.pipe(takeUntil(this._destroy$)).subscribe(() => {
93✔
UNCOV
901
            if (this.errorTooltip.length > 0 && this.active) {
×
902
                // error ocurred
UNCOV
903
                this.cdr.detectChanges();
×
UNCOV
904
                this.openErrorTooltip();
×
905
            }
906
        });
907
    }
908

909
    /**
910
     * @hidden
911
     * @internal
912
     */
913
    public errorShowing = false;
93✔
914

915
    private openErrorTooltip() {
UNCOV
916
        const tooltip = this.errorTooltip.first;
×
UNCOV
917
        tooltip.open(
×
918
            {
919
                target: this.errorIcon.el.nativeElement,
920
                closeOnOutsideClick: true,
921
                excludeFromOutsideClick: [this.nativeElement],
922
                closeOnEscape: false,
923
                outlet: this.grid.outlet,
924
                modal: false,
925
                positionStrategy: new AutoPositionStrategy({
926
                    horizontalStartPoint: HorizontalAlignment.Center,
927
                    horizontalDirection: HorizontalAlignment.Center,
928
                    openAnimation: useAnimation(scaleInCenter, { params: { duration: '150ms' } }),
929
                    closeAnimation: useAnimation(fadeOut, { params: { duration: '75ms' } })
930
                })
931
            }
932
        );
933
    }
934

935
    /**
936
     * @hidden
937
     * @internal
938
     */
939
    public ngOnDestroy() {
940
        this.zone.runOutsideAngular(() => {
93✔
941
            this.nativeElement.removeEventListener('pointerdown', this.pointerdown);
93✔
942
            this.removePointerListeners(this.cellSelectionMode);
93✔
943
        });
944
        this.touchManager.destroy();
93✔
945
        this._destroy$.next();
93✔
946
        this._destroy$.complete();
93✔
947
    }
948

949
    /**
950
     * @hidden
951
     * @internal
952
     */
953
    public ngOnChanges(changes: SimpleChanges): void {
954
        if (changes.editMode && changes.editMode.currentValue && this.formControl) {
93!
955
            // ensure when values change, form control is forced to be marked as touche.
UNCOV
956
            this.formControl.valueChanges.pipe(takeWhile(() => this.editMode)).subscribe(() => this.formControl.markAsTouched());
×
957
            // while in edit mode subscribe to value changes on the current form control and set to editValue
UNCOV
958
            this.formControl.statusChanges.pipe(takeWhile(() => this.editMode)).subscribe(status => {
×
UNCOV
959
                if (status === 'INVALID' && this.errorTooltip.length > 0) {
×
960
                    this.cdr.detectChanges();
×
961
                    const tooltip = this.errorTooltip.first;
×
962
                    this.resizeAndRepositionOverlayById(tooltip.overlayId, this.errorTooltip.first.element.offsetWidth);
×
963
                }
964
            });
965
        }
966
        if (changes.value && !changes.value.firstChange) {
93!
UNCOV
967
            if (this.highlight) {
×
UNCOV
968
                this.highlight.lastSearchInfo.searchText = this.grid.lastSearchInfo.searchText;
×
UNCOV
969
                this.highlight.lastSearchInfo.caseSensitive = this.grid.lastSearchInfo.caseSensitive;
×
UNCOV
970
                this.highlight.lastSearchInfo.exactMatch = this.grid.lastSearchInfo.exactMatch;
×
971
            }
UNCOV
972
            const isInEdit = this.grid.rowEditable ? this.row.inEditMode : this.editMode;
×
UNCOV
973
            if (this.formControl && this.formControl.value !== changes.value.currentValue && !isInEdit) {
×
UNCOV
974
                this.formControl.setValue(changes.value.currentValue);
×
975
            }
976
        }
977
    }
978

979

980

981
    /**
982
     * @hidden @internal
983
     */
984
    private resizeAndRepositionOverlayById(overlayId: string, newSize: number) {
985
        const overlay = this.overlayService.getOverlayById(overlayId);
×
986
        if (!overlay) return;
×
987
        overlay.initialSize.width = newSize;
×
988
        overlay.elementRef.nativeElement.parentElement.style.width = newSize + 'px';
×
989
        this.overlayService.reposition(overlayId);
×
990
    }
991

992
    /**
993
     * Starts/ends edit mode for the cell.
994
     *
995
     * ```typescript
996
     * cell.setEditMode(true);
997
     * ```
998
     */
999
    public setEditMode(value: boolean): void {
UNCOV
1000
        if (this.intRow.deleted) {
×
1001
            return;
×
1002
        }
UNCOV
1003
        if (this.editable && value) {
×
UNCOV
1004
            if (this.grid.crudService.cellInEditMode) {
×
UNCOV
1005
                this.grid.gridAPI.update_cell(this.grid.crudService.cell);
×
UNCOV
1006
                this.grid.crudService.endCellEdit();
×
1007
            }
UNCOV
1008
            this.grid.crudService.enterEditMode(this);
×
1009
        } else {
UNCOV
1010
            this.grid.crudService.endCellEdit();
×
1011
        }
UNCOV
1012
        this.grid.notifyChanges();
×
1013
    }
1014

1015
    /**
1016
     * Sets new value to the cell.
1017
     * ```typescript
1018
     * this.cell.update('New Value');
1019
     * ```
1020
     *
1021
     * @memberof IgxGridCellComponent
1022
     */
1023
    // TODO: Refactor
1024
    public update(val: any) {
UNCOV
1025
        if (this.intRow.deleted) {
×
1026
            return;
×
1027
        }
1028

UNCOV
1029
        let cell = this.grid.crudService.cell;
×
UNCOV
1030
        if (!cell) {
×
UNCOV
1031
            cell = this.grid.crudService.createCell(this);
×
1032
        }
UNCOV
1033
        cell.editValue = val;
×
UNCOV
1034
        this.grid.gridAPI.update_cell(cell);
×
UNCOV
1035
        this.grid.crudService.endCellEdit();
×
UNCOV
1036
        this.cdr.markForCheck();
×
1037
    }
1038

1039
    /**
1040
     *
1041
     * @hidden
1042
     * @internal
1043
     */
1044
    public pointerdown = (event: PointerEvent) => {
93✔
1045

UNCOV
1046
        if (this.isMerged) {
×
1047
            // need an approximation of where in the cell the user clicked to get actual index to be activated.
UNCOV
1048
            const scrollOffset = this.grid.verticalScrollContainer.scrollPosition + (event.y - this.grid.tbody.nativeElement.getBoundingClientRect().y);
×
UNCOV
1049
            const targetRowIndex = this.grid.verticalScrollContainer.getIndexAtScroll(scrollOffset);
×
UNCOV
1050
            if (targetRowIndex != this.rowIndex) {
×
1051
                const row = this.grid.rowList.toArray().find(x => x.index === targetRowIndex);
×
1052
                const actualTarget = row.cells.find(x => x.column === this.column);
×
1053
                actualTarget.pointerdown(event);
×
1054
                return;
×
1055
            }
1056
        }
1057

UNCOV
1058
        if (this.cellSelectionMode !== GridSelectionMode.multiple) {
×
UNCOV
1059
            this.activate(event);
×
UNCOV
1060
            return;
×
1061
        }
UNCOV
1062
        if (!this.platformUtil.isLeftClick(event)) {
×
UNCOV
1063
            event.preventDefault();
×
UNCOV
1064
            this.grid.navigation.setActiveNode({ rowIndex: this.rowIndex, colIndex: this.visibleColumnIndex });
×
UNCOV
1065
            this.selectionService.addKeyboardRange();
×
UNCOV
1066
            this.selectionService.initKeyboardState();
×
UNCOV
1067
            this.selectionService.primaryButton = false;
×
1068
            // Ensure RMB Click on edited cell does not end cell editing
UNCOV
1069
            if (!this.selected) {
×
UNCOV
1070
                this.grid.crudService.updateCell(true, event);
×
1071
            }
UNCOV
1072
            return;
×
1073
        } else {
UNCOV
1074
            this.selectionService.primaryButton = true;
×
1075
        }
UNCOV
1076
        this.selectionService.pointerDown(this.selectionNode, event.shiftKey, event.ctrlKey);
×
UNCOV
1077
        this.activate(event);
×
1078
    };
1079

1080
    /**
1081
     *
1082
     * @hidden
1083
     * @internal
1084
     */
1085
    public pointerenter = (event: PointerEvent) => {
93✔
UNCOV
1086
        const isHierarchicalGrid = this.grid.type === 'hierarchical';
×
UNCOV
1087
        if (isHierarchicalGrid && (!this.grid.navigation?.activeNode?.gridID || this.grid.navigation.activeNode.gridID !== this.gridID)) {
×
UNCOV
1088
            return;
×
1089
        }
UNCOV
1090
        const dragMode = this.selectionService.pointerEnter(this.selectionNode, event);
×
UNCOV
1091
        if (dragMode) {
×
UNCOV
1092
            this.grid.cdr.detectChanges();
×
1093
        }
1094
    };
1095

1096
    /**
1097
     * @hidden
1098
     * @internal
1099
     */
1100
    public focusout = () => {
93✔
UNCOV
1101
        this.closeErrorTooltip();
×
1102
    }
1103

1104
    private closeErrorTooltip() {
UNCOV
1105
        const tooltip = this.errorTooltip.first;
×
UNCOV
1106
        if (tooltip) {
×
1107
            tooltip.close();
×
1108
        }
1109
    }
1110

1111
    /**
1112
     * @hidden
1113
     * @internal
1114
     */
1115
    public pointerup = (event: PointerEvent) => {
93✔
UNCOV
1116
        const isHierarchicalGrid = this.grid.type === 'hierarchical';
×
UNCOV
1117
        if (!this.platformUtil.isLeftClick(event) || (isHierarchicalGrid && (!this.grid.navigation?.activeNode?.gridID ||
×
1118
            this.grid.navigation.activeNode.gridID !== this.gridID))) {
UNCOV
1119
            return;
×
1120
        }
UNCOV
1121
        if (this.selectionService.pointerUp(this.selectionNode, this.grid.rangeSelected)) {
×
UNCOV
1122
            this.grid.cdr.detectChanges();
×
1123
        }
1124
    };
1125

1126
    /**
1127
     * @hidden
1128
     * @internal
1129
     */
1130
    public activate(event: FocusEvent | KeyboardEvent) {
UNCOV
1131
        const node = this.selectionNode;
×
UNCOV
1132
        let shouldEmitSelection = !this.selectionService.isActiveNode(node);
×
1133

UNCOV
1134
        if (this.selectionService.primaryButton) {
×
UNCOV
1135
            const currentActive = this.selectionService.activeElement;
×
UNCOV
1136
            if (this.cellSelectionMode === GridSelectionMode.single && (event as any)?.ctrlKey && this.selected) {
×
UNCOV
1137
                this.selectionService.activeElement = null;
×
UNCOV
1138
                shouldEmitSelection = true;
×
1139
            } else {
UNCOV
1140
                this.selectionService.activeElement = node;
×
1141
            }
UNCOV
1142
            const cancel = this._updateCRUDStatus(event);
×
UNCOV
1143
            if (cancel) {
×
UNCOV
1144
                this.selectionService.activeElement = currentActive;
×
UNCOV
1145
                return;
×
1146
            }
1147

UNCOV
1148
            const activeElement = this.selectionService.activeElement;
×
UNCOV
1149
            const row = activeElement ? this.grid.gridAPI.get_row_by_index(activeElement.row) : null;
×
UNCOV
1150
            if (this.grid.crudService.rowEditingBlocked && row && this.intRow.key !== row.key) {
×
1151
                return;
×
1152
            }
1153

1154
        } else {
1155
            this.selectionService.activeElement = null;
×
1156
            if (this.grid.crudService.cellInEditMode && !this.editMode) {
×
1157
                this.grid.crudService.updateCell(true, event);
×
1158
            }
1159
        }
1160

UNCOV
1161
        this.grid.navigation.setActiveNode({ row: this.rowIndex, column: this.visibleColumnIndex });
×
1162

UNCOV
1163
        const isTargetErrorIcon = event && event.target && event.target === this.errorIcon?.el.nativeElement
×
UNCOV
1164
        if (this.isInvalid && !isTargetErrorIcon) {
×
UNCOV
1165
            this.cdr.detectChanges();
×
UNCOV
1166
            this.openErrorTooltip();
×
UNCOV
1167
            this.grid.activeNodeChange.pipe(first()).subscribe(() => {
×
1168
                this.closeErrorTooltip();
×
1169
            });
1170
        }
UNCOV
1171
        this.selectionService.primaryButton = true;
×
UNCOV
1172
        if (this.cellSelectionMode === GridSelectionMode.multiple && this.selectionService.activeElement) {
×
UNCOV
1173
            if (this.selectionService.isInMap(this.selectionService.activeElement) && (event as any)?.ctrlKey && !(event as any)?.shiftKey) {
×
UNCOV
1174
                this.selectionService.remove(this.selectionService.activeElement);
×
UNCOV
1175
                shouldEmitSelection = true;
×
1176
            } else {
UNCOV
1177
                this.selectionService.add(this.selectionService.activeElement, false); // pointer events handle range generation
×
UNCOV
1178
                this.selectionService.keyboardStateOnFocus(node, this.grid.rangeSelected, this.nativeElement);
×
1179
            }
1180
        }
UNCOV
1181
        if (this.grid.isCellSelectable && shouldEmitSelection) {
×
UNCOV
1182
            this.zone.run(() => this.grid.selected.emit({ cell: this.getCellType(), event }));
×
1183
        }
1184
    }
1185

1186
    /**
1187
     * If the provided string matches the text in the cell, the text gets highlighted.
1188
     * ```typescript
1189
     * this.cell.highlightText('Cell Value', true);
1190
     * ```
1191
     *
1192
     * @memberof IgxGridCellComponent
1193
     */
1194
    public highlightText(text: string, caseSensitive?: boolean, exactMatch?: boolean): number {
1195
        return this.highlight && this.column.searchable ? this.highlight.highlight(text, caseSensitive, exactMatch) : 0;
93!
1196
    }
1197

1198
    /**
1199
     * Clears the highlight of the text in the cell.
1200
     * ```typescript
1201
     * this.cell.clearHighLight();
1202
     * ```
1203
     *
1204
     * @memberof IgxGridCellComponent
1205
     */
1206
    public clearHighlight() {
UNCOV
1207
        if (this.highlight && this.column.searchable) {
×
UNCOV
1208
            this.highlight.clearHighlight();
×
1209
        }
1210
    }
1211

1212
    /**
1213
     * @hidden
1214
     * @internal
1215
     */
1216
    public calculateSizeToFit(range: any): number {
UNCOV
1217
        return this.platformUtil.getNodeSizeViaRange(range, this.nativeElement);
×
1218
    }
1219

1220
    /**
1221
     * @hidden
1222
     * @internal
1223
     */
1224
    public get searchMetadata() {
1225
        const meta = new Map<string, any>();
93✔
1226
        meta.set('pinned', this.grid.isRecordPinnedByViewIndex(this.intRow.index));
93✔
1227
        return meta;
93✔
1228
    }
1229

1230
    /**
1231
     * @hidden
1232
     * @internal
1233
     */
1234
    private _updateCRUDStatus(event?: Event) {
UNCOV
1235
        if (this.editMode) {
×
UNCOV
1236
            return;
×
1237
        }
1238

1239
        let editableArgs;
UNCOV
1240
        const crud = this.grid.crudService;
×
UNCOV
1241
        const editableCell = this.grid.crudService.cell;
×
UNCOV
1242
        const editMode = !!(crud.row || crud.cell);
×
1243

UNCOV
1244
        if (this.editable && editMode && !this.intRow.deleted) {
×
UNCOV
1245
            if (editableCell) {
×
UNCOV
1246
                editableArgs = this.grid.crudService.updateCell(false, event);
×
1247

1248
                /* This check is related with the following issue #6517:
1249
                 * when edit cell that belongs to a column which is sorted and press tab,
1250
                 * the next cell in edit mode is with wrong value /its context is not updated/;
1251
                 * So we reapply sorting before the next cell enters edit mode.
1252
                 * Also we need to keep the notifyChanges below, because of the current
1253
                 * change detection cycle when we have editing with enabled transactions
1254
                 */
UNCOV
1255
                if (this.grid.sortingExpressions.length && this.grid.sortingExpressions.indexOf(editableCell.column.field)) {
×
1256
                    this.grid.cdr.detectChanges();
×
1257
                }
1258

UNCOV
1259
                if (editableArgs && editableArgs.cancel) {
×
UNCOV
1260
                    return true;
×
1261
                }
1262

UNCOV
1263
                crud.exitCellEdit(event);
×
1264
            }
UNCOV
1265
            this.grid.tbody.nativeElement.focus({ preventScroll: true });
×
UNCOV
1266
            this.grid.notifyChanges();
×
UNCOV
1267
            crud.enterEditMode(this, event);
×
UNCOV
1268
            return false;
×
1269
        }
1270

UNCOV
1271
        if (editableCell && crud.sameRow(this.cellID.rowID)) {
×
UNCOV
1272
            this.grid.crudService.updateCell(true, event);
×
UNCOV
1273
        } else if (editMode && !crud.sameRow(this.cellID.rowID)) {
×
UNCOV
1274
            this.grid.crudService.endEdit(true, event);
×
1275
        }
1276
    }
1277

1278
    private addPointerListeners(selection) {
1279
        if (selection !== GridSelectionMode.multiple) {
93!
UNCOV
1280
            return;
×
1281
        }
1282
        this.nativeElement.addEventListener('pointerenter', this.pointerenter);
93✔
1283
        this.nativeElement.addEventListener('pointerup', this.pointerup);
93✔
1284
        this.nativeElement.addEventListener('focusout', this.focusout);
93✔
1285
    }
1286

1287
    private removePointerListeners(selection) {
1288
        if (selection !== GridSelectionMode.multiple) {
93!
UNCOV
1289
            return;
×
1290
        }
1291
        this.nativeElement.removeEventListener('pointerenter', this.pointerenter);
93✔
1292
        this.nativeElement.removeEventListener('pointerup', this.pointerup);
93✔
1293
        this.nativeElement.removeEventListener('focusout', this.focusout);
93✔
1294
    }
1295

1296
    private getCellType(useRow?: boolean): CellType {
UNCOV
1297
        const rowID = useRow ? this.grid.createRow(this.intRow.index, this.intRow.data) : this.intRow.index;
×
UNCOV
1298
        return new IgxGridCell(this.grid, rowID, this.column);
×
1299
    }
1300
}
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