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

IgniteUI / igniteui-angular / 13331632524

14 Feb 2025 02:51PM CUT coverage: 22.015% (-69.6%) from 91.622%
13331632524

Pull #15372

github

web-flow
Merge d52d57714 into bcb78ae0a
Pull Request #15372: chore(*): test ci passing

1990 of 15592 branches covered (12.76%)

431 of 964 new or added lines in 18 files covered. (44.71%)

19956 existing lines in 307 files now uncovered.

6452 of 29307 relevant lines covered (22.02%)

249.17 hits per line

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

2.8
/projects/igniteui-angular/src/lib/tabs/tabs/tabs.component.ts
1
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, HostBinding, Inject, Input, NgZone, OnDestroy, ViewChild } from '@angular/core';
2
import { getResizeObserver, mkenum, PlatformUtil } from '../../core/utils';
3
import { IgxAngularAnimationService } from '../../services/animation/angular-animation-service';
4
import { AnimationService } from '../../services/animation/animation';
5
import { IgxDirectionality } from '../../services/direction/directionality';
6
import { IgxTabsBase } from '../tabs.base';
7
import { IgxTabsDirective } from '../tabs.directive';
8
import { NgClass, NgFor, NgTemplateOutlet, NgIf } from '@angular/common';
9
import { IgxIconComponent } from '../../icon/icon.component';
10
import { IgxRippleDirective } from '../../directives/ripple/ripple.directive';
11
import { IgxIconButtonDirective } from '../../directives/button/icon-button.directive';
12

13
export const IgxTabsAlignment = /*@__PURE__*/mkenum({
2✔
14
    start: 'start',
15
    end: 'end',
16
    center: 'center',
17
    justify: 'justify'
18
});
19

20
/** @hidden */
21
const enum TabScrollButtonStyle {
22
    Enabled = 'enabled',
23
    Disabled = 'disabled',
24
    NotDisplayed = 'not_displayed'
25
}
26

27
export type IgxTabsAlignment = (typeof IgxTabsAlignment)[keyof typeof IgxTabsAlignment];
28

29
/** @hidden */
30
let NEXT_TAB_ID = 0;
2✔
31

32
/**
33
 * Tabs component is used to organize or switch between similar data sets.
34
 *
35
 * @igxModule IgxTabsModule
36
 *
37
 * @igxTheme igx-tabs-theme
38
 *
39
 * @igxKeywords tabs
40
 *
41
 * @igxGroup Layouts
42
 *
43
 * @remarks
44
 * 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.
45
 *
46
 * @example
47
 * ```html
48
 * <igx-tabs>
49
 *     <igx-tab-item>
50
 *         <igx-tab-header>
51
 *             <igx-icon igxTabHeaderIcon>folder</igx-icon>
52
 *             <span igxTabHeaderLabel>Tab 1</span>
53
 *         </igx-tab-header>
54
 *         <igx-tab-content>
55
 *             Content 1
56
 *         </igx-tab-content>
57
 *     </igx-tab-item>
58
 *     ...
59
 * </igx-tabs>
60
 * ```
61
 */
62
@Component({
63
    selector: 'igx-tabs',
64
    templateUrl: 'tabs.component.html',
65
    providers: [{ provide: IgxTabsBase, useExisting: IgxTabsComponent }],
66
    imports: [IgxRippleDirective, IgxIconComponent, NgClass, NgFor, NgTemplateOutlet, NgIf, IgxIconButtonDirective]
67
})
68

