Skip to content

Commit 7942948

Browse files
crisbetokara
authored andcommitted
fix(tabs): re-align the ink bar when the viewport size changes (#3877)
Fixes #3845. Fixes #3044. Fixes #2518. Fixes #1231.
1 parent fe0bfe7 commit 7942948

File tree

4 files changed

+110
-28
lines changed

4 files changed

+110
-28
lines changed

src/lib/tabs/tab-header.spec.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
1+
import {async, ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
22
import {Component, ViewChild, ViewContainerRef} from '@angular/core';
33
import {LayoutDirection, Dir} from '../core/rtl/dir';
44
import {MdTabHeader} from './tab-header';
@@ -11,6 +11,7 @@ import {RIGHT_ARROW, LEFT_ARROW, ENTER} from '../core/keyboard/keycodes';
1111
import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler';
1212
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
1313
import {dispatchKeyboardEvent} from '../core/testing/dispatch-events';
14+
import {dispatchFakeEvent} from '../core/testing/dispatch-events';
1415
import {Subject} from 'rxjs/Subject';
1516

1617

@@ -211,6 +212,21 @@ describe('MdTabHeader', () => {
211212
expect(inkBar.alignToElement).toHaveBeenCalled();
212213
});
213214

215+
it('should re-align the ink bar when the window is resized', fakeAsync(() => {
216+
fixture = TestBed.createComponent(SimpleTabHeaderApp);
217+
fixture.detectChanges();
218+
219+
const inkBar = fixture.componentInstance.mdTabHeader._inkBar;
220+
221+
spyOn(inkBar, 'alignToElement');
222+
223+
dispatchFakeEvent(window, 'resize');
224+
tick(10);
225+
fixture.detectChanges();
226+
227+
expect(inkBar.alignToElement).toHaveBeenCalled();
228+
}));
229+
214230
});
215231
});
216232

src/lib/tabs/tab-header.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,20 @@ import {
1212
AfterContentChecked,
1313
AfterContentInit,
1414
OnDestroy,
15+
NgZone,
1516
} from '@angular/core';
1617
import {RIGHT_ARROW, LEFT_ARROW, ENTER, Dir, LayoutDirection} from '../core';
1718
import {MdTabLabelWrapper} from './tab-label-wrapper';
1819
import {MdInkBar} from './ink-bar';
1920
import {Subscription} from 'rxjs/Subscription';
21+
import {Observable} from 'rxjs/Observable';
2022
import {applyCssTransform} from '../core/style/apply-transform';
2123
import 'rxjs/add/operator/map';
24+
import 'rxjs/add/operator/auditTime';
25+
import 'rxjs/add/observable/of';
26+
import 'rxjs/add/observable/merge';
27+
import 'rxjs/add/operator/startWith';
28+
2229

2330
/**
2431
* 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
6875
/** Whether the header should scroll to the selected index after the view has been checked. */
6976
private _selectedIndexChanged = false;
7077

71-
/** Subscription to changes in the layout direction. */
72-
private _directionChange: Subscription;
78+
/** Combines listeners that will re-align the ink bar whenever they're invoked. */
79+
private _realignInkBar: Subscription = null;
7380

7481
/** Whether the controls for pagination should be displayed */
7582
_showPaginationControls = false;
@@ -92,21 +99,25 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes
9299
private _selectedIndex: number = 0;
93100

