From 4af8f264efb30bb2c7d78326a88541beeeaa7809 Mon Sep 17 00:00:00 2001 From: luxiaobei Date: Mon, 12 Jul 2021 16:20:02 +0800 Subject: [PATCH] feat(nav): nav support responsive (#1273) * feat(nav): nav support responsive * test(nav): add test about responsive * fix(nav): fix generate import relative path when extends mixin class * feat(nav): nav support responsive --- src/nav/examples/module.ts | 8 +- src/nav/examples/responsive/index.md | 4 + .../responsive/responsive.component.html | 20 + .../responsive/responsive.component.ts | 17 + src/nav/nav-link.directive.ts | 101 ++++- src/nav/nav.component.html | 46 +++ src/nav/nav.component.ts | 186 ++++++++- src/nav/nav.module.ts | 17 +- src/nav/styles/mixin.scss | 7 + src/nav/styles/nav.scss | 39 +- src/nav/test/nav.component.spec.ts | 385 ++++++++++++++---- 11 files changed, 719 insertions(+), 111 deletions(-) create mode 100644 src/nav/examples/responsive/index.md create mode 100644 src/nav/examples/responsive/responsive.component.html create mode 100644 src/nav/examples/responsive/responsive.component.ts create mode 100644 src/nav/nav.component.html diff --git a/src/nav/examples/module.ts b/src/nav/examples/module.ts index f768b25ed..4caf4253f 100644 --- a/src/nav/examples/module.ts +++ b/src/nav/examples/module.ts @@ -3,9 +3,12 @@ import { NgxTethysModule } from 'ngx-tethys'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; + import { ThyNavFillExampleComponent } from './fill/fill.component'; import { ThyNavHorizontalExampleComponent } from './horizontal/horizontal.component'; import { ThyNavIconNavExampleComponent } from './icon-nav/icon-nav.component'; +import { ThyNavResponsiveExampleComponent } from './responsive/responsive.component'; import { ThyNavTypeExampleComponent } from './type/type.component'; import { ThyNavVerticalExampleComponent } from './vertical/vertical.component'; @@ -14,13 +17,14 @@ const COMPONENTS = [ ThyNavHorizontalExampleComponent, ThyNavIconNavExampleComponent, ThyNavTypeExampleComponent, - ThyNavVerticalExampleComponent + ThyNavVerticalExampleComponent, + ThyNavResponsiveExampleComponent ]; @NgModule({ declarations: COMPONENTS, entryComponents: COMPONENTS, - imports: [CommonModule, FormsModule, NgxTethysModule], + imports: [CommonModule, FormsModule, NgxTethysModule, RouterModule], exports: [], providers: COMPONENTS }) diff --git a/src/nav/examples/responsive/index.md b/src/nav/examples/responsive/index.md new file mode 100644 index 000000000..2cac8ef2a --- /dev/null +++ b/src/nav/examples/responsive/index.md @@ -0,0 +1,4 @@ +--- +order: 50 +title: 响应式 +--- diff --git a/src/nav/examples/responsive/responsive.component.html b/src/nav/examples/responsive/responsive.component.html new file mode 100644 index 000000000..233322def --- /dev/null +++ b/src/nav/examples/responsive/responsive.component.html @@ -0,0 +1,20 @@ + + 导航一 + 导航二 + 导航三 + 导航四 + 导航五 + 导航六 + 导航七 + 导航八 + 导航九 + + 导航十 + + 导航十一 + + 导航十二 + 导航十三 + +
导航十四
+
diff --git a/src/nav/examples/responsive/responsive.component.ts b/src/nav/examples/responsive/responsive.component.ts new file mode 100644 index 000000000..b86c3ce7c --- /dev/null +++ b/src/nav/examples/responsive/responsive.component.ts @@ -0,0 +1,17 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'thy-nav-responsive-example', + templateUrl: './responsive.component.html' +}) +export class ThyNavResponsiveExampleComponent implements OnInit { + activeIndex = 13; + + constructor() {} + + ngOnInit(): void {} + + select(value: number) { + this.activeIndex = value; + } +} diff --git a/src/nav/nav-link.directive.ts b/src/nav/nav-link.directive.ts index 2c552e201..67e213255 100644 --- a/src/nav/nav-link.directive.ts +++ b/src/nav/nav-link.directive.ts @@ -1,20 +1,105 @@ -import { Component, Directive, ElementRef, Renderer2, Input, HostBinding } from '@angular/core'; -import { coerceBooleanProperty } from 'ngx-tethys/util'; +import { Constructor, InputBoolean, MixinBase, mixinUnsubscribe, ThyUnsubscribe } from 'ngx-tethys/core'; +import { takeUntil } from 'rxjs/operators'; + +import { + AfterViewInit, + ContentChildren, + Directive, + ElementRef, + HostBinding, + Input, + NgZone, + OnDestroy, + Optional, + QueryList, + Renderer2 +} from '@angular/core'; +import { RouterLinkActive } from '@angular/router'; export type ThyNavLink = '' | 'active'; +const _MixinBase: Constructor & typeof MixinBase = mixinUnsubscribe(MixinBase); + @Directive({ selector: '[thyNavLink]' }) -export class ThyNavLinkDirective { +export class ThyNavLinkDirective extends _MixinBase implements AfterViewInit, OnDestroy { + @HostBinding('class.active') @Input() - set thyNavLinkActive(active: string) { - this.navLinkActive = coerceBooleanProperty(active); - } - - @HostBinding('class.active') navLinkActive = false; + @InputBoolean() + thyNavLinkActive: boolean; @HostBinding('class.nav-link') navLinkClass = true; + @ContentChildren(ThyNavLinkDirective, { descendants: true }) links: QueryList; + + @ContentChildren(RouterLinkActive, { descendants: true }) routers: QueryList; + // @HostBinding('attr.href') navLinkHref = 'javascript:;'; + + public offset: { + width: number; + height: number; + left: number; + top: number; + } = { + width: 0, + height: 0, + left: 0, + top: 0 + }; + + public content: HTMLElement; + + public isActive: boolean; + + constructor( + public elementRef: ElementRef, + private renderer: Renderer2, + @Optional() private routerLinkActive: RouterLinkActive, + private ngZone: NgZone + ) { + super(); + } + + ngAfterViewInit() { + this.setOffset(); + + this.content = this.elementRef.nativeElement.outerHTML; + + this.ngZone.onStable.pipe(takeUntil(this.ngUnsubscribe$)).subscribe(() => { + this.isActive = this.linkIsActive(); + }); + } + + setOffset() { + this.offset = { + width: this.elementRef.nativeElement.offsetWidth, + height: this.elementRef.nativeElement.offsetHeight, + left: this.elementRef.nativeElement.offsetLeft, + top: this.elementRef.nativeElement.offsetTop + }; + } + + linkIsActive() { + return ( + this.thyNavLinkActive || + (this.routerLinkActive && this.routerLinkActive.isActive) || + this.routers.some(router => router.isActive) || + this.links.some(item => item.thyNavLinkActive) + ); + } + + setNavLinkHidden(value: boolean) { + if (value) { + this.renderer.addClass(this.elementRef.nativeElement, 'nav-item-hidden'); + } else { + this.renderer.removeClass(this.elementRef.nativeElement, 'nav-item-hidden'); + } + } + + ngOnDestroy() { + this.ngUnsubscribe$.next(); + this.ngUnsubscribe$.complete(); + } } diff --git a/src/nav/nav.component.html b/src/nav/nav.component.html new file mode 100644 index 000000000..74a6d4601 --- /dev/null +++ b/src/nav/nav.component.html @@ -0,0 +1,46 @@ + +
+ +
+ + + + 更多 + + +
+ + + + + + + + + + + + + + + + + + + + + diff --git a/src/nav/nav.component.ts b/src/nav/nav.component.ts index 4c1cb25a1..bdad5d40a 100644 --- a/src/nav/nav.component.ts +++ b/src/nav/nav.component.ts @@ -1,5 +1,31 @@ -import { Component, Directive, ElementRef, Renderer2, Input, HostBinding, OnInit } from '@angular/core'; -import { UpdateHostClassService } from 'ngx-tethys/core'; +import { Constructor, InputBoolean, MixinBase, mixinUnsubscribe, ThyUnsubscribe, UpdateHostClassService } from 'ngx-tethys/core'; +import { ThyPopover } from 'ngx-tethys/popover'; +import { merge } from 'rxjs'; +import { take, takeUntil } from 'rxjs/operators'; + +import { ViewportRuler } from '@angular/cdk/overlay'; +import { + AfterContentChecked, + AfterContentInit, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ContentChildren, + ElementRef, + HostBinding, + Input, + NgZone, + OnDestroy, + OnInit, + QueryList, + TemplateRef +} from '@angular/core'; + +import { ThyNavLinkDirective } from './nav-link.directive'; + +const _MixinBase: Constructor & typeof MixinBase = mixinUnsubscribe(MixinBase); export type ThyNavType = 'primary' | 'secondary' | 'thirdly' | 'secondary-divider'; export type ThyNavSize = '' | 'sm'; @@ -24,20 +50,36 @@ const navHorizontalClassesMap = { @Component({ selector: 'thy-nav', - template: ` - - `, + templateUrl: './nav.component.html', host: { class: 'thy-nav' }, - providers: [UpdateHostClassService] + providers: [UpdateHostClassService], + changeDetection: ChangeDetectionStrategy.OnPush }) -export class ThyNavComponent implements OnInit { +export class ThyNavComponent extends _MixinBase implements OnInit, AfterViewInit, AfterContentInit, AfterContentChecked, OnDestroy { private _type: ThyNavType; private _size: ThyNavSize; private _horizontal: ThyNavHorizontal; private _initialized = false; + public wrapperOffset: { height: number; width: number; left: number; top: number } = { + height: 0, + width: 0, + left: 0, + top: 0 + }; + + public hiddenItems: ThyNavLinkDirective[] = []; + + public moreActive: boolean; + + @ContentChildren(ThyNavLinkDirective, { descendants: true }) links: QueryList; + + @ContentChild('more') moreOperation: TemplateRef; + + @ContentChild('morePopover') morePopover: TemplateRef; + @Input() set thyType(type: ThyNavType) { this._type = type || 'primary'; @@ -62,19 +104,19 @@ export class ThyNavComponent implements OnInit { } } + @HostBinding('class.thy-nav--vertical') @Input() - set thyVertical(value: boolean) { - this._isVertical = value; - } + @InputBoolean() + thyVertical: boolean; + @HostBinding('class.thy-nav--fill') @Input() - set thyFill(value: boolean) { - this._isFill = value; - } + @InputBoolean() + thyFill: boolean; - @HostBinding('class.thy-nav--vertical') _isVertical = false; - - @HostBinding('class.thy-nav--fill') _isFill = false; + @Input() + @InputBoolean() + thyResponsive: boolean; private _updateClasses() { let classNames: string[] = []; @@ -90,7 +132,15 @@ export class ThyNavComponent implements OnInit { this.updateHostClass.updateClass(classNames); } - constructor(private updateHostClass: UpdateHostClassService, private elementRef: ElementRef) { + constructor( + private updateHostClass: UpdateHostClassService, + private elementRef: ElementRef, + private viewportRuler: ViewportRuler, + private ngZone: NgZone, + private changeDetectorRef: ChangeDetectorRef, + private popover: ThyPopover + ) { + super(); this.updateHostClass.initializeElement(elementRef.nativeElement); } @@ -98,4 +148,106 @@ export class ThyNavComponent implements OnInit { this._initialized = true; this._updateClasses(); } + + ngAfterViewInit() { + if (this.thyResponsive) { + this.ngZone.onStable.pipe(take(1)).subscribe(() => { + this.links.toArray().forEach(link => link.setOffset()); + this.setHiddenItems(); + }); + + merge(this.links.changes, this.viewportRuler.change(100)) + .pipe(takeUntil(this.ngUnsubscribe$)) + .subscribe(() => { + this.resetSizes(); + this.setHiddenItems(); + this.calculateMoreIsActive(); + }); + } + } + + ngAfterContentInit(): void { + if (this.thyResponsive) { + this.ngZone.onStable.pipe(take(1)).subscribe(() => { + this.resetSizes(); + }); + } + } + + ngAfterContentChecked() { + this.calculateMoreIsActive(); + } + + private calculateMoreIsActive() { + this.moreActive = this.hiddenItems.some(item => { + return item.linkIsActive(); + }); + this.changeDetectorRef.detectChanges(); + } + + private setHiddenItems() { + this.moreActive = false; + const tabs = this.links.toArray(); + if (!tabs.length) { + this.hiddenItems = []; + return; + } + + const len = tabs.length; + let endIndex = len; + for (let i = len - 1; i >= 0; i -= 1) { + tabs[i].setNavLinkHidden(true); + if (this.thyVertical) { + if (tabs[i].offset.top + tabs[i].offset.height < this.wrapperOffset.height + this.wrapperOffset.top) { + endIndex = i; + break; + } + } else { + if (tabs[i].offset.left + tabs[i].offset.width < this.wrapperOffset.width + this.wrapperOffset.left) { + endIndex = i; + break; + } + } + } + + if (endIndex === len - 1) { + tabs[endIndex].setNavLinkHidden(false); + } + + const showItems = tabs.slice(0, endIndex); + (showItems || []).forEach(item => { + item.setNavLinkHidden(false); + }); + + this.hiddenItems = endIndex === len - 1 ? [] : [...tabs.slice(endIndex)]; + } + + private resetSizes() { + this.wrapperOffset = { + height: this.elementRef.nativeElement.offsetHeight || 0, + width: this.elementRef.nativeElement.offsetWidth || 0, + left: this.elementRef.nativeElement.offsetLeft || 0, + top: this.elementRef.nativeElement.offsetTop || 0 + }; + } + + openMore(event: Event, template: TemplateRef) { + this.popover.open(template, { + origin: event.currentTarget as HTMLElement, + hasBackdrop: true, + backdropClosable: true, + insideClosable: true, + placement: 'bottom', + panelClass: 'thy-nav-list-popover' + }); + } + + navItemClick(item: ThyNavLinkDirective) { + item.elementRef.nativeElement.click(); + } + + ngOnDestroy() { + this.ngUnsubscribe$.next(); + this.ngUnsubscribe$.complete(); + } } diff --git a/src/nav/nav.module.ts b/src/nav/nav.module.ts index 003b732c3..947a30249 100644 --- a/src/nav/nav.module.ts +++ b/src/nav/nav.module.ts @@ -1,14 +1,19 @@ -import { NgModule } from '@angular/core'; +import { ThyActionMenuModule } from 'ngx-tethys/action-menu'; +import { ThyIconModule } from 'ngx-tethys/icon'; +import { ThyPopoverModule } from 'ngx-tethys/popover'; + import { CommonModule } from '@angular/common'; -import { ThyNavComponent } from './nav.component'; -import { ThyNavLinkDirective } from './nav-link.directive'; -import { ThyIconNavComponent } from './icon-nav/icon-nav.component'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + import { ThyIconNavLinkComponent } from './icon-nav/icon-nav-link.directive'; -import { ThyIconModule } from 'ngx-tethys/icon'; +import { ThyIconNavComponent } from './icon-nav/icon-nav.component'; +import { ThyNavLinkDirective } from './nav-link.directive'; +import { ThyNavComponent } from './nav.component'; @NgModule({ declarations: [ThyNavComponent, ThyNavLinkDirective, ThyIconNavComponent, ThyIconNavLinkComponent], - imports: [CommonModule, ThyIconModule], + imports: [CommonModule, ThyIconModule, ThyPopoverModule, ThyActionMenuModule, RouterModule], exports: [ThyNavComponent, ThyNavLinkDirective, ThyIconNavComponent, ThyIconNavLinkComponent] }) export class ThyNavModule {} diff --git a/src/nav/styles/mixin.scss b/src/nav/styles/mixin.scss index 0e4557d6c..0fbb3c31f 100644 --- a/src/nav/styles/mixin.scss +++ b/src/nav/styles/mixin.scss @@ -41,6 +41,13 @@ } } +@mixin nav-item-size($margin-right) { + margin-right: $margin-right; + &:last-child { + margin-right: 0; + } +} + @mixin nav-link-clear-margin-right() { &:last-child { margin-right: 0; diff --git a/src/nav/styles/nav.scss b/src/nav/styles/nav.scss index de2dc74ab..053d45eeb 100644 --- a/src/nav/styles/nav.scss +++ b/src/nav/styles/nav.scss @@ -1,10 +1,11 @@ .thy-nav { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; padding-left: 0; margin-bottom: 0; list-style: none; + .thy-nav-item, .nav-link { display: block; text-align: center; @@ -14,6 +15,7 @@ border-bottom-style: solid; border-bottom-width: $nav-border-width; border-bottom-color: transparent; + flex: 0 auto; @include nav-link-variant($nav-link-color, $nav-link-hover-color, $nav-border-bottom); } @@ -25,6 +27,9 @@ min-width: $nav-link-primary-min-width; @include nav-link-variant($nav-link-primary-color, $nav-link-hover-color, $nav-border-bottom); } + .thy-nav-item { + @include nav-item-size($nav-link-primary-right); + } } .nav-secondary { @@ -103,3 +108,35 @@ } @import './icon-nav.scss'; + +.thy-nav-list { + display: flex; + flex-wrap: nowrap; + flex: 0 auto; + overflow: hidden; + .nav-item-hidden { + display: none; + } +} + +.thy-nav-list-popover { + .thy-nav-item { + display: none; + } + .nav-item-hidden { + display: block; + } + .more-nav-link, + .more-nav-link * { + text-decoration: none; + color: $secondary; + &:hover { + color: $gray-800; + } + } +} + +.thy-nav-more-container { + flex: 1 !important; + flex-grow: 0 !important; +} diff --git a/src/nav/test/nav.component.spec.ts b/src/nav/test/nav.component.spec.ts index 1e3d188fe..330d19af9 100644 --- a/src/nav/test/nav.component.spec.ts +++ b/src/nav/test/nav.component.spec.ts @@ -1,12 +1,17 @@ -import { ThyNavLinkDirective } from './../nav-link.directive'; -import { ThyNavHorizontal } from './../nav.component'; -import { TestBed, ComponentFixture } from '@angular/core/testing'; -import { Component, OnInit } from '@angular/core'; -import { ThyNavModule } from '../nav.module'; -import { ThyIconModule } from '../../icon'; -import { injectDefaultSvgIconSet, bypassSanitizeProvider } from 'ngx-tethys/testing'; +import { bypassSanitizeProvider, dispatchFakeEvent, injectDefaultSvgIconSet } from 'ngx-tethys/testing'; + +import { OverlayContainer } from '@angular/cdk/overlay'; +import { Component, DebugElement, ElementRef, inject, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { ThyNavComponent, ThyNavSize, ThyNavType } from '../nav.component'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { Router, Routes } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { ThyIconModule } from '../../icon'; +import { ThyNavLinkDirective } from '../nav-link.directive'; +import { ThyNavComponent, ThyNavHorizontal, ThyNavSize, ThyNavType } from '../nav.component'; +import { ThyNavModule } from '../nav.module'; const NAV_CLASS = `thy-nav`; const NAV_LINK_CLASS = `nav-link`; @@ -43,104 +48,330 @@ export class NavBasicComponent implements OnInit { ngOnInit(): void {} } +@Component({ + selector: 'app-nav-basic', + template: ` + + {{ item.name }} + + ` +}) +export class NavResponsiveComponent implements OnInit { + type: ThyNavType; + + size: ThyNavSize; + + isFill = false; + + isVertical = false; + + horizontal: ThyNavHorizontal; + + navLinks = [{ name: 'link1' }, { name: 'link2' }, { name: 'link3' }]; + + @ViewChildren(ThyNavLinkDirective) links: ThyNavLinkDirective[]; + + @ViewChildren(ThyNavLinkDirective, { read: ElementRef }) linksElement: QueryList; + + @ViewChild(ThyNavComponent) nav: ThyNavComponent; + + constructor() {} + + ngOnInit(): void {} +} + +@Component({ + selector: 'app-nav-basic', + template: `` +}) +export class NavRouteComponent {} + +const routes: Routes = [ + { + path: 'link2', + component: NavRouteComponent, + data: { + path: 'two' + } + } +]; + describe(`thy-nav`, () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [NavBasicComponent], - imports: [ThyNavModule, ThyIconModule], + declarations: [NavBasicComponent, NavResponsiveComponent, NavRouteComponent], + imports: [ThyNavModule, ThyIconModule, NoopAnimationsModule, RouterTestingModule.withRoutes(routes)], providers: [bypassSanitizeProvider] }); TestBed.compileComponents(); injectDefaultSvgIconSet(); }); - let fixture: ComponentFixture; + describe('basic', () => { + let fixture: ComponentFixture; + beforeEach(() => { + fixture = TestBed.createComponent(NavBasicComponent); + fixture.detectChanges(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(NavBasicComponent); - fixture.detectChanges(); - }); + it(`should get correct class for default nav`, () => { + const navDebugElement = fixture.debugElement.query(By.directive(ThyNavComponent)); + const navElement: HTMLElement = navDebugElement.nativeElement; + expect(navDebugElement).toBeTruthy(); + expect(navElement).toBeTruthy(); + expect(navElement.classList.contains(NAV_CLASS)).toEqual(true); + expect(navElement.classList.contains('nav-primary')).toEqual(true); + expect(navElement.classList.contains('custom-nav')).toEqual(true); + }); - it(`should get correct class for default nav`, () => { - const navDebugElement = fixture.debugElement.query(By.directive(ThyNavComponent)); - const navElement: HTMLElement = navDebugElement.nativeElement; - expect(navDebugElement).toBeTruthy(); - expect(navElement).toBeTruthy(); - expect(navElement.classList.contains(NAV_CLASS)).toEqual(true); - expect(navElement.classList.contains('nav-primary')).toEqual(true); - expect(navElement.classList.contains('custom-nav')).toEqual(true); - }); + it(`should get correct nav links`, () => { + const navDebugElement = fixture.debugElement.query(By.directive(ThyNavComponent)); + const links = navDebugElement.queryAll(By.directive(ThyNavLinkDirective)); + expect(links).toBeTruthy(); + expect(links.length).toEqual(2); + const activeLink: HTMLElement = links[0].nativeElement; + const link2: HTMLElement = links[1].nativeElement; + expect(activeLink.textContent).toContain('Link1'); + expect(activeLink.classList.contains(NAV_LINK_CLASS)).toEqual(true); + expect(activeLink.classList.contains('active')).toEqual(true); - it(`should get correct nav links`, () => { - const navDebugElement = fixture.debugElement.query(By.directive(ThyNavComponent)); - const links = navDebugElement.queryAll(By.directive(ThyNavLinkDirective)); - expect(links).toBeTruthy(); - expect(links.length).toEqual(2); - const activeLink: HTMLElement = links[0].nativeElement; - const link2: HTMLElement = links[1].nativeElement; - expect(activeLink.textContent).toContain('Link1'); - expect(activeLink.classList.contains(NAV_LINK_CLASS)).toEqual(true); - expect(activeLink.classList.contains('active')).toEqual(true); - - expect(link2.textContent).toContain('Link2'); - expect(link2.classList.contains(NAV_LINK_CLASS)).toEqual(true); - expect(link2.classList.contains('active')).toEqual(false); - }); + expect(link2.textContent).toContain('Link2'); + expect(link2.classList.contains(NAV_LINK_CLASS)).toEqual(true); + expect(link2.classList.contains('active')).toEqual(false); + }); + + it(`should get correct class when input type`, () => { + ['primary', 'secondary', 'thirdly', 'secondary-divider'].forEach(type => { + fixture.debugElement.componentInstance.type = type; + fixture.detectChanges(); + const navDebugElement = fixture.debugElement.query(By.directive(ThyNavComponent)); + const navElement: HTMLElement = navDebugElement.nativeElement; + expect(navElement.classList.contains(NAV_CLASS)).toEqual(true); + expect(navElement.classList.contains(`nav-${type}`)).toEqual(true); + }); + }); - it(`should get correct class when input type`, () => { - ['primary', 'secondary', 'thirdly', 'secondary-divider'].forEach(type => { - fixture.debugElement.componentInstance.type = type; + it(`should get correct class when input size`, () => { + ['sm'].forEach(size => { + fixture.debugElement.componentInstance.size = size; + fixture.detectChanges(); + const navDebugElement = fixture.debugElement.query(By.directive(ThyNavComponent)); + const navElement: HTMLElement = navDebugElement.nativeElement; + expect(navElement.classList.contains(NAV_CLASS)).toEqual(true); + expect(navElement.classList.contains(`nav-${size}`)).toEqual(true); + }); + }); + + it(`should get correct class when is fill`, () => { + fixture.debugElement.componentInstance.isFill = true; fixture.detectChanges(); const navDebugElement = fixture.debugElement.query(By.directive(ThyNavComponent)); const navElement: HTMLElement = navDebugElement.nativeElement; expect(navElement.classList.contains(NAV_CLASS)).toEqual(true); - expect(navElement.classList.contains(`nav-${type}`)).toEqual(true); + expect(navElement.classList.contains(`thy-nav--fill`)).toEqual(true); }); - }); - it(`should get correct class when input size`, () => { - ['sm'].forEach(size => { - fixture.debugElement.componentInstance.size = size; + it(`should get correct class when is vertical`, () => { + fixture.debugElement.componentInstance.isVertical = true; fixture.detectChanges(); const navDebugElement = fixture.debugElement.query(By.directive(ThyNavComponent)); const navElement: HTMLElement = navDebugElement.nativeElement; expect(navElement.classList.contains(NAV_CLASS)).toEqual(true); - expect(navElement.classList.contains(`nav-${size}`)).toEqual(true); + expect(navElement.classList.contains(`thy-nav--vertical`)).toEqual(true); }); - }); - it(`should get correct class when is fill`, () => { - fixture.debugElement.componentInstance.isFill = true; - fixture.detectChanges(); - const navDebugElement = fixture.debugElement.query(By.directive(ThyNavComponent)); - const navElement: HTMLElement = navDebugElement.nativeElement; - expect(navElement.classList.contains(NAV_CLASS)).toEqual(true); - expect(navElement.classList.contains(`thy-nav--fill`)).toEqual(true); - }); + it(`should get correct class when input thyHorizontal`, () => { + const navHorizontalClassesMap = { + left: '', + center: 'justify-content-center', + right: 'justify-content-end' + }; - it(`should get correct class when is vertical`, () => { - fixture.debugElement.componentInstance.isVertical = true; - fixture.detectChanges(); - const navDebugElement = fixture.debugElement.query(By.directive(ThyNavComponent)); - const navElement: HTMLElement = navDebugElement.nativeElement; - expect(navElement.classList.contains(NAV_CLASS)).toEqual(true); - expect(navElement.classList.contains(`thy-nav--vertical`)).toEqual(true); + ['center', 'right'].forEach(item => { + fixture.debugElement.componentInstance.horizontal = item; + fixture.detectChanges(); + const navDebugElement = fixture.debugElement.query(By.directive(ThyNavComponent)); + const navElement: HTMLElement = navDebugElement.nativeElement; + expect(navElement.classList.contains(NAV_CLASS)).toEqual(true); + expect(navElement.classList.contains(navHorizontalClassesMap[item])).toEqual(true); + }); + }); }); - it(`should get correct class when input thyHorizontal`, () => { - const navHorizontalClassesMap = { - left: '', - center: 'justify-content-center', - right: 'justify-content-end' - }; + describe('responsive', () => { + let overlayContainer: OverlayContainer; + let fixture: ComponentFixture; - ['center', 'right'].forEach(item => { - fixture.debugElement.componentInstance.horizontal = item; - fixture.detectChanges(); - const navDebugElement = fixture.debugElement.query(By.directive(ThyNavComponent)); - const navElement: HTMLElement = navDebugElement.nativeElement; - expect(navElement.classList.contains(NAV_CLASS)).toEqual(true); - expect(navElement.classList.contains(navHorizontalClassesMap[item])).toEqual(true); + beforeEach(() => { + fixture = TestBed.createComponent(NavResponsiveComponent); + overlayContainer = TestBed.inject(OverlayContainer); }); + + afterEach(() => { + overlayContainer.ngOnDestroy(); + }); + + it('should show more when responsive and overflow in horizontal direction', fakeAsync(() => { + fixture.debugElement.componentInstance.responsive = true; + fixture.detectChanges(); + + spyLinksAndNavOffset(fixture.componentInstance.links, fixture.componentInstance.nav); + dispatchFakeEvent(window, 'resize'); + fixture.detectChanges(); + tick(300); + fixture.detectChanges(); + const moreBtn: DebugElement = fixture.debugElement.query(By.css('.thy-nav-more-container')); + expect(moreBtn).toBeTruthy(); + expect(moreBtn.nativeElement.classList.contains('d-none')).toBeFalsy(); + expect(fixture.debugElement.queryAll(By.css('.nav-item-hidden')).length).toEqual(2); + })); + + it('should active moreBtn when hidden link is active', fakeAsync(() => { + fixture.debugElement.componentInstance.responsive = true; + fixture.debugElement.componentInstance.navLinks = [{ name: 'link1' }, { name: 'link2', isActive: true }, { name: 'link3' }]; + fixture.detectChanges(); + + spyLinksAndNavOffset(fixture.componentInstance.links, fixture.componentInstance.nav); + dispatchFakeEvent(window, 'resize'); + fixture.detectChanges(); + tick(300); + fixture.detectChanges(); + const moreBtn: DebugElement = fixture.debugElement.query(By.css('.thy-nav-more-container')); + expect(moreBtn).toBeTruthy(); + expect(moreBtn.nativeElement.classList.contains('d-none')).toBeFalsy(); + expect(moreBtn.nativeElement.classList.contains('active')).toBeTruthy(); + })); + + it('should active moreBtn when hidden link router is active', fakeAsync(() => { + fixture.debugElement.componentInstance.responsive = true; + const router: Router = TestBed.inject(Router); + router.initialNavigation(); + fixture.detectChanges(); + router.navigate(['.', 'link2']); + spyLinksAndNavOffset(fixture.componentInstance.links, fixture.componentInstance.nav); + dispatchFakeEvent(window, 'resize'); + fixture.detectChanges(); + tick(300); + fixture.detectChanges(); + + const moreBtn: DebugElement = fixture.debugElement.query(By.css('.thy-nav-more-container')); + expect(moreBtn).toBeTruthy(); + expect(moreBtn.nativeElement.classList.contains('d-none')).toBeFalsy(); + expect(moreBtn.nativeElement.classList.contains('active')).toBeTruthy(); + })); + + it('should show more when responsive and overflow in vertical direction', fakeAsync(() => { + fixture.debugElement.componentInstance.responsive = true; + fixture.debugElement.componentInstance.isVertical = true; + fixture.detectChanges(); + spyLinksAndNavOffset(fixture.componentInstance.links, fixture.componentInstance.nav); + dispatchFakeEvent(window, 'resize'); + tick(300); + fixture.detectChanges(); + const moreBtn: DebugElement = fixture.debugElement.query(By.css('.thy-nav-more-container')); + expect(moreBtn).toBeTruthy(); + expect(moreBtn.nativeElement.classList.contains('d-none')).toBeFalsy(); + expect(fixture.debugElement.queryAll(By.css('.nav-item-hidden')).length).toEqual(2); + })); + + it('should hidden moreBtn when has not navLinks', fakeAsync(() => { + fixture.debugElement.componentInstance.responsive = true; + fixture.debugElement.componentInstance.navLinks = []; + + fixture.detectChanges(); + tick(300); + fixture.detectChanges(); + + const moreBtn: DebugElement = fixture.debugElement.query(By.css('.thy-nav-more-container')); + expect(moreBtn).toBeTruthy(); + expect(moreBtn.nativeElement.classList.contains('d-none')).toBeTruthy(); + expect(fixture.debugElement.queryAll(By.css('.test-link')).length).toEqual(0); + })); + + it('should show all navLinks when change navLinks', fakeAsync(() => { + fixture.debugElement.componentInstance.responsive = true; + fixture.detectChanges(); + spyLinksAndNavOffset(fixture.componentInstance.links, fixture.componentInstance.nav); + fixture.detectChanges(); + tick(300); + fixture.detectChanges(); + fixture.debugElement.componentInstance.navLinks = [...fixture.debugElement.componentInstance.navLinks, { name: 'link4' }]; + fixture.detectChanges(); + tick(300); + fixture.detectChanges(); + const moreBtn: DebugElement = fixture.debugElement.query(By.css('.thy-nav-more-container')); + expect(moreBtn).toBeTruthy(); + expect(moreBtn.nativeElement.classList.contains('d-none')).toBeFalsy(); + expect(fixture.debugElement.queryAll(By.css('.test-link')).length).toEqual( + fixture.debugElement.componentInstance.navLinks.length + ); + })); + + it('should show hidden links when click moreBtn', fakeAsync(() => { + fixture.debugElement.componentInstance.responsive = true; + fixture.detectChanges(); + + spyLinksAndNavOffset(fixture.componentInstance.links, fixture.componentInstance.nav); + dispatchFakeEvent(window, 'resize'); + fixture.detectChanges(); + tick(300); + fixture.detectChanges(); + const moreBtn: DebugElement = fixture.debugElement.query(By.css('.thy-nav-more-container')); + dispatchFakeEvent(moreBtn.nativeElement, 'click'); + + const popover = overlayContainer.getContainerElement().querySelector('thy-popover-container'); + expect(popover).toBeTruthy(); + expect(popover.querySelectorAll('.more-nav-link').length).toEqual(2); + })); + + it('should call item event when click navLink in more popover', fakeAsync(() => { + fixture.debugElement.componentInstance.responsive = true; + fixture.detectChanges(); + + spyLinksAndNavOffset(fixture.componentInstance.links, fixture.componentInstance.nav); + dispatchFakeEvent(window, 'resize'); + fixture.detectChanges(); + tick(300); + fixture.detectChanges(); + const moreBtn: DebugElement = fixture.debugElement.query(By.css('.thy-nav-more-container')); + dispatchFakeEvent(moreBtn.nativeElement, 'click'); + const popover = overlayContainer.getContainerElement().querySelector('thy-popover-container'); + const link = popover.querySelectorAll('.more-nav-link')[0]; + const linkSpy = spyOn(fixture.componentInstance.linksElement.toArray()[1].nativeElement, 'click'); + dispatchFakeEvent(link, 'click'); + expect(linkSpy).toHaveBeenCalled(); + })); }); }); + +function spyLinksAndNavOffset(links: ThyNavLinkDirective[], nav: ThyNavComponent) { + (links || []).forEach((link, index) => { + link.offset = { + width: 30, + height: 30, + left: 30 * index, + top: 30 * index + }; + }); + + spyOnProperty(nav['elementRef'].nativeElement, 'offsetWidth', 'get').and.returnValue(70); + spyOnProperty(nav['elementRef'].nativeElement, 'offsetHeight', 'get').and.returnValue(70); + spyOnProperty(nav['elementRef'].nativeElement, 'offsetLeft', 'get').and.returnValue(0); + spyOnProperty(nav['elementRef'].nativeElement, 'offsetTop', 'get').and.returnValue(0); +}