69
export class IgxTabsComponent extends IgxTabsDirective implements AfterViewInit, OnDestroy {
2✔
70

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

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

87
    /**
88
     * Determines the tab activation.
89
     * 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.
90
     * When set to manual, the tab is only focused. The selection happens after pressing Space or Enter.
91
     * Defaults is auto.
92
     */
93
    @Input()
UNCOV
94
    public activation: 'auto' | 'manual' = 'auto';
×
95

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

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

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

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

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

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

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

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

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

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

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

137
    constructor(
138
        @Inject(IgxAngularAnimationService) animationService: AnimationService,
139
        cdr: ChangeDetectorRef,
UNCOV
140
        private ngZone: NgZone,
×
141
        dir: IgxDirectionality,
UNCOV
142
        private platform: PlatformUtil
×
143
    ) {
UNCOV
144
        super(animationService, cdr, dir);
×
145
    }
146

147

148
    /** @hidden @internal */
149
    public override ngAfterViewInit(): void {
UNCOV
150
        super.ngAfterViewInit();
×
151

UNCOV
152
        this.ngZone.runOutsideAngular(() => {
×
UNCOV
153
            if (this.platform.isBrowser) {
×
UNCOV
154
                this._resizeObserver = new (getResizeObserver())(() => {
×
UNCOV
155
                    this.updateScrollButtons();
×
UNCOV
156
                    this.realignSelectedIndicator();
×
157
                });
UNCOV
158
                this._resizeObserver.observe(this.headerContainer.nativeElement);
×
UNCOV
159
                this._resizeObserver.observe(this.viewPort.nativeElement);
×
160
            }
161
        });
162
    }
163

164
    /** @hidden @internal */
165
    public override ngOnDestroy(): void {
UNCOV
166
        super.ngOnDestroy();
×
167

UNCOV
168
        this.ngZone.runOutsideAngular(() => {
×
UNCOV
169
            this._resizeObserver?.disconnect();
×
170
        });
171
    }
172

173
    /** @hidden */
174
    public scrollPrev() {
UNCOV
175
        this.scroll(false);
×
176
    }
177

178
    /** @hidden */
179
    public scrollNext() {
UNCOV
180
        this.scroll(true);
×
181
    }
182

183
    /** @hidden */
184
    public realignSelectedIndicator() {
UNCOV
185
        if (this.selectedIndex >= 0 && this.selectedIndex < this.items.length) {
×
UNCOV
186
            const header = this.items.get(this.selectedIndex).headerComponent.nativeElement;
×
UNCOV
187
            this.alignSelectedIndicator(header, 0);
×
188
        }
189
    }
190

191
    /** @hidden */
192
    public resolveHeaderScrollClasses() {
UNCOV
193
        return {
×
194
            'igx-tabs__header-scroll--start': this.tabAlignment === 'start',
195
            'igx-tabs__header-scroll--end': this.tabAlignment === 'end',
196
            'igx-tabs__header-scroll--center': this.tabAlignment === 'center',
197
            'igx-tabs__header-scroll--justify': this.tabAlignment === 'justify',
198
        };
199
    }
200

201
    /** @hidden */
202
    protected override scrollTabHeaderIntoView() {
UNCOV
203
        if (this.selectedIndex >= 0) {
×
UNCOV
204
            const tabItems = this.items.toArray();
×
UNCOV
205
            const tabHeaderNativeElement = tabItems[this.selectedIndex].headerComponent.nativeElement;
×
206

207
            // Scroll left if there is need
UNCOV
208
            if (this.getElementOffset(tabHeaderNativeElement) < this.offset) {
×
UNCOV
209
                this.scrollElement(tabHeaderNativeElement, false);
×
210
            }
211

212
            // Scroll right if there is need
UNCOV
213
            const viewPortOffsetWidth = this.viewPort.nativeElement.offsetWidth;
×
UNCOV
214
            const delta = (this.getElementOffset(tabHeaderNativeElement) + tabHeaderNativeElement.offsetWidth) - (viewPortOffsetWidth + this.offset);
×
215

216
            // Fix for IE 11, a difference is accumulated from the widths calculations
UNCOV
217
            if (delta > 1) {
×
UNCOV
218
                this.scrollElement(tabHeaderNativeElement, true);
×
219
            }
220

UNCOV
221
            this.alignSelectedIndicator(tabHeaderNativeElement);
×
222
        } else {
UNCOV
223
            this.hideSelectedIndicator();
×
224
        }
225
    }
226

227
    /** @hidden */
228
    protected getNextTabId() {
UNCOV
229
        return NEXT_TAB_ID++;
×
230
    }
231

232
    /** @hidden */
233
    protected override onItemChanges() {
UNCOV
234
        super.onItemChanges();
×
235

UNCOV
236
        Promise.resolve().then(() => {
×
UNCOV
237
            this.updateScrollButtons();
×
238
        });
239
    }
240

241
    private alignSelectedIndicator(element: HTMLElement, duration = 0.3): void {
×
UNCOV
242
        if (this.selectedIndicator) {
×
UNCOV
243
            this.selectedIndicator.nativeElement.style.visibility = 'visible';
×
UNCOV
244
            this.selectedIndicator.nativeElement.style.transitionDuration = duration > 0 ? `${duration}s` : 'initial';
×
UNCOV
245
            this.selectedIndicator.nativeElement.style.width = `${element.offsetWidth}px`;
×
UNCOV
246
            this.selectedIndicator.nativeElement.style.transform = `translate(${element.offsetLeft}px)`;
×
247
        }
248
    }
249

250
    private hideSelectedIndicator(): void {
UNCOV
251
        if (this.selectedIndicator) {
×
UNCOV
252
            this.selectedIndicator.nativeElement.style.visibility = 'hidden';
×
253
        }
254
    }
255

256
    private scroll(scrollNext: boolean): void {
UNCOV
257
        const tabsArray = this.items.toArray();
×
258

UNCOV
259
        for (let index = 0; index < tabsArray.length; index++) {
×
UNCOV
260
            const tab = tabsArray[index];
×
UNCOV
261
            const element = tab.headerComponent.nativeElement;
×
UNCOV
262
            if (scrollNext) {
×
UNCOV
263
                if (element.offsetWidth + this.getElementOffset(element) > this.viewPort.nativeElement.offsetWidth + this.offset) {
×
UNCOV
264
                    this.scrollElement(element, scrollNext);
×
UNCOV
265
                    break;
×
266
                }
267
            } else {
UNCOV
268
                if (this.getElementOffset(element) >= this.offset) {
×
UNCOV
269
                    this.scrollElement(tabsArray[index - 1].headerComponent.nativeElement, scrollNext);
×
UNCOV
270
                    break;
×
271
                }
272
            }
273
        }
274
    }
275

276
    private scrollElement(element: any, scrollNext: boolean): void {
UNCOV
277
        const viewPortWidth = this.viewPort.nativeElement.offsetWidth;
×
278

UNCOV
279
        this.offset = (scrollNext) ? element.offsetWidth + this.getElementOffset(element) - viewPortWidth : this.getElementOffset(element);
×
UNCOV
280
        this.viewPort.nativeElement.scrollLeft = this.getOffset(this.offset);
×
UNCOV
281
        this.updateScrollButtons();
×
282
    }
283

284
    private updateScrollButtons() {
UNCOV
285
        const itemsContainerWidth = this.getTabItemsContainerWidth();
×
286

UNCOV
287
        const scrollPrevButtonStyle = this.resolveLeftScrollButtonStyle(itemsContainerWidth);
×
UNCOV
288
        this.setScrollButtonStyle(this.scrollPrevButton.nativeElement, scrollPrevButtonStyle);
×
289

UNCOV
290
        const scrollNextButtonStyle = this.resolveRightScrollButtonStyle(itemsContainerWidth);
×
UNCOV
291
        this.setScrollButtonStyle(this.scrollNextButton.nativeElement, scrollNextButtonStyle);
×
292
    }
293

294
    private setScrollButtonStyle(button: HTMLButtonElement, buttonStyle: TabScrollButtonStyle) {
UNCOV
295
        if (buttonStyle === TabScrollButtonStyle.Enabled) {
×
UNCOV
296
            button.disabled = false;
×
UNCOV
297
            button.style.display = '';
×
UNCOV
298
        } else if (buttonStyle === TabScrollButtonStyle.Disabled) {
×
UNCOV
299
            button.disabled = true;
×
UNCOV
300
            button.style.display = '';
×
UNCOV
301
        } else if (buttonStyle === TabScrollButtonStyle.NotDisplayed) {
×
UNCOV
302
            button.style.display = 'none';
×
303
        }
304
    }
305
    private resolveLeftScrollButtonStyle(itemsContainerWidth: number): TabScrollButtonStyle {
UNCOV
306
        const headerContainerWidth = this.headerContainer.nativeElement.offsetWidth;
×
UNCOV
307
        const offset = this.offset;
×
308

UNCOV
309
        if (offset === 0) {
×
310
            // Fix for IE 11, a difference is accumulated from the widths calculations.
UNCOV
311
            if (itemsContainerWidth - headerContainerWidth <= 1) {
×
UNCOV
312
                return TabScrollButtonStyle.NotDisplayed;
×
313
            }
UNCOV
314
            return TabScrollButtonStyle.Disabled;
×
315
        } else {
UNCOV
316
            return TabScrollButtonStyle.Enabled;
×
317
        }
318
    }
319

320
    private resolveRightScrollButtonStyle(itemsContainerWidth: number): TabScrollButtonStyle {
UNCOV
321
        const viewPortWidth = this.viewPort.nativeElement.offsetWidth;
×
UNCOV
322
        const headerContainerWidth = this.headerContainer.nativeElement.offsetWidth;
×
UNCOV
323
        const offset = this.offset;
×
UNCOV
324
        const total = offset + viewPortWidth;
×
325

326
        // Fix for IE 11, a difference is accumulated from the widths calculations.
UNCOV
327
        if (itemsContainerWidth - headerContainerWidth <= 1 && offset === 0) {
×
UNCOV
328
            return TabScrollButtonStyle.NotDisplayed;
×
329
        }
330

UNCOV
331
        if (itemsContainerWidth > total) {
×
UNCOV
332
            return TabScrollButtonStyle.Enabled;
×
333
        } else {
UNCOV
334
            return TabScrollButtonStyle.Disabled;
×
335
        }
336
    }
337

338
    private getTabItemsContainerWidth() {
339
        // We use this hacky way to get the width of the itemsContainer,
340
        // because there is inconsistency in IE we cannot use offsetWidth or scrollOffset.
UNCOV
341
        const itemsContainerChildrenCount = this.itemsContainer.nativeElement.children.length;
×
UNCOV
342
        let itemsContainerWidth = 0;
×
343

UNCOV
344
        if (itemsContainerChildrenCount > 1) {
×
UNCOV
345
            const lastTab = this.itemsContainer.nativeElement.children[itemsContainerChildrenCount - 1] as HTMLElement;
×
UNCOV
346
            itemsContainerWidth = this.getElementOffset(lastTab) + lastTab.offsetWidth;
×
347
        }
348

UNCOV
349
        return itemsContainerWidth;
×
350
    }
351

352
    private getOffset(offset: number): number {
UNCOV
353
        return this.dir.rtl ? -offset : offset;
×
354
    }
355

356
    private getElementOffset(element: HTMLElement): number {
UNCOV
357
        return this.dir.rtl ? this.itemsWrapper.nativeElement.offsetWidth - element.offsetLeft - element.offsetWidth : element.offsetLeft;
×
358
    }
359
}
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

© 2025 Coveralls, Inc