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

IgniteUI / igniteui-angular / 26023601418

18 May 2026 08:57AM UTC coverage: 4.854% (-85.3%) from 90.174%
26023601418

Pull #17281

github

web-flow
Merge e7ce7a18e into 5a85df190
Pull Request #17281: feat: Added virtual scroll component and sample implementation

400 of 17347 branches covered (2.31%)

Branch coverage included in aggregate %.

63 of 222 new or added lines in 4 files covered. (28.38%)

27932 existing lines in 341 files now uncovered.

2022 of 32547 relevant lines covered (6.21%)

0.72 hits per line

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

2.08
/projects/igniteui-angular/tabs/src/tabs/tabs/tabs.component.ts
1
import { AfterViewInit, Component, ElementRef, HostBinding, inject, Input, NgZone, OnDestroy, ViewChild } from '@angular/core';
2
import { IgxTabsBase } from '../tabs.base';
3
import { IgxTabsDirective } from '../tabs.directive';
4
import { NgClass, NgTemplateOutlet } from '@angular/common';
5
import { IgxIconButtonDirective, IgxRippleDirective } from 'igniteui-angular/directives';
6
import { IgxIconComponent } from 'igniteui-angular/icon';
7
import { getResizeObserver, PlatformUtil, isLeftToRight } from 'igniteui-angular/core';
8

9
export const IgxTabsAlignment = {
3✔
10
    start: 'start',
11
    end: 'end',
12
    center: 'center',
13
    justify: 'justify'
14
} as const;
15

16
/** @hidden */
17
const enum TabScrollButtonStyle {
18
    Enabled = 'enabled',
19
    Disabled = 'disabled',
20
    NotDisplayed = 'not_displayed'
21
}
22

23
export type IgxTabsAlignment = (typeof IgxTabsAlignment)[keyof typeof IgxTabsAlignment];
24

25
/** @hidden */
26
let NEXT_TAB_ID = 0;
3✔
27

28
/**
29
 * Tabs component is used to organize or switch between similar data sets.
30
 *
31
 * @igxModule IgxTabsModule
32
 *
33
 * @igxTheme igx-tabs-theme
34
 *
35
 * @igxKeywords tabs
36
 *
37
 * @igxGroup Layouts
38
 *
39
 * @remarks
40
 * The Ignite UI for Angular Tabs component places tabs at the top and allows for scrolling when there are multiple tab items on the screen.
41
 *
42
 * @example
43
 * ```html
44
 * <igx-tabs>
45
 *     <igx-tab-item>
46
 *         <igx-tab-header>
47
 *             <igx-icon igxTabHeaderIcon>folder</igx-icon>
48
 *             <span igxTabHeaderLabel>Tab 1</span>
49
 *         </igx-tab-header>
50
 *         <igx-tab-content>
51
 *             Content 1
52
 *         </igx-tab-content>
53
 *     </igx-tab-item>
54
 *     ...
55
 * </igx-tabs>
56
 * ```
57
 */
58
@Component({
59
    selector: 'igx-tabs',
60
    templateUrl: 'tabs.component.html',
61
    providers: [{ provide: IgxTabsBase, useExisting: IgxTabsComponent }],
62
    imports: [IgxRippleDirective, IgxIconComponent, NgClass, NgTemplateOutlet, IgxIconButtonDirective]
63
})
64

