From 84334f1731dabdc78bb19af58c63078536fcf5b3 Mon Sep 17 00:00:00 2001 From: Andrew Seguin Date: Mon, 21 Nov 2016 11:20:45 -0800 Subject: [PATCH 1/8] latest changes --- src/demo-app/tabs/tabs-demo.html | 92 ++++--------------- src/demo-app/tabs/tabs-demo.ts | 37 ++++++-- src/lib/tabs/ink-bar.ts | 12 +++ src/lib/tabs/tab-body.html | 2 +- src/lib/tabs/tab-group.html | 1 - src/lib/tabs/tab-group.spec.ts | 70 +++++++++++++- src/lib/tabs/tabs.ts | 151 +++++++++++++++++++++++-------- src/lib/tooltip/tooltip.spec.ts | 1 - 8 files changed, 242 insertions(+), 124 deletions(-) diff --git a/src/demo-app/tabs/tabs-demo.html b/src/demo-app/tabs/tabs-demo.html index fe5deabe9a41..f68f24481975 100644 --- a/src/demo-app/tabs/tabs-demo.html +++ b/src/demo-app/tabs/tabs-demo.html @@ -13,87 +13,35 @@

Tab Nav Bar

-

Tab Group Demo - Dynamic Height

- - - - {{tab.content}} -
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla venenatis ante augue. - Phasellus volutpat neque ac dui mattis vulputate. Etiam consequat aliquam cursus. - In sodales pretium ultrices. Maecenas lectus est, sollicitudin consectetur felis nec, - feugiat ultricies mi. Aliquam erat volutpat. Nam placerat, tortor in ultrices porttitor, - orci enim rutrum enim, vel tempor sapien arcu a tellus. Vivamus convallis sodales ante varius - gravida. Curabitur a purus vel augue ultrices ultricies id a nisl. Nullam malesuada consequat - diam, a facilisis tortor volutpat et. Sed urna dolor, aliquet vitae posuere vulputate, euismod - ac lorem. Sed felis risus, pulvinar at interdum quis, vehicula sed odio. Phasellus in enim - venenatis, iaculis tortor eu, bibendum ante. Donec ac tellus dictum neque volutpat blandit. - Praesent efficitur faucibus risus, ac auctor purus porttitor vitae. Phasellus ornare dui nec - orci posuere, nec luctus mauris semper. -
-
- Morbi viverra, ante vel aliquet tincidunt, leo dolor pharetra quam, at semper massa orci nec - magna. Donec posuere nec sapien sed laoreet. Etiam cursus nunc in condimentum facilisis. - Etiam in tempor tortor. Vivamus faucibus egestas enim, at convallis diam pulvinar vel. - Cras ac orci eget nisi maximus cursus. Nunc urna libero, viverra sit amet nisl at, hendrerit - tempor turpis. Maecenas facilisis convallis mi vel tempor. Nullam vitae nunc leo. Cras sed - nisl consectetur, rhoncus sapien sit amet, tempus sapien. -
-
- Integer turpis erat, porttitor vitae mi faucibus, laoreet interdum tellus. Curabitur posuere - molestie dictum. Morbi eget congue risus, quis rhoncus quam. Suspendisse vitae hendrerit erat, - at posuere mi. Cras eu fermentum nunc. Sed id ante eu orci commodo volutpat non ac est. - Praesent ligula diam, congue eu enim scelerisque, finibus commodo lectus. -
-
-
- -
-
- - -

Tab Group Demo - Fixed Height

+ + +
+ New tab position: +
+
+ Index to select after add: +
+
+ Selected index: +
- + {{tab.content}} -
-
+

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla venenatis ante augue. - Phasellus volutpat neque ac dui mattis vulputate. Etiam consequat aliquam cursus. - In sodales pretium ultrices. Maecenas lectus est, sollicitudin consectetur felis nec, - feugiat ultricies mi. Aliquam erat volutpat. Nam placerat, tortor in ultrices porttitor, - orci enim rutrum enim, vel tempor sapien arcu a tellus. Vivamus convallis sodales ante varius - gravida. Curabitur a purus vel augue ultrices ultricies id a nisl. Nullam malesuada consequat - diam, a facilisis tortor volutpat et. Sed urna dolor, aliquet vitae posuere vulputate, euismod - ac lorem. Sed felis risus, pulvinar at interdum quis, vehicula sed odio. Phasellus in enim - venenatis, iaculis tortor eu, bibendum ante. Donec ac tellus dictum neque volutpat blandit. - Praesent efficitur faucibus risus, ac auctor purus porttitor vitae. Phasellus ornare dui nec - orci posuere, nec luctus mauris semper. -
-
- Morbi viverra, ante vel aliquet tincidunt, leo dolor pharetra quam, at semper massa orci nec - magna. Donec posuere nec sapien sed laoreet. Etiam cursus nunc in condimentum facilisis. - Etiam in tempor tortor. Vivamus faucibus egestas enim, at convallis diam pulvinar vel. - Cras ac orci eget nisi maximus cursus. Nunc urna libero, viverra sit amet nisl at, hendrerit - tempor turpis. Maecenas facilisis convallis mi vel tempor. Nullam vitae nunc leo. Cras sed - nisl consectetur, rhoncus sapien sit amet, tempus sapien. -
-
- Integer turpis erat, porttitor vitae mi faucibus, laoreet interdum tellus. Curabitur posuere - molestie dictum. Morbi eget congue risus, quis rhoncus quam. Suspendisse vitae hendrerit erat, - at posuere mi. Cras eu fermentum nunc. Sed id ante eu orci commodo volutpat non ac est. - Praesent ligula diam, congue eu enim scelerisque, finibus commodo lectus. + {{tab.content}}
-
-
+

+

