From f8fe53aeb09a3649459792a3bcf7436770a342ec Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Tue, 19 Dec 2017 01:53:38 +0200 Subject: [PATCH] feat(aio): support disabling `DocViewer` animations via class --- .../doc-viewer/doc-viewer.component.spec.ts | 240 ++++++++++-------- .../layout/doc-viewer/doc-viewer.component.ts | 8 +- 2 files changed, 137 insertions(+), 111 deletions(-) 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 cf9d50bed9..e894e325f3 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 @@ -13,7 +13,7 @@ import { TestDocViewerComponent, TestModule, TestParentComponent } from 'testing/doc-viewer-utils'; import { MockLogger } from 'testing/logger.service'; -import { DocViewerComponent } from './doc-viewer.component'; +import { DocViewerComponent, NO_ANIMATIONS } from './doc-viewer.component'; describe('DocViewerComponent', () => { @@ -656,122 +656,142 @@ describe('DocViewerComponent', () => { beforeEach(() => DocViewerComponent.animationsEnabled = animationsEnabled); afterEach(() => DocViewerComponent.animationsEnabled = true); - it('should return an observable', done => { - docViewer.swapViews().subscribe(done, done.fail); - }); + [true, false].forEach(noAnimations => { + describe(`(.${NO_ANIMATIONS}: ${noAnimations})`, () => { + beforeEach(() => docViewerEl.classList[noAnimations ? 'add' : 'remove'](NO_ANIMATIONS)); - 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 emit `docRemoved` after removing the leaving view', async () => { - const onDocRemovedSpy = jasmine.createSpy('onDocRemoved').and.callFake(() => { - expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false); - expect(docViewerEl.contains(oldNextViewContainer)).toBe(false); + expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true); + expect(docViewerEl.contains(oldNextViewContainer)).toBe(false); + expect(docViewer.currViewContainer).toBe(oldCurrViewContainer); + expect(docViewer.nextViewContainer).toBe(oldNextViewContainer); + }); + + it('should emit `docRemoved` after removing the leaving view', async () => { + const onDocRemovedSpy = jasmine.createSpy('onDocRemoved').and.callFake(() => { + expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false); + expect(docViewerEl.contains(oldNextViewContainer)).toBe(false); + }); + + docViewer.docRemoved.subscribe(onDocRemovedSpy); + + expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true); + expect(docViewerEl.contains(oldNextViewContainer)).toBe(false); + + await doSwapViews(); + + expect(onDocRemovedSpy).toHaveBeenCalledTimes(1); + expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false); + expect(docViewerEl.contains(oldNextViewContainer)).toBe(true); + }); + + it('should not emit `docRemoved` if the leaving view is already removed', async () => { + const onDocRemovedSpy = jasmine.createSpy('onDocRemoved'); + + docViewer.docRemoved.subscribe(onDocRemovedSpy); + docViewerEl.removeChild(oldCurrViewContainer); + + await doSwapViews(); + + expect(onDocRemovedSpy).not.toHaveBeenCalled(); + }); + + it('should emit `docInserted` after inserting the entering view', async () => { + const onDocInsertedSpy = jasmine.createSpy('onDocInserted').and.callFake(() => { + expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false); + expect(docViewerEl.contains(oldNextViewContainer)).toBe(true); + }); + + docViewer.docInserted.subscribe(onDocInsertedSpy); + + expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true); + expect(docViewerEl.contains(oldNextViewContainer)).toBe(false); + + await doSwapViews(); + + expect(onDocInsertedSpy).toHaveBeenCalledTimes(1); + expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false); + expect(docViewerEl.contains(oldNextViewContainer)).toBe(true); + }); + + it('should call the callback after inserting the entering view', async () => { + const onInsertedCb = jasmine.createSpy('onInsertedCb').and.callFake(() => { + expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false); + expect(docViewerEl.contains(oldNextViewContainer)).toBe(true); + }); + const onDocInsertedSpy = jasmine.createSpy('onDocInserted'); + + docViewer.docInserted.subscribe(onDocInsertedSpy); + + expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true); + expect(docViewerEl.contains(oldNextViewContainer)).toBe(false); + + await doSwapViews(onInsertedCb); + + expect(onInsertedCb).toHaveBeenCalledTimes(1); + expect(onInsertedCb).toHaveBeenCalledBefore(onDocInsertedSpy); + expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false); + expect(docViewerEl.contains(oldNextViewContainer)).toBe(true); + }); + + it('should empty the previous view', async () => { + await doSwapViews(); + + expect(docViewer.currViewContainer.innerHTML).toBe('Next view'); + 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 && !noAnimations) { + // Only test this when there are animations. 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(''); + }); + } else { + it('should swap views synchronously when animations are disabled', () => { + const cbSpy = jasmine.createSpy('cb'); + + docViewer.swapViews(cbSpy).subscribe(); + + expect(cbSpy).toHaveBeenCalledTimes(1); + 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(''); + }); + } }); - - docViewer.docRemoved.subscribe(onDocRemovedSpy); - - expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true); - expect(docViewerEl.contains(oldNextViewContainer)).toBe(false); - - await doSwapViews(); - - expect(onDocRemovedSpy).toHaveBeenCalledTimes(1); - expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false); - expect(docViewerEl.contains(oldNextViewContainer)).toBe(true); }); - - it('should not emit `docRemoved` if the leaving view is already removed', async () => { - const onDocRemovedSpy = jasmine.createSpy('onDocRemoved'); - - docViewer.docRemoved.subscribe(onDocRemovedSpy); - docViewerEl.removeChild(oldCurrViewContainer); - - await doSwapViews(); - - expect(onDocRemovedSpy).not.toHaveBeenCalled(); - }); - - it('should emit `docInserted` after inserting the entering view', async () => { - const onDocInsertedSpy = jasmine.createSpy('onDocInserted').and.callFake(() => { - expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false); - expect(docViewerEl.contains(oldNextViewContainer)).toBe(true); - }); - - docViewer.docInserted.subscribe(onDocInsertedSpy); - - expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true); - expect(docViewerEl.contains(oldNextViewContainer)).toBe(false); - - await doSwapViews(); - - expect(onDocInsertedSpy).toHaveBeenCalledTimes(1); - expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false); - expect(docViewerEl.contains(oldNextViewContainer)).toBe(true); - }); - - it('should call the callback after inserting the entering view', async () => { - const onInsertedCb = jasmine.createSpy('onInsertedCb').and.callFake(() => { - expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false); - expect(docViewerEl.contains(oldNextViewContainer)).toBe(true); - }); - const onDocInsertedSpy = jasmine.createSpy('onDocInserted'); - - docViewer.docInserted.subscribe(onDocInsertedSpy); - - expect(docViewerEl.contains(oldCurrViewContainer)).toBe(true); - expect(docViewerEl.contains(oldNextViewContainer)).toBe(false); - - await doSwapViews(onInsertedCb); - - expect(onInsertedCb).toHaveBeenCalledTimes(1); - expect(onInsertedCb).toHaveBeenCalledBefore(onDocInsertedSpy); - expect(docViewerEl.contains(oldCurrViewContainer)).toBe(false); - expect(docViewerEl.contains(oldNextViewContainer)).toBe(true); - }); - - it('should empty the previous view', async () => { - await doSwapViews(); - - expect(docViewer.currViewContainer.innerHTML).toBe('Next view'); - 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 9f587a02b3..cdb1809a98 100644 --- a/aio/src/app/layout/doc-viewer/doc-viewer.component.ts +++ b/aio/src/app/layout/doc-viewer/doc-viewer.component.ts @@ -15,6 +15,9 @@ import { Logger } from 'app/shared/logger.service'; import { TocService } from 'app/shared/toc.service'; +// Constants +export const NO_ANIMATIONS = 'no-animations'; + // Initialization prevents flicker once pre-rendering is on const initialDocViewerElement = document.querySelector('aio-doc-viewer'); const initialDocViewerContent = initialDocViewerElement ? initialDocViewerElement.innerHTML : ''; @@ -186,8 +189,11 @@ export class DocViewerComponent implements DoCheck, OnDestroy { }; const animateProp = (elem: HTMLElement, prop: string, from: string, to: string, duration = 333) => { + const animationsDisabled = !DocViewerComponent.animationsEnabled + || this.hostElement.classList.contains(NO_ANIMATIONS); + elem.style.transition = ''; - return !DocViewerComponent.animationsEnabled + return animationsDisabled ? this.void$.do(() => elem.style[prop] = to) : this.void$ // In order to ensure that the `from` value will be applied immediately (i.e.