Skip to content

Commit

Permalink
feat(tabs): Added option for minimal animation (angular#2706)
Browse files Browse the repository at this point in the history
  • Loading branch information
CodySchrank committed Jun 16, 2018
1 parent 48dda50 commit 22ebef2
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 7 deletions.
1 change: 1 addition & 0 deletions src/demo-app/tabs/tabs-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class TabsDemo {
'tab-group-theme',
'tab-group-lazy-loaded',
'tab-group-dynamic',
'tab-group-minimal-animation',
'tab-nav-bar-basic',
];
}
23 changes: 17 additions & 6 deletions src/lib/tabs/tab-body.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
<div class="mat-tab-body-content" #content
[@translateTab]="_position"
(@translateTab.start)="_onTranslateTabStarted($event)"
(@translateTab.done)="_onTranslateTabComplete($event)">
<ng-template matTabBodyHost></ng-template>
</div>
<div *ngIf="_minimalAnimation;then min else full" class="mat-tab-body-content" #content></div>

<ng-template #min>
<div [@translateTabMinimalAnimation]="_position"
(@translateTabMinimalAnimation.start)="_onTranslateTabStarted($event)"
(@translateTabMinimalAnimation.done)="_onTranslateTabComplete($event)">
<ng-template matTabBodyHost></ng-template>
</div>
</ng-template>

<ng-template #full>
<div [@translateTab]="_position"
(@translateTab.start)="_onTranslateTabStarted($event)"
(@translateTab.done)="_onTranslateTabComplete($event)">
<ng-template matTabBodyHost></ng-template>
</div>
</ng-template>
5 changes: 4 additions & 1 deletion src/lib/tabs/tab-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class MatTabBodyPortal extends CdkPortalOutlet implements OnInit, OnDestr
styleUrls: ['tab-body.css'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [matTabsAnimations.translateTab],
animations: [matTabsAnimations.translateTab, matTabsAnimations.translateTabMinimalAnimation],
host: {
'class': 'mat-tab-body',
},
Expand All @@ -132,6 +132,9 @@ export class MatTabBody implements OnInit {
/** The tab body content to display. */
@Input('content') _content: TemplatePortal;

/** Whether to use full animation or minimal one */
@Input('minimalAnimation') _minimalAnimation: boolean;

/** The shifted index position of the tab body, where zero represents the active center tab. */
@Input()
set position(position: number) {
Expand Down
1 change: 1 addition & 0 deletions src/lib/tabs/tab-group.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<mat-tab-body role="tabpanel"
*ngFor="let tab of _tabs; let i = index"
[id]="_getTabContentId(i)"
[minimalAnimation]="minimalAnimation"
[attr.aria-labelledby]="_getTabLabelId(i)"
[class.mat-tab-body-active]="selectedIndex == i"
[content]="tab.content"
Expand Down
248 changes: 248 additions & 0 deletions src/lib/tabs/tab-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,214 @@ describe('MatTabGroup', () => {
}));
});

describe('minimal animation', () => {
let fixture: ComponentFixture<SimpleTabsMinimalAnimationTestApp>;
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;
checkSelectedIndex(0, fixture);

// select the second tab
let tabLabel = fixture.debugElement.queryAll(By.css('.mat-tab-label'))[1];
tabLabel.nativeElement.click();
checkSelectedIndex(1, fixture);

// select the third tab
tabLabel = fixture.debugElement.queryAll(By.css('.mat-tab-label'))[2];
tabLabel.nativeElement.click();
checkSelectedIndex(2, fixture);
});

it('should support two-way binding for selectedIndex', fakeAsync(() => {
let component = fixture.componentInstance;
component.selectedIndex = 0;

fixture.detectChanges();

let tabLabel = fixture.debugElement.queryAll(By.css('.mat-tab-label'))[1];
tabLabel.nativeElement.click();
fixture.detectChanges();
tick();

expect(component.selectedIndex).toBe(1);
}));

// Note: needs to be `async` in order to fail when we expect it to.
it('should set to correct tab on fast change', async(() => {
let component = fixture.componentInstance;
component.selectedIndex = 0;
fixture.detectChanges();

setTimeout(() => {
component.selectedIndex = 1;
fixture.detectChanges();

setTimeout(() => {
component.selectedIndex = 0;
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(component.selectedIndex).toBe(0);
});
}, 1);
}, 1);
}));

