feat(aio): animate the leaving/entering documents (#18428)

This commit adds a simple fade-in/out animation.

Fixes #15629

PR Close #18428
This commit is contained in:
George Kalpakas 2017-11-23 15:04:29 +02:00 committed by Jason Aden
parent 131c8ab6be
commit 1539cd8819
3 changed files with 81 additions and 27 deletions

View File

@ -56,13 +56,18 @@ describe('AppComponent', () => {
tocService = de.injector.get(TocService); tocService = de.injector.get(TocService);
}; };
describe('with proper DocViewer', () => { describe('with proper DocViewer', () => {
beforeEach(() => { beforeEach(() => {
DocViewerComponent.animationsEnabled = false;
createTestingModule('a/b'); createTestingModule('a/b');
initializeTest(); initializeTest();
}); });
afterEach(() => DocViewerComponent.animationsEnabled = true);
it('should create', () => { it('should create', () => {
expect(component).toBeDefined(); expect(component).toBeDefined();
}); });
@ -408,7 +413,6 @@ describe('AppComponent', () => {
}); });
describe('currentDocument', () => { describe('currentDocument', () => {
it('should display a guide page (guide/pipes)', () => { it('should display a guide page (guide/pipes)', () => {
locationService.go('guide/pipes'); locationService.go('guide/pipes');
fixture.detectChanges(); fixture.detectChanges();

View File

@ -573,37 +573,61 @@ describe('DocViewerComponent', () => {
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false); expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
}); });
it('should return an observable', done => { [true, false].forEach(animationsEnabled => {
docViewer.swapViews().subscribe(done, done.fail); describe(`(animationsEnabled: ${animationsEnabled})`, () => {
}); beforeEach(() => DocViewerComponent.animationsEnabled = animationsEnabled);
afterEach(() => DocViewerComponent.animationsEnabled = true);
it('should swap the views', async () => { it('should return an observable', done => {
await doSwapViews(); docViewer.swapViews().subscribe(done, done.fail);
});
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false); it('should swap the views', async () => {
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true); await doSwapViews();
expect(docViewer.currViewContainer).toBe(oldNextViewContainer);
expect(docViewer.nextViewContainer).toBe(oldCurrViewContainer);
await doSwapViews(); expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
expect(docViewer.currViewContainer).toBe(oldNextViewContainer);
expect(docViewer.nextViewContainer).toBe(oldCurrViewContainer);
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true); await doSwapViews();
expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
expect(docViewer.currViewContainer).toBe(oldCurrViewContainer);
expect(docViewer.nextViewContainer).toBe(oldNextViewContainer);
});
it('should empty the previous view', async () => { expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true);
await doSwapViews(); expect(docViewerEl.contains(oldNextViewContainer)).toBe(false);
expect(docViewer.currViewContainer).toBe(oldCurrViewContainer);
expect(docViewer.nextViewContainer).toBe(oldNextViewContainer);
});
expect(docViewer.currViewContainer.innerHTML).toBe('Next view'); it('should empty the previous view', async () => {
expect(docViewer.nextViewContainer.innerHTML).toBe(''); await doSwapViews();
docViewer.nextViewContainer.innerHTML = 'Next view 2'; expect(docViewer.currViewContainer.innerHTML).toBe('Next view');
await doSwapViews(); expect(docViewer.nextViewContainer.innerHTML).toBe('');
expect(docViewer.currViewContainer.innerHTML).toBe('Next view 2'); docViewer.nextViewContainer.innerHTML = 'Next view 2';
expect(docViewer.nextViewContainer.innerHTML).toBe(''); await doSwapViews();
expect(docViewer.currViewContainer.innerHTML).toBe('Next view 2');
expect(docViewer.nextViewContainer.innerHTML).toBe('');
});
if (animationsEnabled) {
// Without animations, the views are swapped synchronously,
// so there is no need (or way) to abort.
it('should abort swapping if the returned observable is unsubscribed from', async () => {
docViewer.swapViews().subscribe().unsubscribe();
await doSwapViews();
// Since the first call was cancelled, only one swapping should have taken place.
expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false);
expect(docViewerEl.contains(oldNextViewContainer)).toBe(true);
expect(docViewer.currViewContainer).toBe(oldNextViewContainer);
expect(docViewer.nextViewContainer).toBe(oldCurrViewContainer);
expect(docViewer.currViewContainer.innerHTML).toBe('Next view');
expect(docViewer.nextViewContainer.innerHTML).toBe('');
});
}
});
}); });
}); });
}); });

View File

@ -3,6 +3,7 @@ import { Title } from '@angular/platform-browser';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of'; import { of } from 'rxjs/observable/of';
import { timer } from 'rxjs/observable/timer';
import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/do'; import 'rxjs/add/operator/do';
import 'rxjs/add/operator/switchMap'; import 'rxjs/add/operator/switchMap';
@ -25,6 +26,8 @@ const initialDocViewerContent = initialDocViewerElement ? initialDocViewerElemen
// encapsulation: ViewEncapsulation.Native // encapsulation: ViewEncapsulation.Native
}) })
export class DocViewerComponent implements DoCheck, OnDestroy { export class DocViewerComponent implements DoCheck, OnDestroy {
// Enable/Disable view transition animations.
static animationsEnabled = true;
private hostElement: HTMLElement; private hostElement: HTMLElement;
@ -138,9 +141,32 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
* components.) * components.)
*/ */
protected swapViews(): Observable<void> { protected swapViews(): Observable<void> {
// Placeholders for actual animations. const raf$ = new Observable<void>(subscriber => {
const animateLeave = (elem: HTMLElement) => this.void$; const rafId = requestAnimationFrame(() => {
const animateEnter = (elem: HTMLElement) => this.void$; subscriber.next();
subscriber.complete();
});
return () => cancelAnimationFrame(rafId);
});
const animateProp =
(elem: HTMLElement, prop: string, from: string, to: string, duration = 333) => {
elem.style.transition = '';
return !DocViewerComponent.animationsEnabled
? this.void$.do(() => elem.style[prop] = to)
: this.void$
// In order to ensure that the `from` value will be applied immediately (i.e.
// without transition) and that the `to` value will be affected by the
// `transition` style, we need to ensure an animation frame has passed between
// setting each style.
.switchMap(() => raf$).do(() => elem.style[prop] = from)
.switchMap(() => raf$).do(() => elem.style.transition = `all ${duration}ms ease-in-out`)
.switchMap(() => raf$).do(() => elem.style[prop] = to)
.switchMap(() => timer(duration)).switchMap(() => this.void$);
};
const animateLeave = (elem: HTMLElement) => animateProp(elem, 'opacity', '1', '0.25');
const animateEnter = (elem: HTMLElement) => animateProp(elem, 'opacity', '0.25', '1');
let done$ = this.void$; let done$ = this.void$;