chore(tabs): move tabs to smart portal directive. (#7266)

This commit is contained in:
Joey Perrott
2017-11-10 09:54:35 -08:00
committed by GitHub
parent 691bb7321a
commit 0e03bf4400
6 changed files with 87 additions and 65 deletions
+6 -3
View File
@@ -9,11 +9,14 @@
export * from './tabs-module';
export * from './tab-group';
export {MatInkBar} from './ink-bar';
export {MatTabBody, MatTabBodyOriginState, MatTabBodyPositionState} from './tab-body';
export {
MatTabBody,
MatTabBodyOriginState,
MatTabBodyPositionState,
MatTabBodyPortal
} from './tab-body';
export {MatTabHeader, ScrollDirection} from './tab-header';
export {MatTabLabelWrapper} from './tab-label-wrapper';
export {MatTab} from './tab';
export {MatTabLabel} from './tab-label';
export {MatTabNav, MatTabLink} from './tab-nav-bar/index';
+1 -1
View File
@@ -2,5 +2,5 @@
[@translateTab]="_position"
(@translateTab.start)="_onTranslateTabStarted($event)"
(@translateTab.done)="_onTranslateTabComplete($event)">
<ng-template cdkPortalOutlet></ng-template>
<ng-template matTabBodyHost></ng-template>
</div>
+3 -26
View File
@@ -2,10 +2,10 @@ import {Direction, Directionality} from '@angular/cdk/bidi';
import {PortalModule, TemplatePortal} from '@angular/cdk/portal';
import {CommonModule} from '@angular/common';
import {Component, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
import {async, ComponentFixture, fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MatRippleModule} from '@angular/material/core';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {MatTabBody} from './tab-body';
import {MatTabBody, MatTabBodyPortal} from './tab-body';
describe('MatTabBody', () => {
@@ -17,6 +17,7 @@ describe('MatTabBody', () => {
imports: [CommonModule, PortalModule, MatRippleModule, NoopAnimationsModule],
declarations: [
MatTabBody,
MatTabBodyPortal,
SimpleTabBodyApp,
],
providers: [
@@ -145,30 +146,6 @@ describe('MatTabBody', () => {
expect(fixture.componentInstance.tabBody._position).toBe('left');
});
});
describe('on centered', () => {
let fixture: ComponentFixture<SimpleTabBodyApp>;
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(SimpleTabBodyApp);
}));
it('should attach the content when centered and detach when not', fakeAsync(() => {
fixture.componentInstance.position = 1;
fixture.detectChanges();
expect(fixture.componentInstance.tabBody._portalOutlet.hasAttached()).toBe(false);
fixture.componentInstance.position = 0;
fixture.detectChanges();
expect(fixture.componentInstance.tabBody._portalOutlet.hasAttached()).toBe(true);
fixture.componentInstance.position = 1;
fixture.detectChanges();
flushMicrotasks(); // Finish animation and let it detach in animation done handler
expect(fixture.componentInstance.tabBody._portalOutlet.hasAttached()).toBe(false);
}));
});
});
+55 -24
View File
@@ -7,17 +7,21 @@
*/
import {
ViewChild,
Component,
Input,
Inject,
Output,
EventEmitter,
OnDestroy,
OnInit,
ElementRef,
Directive,
Optional,
AfterViewChecked,
ViewEncapsulation,
ChangeDetectionStrategy,
ComponentFactoryResolver,
ViewContainerRef,
forwardRef,
} from '@angular/core';
import {
trigger,
@@ -29,7 +33,10 @@ import {
} from '@angular/animations';
import {TemplatePortal, CdkPortalOutlet} from '@angular/cdk/portal';
import {Directionality, Direction} from '@angular/cdk/bidi';
import {Subscription} from 'rxjs/Subscription';
/** Workaround for https://github.com/angular/angular/issues/17849 */
export const _MatTabBodyPortalBaseClass = CdkPortalOutlet;
/**
* These position states are used internally as animation states for the tab body. Setting the
@@ -52,6 +59,44 @@ export type MatTabBodyPositionState =
*/
export type MatTabBodyOriginState = 'left' | 'right';
/**
* The portal host directive for the contents of the tab.
* @docs-private
*/
@Directive({
selector: '[matTabBodyHost]'
})
export class MatTabBodyPortal extends _MatTabBodyPortalBaseClass implements OnInit, OnDestroy {
/** A subscription to events for when the tab body begins centering. */
private _centeringSub: Subscription;
constructor(
_componentFactoryResolver: ComponentFactoryResolver,
_viewContainerRef: ViewContainerRef,
@Inject(forwardRef(() => MatTabBody)) private _host: MatTabBody) {
super(_componentFactoryResolver, _viewContainerRef);
}
/** Set initial visibility or set up subscription for changing visibility. */
ngOnInit(): void {
if (this._host._isCenterPosition(this._host._position)) {
this.attach(this._host._content);
} else {
this._centeringSub = this._host._beforeCentering.subscribe(() => {
this.attach(this._host._content);
this._centeringSub.unsubscribe();
});
}
}
/** Clean up subscription if necessary. */
ngOnDestroy(): void {
if (this._centeringSub && !this._centeringSub.closed) {
this._centeringSub.unsubscribe();
}
}
}
/**
* Wrapper for the contents of a tab.
* @docs-private
@@ -86,13 +131,13 @@ export type MatTabBodyOriginState = 'left' | 'right';
])
]
})
export class MatTabBody implements OnInit, AfterViewChecked {
/** The portal outlet inside of this container into which the tab body content will be loaded. */
@ViewChild(CdkPortalOutlet) _portalOutlet: CdkPortalOutlet;
export class MatTabBody implements OnInit {
/** Event emitted when the tab begins to animate towards the center as the active tab. */
@Output() _onCentering: EventEmitter<number> = new EventEmitter<number>();
/** Event emitted before the centering of the tab begins. */
@Output() _beforeCentering: EventEmitter<number> = new EventEmitter<number>();
/** Event emitted when the tab completes its animation towards the center. */
@Output() _onCentered: EventEmitter<void> = new EventEmitter<void>(true);
@@ -139,28 +184,14 @@ export class MatTabBody implements OnInit, AfterViewChecked {
}
}
/**
* After the view has been set, check if the tab content is set to the center and attach the
* content if it is not already attached.
*/
ngAfterViewChecked() {
if (this._isCenterPosition(this._position) && !this._portalOutlet.hasAttached()) {
this._portalOutlet.attach(this._content);
}
}
_onTranslateTabStarted(e: AnimationEvent) {
_onTranslateTabStarted(e: AnimationEvent): void {
if (this._isCenterPosition(e.toState)) {
this._beforeCentering.emit();
this._onCentering.emit(this._elementRef.nativeElement.clientHeight);
}
}
_onTranslateTabComplete(e: AnimationEvent) {
// If the end state is that the tab is not centered, then detach the content.
if (!this._isCenterPosition(e.toState) && !this._isCenterPosition(this._position)) {
this._portalOutlet.detach();
}
_onTranslateTabComplete(e: AnimationEvent): void {
// If the transition to the center is complete, emit an event.
if (this._isCenterPosition(e.toState) && this._isCenterPosition(this._position)) {
this._onCentered.emit();
@@ -173,7 +204,7 @@ export class MatTabBody implements OnInit, AfterViewChecked {
}
/** Whether the provided position state is considered center, regardless of origin. */
private _isCenterPosition(position: MatTabBodyPositionState|string): boolean {
_isCenterPosition(position: MatTabBodyPositionState|string): boolean {
return position == 'center' ||
position == 'left-origin-center' ||
position == 'right-origin-center';
+20 -10
View File
@@ -26,15 +26,22 @@ describe('MatTabGroup', () => {
describe('basic behavior', () => {
let fixture: ComponentFixture<SimpleTabsTestApp>;
let element: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(SimpleTabsTestApp);
element = fixture.nativeElement;
});
it('should default to the first tab', () => {
checkSelectedIndex(1, fixture);
});
it('will properly load content on first change detection pass', () => {
fixture.detectChanges();
expect(element.querySelectorAll('.mat-tab-body')[1].querySelectorAll('span').length).toBe(3);
});
it('should change selected index on click', () => {
let component = fixture.debugElement.componentInstance;
component.selectedIndex = 0;
@@ -318,23 +325,26 @@ describe('MatTabGroup', () => {
fixture.debugElement.query(By.directive(MatTabGroup)).componentInstance as MatTabGroup;
});
it('should support a tab-group with the simple api', () => {
it('should support a tab-group with the simple api', async(() => {
expect(getSelectedLabel(fixture).textContent).toMatch('Junk food');
expect(getSelectedContent(fixture).textContent).toMatch('Pizza, fries');
tabGroup.selectedIndex = 2;
fixture.detectChanges();
// Use whenStable to wait for async observables and change detection to run in content.
fixture.whenStable().then(() => {
expect(getSelectedLabel(fixture).textContent).toMatch('Fruit');
expect(getSelectedContent(fixture).textContent).toMatch('Apples, grapes');
expect(getSelectedLabel(fixture).textContent).toMatch('Fruit');
expect(getSelectedContent(fixture).textContent).toMatch('Apples, grapes');
fixture.componentInstance.otherLabel = 'Chips';
fixture.componentInstance.otherContent = 'Salt, vinegar';
fixture.detectChanges();
fixture.componentInstance.otherLabel = 'Chips';
fixture.componentInstance.otherContent = 'Salt, vinegar';
fixture.detectChanges();
expect(getSelectedLabel(fixture).textContent).toMatch('Chips');
expect(getSelectedContent(fixture).textContent).toMatch('Salt, vinegar');
});
expect(getSelectedLabel(fixture).textContent).toMatch('Chips');
expect(getSelectedContent(fixture).textContent).toMatch('Salt, vinegar');
});
}));
it('should support @ViewChild in the tab content', () => {
expect(fixture.componentInstance.legumes).toBeTruthy();
@@ -415,7 +425,7 @@ describe('nested MatTabGroup with enabled animations', () => {
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>Tab Two</ng-template>
Tab two content
<span>Tab </span><span>two</span><span>content</span>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>Tab Three</ng-template>
+2 -1
View File
@@ -14,7 +14,7 @@ import {NgModule} from '@angular/core';
import {MatCommonModule, MatRippleModule} from '@angular/material/core';
import {MatInkBar} from './ink-bar';
import {MatTab} from './tab';
import {MatTabBody} from './tab-body';
import {MatTabBody, MatTabBodyPortal} from './tab-body';
import {MatTabGroup} from './tab-group';
import {MatTabHeader} from './tab-header';
import {MatTabLabel} from './tab-label';
@@ -49,6 +49,7 @@ import {MatTabLink, MatTabNav} from './tab-nav-bar/tab-nav-bar';
MatTabNav,
MatTabLink,
MatTabBody,
MatTabBodyPortal,
MatTabHeader
],
providers: [VIEWPORT_RULER_PROVIDER],