it('should change tabs based on selectedIndex', fakeAsync(() => {
let component = fixture.componentInstance;
let tabComponent = fixture.debugElement.query(By.css('mat-tab-group')).componentInstance;

spyOn(component, 'handleSelection').and.callThrough();

checkSelectedIndex(1, fixture);

tabComponent.selectedIndex = 2;

checkSelectedIndex(2, fixture);
tick();

expect(component.handleSelection).toHaveBeenCalledTimes(1);
expect(component.selectEvent.index).toBe(2);
}));

it('should update tab positions when selected index is changed', () => {
fixture.detectChanges();
const component: MatTabGroup =
fixture.debugElement.query(By.css('mat-tab-group')).componentInstance;
const tabs: MatTab[] = component._tabs.toArray();

expect(tabs[0].position).toBeLessThan(0);
expect(tabs[1].position).toBe(0);
expect(tabs[2].position).toBeGreaterThan(0);

// Move to third tab
component.selectedIndex = 2;
fixture.detectChanges();
expect(tabs[0].position).toBeLessThan(0);
expect(tabs[1].position).toBeLessThan(0);
expect(tabs[2].position).toBe(0);

// Move to the first tab
component.selectedIndex = 0;
fixture.detectChanges();
expect(tabs[0].position).toBe(0);
expect(tabs[1].position).toBeGreaterThan(0);
expect(tabs[2].position).toBeGreaterThan(0);
});

it('should clamp the selected index to the size of the number of tabs', () => {
fixture.detectChanges();
const component: MatTabGroup =
fixture.debugElement.query(By.css('mat-tab-group')).componentInstance;

// Set the index to be negative, expect first tab selected
fixture.componentInstance.selectedIndex = -1;
fixture.detectChanges();
expect(component.selectedIndex).toBe(0);

// Set the index beyond the size of the tabs, expect last tab selected
fixture.componentInstance.selectedIndex = 3;
fixture.detectChanges();
expect(component.selectedIndex).toBe(2);
});

it('should not crash when setting the selected index to NaN', () => {
let component = fixture.debugElement.componentInstance;

expect(() => {
component.selectedIndex = NaN;
fixture.detectChanges();
}).not.toThrow();
});

it('should show ripples for tab-group labels', () => {
fixture.detectChanges();

const testElement = fixture.nativeElement;
const tabLabel = fixture.debugElement.queryAll(By.css('.mat-tab-label'))[1];

expect(testElement.querySelectorAll('.mat-ripple-element').length)
.toBe(0, 'Expected no ripples to show up initially.');

dispatchFakeEvent(tabLabel.nativeElement, 'mousedown');
dispatchFakeEvent(tabLabel.nativeElement, 'mouseup');

expect(testElement.querySelectorAll('.mat-ripple-element').length)
.toBe(1, 'Expected one ripple to show up on label mousedown.');
});

it('should allow disabling ripples for tab-group labels', () => {
fixture.componentInstance.disableRipple = true;
fixture.detectChanges();

const testElement = fixture.nativeElement;
const tabLabel = fixture.debugElement.queryAll(By.css('.mat-tab-label'))[1];

expect(testElement.querySelectorAll('.mat-ripple-element').length)
.toBe(0, 'Expected no ripples to show up initially.');

dispatchFakeEvent(tabLabel.nativeElement, 'mousedown');
dispatchFakeEvent(tabLabel.nativeElement, 'mouseup');

expect(testElement.querySelectorAll('.mat-ripple-element').length)
.toBe(0, 'Expected no ripple to show up on label mousedown.');
});

it('should set the isActive flag on each of the tabs', () => {
fixture.detectChanges();

const tabs = fixture.componentInstance.tabs.toArray();

expect(tabs[0].isActive).toBe(false);
expect(tabs[1].isActive).toBe(true);
expect(tabs[2].isActive).toBe(false);

fixture.componentInstance.selectedIndex = 2;
fixture.detectChanges();

expect(tabs[0].isActive).toBe(false);
expect(tabs[1].isActive).toBe(false);
expect(tabs[2].isActive).toBe(true);
});

it('should fire animation done event', fakeAsync(() => {
fixture.detectChanges();

spyOn(fixture.componentInstance, 'animationDone');
let tabLabel = fixture.debugElement.queryAll(By.css('.mat-tab-label'))[1];
tabLabel.nativeElement.click();
fixture.detectChanges();
tick();

expect(fixture.componentInstance.animationDone).toHaveBeenCalled();
}));

