diff --git a/projects/igniteui-angular/src/lib/tabbar/bottom-nav-routing-test-guard.spec.ts b/projects/igniteui-angular/src/lib/tabbar/bottom-nav-routing-test-guard.spec.ts new file mode 100644 index 00000000000..fce82988a5b --- /dev/null +++ b/projects/igniteui-angular/src/lib/tabbar/bottom-nav-routing-test-guard.spec.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; + +@Injectable() +export class BottomNavRoutingTestGuard implements CanActivate { + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + if (state.url === '/view5') { + return false; + } else { + return true; + } + } +} diff --git a/projects/igniteui-angular/src/lib/tabbar/routing-view-components.spec.ts b/projects/igniteui-angular/src/lib/tabbar/routing-view-components.spec.ts index b6946fac0b5..842d3a76714 100644 --- a/projects/igniteui-angular/src/lib/tabbar/routing-view-components.spec.ts +++ b/projects/igniteui-angular/src/lib/tabbar/routing-view-components.spec.ts @@ -18,12 +18,26 @@ export class BottomNavRoutingView2Component { export class BottomNavRoutingView3Component { } +@Component({ + template: `This is a content from view component # 4` +}) +export class BottomNavRoutingView4Component { +} + +@Component({ + template: `This is a content from view component # 5` +}) +export class BottomNavRoutingView5Component { +} + /** * @hidden */ @NgModule({ - declarations: [BottomNavRoutingView1Component, BottomNavRoutingView2Component, BottomNavRoutingView3Component], - exports: [BottomNavRoutingView1Component, BottomNavRoutingView2Component, BottomNavRoutingView3Component], + declarations: [BottomNavRoutingView1Component, BottomNavRoutingView2Component, BottomNavRoutingView3Component, + BottomNavRoutingView4Component, BottomNavRoutingView5Component], + exports: [BottomNavRoutingView1Component, BottomNavRoutingView2Component, BottomNavRoutingView3Component, + BottomNavRoutingView4Component, BottomNavRoutingView5Component] }) export class BottomNavRoutingViewComponentsModule { } diff --git a/projects/igniteui-angular/src/lib/tabbar/tab-bar-content.component.html b/projects/igniteui-angular/src/lib/tabbar/tab-bar-content.component.html index e18191e0054..46fdc9c5ec7 100644 --- a/projects/igniteui-angular/src/lib/tabbar/tab-bar-content.component.html +++ b/projects/igniteui-angular/src/lib/tabbar/tab-bar-content.component.html @@ -2,9 +2,13 @@
- - + + > -
\ No newline at end of file + diff --git a/projects/igniteui-angular/src/lib/tabbar/tabbar.component.spec.ts b/projects/igniteui-angular/src/lib/tabbar/tabbar.component.spec.ts index cd11abcd7ee..922cdeb8215 100644 --- a/projects/igniteui-angular/src/lib/tabbar/tabbar.component.spec.ts +++ b/projects/igniteui-angular/src/lib/tabbar/tabbar.component.spec.ts @@ -9,10 +9,13 @@ import { configureTestSuite } from '../test-utils/configure-suite'; import { BottomNavRoutingViewComponentsModule, BottomNavRoutingView1Component, BottomNavRoutingView2Component, - BottomNavRoutingView3Component } from './routing-view-components.spec'; + BottomNavRoutingView3Component, + BottomNavRoutingView4Component, + BottomNavRoutingView5Component} from './routing-view-components.spec'; import { Router } from '@angular/router'; import { Location } from '@angular/common'; import { UIInteractions } from '../test-utils/ui-interactions.spec'; +import { BottomNavRoutingTestGuard } from './bottom-nav-routing-test-guard.spec'; describe('IgxBottomNav', () => { configureTestSuite(); @@ -22,19 +25,61 @@ describe('IgxBottomNav', () => { beforeAll(async(() => { const testRoutes = [ - { path: 'view1', component: BottomNavRoutingView1Component }, - { path: 'view2', component: BottomNavRoutingView2Component }, - { path: 'view3', component: BottomNavRoutingView3Component } + { path: 'view1', component: BottomNavRoutingView1Component, canActivate: [BottomNavRoutingTestGuard] }, + { path: 'view2', component: BottomNavRoutingView2Component, canActivate: [BottomNavRoutingTestGuard] }, + { path: 'view3', component: BottomNavRoutingView3Component, canActivate: [BottomNavRoutingTestGuard] }, + { path: 'view4', component: BottomNavRoutingView4Component, canActivate: [BottomNavRoutingTestGuard] }, + { path: 'view5', component: BottomNavRoutingView5Component, canActivate: [BottomNavRoutingTestGuard] }, ]; TestBed.configureTestingModule({ declarations: [TabBarTestComponent, BottomTabBarTestComponent, TemplatedTabBarTestComponent, TabBarRoutingTestComponent, - TabBarTabsOnlyModeTestComponent], - imports: [IgxBottomNavModule, BottomNavRoutingViewComponentsModule, RouterTestingModule.withRoutes(testRoutes)] + TabBarTabsOnlyModeTestComponent, BottomNavRoutingGuardTestComponent, BottomNavTestHtmlAttributesComponent], + imports: [IgxBottomNavModule, BottomNavRoutingViewComponentsModule, RouterTestingModule.withRoutes(testRoutes)], + providers: [BottomNavRoutingTestGuard] }).compileComponents(); })); - describe('IgxBottomNav Component with Panels Definitions', () => { + describe('Html Attributes', () => { + let fixture; + let tabbar; + + beforeEach(async(() => { + fixture = TestBed.createComponent(BottomNavTestHtmlAttributesComponent); + tabbar = fixture.componentInstance.tabbar; + })); + + it('should set the correct attributes on the html elements', () => { + fixture.detectChanges(); + + const igxBottomNavs = document.querySelectorAll('igx-bottom-nav'); + expect(igxBottomNavs.length).toBe(2); + + const startIndex = parseInt(igxBottomNavs[0].id.replace('igx-bottom-nav-', ''), 10); + for (let tabIndex = startIndex; tabIndex < startIndex + 2; tabIndex++) { + const tab = igxBottomNavs[tabIndex - startIndex]; + expect(tab.id).toEqual(`igx-bottom-nav-${tabIndex}`); + + const headers = tab.querySelectorAll('igx-tab'); + const contents = tab.querySelectorAll('igx-tab-panel'); + expect(headers.length).toBe(3); + expect(contents.length).toBe(3); + + for (let itemIndex = 0; itemIndex < 3; itemIndex++) { + const headerId = `igx-tab-${tabIndex}-${itemIndex}`; + const contentId = `igx-tab-panel-${tabIndex}-${itemIndex}`; + + expect(headers[itemIndex].id).toEqual(headerId); + expect(headers[itemIndex].getAttribute('aria-controls')).toEqual(contentId); + + expect(contents[itemIndex].id).toEqual(contentId); + expect(contents[itemIndex].getAttribute('aria-labelledby')).toEqual(headerId); + } + } + }); + }); + + describe('Component with Panels Definitions', () => { let fixture; let tabbar; @@ -187,7 +232,7 @@ describe('IgxBottomNav', () => { }); - describe('BottomNav Component with Custom Template', () => { + describe('Component with Custom Template', () => { let fixture; let tabbar; @@ -271,6 +316,33 @@ describe('IgxBottomNav', () => { expect(theTabs[2].isSelected).toBe(false); })); + it('should not navigate to an URL blocked by activate guard', fakeAsync(() => { + fixture = TestBed.createComponent(BottomNavRoutingGuardTestComponent); + bottomNav = fixture.componentInstance.bottomNavComp; + fixture.detectChanges(); + theTabs = bottomNav.contentTabs.toArray(); + + fixture.ngZone.run(() => { router.initialNavigation(); }); + tick(); + expect(location.path()).toBe('/'); + + fixture.ngZone.run(() => { UIInteractions.clickElement(theTabs[0].elementRef()); }); + tick(); + expect(location.path()).toBe('/view1'); + fixture.detectChanges(); + expect(bottomNav.selectedIndex).toBe(0); + expect(theTabs[0].isSelected).toBe(true); + expect(theTabs[1].isSelected).toBe(false); + + fixture.ngZone.run(() => { UIInteractions.clickElement(theTabs[1].elementRef()); }); + tick(); + expect(location.path()).toBe('/view1'); + fixture.detectChanges(); + expect(bottomNav.selectedIndex).toBe(0); + expect(theTabs[0].isSelected).toBe(true); + expect(theTabs[1].isSelected).toBe(false); + })); + }); describe('Tabs-only Mode Tests', () => { @@ -295,38 +367,7 @@ describe('IgxBottomNav', () => { expect(theTabs[2].isSelected).toBe(false); expect(theTabs[2].elementRef().nativeElement.classList.contains(tabItemNormalCssClass)).toBe(true); }); - - it('should have the correct selection set even when no active link is present on the tabs', () => { - expect(theTabs[0].isSelected).toBe(false); - expect(theTabs[0].elementRef().nativeElement.classList.contains(tabItemNormalCssClass)).toBe(true); - expect(theTabs[1].isSelected).toBe(true); - expect(theTabs[1].elementRef().nativeElement.classList.contains(tabItemSelectedCssClass)).toBe(true); - expect(theTabs[2].isSelected).toBe(false); - expect(theTabs[2].elementRef().nativeElement.classList.contains(tabItemNormalCssClass)).toBe(true); - - theTabs[0].elementRef().nativeElement.dispatchEvent(new Event('click')); - fixture.detectChanges(); - - expect(theTabs[0].isSelected).toBe(true); - expect(theTabs[0].elementRef().nativeElement.classList.contains(tabItemSelectedCssClass)).toBe(true); - expect(theTabs[1].isSelected).toBe(false); - expect(theTabs[1].elementRef().nativeElement.classList.contains(tabItemNormalCssClass)).toBe(true); - expect(theTabs[2].isSelected).toBe(false); - expect(theTabs[2].elementRef().nativeElement.classList.contains(tabItemNormalCssClass)).toBe(true); - - theTabs[2].elementRef().nativeElement.dispatchEvent(new Event('click')); - fixture.detectChanges(); - - expect(theTabs[0].isSelected).toBe(false); - expect(theTabs[0].elementRef().nativeElement.classList.contains(tabItemNormalCssClass)).toBe(true); - expect(theTabs[1].isSelected).toBe(false); - expect(theTabs[1].elementRef().nativeElement.classList.contains(tabItemNormalCssClass)).toBe(true); - expect(theTabs[2].isSelected).toBe(true); - expect(theTabs[2].elementRef().nativeElement.classList.contains(tabItemSelectedCssClass)).toBe(true); - }); - }); - }); @Component({ @@ -478,3 +519,61 @@ class TabBarTabsOnlyModeTestComponent { @ViewChild(IgxBottomNavComponent, { static: true }) public bottomNavComp: IgxBottomNavComponent; } + +@Component({ + template: ` +
+
+ +
+ + + + + + +
+ ` +}) +class BottomNavRoutingGuardTestComponent { + @ViewChild(IgxBottomNavComponent, { static: true }) + public bottomNavComp: IgxBottomNavComponent; +} + +@Component({ + template: ` +
+
+ + +
Content 1
+
+ +
Content 2
+
+ +
Content 3
+
+
+
+
+ + +
Content 4
+
+ +
Content 5
+
+ +
Content 6
+
+
+
+
+ ` +}) +class BottomNavTestHtmlAttributesComponent { + @ViewChild(IgxBottomNavComponent, { static: true }) public tabbar: IgxBottomNavComponent; +} diff --git a/projects/igniteui-angular/src/lib/tabbar/tabbar.component.ts b/projects/igniteui-angular/src/lib/tabbar/tabbar.component.ts index 6657b56cbba..bd3174ce2dc 100644 --- a/projects/igniteui-angular/src/lib/tabbar/tabbar.component.ts +++ b/projects/igniteui-angular/src/lib/tabbar/tabbar.component.ts @@ -1,7 +1,6 @@ import { CommonModule } from '@angular/common'; import { AfterContentInit, - AfterViewChecked, AfterViewInit, Component, ContentChild, @@ -18,11 +17,13 @@ import { QueryList, TemplateRef, ViewChild, - ViewChildren + ViewChildren, + OnDestroy } from '@angular/core'; import { IgxBadgeModule } from '../badge/badge.component'; import { IgxIconModule } from '../icon/index'; import { IBaseEventArgs } from '../core/utils'; +import { Subscription } from 'rxjs'; export interface ISelectTabEventArgs extends IBaseEventArgs { tab: IgxTabComponent; @@ -63,7 +64,9 @@ export class IgxTabTemplateDirective { } `] }) -export class IgxBottomNavComponent implements AfterViewInit { +export class IgxBottomNavComponent implements AfterViewInit, OnDestroy { + private _currentBottomNavId = NEXT_ID++; + private _panelsChanges$: Subscription; /** * Gets the `IgxTabComponent` elements in the tab bar component created based on the provided panels. @@ -120,7 +123,7 @@ export class IgxBottomNavComponent implements AfterViewInit { */ @HostBinding('attr.id') @Input() - public id = `igx-bottom-nav-${NEXT_ID++}`; + public id = `igx-bottom-nav-${this._currentBottomNavId}`; /** * Emits an event when a new tab is selected. @@ -194,6 +197,11 @@ export class IgxBottomNavComponent implements AfterViewInit { * @hidden */ public ngAfterViewInit() { + this.setPanelsAttributes(); + this._panelsChanges$ = this.panels.changes.subscribe(() => { + this.setPanelsAttributes(); + }); + // initial selection setTimeout(() => { if (this.selectedIndex === -1) { @@ -206,6 +214,24 @@ export class IgxBottomNavComponent implements AfterViewInit { }, 0); } + /** + * @hidden + */ + public ngOnDestroy(): void { + if (this._panelsChanges$) { + this._panelsChanges$.unsubscribe(); + } + } + + private setPanelsAttributes() { + const panelsArray = Array.from(this.panels); + for (let index = 0; index < this.panels.length; index++) { + const tabPanels = panelsArray[index] as IgxTabPanelComponent; + tabPanels.nativeElement.setAttribute('id', this.getTabPanelId(index)); + tabPanels.nativeElement.setAttribute('aria-labelledby', this.getTabId(index)); + } + } + /** * @hidden */ @@ -247,6 +273,20 @@ export class IgxBottomNavComponent implements AfterViewInit { aTab.isSelected = false; this.onTabDeselected.emit({ tab: aTab, panel: null }); } + + /** + * @hidden + */ + public getTabId(index: number): string { + return `igx-tab-${this._currentBottomNavId}-${index}`; + } + + /** + * @hidden + */ + public getTabPanelId(index: number): string { + return `igx-tab-panel-${this._currentBottomNavId}-${index}`; + } } // ================================= IgxTabPanelComponent ====================================== @@ -255,7 +295,7 @@ export class IgxBottomNavComponent implements AfterViewInit { selector: 'igx-tab-panel', templateUrl: 'tab-panel.component.html' }) -export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked { +export class IgxTabPanelComponent implements AfterContentInit { /** * @hidden @@ -359,6 +399,16 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked return this._itemStyle; } + /** + * Returns the native element of the tab-panel component + * ```typescript + * const mytabPanelElement: HTMLElement = tabPanel.nativeElement; + * ``` + */ + public get nativeElement() { + return this._element.nativeElement; + } + /** * Gets the tab associated with the panel. * ```typescript @@ -439,13 +489,6 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked } } - /** - * @hidden - */ - public ngAfterViewChecked() { - this._element.nativeElement.setAttribute('aria-labelledby', `igx-tab-${this.index}`); - this._element.nativeElement.setAttribute('id', `igx-bottom-nav__panel-${this.index}`); - } /** * Selects the current tab and the tab panel. @@ -481,12 +524,6 @@ export class IgxTabComponent { @HostBinding('attr.role') public role = 'tab'; - /** - * @hidden @internal - */ - @HostBinding('attr.id') - public id = 'igx-tab-' + this.index; - /** * @hidden @internal */ @@ -505,11 +542,6 @@ export class IgxTabComponent { @HostBinding('attr.aria-selected') public ariaSelected = this.isSelected; - /** - * @hidden @internal - */ - @HostBinding('attr.aria-controls') - public ariaControls = 'igx-tab-panel-' + this.index; /** * Gets the panel associated with the tab. @@ -632,6 +664,13 @@ export class IgxTabComponent { return this.relatedPanel ? this.relatedPanel.isSelected : this._selected; } + /** + * @hidden @internal + * Set to true when the tab is automatically generated from the IgxBottomNavComponent when tab panels are defined. + */ + @Input() + public autoGenerated: boolean; + @HostBinding('class.igx-bottom-nav__menu-item--selected') public get cssClassSelected(): boolean { return this.isSelected; @@ -719,7 +758,9 @@ export class IgxTabComponent { */ @HostListener('click') public onClick() { - this.select(); + if (this.autoGenerated) { + this.select(); + } } public elementRef(): ElementRef {