From e8daabf5c81df3c488ff2ef768f14cb6fe2c8358 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sun, 2 Apr 2017 16:31:12 +0200 Subject: [PATCH 1/2] fix(tabs): re-align the ink bar when the viewport size changes * Recalculates the ink bar position, as well as the pagination, if the viewport size changes. * Adds a missing test on the `tab-nav-bar` regarding repositioning on direction changes. Fixes #3845. Fixes #3044. Fixes #2518. Fixes #1231. --- src/lib/tabs/tab-header.spec.ts | 18 +++++++- src/lib/tabs/tab-header.ts | 43 ++++++++++++++------ src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts | 39 ++++++++++++++++-- src/lib/tabs/tab-nav-bar/tab-nav-bar.ts | 36 +++++++++++----- 4 files changed, 108 insertions(+), 28 deletions(-) diff --git a/src/lib/tabs/tab-header.spec.ts b/src/lib/tabs/tab-header.spec.ts index 89d931d4c108..8f6a065b213d 100644 --- a/src/lib/tabs/tab-header.spec.ts +++ b/src/lib/tabs/tab-header.spec.ts @@ -1,4 +1,4 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {async, ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing'; import {Component, ViewChild, ViewContainerRef} from '@angular/core'; import {LayoutDirection, Dir} from '../core/rtl/dir'; import {MdTabHeader} from './tab-header'; @@ -11,6 +11,7 @@ import {RIGHT_ARROW, LEFT_ARROW, ENTER} from '../core/keyboard/keycodes'; import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler'; import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; import {dispatchKeyboardEvent} from '../core/testing/dispatch-events'; +import {dispatchFakeEvent} from '../core/testing/dispatch-events'; import {Subject} from 'rxjs/Subject'; @@ -211,6 +212,21 @@ describe('MdTabHeader', () => { expect(inkBar.alignToElement).toHaveBeenCalled(); }); + it('should re-align the ink bar when the window is resized', fakeAsync(() => { + fixture = TestBed.createComponent(SimpleTabHeaderApp); + fixture.detectChanges(); + + const inkBar = fixture.componentInstance.mdTabHeader._inkBar; + + spyOn(inkBar, 'alignToElement'); + + dispatchFakeEvent(window, 'resize'); + tick(10); + fixture.detectChanges(); + + expect(inkBar.alignToElement).toHaveBeenCalled(); + })); + }); }); diff --git a/src/lib/tabs/tab-header.ts b/src/lib/tabs/tab-header.ts index 3fee07a9f749..0a3d53116c24 100644 --- a/src/lib/tabs/tab-header.ts +++ b/src/lib/tabs/tab-header.ts @@ -12,13 +12,20 @@ import { AfterContentChecked, AfterContentInit, OnDestroy, + NgZone, } from '@angular/core'; import {RIGHT_ARROW, LEFT_ARROW, ENTER, Dir, LayoutDirection} from '../core'; import {MdTabLabelWrapper} from './tab-label-wrapper'; import {MdInkBar} from './ink-bar'; import {Subscription} from 'rxjs/Subscription'; +import {Observable} from 'rxjs/Observable'; import {applyCssTransform} from '../core/style/apply-transform'; import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/auditTime'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/observable/merge'; +import 'rxjs/add/operator/startWith'; + /** * The directions that scrolling can go in when the header's tabs exceed the header width. 'After' @@ -68,8 +75,8 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes /** Whether the header should scroll to the selected index after the view has been checked. */ private _selectedIndexChanged = false; - /** Subscription to changes in the layout direction. */ - private _directionChange: Subscription; + /** Combines listeners that will re-align the ink bar whenever they're invoked. */ + private _realignInkBar: Subscription = null; /** Whether the controls for pagination should be displayed */ _showPaginationControls = false; @@ -92,13 +99,14 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes private _selectedIndex: number = 0; /** The index of the active tab. */ - @Input() set selectedIndex(value: number) { + @Input() + get selectedIndex(): number { return this._selectedIndex; } + set selectedIndex(value: number) { this._selectedIndexChanged = this._selectedIndex != value; this._selectedIndex = value; this._focusIndex = value; } - get selectedIndex(): number { return this._selectedIndex; } /** Event emitted when the option is selected. */ @Output() selectFocusedIndex = new EventEmitter(); @@ -106,7 +114,10 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes /** Event emitted when a label is focused. */ @Output() indexFocused = new EventEmitter(); - constructor(private _elementRef: ElementRef, @Optional() private _dir: Dir) {} + constructor( + private _elementRef: ElementRef, + private _ngZone: NgZone, + @Optional() private _dir: Dir) { } ngAfterContentChecked(): void { // If the number of tab labels have changed, check if scrolling should be enabled @@ -150,17 +161,23 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes * Aligns the ink bar to the selected tab on load. */ ngAfterContentInit() { - this._alignInkBarToSelectedTab(); - - if (this._dir) { - this._directionChange = this._dir.dirChange.subscribe(() => this._alignInkBarToSelectedTab()); - } + this._realignInkBar = this._ngZone.runOutsideAngular(() => { + return Observable.merge( + this._dir ? this._dir.dirChange : Observable.of(null), + Observable.fromEvent(window, 'resize').auditTime(10) + ) + .startWith(null) + .subscribe(() => { + this._updatePagination(); + this._alignInkBarToSelectedTab(); + }); + }); } ngOnDestroy() { - if (this._directionChange) { - this._directionChange.unsubscribe(); - this._directionChange = null; + if (this._realignInkBar) { + this._realignInkBar.unsubscribe(); + this._realignInkBar = null; } } diff --git a/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts b/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts index 54d98ad04de5..4123893d637c 100644 --- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts +++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts @@ -1,13 +1,18 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {async, ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing'; import {MdTabsModule} from '../index'; -import {Component} from '@angular/core'; +import {MdTabNavBar} from './tab-nav-bar'; +import {Component, ViewChild} from '@angular/core'; import {By} from '@angular/platform-browser'; import {ViewportRuler} from '../../core/overlay/position/viewport-ruler'; import {FakeViewportRuler} from '../../core/overlay/position/fake-viewport-ruler'; -import {dispatchMouseEvent} from '../../core/testing/dispatch-events'; +import {dispatchMouseEvent, dispatchFakeEvent} from '../../core/testing/dispatch-events'; +import {LayoutDirection, Dir} from '../../core/rtl/dir'; +import {Subject} from 'rxjs/Subject'; describe('MdTabNavBar', () => { + let dir: LayoutDirection = 'ltr'; + let dirChange = new Subject(); beforeEach(async(() => { TestBed.configureTestingModule({ @@ -17,6 +22,9 @@ describe('MdTabNavBar', () => { TabLinkWithNgIf, ], providers: [ + {provide: Dir, useFactory: () => { + return {value: dir, dirChange: dirChange.asObservable()}; + }}, {provide: ViewportRuler, useClass: FakeViewportRuler}, ] }); @@ -44,6 +52,29 @@ describe('MdTabNavBar', () => { tabLink.nativeElement.click(); expect(component.activeIndex).toBe(2); }); + + it('should re-align the ink bar when the direction changes', () => { + const inkBar = fixture.componentInstance.tabNavBar._inkBar; + + spyOn(inkBar, 'alignToElement'); + + dirChange.next(); + fixture.detectChanges(); + + expect(inkBar.alignToElement).toHaveBeenCalled(); + }); + + it('should re-align the ink bar when the window is resized', fakeAsync(() => { + const inkBar = fixture.componentInstance.tabNavBar._inkBar; + + spyOn(inkBar, 'alignToElement'); + + dispatchFakeEvent(window, 'resize'); + tick(10); + fixture.detectChanges(); + + expect(inkBar.alignToElement).toHaveBeenCalled(); + })); }); it('should clean up the ripple event handlers on destroy', () => { @@ -73,6 +104,8 @@ describe('MdTabNavBar', () => { ` }) class SimpleTabNavBarTestApp { + @ViewChild(MdTabNavBar) tabNavBar: MdTabNavBar; + activeIndex = 0; } diff --git a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts index 6bc6d6dec836..a50ee8218986 100644 --- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts +++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts @@ -9,12 +9,17 @@ import { Inject, Optional, OnDestroy, + AfterContentInit, } from '@angular/core'; import {MdInkBar} from '../ink-bar'; import {MdRipple} from '../../core/ripple/index'; import {ViewportRuler} from '../../core/overlay/position/viewport-ruler'; import {MD_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions, Dir} from '../../core'; +import {Observable} from 'rxjs/Observable'; import {Subscription} from 'rxjs/Subscription'; +import 'rxjs/add/operator/auditTime'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/observable/merge'; /** * Navigation component matching the styles of the tab group header. @@ -30,18 +35,16 @@ import {Subscription} from 'rxjs/Subscription'; }, encapsulation: ViewEncapsulation.None, }) -export class MdTabNavBar implements OnDestroy { - private _directionChange: Subscription; +export class MdTabNavBar implements AfterContentInit, OnDestroy { + /** Combines listeners that will re-align the ink bar whenever they're invoked. */ + private _realignInkBar: Subscription = null; + _activeLinkChanged: boolean; _activeLinkElement: ElementRef; @ViewChild(MdInkBar) _inkBar: MdInkBar; - constructor(@Optional() private _dir: Dir) { - if (_dir) { - this._directionChange = _dir.dirChange.subscribe(() => this._alignInkBar()); - } - } + constructor(@Optional() private _dir: Dir, private _ngZone: NgZone) { } /** Notifies the component that the active link has been changed. */ updateActiveLink(element: ElementRef) { @@ -49,6 +52,15 @@ export class MdTabNavBar implements OnDestroy { this._activeLinkElement = element; } + ngAfterContentInit(): void { + this._realignInkBar = this._ngZone.runOutsideAngular(() => { + return Observable.merge( + this._dir ? this._dir.dirChange : Observable.of(null), + Observable.fromEvent(window, 'resize').auditTime(10) + ).subscribe(() => this._alignInkBar()); + }); + } + /** Checks if the active link has been changed and, if so, will update the ink bar. */ ngAfterContentChecked(): void { if (this._activeLinkChanged) { @@ -58,15 +70,17 @@ export class MdTabNavBar implements OnDestroy { } ngOnDestroy() { - if (this._directionChange) { - this._directionChange.unsubscribe(); - this._directionChange = null; + if (this._realignInkBar) { + this._realignInkBar.unsubscribe(); + this._realignInkBar = null; } } /** Aligns the ink bar to the active link. */ private _alignInkBar(): void { - this._inkBar.alignToElement(this._activeLinkElement.nativeElement); + if (this._activeLinkElement) { + this._inkBar.alignToElement(this._activeLinkElement.nativeElement); + } } } From bf17e947627fb8548cfdc9dbf8f6207cc77789df Mon Sep 17 00:00:00 2001 From: crisbeto Date: Mon, 17 Apr 2017 15:47:45 +0200 Subject: [PATCH 2/2] fix: avoid issues with platform-server --- src/lib/tabs/tab-header.ts | 18 +++++++++--------- src/lib/tabs/tab-nav-bar/tab-nav-bar.ts | 10 ++++++---- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/lib/tabs/tab-header.ts b/src/lib/tabs/tab-header.ts index 0a3d53116c24..bbaea86d2beb 100644 --- a/src/lib/tabs/tab-header.ts +++ b/src/lib/tabs/tab-header.ts @@ -162,15 +162,15 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes */ ngAfterContentInit() { this._realignInkBar = this._ngZone.runOutsideAngular(() => { - return Observable.merge( - this._dir ? this._dir.dirChange : Observable.of(null), - Observable.fromEvent(window, 'resize').auditTime(10) - ) - .startWith(null) - .subscribe(() => { - this._updatePagination(); - this._alignInkBarToSelectedTab(); - }); + let dirChange = this._dir ? this._dir.dirChange : Observable.of(null); + let resize = typeof window !== 'undefined' ? + Observable.fromEvent(window, 'resize').auditTime(10) : + Observable.of(null); + + return Observable.merge(dirChange, resize).startWith(null).subscribe(() => { + this._updatePagination(); + this._alignInkBarToSelectedTab(); + }); }); } diff --git a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts index a50ee8218986..76c363d108b9 100644 --- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts +++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts @@ -54,10 +54,12 @@ export class MdTabNavBar implements AfterContentInit, OnDestroy { ngAfterContentInit(): void { this._realignInkBar = this._ngZone.runOutsideAngular(() => { - return Observable.merge( - this._dir ? this._dir.dirChange : Observable.of(null), - Observable.fromEvent(window, 'resize').auditTime(10) - ).subscribe(() => this._alignInkBar()); + let dirChange = this._dir ? this._dir.dirChange : Observable.of(null); + let resize = typeof window !== 'undefined' ? + Observable.fromEvent(window, 'resize').auditTime(10) : + Observable.of(null); + + return Observable.merge(dirChange, resize).subscribe(() => this._alignInkBar()); }); }