65
export class IgxTabsComponent extends IgxTabsDirective implements AfterViewInit, OnDestroy {
3✔
UNCOV
66
    private ngZone = inject(NgZone);
×
UNCOV
67
    private platform = inject(PlatformUtil);
×
68

69

70
    /**
71
     * Gets/Sets the tab alignment. Defaults to `start`.
72
     */
73
    @Input()
74
    public get tabAlignment(): string | IgxTabsAlignment {
UNCOV
75
        return this._tabAlignment;
×
76
    }
77

78
    public set tabAlignment(value: string | IgxTabsAlignment) {
UNCOV
79
        this._tabAlignment = value;
×
UNCOV
80
        requestAnimationFrame(() => {
×
UNCOV
81
            this.updateScrollButtons();
×
UNCOV
82
            this.realignSelectedIndicator();
×
83
        });
84
    }
85

86
    /**
87
     * Determines the tab activation.
88
     * When set to auto, the tab is instantly selected while navigating with the Left/Right Arrows, Home or End keys and the corresponding panel is displayed.
89
     * When set to manual, the tab is only focused. The selection happens after pressing Space or Enter.
90
     * Defaults is auto.
91
     */
92
    @Input()
UNCOV
93
    public activation: 'auto' | 'manual' = 'auto';
×
94

95
    /** @hidden */
96
    @ViewChild('headerContainer', { static: true })
97
    public headerContainer: ElementRef<HTMLElement>;
98

99
    /** @hidden */
100
    @ViewChild('viewPort', { static: true })
101
    public viewPort: ElementRef<HTMLElement>;
102

103
    /** @hidden */
104
    @ViewChild('itemsWrapper', { static: true })
105
    public itemsWrapper: ElementRef<HTMLElement>;
106

107
    /** @hidden */
108
    @ViewChild('itemsContainer', { static: true })
109
    public itemsContainer: ElementRef<HTMLElement>;
110

111
    /** @hidden */
112
    @ViewChild('selectedIndicator')
113
    public selectedIndicator: ElementRef<HTMLElement>;
114

115
    /** @hidden */
116
    @ViewChild('scrollPrevButton')
117
    public scrollPrevButton: ElementRef<HTMLButtonElement>;
118

119
    /** @hidden */
120
    @ViewChild('scrollNextButton')
121
    public scrollNextButton: ElementRef<HTMLButtonElement>;
122

123
    /** @hidden */
124
    @HostBinding('class.igx-tabs')
UNCOV
125
    public defaultClass = true;
×
126

127
    /**  @hidden */
UNCOV
128
    public offset = 0;
×
129

130
    /** @hidden */
UNCOV
131
    protected override componentName = 'igx-tabs';
×
132

UNCOV
133
    private _tabAlignment: string | IgxTabsAlignment = 'start';
×
134
    private _resizeObserver: ResizeObserver;
135

136
    /** @hidden @internal */
137
    public override ngAfterViewInit(): void {
UNCOV
138
        super.ngAfterViewInit();
×
139

UNCOV
140
        this.ngZone.runOutsideAngular(() => {
×
UNCOV
141
            if (this.platform.isBrowser) {
×
UNCOV
142
                this._resizeObserver = new (getResizeObserver())(() => {
×
UNCOV
143
                    this.updateScrollButtons();
×
UNCOV
144
                    this.realignSelectedIndicator();
×
145
                });
UNCOV
146
                this._resizeObserver.observe(this.headerContainer.nativeElement);
×
UNCOV
147
                this._resizeObserver.observe(this.viewPort.nativeElement);
×
148
            }
149
        });
150
    }
151

152
    /** @hidden @internal */
153
    public override ngOnDestroy(): void {
UNCOV
154
        super.ngOnDestroy();
×
155

UNCOV
156
        this.ngZone.runOutsideAngular(() => {
×
UNCOV
157
            this._resizeObserver?.disconnect();
×
158
        });
159
    }
160

161
    /** @hidden */
162
    public scrollPrev() {
UNCOV
163
        this.scroll(false);
×
164
    }
165

166
    /** @hidden */
167
    public scrollNext() {
UNCOV
168
        this.scroll(true);
×
169
    }
170

171
    /** @hidden */
172
    public realignSelectedIndicator() {
UNCOV
173
        if (this.selectedIndex >= 0 && this.selectedIndex < this.items.length) {
×
UNCOV
174
            const header = this.items.get(this.selectedIndex).headerComponent.nativeElement;
×
UNCOV
175
            this.alignSelectedIndicator(header, 0);
×
176
        }
177
    }
178

179
    /** @hidden */
180
    public resolveHeaderScrollClasses() {
UNCOV
181
        return {
×
182
            'igx-tabs__header-scroll--start': this.tabAlignment === 'start',
183
            'igx-tabs__header-scroll--end': this.tabAlignment === 'end',
184
            'igx-tabs__header-scroll--center': this.tabAlignment === 'center',
185
            'igx-tabs__header-scroll--justify': this.tabAlignment === 'justify',
186
        };
187
    }
188

189
    /** @hidden */
190
    protected override scrollTabHeaderIntoView() {
UNCOV
191
        if (this.selectedIndex >= 0) {
×
UNCOV
192
            const tabItems = this.items.toArray();
×
UNCOV
193
            const tabHeaderNativeElement = tabItems[this.selectedIndex].headerComponent.nativeElement;
×
194

195
            // Scroll left if there is need
UNCOV
196
            if (this.getElementOffset(tabHeaderNativeElement) < this.offset) {
×
UNCOV
197
                this.scrollElement(tabHeaderNativeElement, false);
×
198
            }
199

200
            // Scroll right if there is need
UNCOV
201
            const viewPortOffsetWidth = this.viewPort.nativeElement.offsetWidth;
×
UNCOV
202
            const delta = (this.getElementOffset(tabHeaderNativeElement) + tabHeaderNativeElement.offsetWidth) - (viewPortOffsetWidth + this.offset);
×
203

204
            // Fix for IE 11, a difference is accumulated from the widths calculations
UNCOV
205
            if (delta > 1) {
×
UNCOV
206
                this.scrollElement(tabHeaderNativeElement, true);
×
207
            }
208

UNCOV
209
            this.alignSelectedIndicator(tabHeaderNativeElement);
×
210
        } else {
UNCOV
211
            this.hideSelectedIndicator();
×
212
        }
213
    }
214

215
    /** @hidden */
216
    protected getNextTabId() {
UNCOV
217
        return NEXT_TAB_ID++;
×
218
    }
219

220
    /** @hidden */
221
    protected override onItemChanges() {
UNCOV
222
        super.onItemChanges();
×
223

UNCOV
224
        Promise.resolve().then(() => {
×
UNCOV
225
            this.updateScrollButtons();
×
226
        });
227
    }
228

229
    private alignSelectedIndicator(element: HTMLElement, duration = 0.3): void {
×
UNCOV
230
        if (this.selectedIndicator) {
×
UNCOV
231
            this.selectedIndicator.nativeElement.style.visibility = 'visible';
×
UNCOV
232
            this.selectedIndicator.nativeElement.style.transitionDuration = duration > 0 ? `${duration}s` : 'initial';
×
UNCOV
233
            this.selectedIndicator.nativeElement.style.width = `${element.offsetWidth}px`;
×
UNCOV
234
            this.selectedIndicator.nativeElement.style.transform = `translate(${element.offsetLeft}px)`;
×
235
        }
236
    }
237

238
    private hideSelectedIndicator(): void {
UNCOV
239
        if (this.selectedIndicator) {
×
UNCOV
240
            this.selectedIndicator.nativeElement.style.visibility = 'hidden';
×
241
        }
242
    }
243

244
    private scroll(scrollNext: boolean): void {
UNCOV
245
        const tabsArray = this.items.toArray();
×
246

UNCOV
247
        for (let index = 0; index < tabsArray.length; index++) {
×
UNCOV
248
            const tab = tabsArray[index];
×
UNCOV
249
            const element = tab.headerComponent.nativeElement;
×
UNCOV
250
            if (scrollNext) {
×
UNCOV
251
                if (element.offsetWidth + this.getElementOffset(element) > this.viewPort.nativeElement.offsetWidth + this.offset) {
×
UNCOV
252
                    this.scrollElement(element, scrollNext);
×
UNCOV
253
                    break;
×
254
                }
255
            } else {
UNCOV
256
                if (this.getElementOffset(element) >= this.offset) {
×
UNCOV
257
                    this.scrollElement(tabsArray[index - 1].headerComponent.nativeElement, scrollNext);
×
UNCOV
258
                    break;
×
259
                }
260
            }
261
        }
262
    }
263

264
    private scrollElement(element: any, scrollNext: boolean): void {
UNCOV
265
        const viewPortWidth = this.viewPort.nativeElement.offsetWidth;
×
266

UNCOV
267
        this.offset = (scrollNext) ? element.offsetWidth + this.getElementOffset(element) - viewPortWidth : this.getElementOffset(element);
×
UNCOV
268
        this.viewPort.nativeElement.scrollLeft = this.getOffset(this.offset);
×
UNCOV
269
        this.updateScrollButtons();
×
270
    }
271

272
    private updateScrollButtons() {
UNCOV
273
        const itemsContainerWidth = this.getTabItemsContainerWidth();
×
274

UNCOV
275
        const scrollPrevButtonStyle = this.resolveLeftScrollButtonStyle(itemsContainerWidth);
×
UNCOV
276
        this.setScrollButtonStyle(this.scrollPrevButton.nativeElement, scrollPrevButtonStyle);
×
277

UNCOV
278
        const scrollNextButtonStyle = this.resolveRightScrollButtonStyle(itemsContainerWidth);
×
UNCOV
279
        this.setScrollButtonStyle(this.scrollNextButton.nativeElement, scrollNextButtonStyle);
×
280
    }
281

282
    private setScrollButtonStyle(button: HTMLButtonElement, buttonStyle: TabScrollButtonStyle) {
UNCOV
283
        if (buttonStyle === TabScrollButtonStyle.Enabled) {
×
UNCOV
284
            button.disabled = false;
×
UNCOV
285
            button.style.display = '';
×
UNCOV
286
        } else if (buttonStyle === TabScrollButtonStyle.Disabled) {
×
UNCOV
287
            button.disabled = true;
×
UNCOV
288
            button.style.display = '';
×
UNCOV
289
        } else if (buttonStyle === TabScrollButtonStyle.NotDisplayed) {
×
UNCOV
290
            button.style.display = 'none';
×
291
        }
292
    }
293
    private resolveLeftScrollButtonStyle(itemsContainerWidth: number): TabScrollButtonStyle {
UNCOV
294
        const headerContainerWidth = this.headerContainer.nativeElement.offsetWidth;
×
UNCOV
295
        const offset = this.offset;
×
296

UNCOV
297
        if (offset === 0) {
×
298
            // Fix for IE 11, a difference is accumulated from the widths calculations.
UNCOV
299
            if (itemsContainerWidth - headerContainerWidth <= 1) {
×
UNCOV
300
                return TabScrollButtonStyle.NotDisplayed;
×
301
            }
UNCOV
302
            return TabScrollButtonStyle.Disabled;
×
303
        } else {
UNCOV
304
            return TabScrollButtonStyle.Enabled;
×
305
        }
306
    }
307

308
    private resolveRightScrollButtonStyle(itemsContainerWidth: number): TabScrollButtonStyle {
UNCOV
309
        const viewPortWidth = this.viewPort.nativeElement.offsetWidth;
×
UNCOV
310
        const headerContainerWidth = this.headerContainer.nativeElement.offsetWidth;
×
UNCOV
311
        const offset = this.offset;
×
UNCOV
312
        const total = offset + viewPortWidth;
×
313

314
        // Fix for IE 11, a difference is accumulated from the widths calculations.
UNCOV
315
        if (itemsContainerWidth - headerContainerWidth <= 1 && offset === 0) {
×
UNCOV
316
            return TabScrollButtonStyle.NotDisplayed;
×
317
        }
318

UNCOV
319
        if (itemsContainerWidth > total) {
×
UNCOV
320
            return TabScrollButtonStyle.Enabled;
×
321
        } else {
UNCOV
322
            return TabScrollButtonStyle.Disabled;
×
323
        }
324
    }
325

326
    private getTabItemsContainerWidth() {
327
        // We use this hacky way to get the width of the itemsContainer,
328
        // because there is inconsistency in IE we cannot use offsetWidth or scrollOffset.
UNCOV
329
        const itemsContainerChildrenCount = this.itemsContainer.nativeElement.children.length;
×
UNCOV
330
        let itemsContainerWidth = 0;
×
331

UNCOV
332
        if (itemsContainerChildrenCount > 1) {
×
UNCOV
333
            const lastTab = this.itemsContainer.nativeElement.children[itemsContainerChildrenCount - 1] as HTMLElement;
×
UNCOV
334
            itemsContainerWidth = this.getElementOffset(lastTab) + lastTab.offsetWidth;
×
335
        }
336

UNCOV
337
        return itemsContainerWidth;
×
338
    }
339

340
    private getOffset(offset: number): number {
UNCOV
341
        return isLeftToRight(this._element.nativeElement) ? offset : -offset;
×
342
    }
343

344
    private getElementOffset(element: HTMLElement): number {
UNCOV
345
        const rtl = !isLeftToRight(this._element.nativeElement);
×
UNCOV
346
        return rtl ? this.itemsWrapper.nativeElement.offsetWidth - element.offsetLeft - element.offsetWidth : element.offsetLeft;
×
347
    }
348
}
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