+
diff --git a/src/demo-app/tabs/tabs-demo.ts b/src/demo-app/tabs/tabs-demo.ts index 488f28082077..762bab025edd 100644 --- a/src/demo-app/tabs/tabs-demo.ts +++ b/src/demo-app/tabs/tabs-demo.ts @@ -17,20 +17,24 @@ export class TabsDemo { ]; activeLinkIndex = 0; + selectedIndex = 0; + activeTabIndex = 0; + addTabPosition = 0; + indexAfterAdd = 0; tabs = [ { - label: 'Tab One', - content: 'This is the body of the first tab'}, - { - label: 'Tab Two', + label: 'Tab 1', + content: 'This is the body of the first tab' + }, { + label: 'Tab 2', disabled: true, - content: 'This is the body of the second tab'}, - { - label: 'Tab Three', + content: 'This is the body of the second tab' + }, { + label: 'Tab 3', extraContent: true, - content: 'This is the body of the third tab'}, - { - label: 'Tab Four', + content: 'This is the body of the third tab' + }, { + label: 'Tab 4', content: 'This is the body of the fourth tab' }, ]; @@ -50,6 +54,19 @@ export class TabsDemo { this.activeLinkIndex = this.tabLinks.indexOf(this.tabLinks.find(tab => router.url.indexOf(tab.link) != -1)); } + + addTab(includeExtraContent: boolean): void { + this.tabs.splice(3, 0, { + label: 'New Tab ' + (this.tabs.length + 1), + content: 'New tab contents ' + (this.tabs.length + 1), + extraContent: includeExtraContent + }); + this.activeTabIndex = 3; + } + + deleteTab(tab: any) { + this.tabs.splice(this.tabs.indexOf(tab), 1); + } } diff --git a/src/lib/tabs/ink-bar.ts b/src/lib/tabs/ink-bar.ts index eeac3943f134..695116dbd0f5 100644 --- a/src/lib/tabs/ink-bar.ts +++ b/src/lib/tabs/ink-bar.ts @@ -10,15 +10,27 @@ export class MdInkBar { /** * Calculates the styles from the provided element in order to align the ink-bar to that element. + * Shows the ink bar if previously set as hidden. * @param element */ alignToElement(element: HTMLElement) { + this.show(); this._renderer.setElementStyle(this._elementRef.nativeElement, 'left', this._getLeftPosition(element)); this._renderer.setElementStyle(this._elementRef.nativeElement, 'width', this._getElementWidth(element)); } + /** Shows the ink bar. */ + show(): void { + this._renderer.setElementStyle(this._elementRef.nativeElement, 'visibility', 'visible'); + } + + /** Hides the ink bar. */ + hide(): void { + this._renderer.setElementStyle(this._elementRef.nativeElement, 'visibility', 'hidden'); + } + /** * Generates the pixel distance from the left based on the provided element in string format. * @param element diff --git a/src/lib/tabs/tab-body.html b/src/lib/tabs/tab-body.html index 2d89174484c7..f5da5de92cd9 100644 --- a/src/lib/tabs/tab-body.html +++ b/src/lib/tabs/tab-body.html @@ -1,4 +1,4 @@ -
diff --git a/src/lib/tabs/tab-group.html b/src/lib/tabs/tab-group.html index 3a86a570b732..299dc7ab43cb 100644 --- a/src/lib/tabs/tab-group.html +++ b/src/lib/tabs/tab-group.html @@ -26,7 +26,6 @@ [id]="_getTabContentId(i)" [attr.aria-labelledby]="_getTabLabelId(i)" [class.md-tab-body-active]="selectedIndex == i" - [md-tab-body-position]="i - selectedIndex" [md-tab-body-content]="tab.content" (onTabBodyCentered)="_removeTabBodyWrapperHeight()" (onTabBodyCentering)="_setTabBodyWrapperHeight($event)"> diff --git a/src/lib/tabs/tab-group.spec.ts b/src/lib/tabs/tab-group.spec.ts index f4e8adfeca9f..aef5a5ecca64 100644 --- a/src/lib/tabs/tab-group.spec.ts +++ b/src/lib/tabs/tab-group.spec.ts @@ -9,7 +9,7 @@ import {Observable} from 'rxjs/Observable'; import {LayoutDirection, Dir} from '../core/rtl/dir'; -describe('MdTabGroup', () => { +fdescribe('MdTabGroup', () => { let dir: LayoutDirection = 'ltr'; beforeEach(async(() => { @@ -17,6 +17,7 @@ describe('MdTabGroup', () => { imports: [MdTabsModule.forRoot()], declarations: [ SimpleTabsTestApp, + BindedTabsTestApp, AsyncTabsTestApp, DisabledTabsTestApp, TabGroupWithSimpleApi, @@ -73,6 +74,21 @@ describe('MdTabGroup', () => { }); })); + it('should support adding new tab and setting active', async(() => { + let component = fixture.componentInstance; + component.selectedIndex = 0; + + fixture.detectChanges(); + + let tabLabel = fixture.debugElement.queryAll(By.css('.md-tab-label'))[1]; + tabLabel.nativeElement.click(); + + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(component.selectedIndex).toBe(1); + }); + })); + it('should cycle tab focus with focusNextTab/focusPreviousTab functions', fakeAsync(() => { let testComponent = fixture.componentInstance; let tabComponent = fixture.debugElement.query(By.css('md-tab-group')).componentInstance; @@ -229,6 +245,33 @@ describe('MdTabGroup', () => { })); }); + fdescribe('dynamic binding tabs', () => { + let fixture: ComponentFixture; + + beforeEach(async(() => { + fixture = TestBed.createComponent(BindedTabsTestApp); + fixture.detectChanges(); + })); + + it('should be able to add a new tab and set as selected', () => { + let component: MdTabGroup = fixture.debugElement.query(By.css('md-tab-group')) + .componentInstance; + + expect(component._tabs.length).toBe(fixture.componentInstance.tabs.length); + + fixture.componentInstance.addNewActiveTab(); + fixture.whenStable().then(() => { + fixture.whenStable().then(() => { + expect(true).toBe(false); + expect(component.selectedIndex).toBe(fixture.componentInstance.selectedIndex); + expect(component._tabs.length).toBe(fixture.componentInstance.tabs.length); + }) + }); + + }); + + }); + describe('disabled tabs', () => { let fixture: ComponentFixture; @@ -430,6 +473,31 @@ class SimpleTabsTestApp { } } +@Component({ + template: ` + + + {{tab.content}} + + + ` +}) +class BindedTabsTestApp { + tabs = [ + { label: 'one', content: 'one' }, + { label: 'two', content: 'two' } + ]; + selectedIndex = 0; + + addNewActiveTab(): void { + this.tabs.push({ + label: 'new tab', + content: 'new content' + }); + this.selectedIndex = this.tabs.length - 1; + } +} + @Component({ selector: 'test-app', template: ` diff --git a/src/lib/tabs/tabs.ts b/src/lib/tabs/tabs.ts index 628141b44ad5..d58efc417cc3 100644 --- a/src/lib/tabs/tabs.ts +++ b/src/lib/tabs/tabs.ts @@ -22,7 +22,7 @@ import { AnimationTransitionEvent, ElementRef, Renderer, - Optional, + Optional, forwardRef, } from '@angular/core'; import {CommonModule} from '@angular/common'; import { @@ -38,15 +38,15 @@ import { } from '../core'; import {MdTabLabel} from './tab-label'; import {MdTabLabelWrapper} from './tab-label-wrapper'; -import {MdTabNavBar, MdTabLink, MdTabLinkRipple} from './tab-nav-bar/tab-nav-bar'; +import {MdTabNavBar, MdTabLink} from './tab-nav-bar/tab-nav-bar'; import {MdInkBar} from './ink-bar'; import {Observable} from 'rxjs/Observable'; import 'rxjs/add/operator/map'; -import {MdRippleModule} from '../core/ripple/ripple'; /** Used to generate unique ID's for each tab component */ let nextId = 0; + /** A simple change event emitted on focus or selection changes. */ export class MdTabChangeEvent { index: number; @@ -99,6 +99,7 @@ export class MdTab implements OnInit { export class MdTabGroup { @ContentChildren(MdTab) _tabs: QueryList; + @ViewChildren(forwardRef(() => MdTabBody)) _tabBodies: QueryList; @ViewChildren(MdTabLabelWrapper) _labelWrappers: QueryList; @ViewChildren(MdInkBar) _inkBar: QueryList; @@ -106,6 +107,8 @@ export class MdTabGroup { private _isInitialized: boolean = false; + desiredSelectedIndex = 0; + /** Snapshot of the height of the tab body wrapper before another tab is activated. */ private _tabBodyWrapperHeight: number = 0; @@ -116,18 +119,15 @@ export class MdTabGroup { } /** The index of the active tab. */ - private _selectedIndex: number = 0; + private _selectedIndex: number = null; @Input() set selectedIndex(value: number) { - this._tabBodyWrapperHeight = this._tabBodyWrapper.nativeElement.clientHeight; - if (value != this._selectedIndex && this.isValidIndex(value)) { - this._selectedIndex = value; - - if (this._isInitialized) { - this._onSelectChange.emit(this._createChangeEvent(value)); - } - } + this.desiredSelectedIndex = value; } get selectedIndex(): number { + if (this._selectedIndex > this._tabs.length - 1) { + return this._tabs.length - 1; + } + return this._selectedIndex; } @@ -141,7 +141,8 @@ export class MdTabGroup { return this._onFocusChange.asObservable(); } - private _onSelectChange: EventEmitter = new EventEmitter(); + private _onSelectChange: EventEmitter = + new EventEmitter(true); @Output() get selectChange(): Observable { return this._onSelectChange.asObservable(); } @@ -153,12 +154,20 @@ export class MdTabGroup { this._groupId = nextId++; } - /** - * Waits one frame for the view to update, then updates the ink bar - * Note: This must be run outside of the zone or it will create an infinite change detection loop - * TODO: internal - */ + ngAfterContentChecked(): void { + this._selectedIndex = this.desiredSelectedIndex; + + console.log('Content checked - tabs: ', this._tabs.length, '; bodies: ', + this._tabBodies ? this._tabBodies.length : 0); + } + ngAfterViewChecked(): void { + // Set the position for each tab body based on the selected index + + this._tabBodies.forEach((tabBody, i) => { + tabBody.position = i - this.selectedIndex; + // tabBody.originPosition = i <= this._selectedIndex ? 'left' : 'right'; + }); this._zone.runOutsideAngular(() => { window.requestAnimationFrame(() => { this._updateInkBar(); @@ -167,6 +176,28 @@ export class MdTabGroup { this._isInitialized = true; } + /** + * Waits one frame for the view to update, then updates the ink bar + * Note: This must be run outside of the zone or it will create an infinite change detection loop + * TODO: internal + */ + ngAfterViewInit() { + this._tabs.changes.forEach((tabs) => { + if (this.selectedIndex > tabs.length - 1) { + this.selectedIndex = tabs.length - 1; + } + }); + } + + + _getTabOrigin(index: number): MdTabBodyState { + if (index <= this._selectedIndex) { + return 'left'; + } else if (index > this._selectedIndex) { + return 'right'; + } + } + /** * Determines if an index is valid. If the tabs are not ready yet, we assume that the user is * providing a valid index and return true. @@ -182,7 +213,9 @@ export class MdTabGroup { /** Tells the ink-bar to align itself to the current label wrapper */ private _updateInkBar(): void { - this._inkBar.toArray()[0].alignToElement(this._currentLabelWrapper); + if (this._currentLabelWrapper) { + this._inkBar.toArray()[0].alignToElement(this._currentLabelWrapper); + } } /** @@ -190,7 +223,8 @@ export class MdTabGroup { * ViewChildren references are ready. */ private get _currentLabelWrapper(): HTMLElement { - return this._labelWrappers && this._labelWrappers.length + return this._labelWrappers && this._labelWrappers.length && + this._labelWrappers.toArray()[this.selectedIndex] ? this._labelWrappers.toArray()[this.selectedIndex].elementRef.nativeElement : null; } @@ -293,11 +327,21 @@ export class MdTabGroup { /** Removes the height of the tab body wrapper. */ _removeTabBodyWrapperHeight(): void { + this._tabBodyWrapperHeight = this._tabBodyWrapper.nativeElement.clientHeight; this._renderer.setElementStyle(this._tabBodyWrapper.nativeElement, 'height', ''); } } -export type MdTabBodyActiveState = 'left' | 'center' | 'right'; +export type CenteringEvent = { + tabHeight: number, + tab: MdTabBody +} +export type MdTabBodyState = 'left' | 'center' | 'right' | + 'left-origin-center' | 'right-origin-center'; +export type MdTabBodyPosition = { + origin?: MdTabBodyState, + state: MdTabBodyState +} @Component({ moduleId: module.id, @@ -306,15 +350,30 @@ export type MdTabBodyActiveState = 'left' | 'center' | 'right'; animations: [ trigger('translateTab', [ state('left', style({transform: 'translate3d(-100%, 0, 0)'})), + state('left-origin-center', style({transform: 'translate3d(0, 0, 0)'})), + state('right-origin-center', style({transform: 'translate3d(0, 0, 0)'})), state('center', style({transform: 'translate3d(0, 0, 0)'})), state('right', style({transform: 'translate3d(100%, 0, 0)'})), - transition('* => *', animate('500ms cubic-bezier(0.35, 0, 0.25, 1)')), + transition('left <=> center, right <=> center', + animate('500ms cubic-bezier(0.35, 0, 0.25, 1)')), + transition('void => left-origin-center', [ + style({transform: 'translate3d(-100%, 0, 0)'}), + animate('500ms cubic-bezier(0.35, 0, 0.25, 1)') + ]), + transition('void => right-origin-center', [ + style({transform: 'translate3d(100%, 0, 0)'}), + animate('500ms cubic-bezier(0.35, 0, 0.25, 1)') + ]) ]) - ] + ], + host: { + 'md-tab-body-active': "'this._position == 'center'" + } }) export class MdTabBody implements OnInit { /** The portal host inside of this container into which the tab body content will be loaded. */ @ViewChild(PortalHostDirective) _portalHost: PortalHostDirective; + @ViewChild('content') _contentElement: ElementRef; /** Event emitted when the tab begins to animate towards the center as the active tab. */ @Output() @@ -322,48 +381,64 @@ export class MdTabBody implements OnInit { /** Event emitted when the tab completes its animation towards the center. */ @Output() - onTabBodyCentered: EventEmitter = new EventEmitter(); + onTabBodyCentered: EventEmitter = new EventEmitter(true); /** The tab body content to display. */ @Input('md-tab-body-content') _content: TemplatePortal; + /** The tab body content to display. */ + @Input('md-tab-body-index') index: number; + /** The shifted index position of the tab body, where zero represents the active center tab. */ - _position: MdTabBodyActiveState; - @Input('md-tab-body-position') set position(v: number) { - if (v < 0) { - this._position = this.getLayoutDirection() == 'ltr' ? 'left' : 'right'; - } else if (v > 0) { - this._position = this.getLayoutDirection() == 'ltr' ? 'right' : 'left'; + _position: MdTabBodyState; + set position(position: number) { + if (position < 0) { + this._position = 'left'; + } else if (position > 0) { + this._position = 'right'; } else { this._position = 'center'; } - if (this._position === 'center' && !this._portalHost.hasAttached() && this._content) { + if (this.isPositionCenter() && !this._portalHost.hasAttached() && this._content) { this._portalHost.attach(this._content); } } - constructor(private _elementRef: ElementRef, @Optional() private _dir: Dir) {} + _centering: boolean; + origin: string; + + constructor(private _elementRef: ElementRef, + @Optional() private _dir: Dir, + private _renderer: Renderer) {} ngOnInit() { - if (this._position == 'center' && !this._portalHost.hasAttached()) { + if (this.isPositionCenter() && !this._portalHost.hasAttached()) { this._portalHost.attach(this._content); } } + isPositionCenter(): boolean { + return this._position == 'center' || + this._position == 'left-origin-center' || + this._position == 'right-origin-center'; + } + _onTranslateTabStarted(e: AnimationTransitionEvent) { + this._centering = true; + console.log('Tab animation started, ', e.toState); if (e.fromState != 'void' && e.toState == 'center') { this.onTabBodyCentering.emit(this._elementRef.nativeElement.clientHeight); } } _onTranslateTabComplete(e: AnimationTransitionEvent) { - if ((e.toState == 'left' || e.toState == 'right') && this._position !== 'center') { + if ((e.toState == 'left' || e.toState == 'right') && !this.isPositionCenter()) { // If the end state is that the tab is not centered, then detach the content. this._portalHost.detach(); } - if ((e.toState == 'center') && this._position == 'center') { + if ((e.toState == 'center') && this.isPositionCenter()) { this.onTabBodyCentered.emit(); } } @@ -375,11 +450,11 @@ export class MdTabBody implements OnInit { } @NgModule({ - imports: [CommonModule, PortalModule, MdRippleModule], + imports: [CommonModule, PortalModule], // Don't export MdInkBar or MdTabLabelWrapper, as they are internal implementation details. - exports: [MdTabGroup, MdTabLabel, MdTab, MdTabNavBar, MdTabLink, MdTabLinkRipple], + exports: [MdTabGroup, MdTabLabel, MdTab, MdTabNavBar, MdTabLink], declarations: [MdTabGroup, MdTabLabel, MdTab, MdInkBar, MdTabLabelWrapper, - MdTabNavBar, MdTabLink, MdTabBody, MdTabLinkRipple], + MdTabNavBar, MdTabLink, MdTabBody], }) export class MdTabsModule { static forRoot(): ModuleWithProviders { diff --git a/src/lib/tooltip/tooltip.spec.ts b/src/lib/tooltip/tooltip.spec.ts index 05e894fc504c..b6a25bf8f5e4 100644 --- a/src/lib/tooltip/tooltip.spec.ts +++ b/src/lib/tooltip/tooltip.spec.ts @@ -152,7 +152,6 @@ describe('MdTooltip', () => { fromState: 'visible', toState: 'hidden', totalTime: 150, - phaseName: '', })); })); }); From 78791a8b6aac76ba3d6662febc7fa90130931df3 Mon Sep 17 00:00:00 2001 From: Andrew Seguin Date: Tue, 22 Nov 2016 13:58:22 -0800 Subject: [PATCH 2/8] Use content checked --- src/demo-app/tabs/tabs-demo.html | 5 +- src/demo-app/tabs/tabs-demo.ts | 4 +- src/lib/tabs/tab-group.html | 2 + src/lib/tabs/tabs.ts | 162 ++++++++++++++++--------------- 4 files changed, 88 insertions(+), 85 deletions(-) diff --git a/src/demo-app/tabs/tabs-demo.html b/src/demo-app/tabs/tabs-demo.html index f68f24481975..e70dc486cc9f 100644 --- a/src/demo-app/tabs/tabs-demo.html +++ b/src/demo-app/tabs/tabs-demo.html @@ -24,13 +24,12 @@

Tab Group Demo - Dynamic Height

Index to select after add:
- Selected index: + Selected index:
+ [(selectedIndex)]="activeTabIndex"> {{tab.content}} diff --git a/src/demo-app/tabs/tabs-demo.ts b/src/demo-app/tabs/tabs-demo.ts index 762bab025edd..fb4ebfe23b20 100644 --- a/src/demo-app/tabs/tabs-demo.ts +++ b/src/demo-app/tabs/tabs-demo.ts @@ -56,12 +56,12 @@ export class TabsDemo { } addTab(includeExtraContent: boolean): void { - this.tabs.splice(3, 0, { + this.tabs.splice(this.addTabPosition, 0, { label: 'New Tab ' + (this.tabs.length + 1), content: 'New tab contents ' + (this.tabs.length + 1), extraContent: includeExtraContent }); - this.activeTabIndex = 3; + this.activeTabIndex = this.indexAfterAdd; } deleteTab(tab: any) { diff --git a/src/lib/tabs/tab-group.html b/src/lib/tabs/tab-group.html index 299dc7ab43cb..8de8ff9b3838 100644 --- a/src/lib/tabs/tab-group.html +++ b/src/lib/tabs/tab-group.html @@ -27,6 +27,8 @@ [attr.aria-labelledby]="_getTabLabelId(i)" [class.md-tab-body-active]="selectedIndex == i" [md-tab-body-content]="tab.content" + [md-tab-position]="tab.position" + [md-tab-origin]="tab.origin" (onTabBodyCentered)="_removeTabBodyWrapperHeight()" (onTabBodyCentering)="_setTabBodyWrapperHeight($event)"> diff --git a/src/lib/tabs/tabs.ts b/src/lib/tabs/tabs.ts index d58efc417cc3..18fb3b819585 100644 --- a/src/lib/tabs/tabs.ts +++ b/src/lib/tabs/tabs.ts @@ -22,7 +22,7 @@ import { AnimationTransitionEvent, ElementRef, Renderer, - Optional, forwardRef, + Optional, } from '@angular/core'; import {CommonModule} from '@angular/common'; import { @@ -53,6 +53,8 @@ export class MdTabChangeEvent { tab: MdTab; } +export type MdTabBodyOriginState = 'left' | 'right'; + @Component({ moduleId: module.id, selector: 'md-tab', @@ -68,20 +70,30 @@ export class MdTab implements OnInit { /** The plain text label for the tab, used when there is no template label. */ @Input('label') textLabel: string = ''; + /** The portal that will be the hosted content of the tab */ private _contentPortal: TemplatePortal = null; + get content(): TemplatePortal { return this._contentPortal; } - constructor(private _viewContainerRef: ViewContainerRef) { } + /** + * The relatively indexed position where 0 represents the center, negative is left, and positive + * represents the right. + */ + position: number = null; - ngOnInit() { - this._contentPortal = new TemplatePortal(this._content, this._viewContainerRef); - } + /** + * The initial origin of the tab if it was created and selected after there was already a + * selected tab. Provides context of what position the tab should originate from. + */ + origin: MdTabBodyOriginState = null; private _disabled = false; @Input() set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); } get disabled(): boolean { return this._disabled; } - get content(): TemplatePortal { - return this._contentPortal; + constructor(private _viewContainerRef: ViewContainerRef) { } + + ngOnInit() { + this._contentPortal = new TemplatePortal(this._content, this._viewContainerRef); } } @@ -99,10 +111,8 @@ export class MdTab implements OnInit { export class MdTabGroup { @ContentChildren(MdTab) _tabs: QueryList; - @ViewChildren(forwardRef(() => MdTabBody)) _tabBodies: QueryList; @ViewChildren(MdTabLabelWrapper) _labelWrappers: QueryList; - @ViewChildren(MdInkBar) _inkBar: QueryList; - + @ViewChild(MdInkBar) _inkBar: MdInkBar; @ViewChild('tabBodyWrapper') _tabBodyWrapper: ElementRef; private _isInitialized: boolean = false; @@ -124,10 +134,6 @@ export class MdTabGroup { this.desiredSelectedIndex = value; } get selectedIndex(): number { - if (this._selectedIndex > this._tabs.length - 1) { - return this._tabs.length - 1; - } - return this._selectedIndex; } @@ -154,26 +160,34 @@ export class MdTabGroup { this._groupId = nextId++; } + /** + * After the content is checked, this component knows what tabs have been defined by the user + * and what the selected index should be. This is where we can know exactly what position + * each tab should be in according to the new selected index, and additionally we know how + * a new selected tab should transition in (from the left or right). + */ ngAfterContentChecked(): void { - this._selectedIndex = this.desiredSelectedIndex; + // Clamp the next selected index to the bounds of 0 and the tabs length. + this.desiredSelectedIndex = + Math.min(this._tabs.length - 1, Math.max(this.desiredSelectedIndex, 0)); - console.log('Content checked - tabs: ', this._tabs.length, '; bodies: ', - this._tabBodies ? this._tabBodies.length : 0); - } + // If there is a change in selected index, emit a change event. + if (this._selectedIndex != this.desiredSelectedIndex) { + this._onSelectChange.emit(this._createChangeEvent(this.desiredSelectedIndex)); + } - ngAfterViewChecked(): void { - // Set the position for each tab body based on the selected index + // Setup the position for each tab and optionally setup an origin on the next selected tab. + this._tabs.forEach((tab: MdTab, index: number) => { + tab.position = index - this.desiredSelectedIndex; - this._tabBodies.forEach((tabBody, i) => { - tabBody.position = i - this.selectedIndex; - // tabBody.originPosition = i <= this._selectedIndex ? 'left' : 'right'; - }); - this._zone.runOutsideAngular(() => { - window.requestAnimationFrame(() => { - this._updateInkBar(); - }); + // If there is already a selected tab, then set up an origin for the next selected tab + // if it doesn't have one already. + if (this._selectedIndex != null && tab.position == 0 && !tab.origin) { + tab.origin = (this.desiredSelectedIndex - this._selectedIndex) <= 0 ? 'left' : 'right'; + } }); - this._isInitialized = true; + + this._selectedIndex = this.desiredSelectedIndex; } /** @@ -181,21 +195,13 @@ export class MdTabGroup { * Note: This must be run outside of the zone or it will create an infinite change detection loop * TODO: internal */ - ngAfterViewInit() { - this._tabs.changes.forEach((tabs) => { - if (this.selectedIndex > tabs.length - 1) { - this.selectedIndex = tabs.length - 1; - } + ngAfterViewChecked(): void { + this._zone.runOutsideAngular(() => { + window.requestAnimationFrame(() => { + this._updateInkBar(); + }); }); - } - - - _getTabOrigin(index: number): MdTabBodyState { - if (index <= this._selectedIndex) { - return 'left'; - } else if (index > this._selectedIndex) { - return 'right'; - } + this._isInitialized = true; } /** @@ -214,7 +220,7 @@ export class MdTabGroup { /** Tells the ink-bar to align itself to the current label wrapper */ private _updateInkBar(): void { if (this._currentLabelWrapper) { - this._inkBar.toArray()[0].alignToElement(this._currentLabelWrapper); + this._inkBar.alignToElement(this._currentLabelWrapper); } } @@ -332,16 +338,8 @@ export class MdTabGroup { } } -export type CenteringEvent = { - tabHeight: number, - tab: MdTabBody -} -export type MdTabBodyState = 'left' | 'center' | 'right' | +export type MdTabBodyPositionState = 'left' | 'center' | 'right' | 'left-origin-center' | 'right-origin-center'; -export type MdTabBodyPosition = { - origin?: MdTabBodyState, - state: MdTabBodyState -} @Component({ moduleId: module.id, @@ -354,7 +352,7 @@ export type MdTabBodyPosition = { state('right-origin-center', style({transform: 'translate3d(0, 0, 0)'})), state('center', style({transform: 'translate3d(0, 0, 0)'})), state('right', style({transform: 'translate3d(100%, 0, 0)'})), - transition('left <=> center, right <=> center', + transition('* => left, * => right, left => center, right => center', animate('500ms cubic-bezier(0.35, 0, 0.25, 1)')), transition('void => left-origin-center', [ style({transform: 'translate3d(-100%, 0, 0)'}), @@ -373,7 +371,6 @@ export type MdTabBodyPosition = { export class MdTabBody implements OnInit { /** The portal host inside of this container into which the tab body content will be loaded. */ @ViewChild(PortalHostDirective) _portalHost: PortalHostDirective; - @ViewChild('content') _contentElement: ElementRef; /** Event emitted when the tab begins to animate towards the center as the active tab. */ @Output() @@ -386,12 +383,9 @@ export class MdTabBody implements OnInit { /** The tab body content to display. */ @Input('md-tab-body-content') _content: TemplatePortal; - /** The tab body content to display. */ - @Input('md-tab-body-index') index: number; - /** The shifted index position of the tab body, where zero represents the active center tab. */ - _position: MdTabBodyState; - set position(position: number) { + _position: MdTabBodyPositionState; + @Input('md-tab-position') set position(position: number) { if (position < 0) { this._position = 'left'; } else if (position > 0) { @@ -399,46 +393,54 @@ export class MdTabBody implements OnInit { } else { this._position = 'center'; } - - if (this.isPositionCenter() && !this._portalHost.hasAttached() && this._content) { - this._portalHost.attach(this._content); - } } - _centering: boolean; - origin: string; + /** The origin position from which this tab should appear when it is centered into view. */ + @Input('md-tab-origin') _origin: MdTabBodyOriginState; - constructor(private _elementRef: ElementRef, - @Optional() private _dir: Dir, - private _renderer: Renderer) {} + constructor(private _elementRef: ElementRef, @Optional() private _dir: Dir) {} + /** + * After initialized, check if the content is centered and has an origin. If so, set the + * special position states that transition the tab from the left or right before centering. + */ ngOnInit() { - if (this.isPositionCenter() && !this._portalHost.hasAttached()) { + if (this._position == 'center' && this._origin) { + this._position = this._origin == 'left' ? 'left-origin-center' : 'right-origin-center'; + } + } + + /** + * 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._portalHost.hasAttached()) { this._portalHost.attach(this._content); } } - isPositionCenter(): boolean { - return this._position == 'center' || - this._position == 'left-origin-center' || - this._position == 'right-origin-center'; + /** Whether the provided position state is considered center, regardless of origin. */ + private _isCenterPosition(position: MdTabBodyPositionState|string): boolean { + return position == 'center' || + position == 'left-origin-center' || + position == 'right-origin-center'; } _onTranslateTabStarted(e: AnimationTransitionEvent) { - this._centering = true; - console.log('Tab animation started, ', e.toState); - if (e.fromState != 'void' && e.toState == 'center') { + if (e.fromState != 'void' && this._isCenterPosition(e.toState)) { this.onTabBodyCentering.emit(this._elementRef.nativeElement.clientHeight); } } _onTranslateTabComplete(e: AnimationTransitionEvent) { - if ((e.toState == 'left' || e.toState == 'right') && !this.isPositionCenter()) { - // If the end state is that the tab is not centered, then detach the content. + // 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._portalHost.detach(); } - if ((e.toState == 'center') && this.isPositionCenter()) { + // If the transition to the center is complete, emit an event. + if (this._isCenterPosition(e.toState) && this._isCenterPosition(this._position)) { this.onTabBodyCentered.emit(); } } From 845dd79bb3e44d23baff996ce116577d79e3b581 Mon Sep 17 00:00:00 2001 From: Andrew Seguin Date: Tue, 22 Nov 2016 16:00:47 -0800 Subject: [PATCH 3/8] add demo --- src/demo-app/tabs/tabs-demo.html | 154 +++++++++++++++++++++++++++---- src/demo-app/tabs/tabs-demo.scss | 6 ++ src/demo-app/tabs/tabs-demo.ts | 32 ++++++- src/lib/tabs/tabs.ts | 35 +++---- 4 files changed, 190 insertions(+), 37 deletions(-) diff --git a/src/demo-app/tabs/tabs-demo.html b/src/demo-app/tabs/tabs-demo.html index e70dc486cc9f..e8977115799e 100644 --- a/src/demo-app/tabs/tabs-demo.html +++ b/src/demo-app/tabs/tabs-demo.html @@ -13,47 +13,169 @@

Tab Nav Bar

-

Tab Group Demo - Dynamic Height

- - -
- New tab position: -
-
- Index to select after add: -
+

Tab Group Demo - Dynamic Tabs

+ + + Add New Tab + +
+ + Include extra content + +
+
+ Position index: + +
+
+ Selected index after create: + +
+ +
+
+
- Selected index: + Selected tab index: +
- + {{tab.content}} -

+
+
- {{tab.content}} + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla venenatis ante augue. + Phasellus volutpat neque ac dui mattis vulputate. Etiam consequat aliquam cursus. + In sodales pretium ultrices. Maecenas lectus est, sollicitudin consectetur felis nec, + feugiat ultricies mi. Aliquam erat volutpat. Nam placerat, tortor in ultrices porttitor, + orci enim rutrum enim, vel tempor sapien arcu a tellus. Vivamus convallis sodales ante varius + gravida. Curabitur a purus vel augue ultrices ultricies id a nisl. Nullam malesuada consequat + diam, a facilisis tortor volutpat et. Sed urna dolor, aliquet vitae posuere vulputate, euismod + ac lorem. Sed felis risus, pulvinar at interdum quis, vehicula sed odio. Phasellus in enim + venenatis, iaculis tortor eu, bibendum ante. Donec ac tellus dictum neque volutpat blandit. + Praesent efficitur faucibus risus, ac auctor purus porttitor vitae. Phasellus ornare dui nec + orci posuere, nec luctus mauris semper. +
+
+ Morbi viverra, ante vel aliquet tincidunt, leo dolor pharetra quam, at semper massa orci nec + magna. Donec posuere nec sapien sed laoreet. Etiam cursus nunc in condimentum facilisis. + Etiam in tempor tortor. Vivamus faucibus egestas enim, at convallis diam pulvinar vel. + Cras ac orci eget nisi maximus cursus. Nunc urna libero, viverra sit amet nisl at, hendrerit + tempor turpis. Maecenas facilisis convallis mi vel tempor. Nullam vitae nunc leo. Cras sed + nisl consectetur, rhoncus sapien sit amet, tempus sapien. +
+
+ Integer turpis erat, porttitor vitae mi faucibus, laoreet interdum tellus. Curabitur posuere + molestie dictum. Morbi eget congue risus, quis rhoncus quam. Suspendisse vitae hendrerit erat, + at posuere mi. Cras eu fermentum nunc. Sed id ante eu orci commodo volutpat non ac est. + Praesent ligula diam, congue eu enim scelerisque, finibus commodo lectus.
-

+
+


-

Stretched Tabs

+

Tab Group Demo - Dynamic Height

- + + + + {{tab.content}} +
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla venenatis ante augue. + Phasellus volutpat neque ac dui mattis vulputate. Etiam consequat aliquam cursus. + In sodales pretium ultrices. Maecenas lectus est, sollicitudin consectetur felis nec, + feugiat ultricies mi. Aliquam erat volutpat. Nam placerat, tortor in ultrices porttitor, + orci enim rutrum enim, vel tempor sapien arcu a tellus. Vivamus convallis sodales ante varius + gravida. Curabitur a purus vel augue ultrices ultricies id a nisl. Nullam malesuada consequat + diam, a facilisis tortor volutpat et. Sed urna dolor, aliquet vitae posuere vulputate, euismod + ac lorem. Sed felis risus, pulvinar at interdum quis, vehicula sed odio. Phasellus in enim + venenatis, iaculis tortor eu, bibendum ante. Donec ac tellus dictum neque volutpat blandit. + Praesent efficitur faucibus risus, ac auctor purus porttitor vitae. Phasellus ornare dui nec + orci posuere, nec luctus mauris semper. +
+
+ Morbi viverra, ante vel aliquet tincidunt, leo dolor pharetra quam, at semper massa orci nec + magna. Donec posuere nec sapien sed laoreet. Etiam cursus nunc in condimentum facilisis. + Etiam in tempor tortor. Vivamus faucibus egestas enim, at convallis diam pulvinar vel. + Cras ac orci eget nisi maximus cursus. Nunc urna libero, viverra sit amet nisl at, hendrerit + tempor turpis. Maecenas facilisis convallis mi vel tempor. Nullam vitae nunc leo. Cras sed + nisl consectetur, rhoncus sapien sit amet, tempus sapien. +
+
+ Integer turpis erat, porttitor vitae mi faucibus, laoreet interdum tellus. Curabitur posuere + molestie dictum. Morbi eget congue risus, quis rhoncus quam. Suspendisse vitae hendrerit erat, + at posuere mi. Cras eu fermentum nunc. Sed id ante eu orci commodo volutpat non ac est. + Praesent ligula diam, congue eu enim scelerisque, finibus commodo lectus. +
+
+
+ +
+
+ + +

Tab Group Demo - Fixed Height

+ + {{tab.content}} +
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla venenatis ante augue. + Phasellus volutpat neque ac dui mattis vulputate. Etiam consequat aliquam cursus. + In sodales pretium ultrices. Maecenas lectus est, sollicitudin consectetur felis nec, + feugiat ultricies mi. Aliquam erat volutpat. Nam placerat, tortor in ultrices porttitor, + orci enim rutrum enim, vel tempor sapien arcu a tellus. Vivamus convallis sodales ante varius + gravida. Curabitur a purus vel augue ultrices ultricies id a nisl. Nullam malesuada consequat + diam, a facilisis tortor volutpat et. Sed urna dolor, aliquet vitae posuere vulputate, euismod + ac lorem. Sed felis risus, pulvinar at interdum quis, vehicula sed odio. Phasellus in enim + venenatis, iaculis tortor eu, bibendum ante. Donec ac tellus dictum neque volutpat blandit. + Praesent efficitur faucibus risus, ac auctor purus porttitor vitae. Phasellus ornare dui nec + orci posuere, nec luctus mauris semper. +
+
+ Morbi viverra, ante vel aliquet tincidunt, leo dolor pharetra quam, at semper massa orci nec + magna. Donec posuere nec sapien sed laoreet. Etiam cursus nunc in condimentum facilisis. + Etiam in tempor tortor. Vivamus faucibus egestas enim, at convallis diam pulvinar vel. + Cras ac orci eget nisi maximus cursus. Nunc urna libero, viverra sit amet nisl at, hendrerit + tempor turpis. Maecenas facilisis convallis mi vel tempor. Nullam vitae nunc leo. Cras sed + nisl consectetur, rhoncus sapien sit amet, tempus sapien. +
+
+ Integer turpis erat, porttitor vitae mi faucibus, laoreet interdum tellus. Curabitur posuere + molestie dictum. Morbi eget congue risus, quis rhoncus quam. Suspendisse vitae hendrerit erat, + at posuere mi. Cras eu fermentum nunc. Sed id ante eu orci commodo volutpat non ac est. + Praesent ligula diam, congue eu enim scelerisque, finibus commodo lectus. +
+
+
+
+

Stretched Tabs

+ + + + + {{tab.content}} + +

Async Tabs

diff --git a/src/demo-app/tabs/tabs-demo.scss b/src/demo-app/tabs/tabs-demo.scss index 2dc75c934f18..7be5d572a809 100644 --- a/src/demo-app/tabs/tabs-demo.scss +++ b/src/demo-app/tabs/tabs-demo.scss @@ -1,5 +1,6 @@ .demo-nav-bar { border: 1px solid #e0e0e0; + margin-bottom: 40px; [md-tab-nav-bar] { background: #f9f9f9; } @@ -13,10 +14,15 @@ .demo-tab-group { border: 1px solid #e0e0e0; + margin-bottom: 40px; .md-tab-header { background: #f9f9f9; } .md-tab-body-content { padding: 12px; } +} + +tabs-demo md-card { + width: 360px; } \ No newline at end of file diff --git a/src/demo-app/tabs/tabs-demo.ts b/src/demo-app/tabs/tabs-demo.ts index fb4ebfe23b20..33aafd6660c3 100644 --- a/src/demo-app/tabs/tabs-demo.ts +++ b/src/demo-app/tabs/tabs-demo.ts @@ -10,6 +10,7 @@ import {Observable} from 'rxjs/Observable'; encapsulation: ViewEncapsulation.None, }) export class TabsDemo { + // Nav bar demo tabLinks = [ {label: 'Sun', link: 'sunny-tab'}, {label: 'Rain', link: 'rainy-tab'}, @@ -17,11 +18,32 @@ export class TabsDemo { ]; activeLinkIndex = 0; + // Standard tabs demo + tabs = [ + { + label: 'Tab 1', + content: 'This is the body of the first tab' + }, { + label: 'Tab 2', + disabled: true, + content: 'This is the body of the second tab' + }, { + label: 'Tab 3', + extraContent: true, + content: 'This is the body of the third tab' + }, { + label: 'Tab 4', + content: 'This is the body of the fourth tab' + }, + ]; + + // Dynamic tabs demo selectedIndex = 0; activeTabIndex = 0; addTabPosition = 0; indexAfterAdd = 0; - tabs = [ + createWithLongContent = false; + dynamicTabs = [ { label: 'Tab 1', content: 'This is the body of the first tab' @@ -56,16 +78,16 @@ export class TabsDemo { } addTab(includeExtraContent: boolean): void { - this.tabs.splice(this.addTabPosition, 0, { - label: 'New Tab ' + (this.tabs.length + 1), - content: 'New tab contents ' + (this.tabs.length + 1), + this.dynamicTabs.splice(this.addTabPosition, 0, { + label: 'New Tab ' + (this.dynamicTabs.length + 1), + content: 'New tab contents ' + (this.dynamicTabs.length + 1), extraContent: includeExtraContent }); this.activeTabIndex = this.indexAfterAdd; } deleteTab(tab: any) { - this.tabs.splice(this.tabs.indexOf(tab), 1); + this.dynamicTabs.splice(this.dynamicTabs.indexOf(tab), 1); } } diff --git a/src/lib/tabs/tabs.ts b/src/lib/tabs/tabs.ts index 18fb3b819585..d82feb4fd41a 100644 --- a/src/lib/tabs/tabs.ts +++ b/src/lib/tabs/tabs.ts @@ -38,10 +38,11 @@ import { } from '../core'; import {MdTabLabel} from './tab-label'; import {MdTabLabelWrapper} from './tab-label-wrapper'; -import {MdTabNavBar, MdTabLink} from './tab-nav-bar/tab-nav-bar'; +import {MdTabNavBar, MdTabLink, MdTabLinkRipple} from './tab-nav-bar/tab-nav-bar'; import {MdInkBar} from './ink-bar'; import {Observable} from 'rxjs/Observable'; import 'rxjs/add/operator/map'; +import {MdRippleModule} from '../core/ripple/ripple'; /** Used to generate unique ID's for each tab component */ @@ -84,7 +85,7 @@ export class MdTab implements OnInit { * The initial origin of the tab if it was created and selected after there was already a * selected tab. Provides context of what position the tab should originate from. */ - origin: MdTabBodyOriginState = null; + origin: number = null; private _disabled = false; @Input() set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); } @@ -106,7 +107,7 @@ export class MdTab implements OnInit { moduleId: module.id, selector: 'md-tab-group', templateUrl: 'tab-group.html', - styleUrls: ['tab-group.css'] + styleUrls: ['tab-group.css'], }) export class MdTabGroup { @ContentChildren(MdTab) _tabs: QueryList; @@ -115,9 +116,11 @@ export class MdTabGroup { @ViewChild(MdInkBar) _inkBar: MdInkBar; @ViewChild('tabBodyWrapper') _tabBodyWrapper: ElementRef; + /** Whether this component has been initialized. */ private _isInitialized: boolean = false; - desiredSelectedIndex = 0; + /** The tab index that should be selected after the content has been checked. */ + private _indexToSelect = 0; /** Snapshot of the height of the tab body wrapper before another tab is activated. */ private _tabBodyWrapperHeight: number = 0; @@ -131,7 +134,7 @@ export class MdTabGroup { /** The index of the active tab. */ private _selectedIndex: number = null; @Input() set selectedIndex(value: number) { - this.desiredSelectedIndex = value; + this._indexToSelect = value; } get selectedIndex(): number { return this._selectedIndex; @@ -168,26 +171,26 @@ export class MdTabGroup { */ ngAfterContentChecked(): void { // Clamp the next selected index to the bounds of 0 and the tabs length. - this.desiredSelectedIndex = - Math.min(this._tabs.length - 1, Math.max(this.desiredSelectedIndex, 0)); + this._indexToSelect = + Math.min(this._tabs.length - 1, Math.max(this._indexToSelect, 0)); // If there is a change in selected index, emit a change event. - if (this._selectedIndex != this.desiredSelectedIndex) { - this._onSelectChange.emit(this._createChangeEvent(this.desiredSelectedIndex)); + if (this._selectedIndex != this._indexToSelect) { + this._onSelectChange.emit(this._createChangeEvent(this._indexToSelect)); } // Setup the position for each tab and optionally setup an origin on the next selected tab. this._tabs.forEach((tab: MdTab, index: number) => { - tab.position = index - this.desiredSelectedIndex; + tab.position = index - this._indexToSelect; // If there is already a selected tab, then set up an origin for the next selected tab // if it doesn't have one already. if (this._selectedIndex != null && tab.position == 0 && !tab.origin) { - tab.origin = (this.desiredSelectedIndex - this._selectedIndex) <= 0 ? 'left' : 'right'; + tab.origin = this._indexToSelect - this._selectedIndex; } }); - this._selectedIndex = this.desiredSelectedIndex; + this._selectedIndex = this._indexToSelect; } /** @@ -396,7 +399,7 @@ export class MdTabBody implements OnInit { } /** The origin position from which this tab should appear when it is centered into view. */ - @Input('md-tab-origin') _origin: MdTabBodyOriginState; + @Input('md-tab-origin') set origin: MdTabBodyOriginState; constructor(private _elementRef: ElementRef, @Optional() private _dir: Dir) {} @@ -452,11 +455,11 @@ export class MdTabBody implements OnInit { } @NgModule({ - imports: [CommonModule, PortalModule], + imports: [CommonModule, PortalModule, MdRippleModule], // Don't export MdInkBar or MdTabLabelWrapper, as they are internal implementation details. - exports: [MdTabGroup, MdTabLabel, MdTab, MdTabNavBar, MdTabLink], + exports: [MdTabGroup, MdTabLabel, MdTab, MdTabNavBar, MdTabLink, MdTabLinkRipple], declarations: [MdTabGroup, MdTabLabel, MdTab, MdInkBar, MdTabLabelWrapper, - MdTabNavBar, MdTabLink, MdTabBody], + MdTabNavBar, MdTabLink, MdTabBody, MdTabLinkRipple], }) export class MdTabsModule { static forRoot(): ModuleWithProviders { From 20d578a676a7832a62a40b286e038743eed4ee3f Mon Sep 17 00:00:00 2001 From: Andrew Seguin Date: Tue, 22 Nov 2016 16:40:54 -0800 Subject: [PATCH 4/8] cleanup demo --- src/demo-app/tabs/tabs-demo.html | 24 ++++++++++++------------ src/demo-app/tabs/tabs-demo.scss | 15 ++++++++++++++- src/demo-app/tabs/tabs-demo.ts | 7 +++++-- src/lib/tabs/tabs.ts | 17 ++++++++++++----- 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/src/demo-app/tabs/tabs-demo.html b/src/demo-app/tabs/tabs-demo.html index e8977115799e..288bbd0c9cd7 100644 --- a/src/demo-app/tabs/tabs-demo.html +++ b/src/demo-app/tabs/tabs-demo.html @@ -19,20 +19,20 @@

Tab Group Demo - Dynamic Tabs

Add New Tab -
- - Include extra content - -
-
- Position index: + + Include extra content + + + Select after adding + +
+ Position:
-
- Selected index after create: - -
- + diff --git a/src/demo-app/tabs/tabs-demo.scss b/src/demo-app/tabs/tabs-demo.scss index 7be5d572a809..19dad28662d4 100644 --- a/src/demo-app/tabs/tabs-demo.scss +++ b/src/demo-app/tabs/tabs-demo.scss @@ -24,5 +24,18 @@ } tabs-demo md-card { - width: 360px; + width: 160px; + + md-checkbox { + display: block; + margin-top: 8px; + } + + md-input { + width: 100px; + } + + button { + width: 100% + } } \ No newline at end of file diff --git a/src/demo-app/tabs/tabs-demo.ts b/src/demo-app/tabs/tabs-demo.ts index 33aafd6660c3..a7b381119350 100644 --- a/src/demo-app/tabs/tabs-demo.ts +++ b/src/demo-app/tabs/tabs-demo.ts @@ -41,7 +41,7 @@ export class TabsDemo { selectedIndex = 0; activeTabIndex = 0; addTabPosition = 0; - indexAfterAdd = 0; + gotoNewTabAfterAdding = false; createWithLongContent = false; dynamicTabs = [ { @@ -83,7 +83,10 @@ export class TabsDemo { content: 'New tab contents ' + (this.dynamicTabs.length + 1), extraContent: includeExtraContent }); - this.activeTabIndex = this.indexAfterAdd; + + if (this.gotoNewTabAfterAdding) { + this.activeTabIndex = this.addTabPosition; + } } deleteTab(tab: any) { diff --git a/src/lib/tabs/tabs.ts b/src/lib/tabs/tabs.ts index d82feb4fd41a..cd33c6bd4d4e 100644 --- a/src/lib/tabs/tabs.ts +++ b/src/lib/tabs/tabs.ts @@ -82,8 +82,8 @@ export class MdTab implements OnInit { position: number = null; /** - * The initial origin of the tab if it was created and selected after there was already a - * selected tab. Provides context of what position the tab should originate from. + * The initial relatively index origin of the tab if it was created and selected after there + * was already a selected tab. Provides context of what position the tab should originate from. */ origin: number = null; @@ -390,16 +390,23 @@ export class MdTabBody implements OnInit { _position: MdTabBodyPositionState; @Input('md-tab-position') set position(position: number) { if (position < 0) { - this._position = 'left'; + this._position = this.getLayoutDirection() == 'ltr' ? 'left' : 'right'; } else if (position > 0) { - this._position = 'right'; + this._position = this.getLayoutDirection() == 'ltr' ? 'right' : 'left'; } else { this._position = 'center'; } } /** The origin position from which this tab should appear when it is centered into view. */ - @Input('md-tab-origin') set origin: MdTabBodyOriginState; + _origin: MdTabBodyOriginState; + @Input('md-tab-origin') set origin(origin: number) { + if (origin <= 0) { + this._origin = this.getLayoutDirection() == 'ltr' ? 'left' : 'right'; + } else { + this._origin = this.getLayoutDirection() == 'ltr' ? 'right' : 'left'; + } + } constructor(private _elementRef: ElementRef, @Optional() private _dir: Dir) {} From 8f5f8d8796232be55ea885ba03a1d7d25a733ccb Mon Sep 17 00:00:00 2001 From: Andrew Seguin Date: Wed, 23 Nov 2016 14:39:51 -0800 Subject: [PATCH 5/8] fix dynamic height on new tabs --- src/demo-app/tabs/tabs-demo.html | 9 +- src/demo-app/tabs/tabs-demo.ts | 3 +- src/lib/tabs/index.ts | 2 +- src/lib/tabs/tab-body.ts | 144 ++++++++++++ src/lib/tabs/tab-group.spec.ts | 2 +- src/lib/tabs/{tabs.ts => tab-group.ts} | 223 ++----------------- src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts | 2 +- src/lib/tabs/tab.ts | 50 +++++ 8 files changed, 227 insertions(+), 208 deletions(-) create mode 100644 src/lib/tabs/tab-body.ts rename src/lib/tabs/{tabs.ts => tab-group.ts} (57%) create mode 100644 src/lib/tabs/tab.ts diff --git a/src/demo-app/tabs/tabs-demo.html b/src/demo-app/tabs/tabs-demo.html index 288bbd0c9cd7..0476f408c462 100644 --- a/src/demo-app/tabs/tabs-demo.html +++ b/src/demo-app/tabs/tabs-demo.html @@ -40,7 +40,6 @@

Tab Group Demo - Dynamic Tabs

Selected tab index:
- @@ -80,7 +79,11 @@

Tab Group Demo - Dynamic Tabs




- +
@@ -198,4 +201,4 @@

Tabs with simplified api

This tab is about combustion! -
+
\ No newline at end of file diff --git a/src/demo-app/tabs/tabs-demo.ts b/src/demo-app/tabs/tabs-demo.ts index a7b381119350..aa3d9ce124a4 100644 --- a/src/demo-app/tabs/tabs-demo.ts +++ b/src/demo-app/tabs/tabs-demo.ts @@ -36,9 +36,8 @@ export class TabsDemo { content: 'This is the body of the fourth tab' }, ]; - + // Dynamic tabs demo - selectedIndex = 0; activeTabIndex = 0; addTabPosition = 0; gotoNewTabAfterAdding = false; diff --git a/src/lib/tabs/index.ts b/src/lib/tabs/index.ts index c2d1b4e91b22..f40a858afd57 100644 --- a/src/lib/tabs/index.ts +++ b/src/lib/tabs/index.ts @@ -1 +1 @@ -export * from './tabs'; +export * from './tab-group'; diff --git a/src/lib/tabs/tab-body.ts b/src/lib/tabs/tab-body.ts new file mode 100644 index 000000000000..37cbcabce66a --- /dev/null +++ b/src/lib/tabs/tab-body.ts @@ -0,0 +1,144 @@ +import { + ViewChild, + Component, + Input, + Output, + EventEmitter, + OnInit, + trigger, + state, + style, + animate, + transition, + AnimationTransitionEvent, + ElementRef, + Optional +} from '@angular/core'; +import {TemplatePortal, PortalHostDirective, Dir, LayoutDirection} from '../core'; +import 'rxjs/add/operator/map'; + +export type MdTabBodyPositionState = + 'left' | 'center' | 'right' | 'left-origin-center' | 'right-origin-center'; + +export type MdTabBodyOriginState = 'left' | 'right'; + +@Component({ + moduleId: module.id, + selector: 'md-tab-body', + templateUrl: 'tab-body.html', + animations: [ + trigger('translateTab', [ + state('left', style({transform: 'translate3d(-100%, 0, 0)'})), + state('left-origin-center', style({transform: 'translate3d(0, 0, 0)'})), + state('right-origin-center', style({transform: 'translate3d(0, 0, 0)'})), + state('center', style({transform: 'translate3d(0, 0, 0)'})), + state('right', style({transform: 'translate3d(100%, 0, 0)'})), + transition('* => left, * => right, left => center, right => center', + animate('500ms cubic-bezier(0.35, 0, 0.25, 1)')), + transition('void => left-origin-center', [ + style({transform: 'translate3d(-100%, 0, 0)'}), + animate('500ms cubic-bezier(0.35, 0, 0.25, 1)') + ]), + transition('void => right-origin-center', [ + style({transform: 'translate3d(100%, 0, 0)'}), + animate('500ms cubic-bezier(0.35, 0, 0.25, 1)') + ]) + ]) + ], + host: { + 'md-tab-body-active': "'this._position == 'center'" + } +}) +export class MdTabBody implements OnInit { + /** The portal host inside of this container into which the tab body content will be loaded. */ + @ViewChild(PortalHostDirective) _portalHost: PortalHostDirective; + + /** Event emitted when the tab begins to animate towards the center as the active tab. */ + @Output() + onTabBodyCentering: EventEmitter = new EventEmitter(); + + /** Event emitted when the tab completes its animation towards the center. */ + @Output() + onTabBodyCentered: EventEmitter = new EventEmitter(true); + + /** The tab body content to display. */ + @Input('md-tab-body-content') _content: TemplatePortal; + + /** The shifted index position of the tab body, where zero represents the active center tab. */ + _position: MdTabBodyPositionState; + @Input('md-tab-position') set position(position: number) { + if (position < 0) { + this._position = this._getLayoutDirection() == 'ltr' ? 'left' : 'right'; + } else if (position > 0) { + this._position = this._getLayoutDirection() == 'ltr' ? 'right' : 'left'; + } else { + this._position = 'center'; + } + } + + /** The origin position from which this tab should appear when it is centered into view. */ + _origin: MdTabBodyOriginState; + @Input('md-tab-origin') set origin(origin: number) { + if (origin == null) { return; } + + if (origin <= 0) { + this._origin = this._getLayoutDirection() == 'ltr' ? 'left' : 'right'; + } else { + this._origin = this._getLayoutDirection() == 'ltr' ? 'right' : 'left'; + } + } + + constructor(private _elementRef: ElementRef, @Optional() private _dir: Dir) {} + + /** + * After initialized, check if the content is centered and has an origin. If so, set the + * special position states that transition the tab from the left or right before centering. + */ + ngOnInit() { + if (this._position == 'center' && this._origin) { + this._position = this._origin == 'left' ? 'left-origin-center' : 'right-origin-center'; + } + } + + /** + * 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._portalHost.hasAttached()) { + this._portalHost.attach(this._content); + } + } + + _onTranslateTabStarted(e: AnimationTransitionEvent) { + console.log('Animation began with position ', this._position); + if (this._isCenterPosition(e.toState)) { + this.onTabBodyCentering.emit(this._elementRef.nativeElement.clientHeight); + } + } + + _onTranslateTabComplete(e: AnimationTransitionEvent) { + // 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._portalHost.detach(); + } + + // If the transition to the center is complete, emit an event. + if (this._isCenterPosition(e.toState) && this._isCenterPosition(this._position)) { + this.onTabBodyCentered.emit(); + } + } + + /** The text direction of the containing app. */ + _getLayoutDirection(): LayoutDirection { + return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr'; + } + + + /** Whether the provided position state is considered center, regardless of origin. */ + private _isCenterPosition(position: MdTabBodyPositionState|string): boolean { + return position == 'center' || + position == 'left-origin-center' || + position == 'right-origin-center'; + } +} diff --git a/src/lib/tabs/tab-group.spec.ts b/src/lib/tabs/tab-group.spec.ts index aef5a5ecca64..a211ed5aaabd 100644 --- a/src/lib/tabs/tab-group.spec.ts +++ b/src/lib/tabs/tab-group.spec.ts @@ -2,7 +2,7 @@ import { async, fakeAsync, tick, ComponentFixture, TestBed, flushMicrotasks } from '@angular/core/testing'; -import {MdTabGroup, MdTabsModule} from './tabs'; +import {MdTabGroup, MdTabsModule} from './tab-group'; import {Component, ViewChild} from '@angular/core'; import {By} from '@angular/platform-browser'; import {Observable} from 'rxjs/Observable'; diff --git a/src/lib/tabs/tabs.ts b/src/lib/tabs/tab-group.ts similarity index 57% rename from src/lib/tabs/tabs.ts rename to src/lib/tabs/tab-group.ts index cd33c6bd4d4e..afcc5198e1f1 100644 --- a/src/lib/tabs/tabs.ts +++ b/src/lib/tabs/tab-group.ts @@ -1,40 +1,25 @@ import { - NgModule, - ModuleWithProviders, - ContentChild, - ViewChild, - Component, - Input, - Output, - ViewChildren, - NgZone, - EventEmitter, - QueryList, - ContentChildren, - TemplateRef, - ViewContainerRef, - OnInit, - trigger, - state, - style, - animate, - transition, - AnimationTransitionEvent, - ElementRef, - Renderer, - Optional, + NgModule, + ModuleWithProviders, + ViewChild, + Component, + Input, + Output, + ViewChildren, + NgZone, + EventEmitter, + QueryList, + ContentChildren, + ElementRef, + Renderer } from '@angular/core'; import {CommonModule} from '@angular/common'; import { - PortalModule, - TemplatePortal, - RIGHT_ARROW, - LEFT_ARROW, - ENTER, - coerceBooleanProperty, - PortalHostDirective, - Dir, - LayoutDirection + PortalModule, + RIGHT_ARROW, + LEFT_ARROW, + ENTER, + coerceBooleanProperty } from '../core'; import {MdTabLabel} from './tab-label'; import {MdTabLabelWrapper} from './tab-label-wrapper'; @@ -43,6 +28,8 @@ import {MdInkBar} from './ink-bar'; import {Observable} from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import {MdRippleModule} from '../core/ripple/ripple'; +import {MdTab} from './tab'; +import {MdTabBody} from './tab-body'; /** Used to generate unique ID's for each tab component */ @@ -54,50 +41,6 @@ export class MdTabChangeEvent { tab: MdTab; } -export type MdTabBodyOriginState = 'left' | 'right'; - -@Component({ - moduleId: module.id, - selector: 'md-tab', - templateUrl: 'tab.html', -}) -export class MdTab implements OnInit { - /** Content for the tab label given by