94101
/** The index of the active tab. */
95-
@Input() set selectedIndex(value: number) {
102+
@Input()
103+
get selectedIndex(): number { return this._selectedIndex; }
104+
set selectedIndex(value: number) {
96105
this._selectedIndexChanged = this._selectedIndex != value;
97106

98107
this._selectedIndex = value;
99108
this._focusIndex = value;
100109
}
101-
get selectedIndex(): number { return this._selectedIndex; }
102110

103111
/** Event emitted when the option is selected. */
104112
@Output() selectFocusedIndex = new EventEmitter();
105113

106114
/** Event emitted when a label is focused. */
107115
@Output() indexFocused = new EventEmitter();
108116

109-
constructor(private _elementRef: ElementRef, @Optional() private _dir: Dir) {}
117+
constructor(
118+
private _elementRef: ElementRef,
119+
private _ngZone: NgZone,
120+
@Optional() private _dir: Dir) { }
110121

111122
ngAfterContentChecked(): void {
112123
// 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
150161
* Aligns the ink bar to the selected tab on load.
151162
*/
152163
ngAfterContentInit() {
153-
this._alignInkBarToSelectedTab();
154-
155-
if (this._dir) {
156-
this._directionChange = this._dir.dirChange.subscribe(() => this._alignInkBarToSelectedTab());
157-
}
164+
this._realignInkBar = this._ngZone.runOutsideAngular(() => {
165+
let dirChange = this._dir ? this._dir.dirChange : Observable.of(null);
166+
let resize = typeof window !== 'undefined' ?
167+
Observable.fromEvent(window, 'resize').auditTime(10) :
168+
Observable.of(null);
169+
170+
return Observable.merge(dirChange, resize).startWith(null).subscribe(() => {
171+
this._updatePagination();
172+
this._alignInkBarToSelectedTab();
173+
});
174+
});
158175
}
159176

160177
ngOnDestroy() {
161-
if (this._directionChange) {
162-
this._directionChange.unsubscribe();
163-
this._directionChange = null;
178+
if (this._realignInkBar) {
179+
this._realignInkBar.unsubscribe();
180+
this._realignInkBar = null;
164181
}
165182
}
166183

src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
1+
import {async, ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
22
import {MdTabsModule} from '../index';
3-
import {Component} from '@angular/core';
3+
import {MdTabNavBar} from './tab-nav-bar';
4+
import {Component, ViewChild} from '@angular/core';
45
import {By} from '@angular/platform-browser';
56
import {ViewportRuler} from '../../core/overlay/position/viewport-ruler';
67
import {FakeViewportRuler} from '../../core/overlay/position/fake-viewport-ruler';
7-
import {dispatchMouseEvent} from '../../core/testing/dispatch-events';
8+
import {dispatchMouseEvent, dispatchFakeEvent} from '../../core/testing/dispatch-events';
9+
import {LayoutDirection, Dir} from '../../core/rtl/dir';
10+
import {Subject} from 'rxjs/Subject';
811

912

1013
describe('MdTabNavBar', () => {
14+
let dir: LayoutDirection = 'ltr';
15+
let dirChange = new Subject();
1116

1217
beforeEach(async(() => {
1318
TestBed.configureTestingModule({
@@ -17,6 +22,9 @@ describe('MdTabNavBar', () => {
1722
TabLinkWithNgIf,
1823
],
1924
providers: [
25+
{provide: Dir, useFactory: () => {
26+
return {value: dir, dirChange: dirChange.asObservable()};
27+
}},
2028
{provide: ViewportRuler, useClass: FakeViewportRuler},
2129
]
2230
});
@@ -44,6 +52,29 @@ describe('MdTabNavBar', () => {
4452
tabLink.nativeElement.click();
4553
expect(component.activeIndex).toBe(2);
4654
});
55+
56+
it('should re-align the ink bar when the direction changes', () => {
57+
const inkBar = fixture.componentInstance.tabNavBar._inkBar;
58+
59+
spyOn(inkBar, 'alignToElement');
60+
61+
dirChange.next();
62+
fixture.detectChanges();
63+
64+
expect(inkBar.alignToElement).toHaveBeenCalled();
65+
});
66+
67+
it('should re-align the ink bar when the window is resized', fakeAsync(() => {
68+
const inkBar = fixture.componentInstance.tabNavBar._inkBar;
69+
70+
spyOn(inkBar, 'alignToElement');
71+
72+
dispatchFakeEvent(window, 'resize');
73+
tick(10);
74+
fixture.detectChanges();
75+
76+
expect(inkBar.alignToElement).toHaveBeenCalled();
77+
}));
4778
});
4879

4980
it('should clean up the ripple event handlers on destroy', () => {
@@ -73,6 +104,8 @@ describe('MdTabNavBar', () => {
73104
`
74105
})
75106
class SimpleTabNavBarTestApp {
107+
@ViewChild(MdTabNavBar) tabNavBar: MdTabNavBar;
108+
76109
activeIndex = 0;
77110
}
78111

src/lib/tabs/tab-nav-bar/tab-nav-bar.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@ import {
99
Inject,
1010
Optional,
1111
OnDestroy,
12+
AfterContentInit,
1213
} from '@angular/core';
1314
import {MdInkBar} from '../ink-bar';
1415
import {MdRipple} from '../../core/ripple/index';
1516
import {ViewportRuler} from '../../core/overlay/position/viewport-ruler';
1617
import {MD_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions, Dir} from '../../core';
18+
import {Observable} from 'rxjs/Observable';
1719
import {Subscription} from 'rxjs/Subscription';
20+
import 'rxjs/add/operator/auditTime';
21+
import 'rxjs/add/observable/of';
22+
import 'rxjs/add/observable/merge';
1823

1924
/**
2025
* Navigation component matching the styles of the tab group header.
@@ -30,25 +35,34 @@ import {Subscription} from 'rxjs/Subscription';
3035
},
3136
encapsulation: ViewEncapsulation.None,
3237
})
33-
export class MdTabNavBar implements OnDestroy {
34-
private _directionChange: Subscription;
38+
export class MdTabNavBar implements AfterContentInit, OnDestroy {
39+
/** Combines listeners that will re-align the ink bar whenever they're invoked. */
40+
private _realignInkBar: Subscription = null;
41+
3542
_activeLinkChanged: boolean;
3643
_activeLinkElement: ElementRef;
3744

3845
@ViewChild(MdInkBar) _inkBar: MdInkBar;
3946

40-
constructor(@Optional() private _dir: Dir) {
41-
if (_dir) {
42-
this._directionChange = _dir.dirChange.subscribe(() => this._alignInkBar());
43-
}
44-
}
47+
constructor(@Optional() private _dir: Dir, private _ngZone: NgZone) { }
4548

4649
/** Notifies the component that the active link has been changed. */
4750
updateActiveLink(element: ElementRef) {
4851
this._activeLinkChanged = this._activeLinkElement != element;
4952
this._activeLinkElement = element;
5053
}
5154

55+
ngAfterContentInit(): void {
56+
this._realignInkBar = this._ngZone.runOutsideAngular(() => {
57+
let dirChange = this._dir ? this._dir.dirChange : Observable.of(null);
58+
let resize = typeof window !== 'undefined' ?
59+
Observable.fromEvent(window, 'resize').auditTime(10) :
60+
Observable.of(null);
61+
62+
return Observable.merge(dirChange, resize).subscribe(() => this._alignInkBar());
63+
});
64+
}
65+
5266
/** Checks if the active link has been changed and, if so, will update the ink bar. */
5367
ngAfterContentChecked(): void {
5468
if (this._activeLinkChanged) {
@@ -58,15 +72,17 @@ export class MdTabNavBar implements OnDestroy {
5872
}
5973

6074
ngOnDestroy() {
61-
if (this._directionChange) {
62-
this._directionChange.unsubscribe();
63-
this._directionChange = null;
75+
if (this._realignInkBar) {
76+
this._realignInkBar.unsubscribe();
77+
this._realignInkBar = null;
6478
}
6579
}
6680

6781
/** Aligns the ink bar to the active link. */
6882
private _alignInkBar(): void {
69-
this._inkBar.alignToElement(this._activeLinkElement.nativeElement);
83+
if (this._activeLinkElement) {
84+
this._inkBar.alignToElement(this._activeLinkElement.nativeElement);
85+
}
7086
}
7187
}
7288

0 commit comments

Comments
 (0)