it('should add the proper `aria-setsize` and `aria-posinset`', () => {
fixture.detectChanges();

const labels = Array.from(element.querySelectorAll('.mat-tab-label'));

expect(labels.map(label => label.getAttribute('aria-posinset'))).toEqual(['1', '2', '3']);
expect(labels.every(label => label.getAttribute('aria-setsize') === '3')).toBe(true);
});

});

/**
* Checks that the `selectedIndex` has been updated; checks that the label and body have their
* respective `active` classes
Expand Down Expand Up @@ -661,3 +869,43 @@ class NestedTabs {}
`,
})
class TemplateTabs {}
@Component({
template: `
<mat-tab-group class="tab-group"
[(selectedIndex)]="selectedIndex"
[headerPosition]="headerPosition"
[minimalAnimation]="true"
[disableRipple]="disableRipple"
(animationDone)="animationDone()"
(focusChange)="handleFocus($event)"
(selectedTabChange)="handleSelection($event)">
<mat-tab>
<ng-template mat-tab-label>Tab One</ng-template>
Tab one content
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>Tab Two</ng-template>
<span>Tab </span><span>two</span><span>content</span>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>Tab Three</ng-template>
Tab three content
</mat-tab>
</mat-tab-group>
`
})
class SimpleTabsMinimalAnimationTestApp {
@ViewChildren(MatTab) tabs: QueryList<MatTab>;
selectedIndex: number = 1;
focusEvent: any;
selectEvent: any;
disableRipple: boolean = false;
headerPosition: MatTabHeaderPosition = 'above';
handleFocus(event: any) {
this.focusEvent = event;
}
handleSelection(event: any) {
this.selectEvent = event;
}
animationDone() { }
}
6 changes: 6 additions & 0 deletions src/lib/tabs/tab-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ export class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentIn
set dynamicHeight(value: boolean) { this._dynamicHeight = coerceBooleanProperty(value); }
private _dynamicHeight: boolean = false;

/** Whether to use full animation or minimal one */
@Input()
get minimalAnimation(): boolean { return this._minimalAnimation; }
set minimalAnimation(value: boolean) { this._minimalAnimation = coerceBooleanProperty(value); }
private _minimalAnimation: boolean = false;

/** The index of the active tab. */
@Input()
get selectedIndex(): number | null { return this._selectedIndex; }
Expand Down
17 changes: 17 additions & 0 deletions src/lib/tabs/tabs-animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
/** Animations used by the Material tabs. */
export const matTabsAnimations: {
readonly translateTab: AnimationTriggerMetadata;
readonly translateTabMinimalAnimation: AnimationTriggerMetadata;
} = {
/** Animation translates a tab along the X axis. */
translateTab: trigger('translateTab', [
Expand All @@ -34,5 +35,21 @@ export const matTabsAnimations: {
style({transform: 'translate3d(100%, 0, 0)'}),
animate('500ms cubic-bezier(0.35, 0, 0.25, 1)')
])
]),
translateTabMinimalAnimation: trigger('translateTabMinimalAnimation', [
// Note: 1ms animation to give the illusion of no animation, but still triggers events
state('center, void, left-origin-center, right-origin-center', style({transform: 'none'})),
state('left', style({transform: 'translate3d(-100%, 0, 0)'})),
state('right', style({transform: 'translate3d(100%, 0, 0)'})),
transition('* => left, * => right, left => center, right => center',
animate('1ms cubic-bezier(0.35, 0, 0.25, 1)')),
transition('void => left-origin-center', [
style({transform: 'translate3d(-100%, 0, 0)'}),
animate('1ms cubic-bezier(0.35, 0, 0.25, 1)')
]),
transition('void => right-origin-center', [
style({transform: 'translate3d(100%, 0, 0)'}),
animate('1ms cubic-bezier(0.35, 0, 0.25, 1)')
])
])
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/** No CSS for this example */
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<mat-tab-group [minimalAnimation]="true">
<mat-tab label="First"> Content 1 </mat-tab>
<mat-tab label="Second"> Content 2 </mat-tab>
<mat-tab label="Third"> Content 3 </mat-tab>
</mat-tab-group>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {Component} from '@angular/core';

/**
* @title Basic use of the no animation selector
*/
@Component({
selector: 'tab-group-minimal-animation-example',
templateUrl: 'tab-group-minimal-animation-example.html',
styleUrls: ['tab-group-minimal-animation-example.css'],
})
export class TabGroupMinimalAnimationExample {}

0 comments on commit 22ebef2

Please sign in to comment.