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:
parent
131c8ab6be
commit
1539cd8819
|
@ -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();
|
||||||
|
|
|
@ -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('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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$;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue