diff --git a/aio/src/app/app.component.spec.ts b/aio/src/app/app.component.spec.ts index 4245cd1aed..b5addb5858 100644 --- a/aio/src/app/app.component.spec.ts +++ b/aio/src/app/app.component.spec.ts @@ -56,13 +56,18 @@ describe('AppComponent', () => { tocService = de.injector.get(TocService); }; + describe('with proper DocViewer', () => { beforeEach(() => { + DocViewerComponent.animationsEnabled = false; + createTestingModule('a/b'); initializeTest(); }); + afterEach(() => DocViewerComponent.animationsEnabled = true); + it('should create', () => { expect(component).toBeDefined(); }); @@ -408,7 +413,6 @@ describe('AppComponent', () => { }); describe('currentDocument', () => { - it('should display a guide page (guide/pipes)', () => { locationService.go('guide/pipes'); fixture.detectChanges(); diff --git a/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts b/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts index 009299fff8..609d3b4334 100644 --- a/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts +++ b/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts @@ -573,37 +573,61 @@ describe('DocViewerComponent', () => { expect(docViewerEl.contains(oldNextViewContainer)).toBe(false); }); - it('should return an observable', done => { - docViewer.swapViews().subscribe(done, done.fail); - }); + [true, false].forEach(animationsEnabled => { + describe(`(animationsEnabled: ${animationsEnabled})`, () => { + beforeEach(() => DocViewerComponent.animationsEnabled = animationsEnabled); + afterEach(() => DocViewerComponent.animationsEnabled = true); - it('should swap the views', async () => { - await doSwapViews(); + it('should return an observable', done => { + docViewer.swapViews().subscribe(done, done.fail); + }); - expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false); - expect(docViewerEl.contains(oldNextViewContainer)).toBe(true); - expect(docViewer.currViewContainer).toBe(oldNextViewContainer); - expect(docViewer.nextViewContainer).toBe(oldCurrViewContainer); + it('should swap the views', async () => { + await doSwapViews(); - 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); - expect(docViewerEl.contains(oldNextViewContainer)).toBe(false); - expect(docViewer.currViewContainer).toBe(oldCurrViewContainer); - expect(docViewer.nextViewContainer).toBe(oldNextViewContainer); - }); + await doSwapViews(); - it('should empty the previous view', async () => { - await doSwapViews(); + expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true); + expect(docViewerEl.contains(oldNextViewContainer)).toBe(false); + expect(docViewer.currViewContainer).toBe(oldCurrViewContainer); + expect(docViewer.nextViewContainer).toBe(oldNextViewContainer); + }); - expect(docViewer.currViewContainer.innerHTML).toBe('Next view'); - expect(docViewer.nextViewContainer.innerHTML).toBe(''); + it('should empty the previous view', async () => { + await doSwapViews(); - docViewer.nextViewContainer.innerHTML = 'Next view 2'; - await doSwapViews(); + expect(docViewer.currViewContainer.innerHTML).toBe('Next view'); + expect(docViewer.nextViewContainer.innerHTML).toBe(''); - expect(docViewer.currViewContainer.innerHTML).toBe('Next view 2'); - expect(docViewer.nextViewContainer.innerHTML).toBe(''); + docViewer.nextViewContainer.innerHTML = 'Next view 2'; + 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(''); + }); + } + }); }); }); }); diff --git a/aio/src/app/layout/doc-viewer/doc-viewer.component.ts b/aio/src/app/layout/doc-viewer/doc-viewer.component.ts index 0130ebd2a6..40de01b11e 100644 --- a/aio/src/app/layout/doc-viewer/doc-viewer.component.ts +++ b/aio/src/app/layout/doc-viewer/doc-viewer.component.ts @@ -3,6 +3,7 @@ import { Title } from '@angular/platform-browser'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; +import { timer } from 'rxjs/observable/timer'; import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/switchMap'; @@ -25,6 +26,8 @@ const initialDocViewerContent = initialDocViewerElement ? initialDocViewerElemen // encapsulation: ViewEncapsulation.Native }) export class DocViewerComponent implements DoCheck, OnDestroy { + // Enable/Disable view transition animations. + static animationsEnabled = true; private hostElement: HTMLElement; @@ -138,9 +141,32 @@ export class DocViewerComponent implements DoCheck, OnDestroy { * components.) */ protected swapViews(): Observable { - // Placeholders for actual animations. - const animateLeave = (elem: HTMLElement) => this.void$; - const animateEnter = (elem: HTMLElement) => this.void$; + const raf$ = new Observable(subscriber => { + const rafId = requestAnimationFrame(() => { + 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$;