Skip to content

Commit

Permalink
feat(animations): introduce 'endFn' returned by 'startFn'
Browse files Browse the repository at this point in the history
  • Loading branch information
maxokorokov committed May 18, 2020
1 parent 4efea05 commit 5ac913d
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 8 deletions.
13 changes: 13 additions & 0 deletions src/test/test-styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,16 @@
opacity: 0;
transition: opacity 1s linear;
}

.ngb-test-before {
opacity: 1;
}

.ngb-test-during {
opacity: 0;
transition: opacity 0.01s linear;
}

.ngb-test-after {
opacity: 0;
}
41 changes: 38 additions & 3 deletions src/util/transition/ngbTransition.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import {ngbRunTransition} from './ngbTransition';
import createSpy = jasmine.createSpy;
import {Component, ElementRef, ViewChild} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {isBrowserVisible} from '../../test/common';
import {isBrowser, isBrowserVisible} from '../../test/common';

/**
* This is sometimes necessary only for IE when it fails to recalculate styles synchronously
* after the 'transitionend' event was fired. To remove when not supporting IE anymore.
*/
function getComputedStyleAsync(element: HTMLElement, style: keyof CSSStyleDeclaration) {
return new Promise<string>(resolve => setTimeout(() => resolve(window.getComputedStyle(element)[style])));
function getComputedStyleAsync(element: HTMLElement, style: keyof CSSStyleDeclaration): Promise<string> {
const getStyle = () => window.getComputedStyle(element)[style];
return isBrowser('ie') ? new Promise<string>(resolve => setTimeout(() => resolve(getStyle()), 16)) :
Promise.resolve(getStyle());
}

function fadeFn({classList}: HTMLElement) {
Expand Down Expand Up @@ -167,6 +169,39 @@ if (isBrowserVisible('ngbRunTransition')) {
expect(window.getComputedStyle(element).opacity).toBe('1');
expect(element.classList.contains('ngb-test-long-duration')).toBe(true);
});

it(`should execute the end function if provided`, (done) => {
const startFn = ({classList}: HTMLElement) => {
classList.add('ngb-test-during');
return () => {
classList.remove('ngb-test-before');
classList.remove('ngb-test-during');
classList.add('ngb-test-after');
};
};

element.classList.add('ngb-test-before');

const nextSpy = createSpy();
const errorSpy = createSpy();

ngbRunTransition(element, startFn, {animation: true, runningTransition: 'continue'})
.subscribe(nextSpy, errorSpy, async() => {
expect(component.componentInstance.onTransitionEnd).toHaveBeenCalledTimes(1);
expect(nextSpy).toHaveBeenCalledWith(undefined);
expect(element.classList.contains('ngb-test-before')).toBe(false);
expect(element.classList.contains('ngb-test-during')).toBe(false);
expect(element.classList.contains('ngb-test-after')).toBe(true);
expect(await getComputedStyleAsync(element, 'opacity')).toBe('0');
expect(errorSpy).not.toHaveBeenCalled();
done();
});

expect(window.getComputedStyle(element).opacity).toBe('1');
expect(element.classList.contains('ngb-test-before')).toBe(true);
expect(element.classList.contains('ngb-test-during')).toBe(true);
expect(element.classList.contains('ngb-test-after')).toBe(false);
});
});
}

Expand Down
14 changes: 9 additions & 5 deletions src/util/transition/ngbTransition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import {takeUntil} from 'rxjs/operators';
import {getTransitionDurationMs} from './util';
import {environment} from '../../environment';

export type NgbTransitionStartFn = (element: HTMLElement) => void;
export type NgbTransitionStartFn = (element: HTMLElement) => NgbTransitionEndFn | void;
export type NgbTransitionEndFn = () => void;

export interface NgbTransitionOptions {
animation: boolean;
runningTransition: 'continue' | 'stop';
}

const noopFn: NgbTransitionEndFn = () => {};

const {transitionTimerDelayMs} = environment;
const runningTransitions = new Map<HTMLElement, Subject<any>>();

Expand All @@ -32,19 +35,19 @@ export const ngbRunTransition =
}

// If 'prefer-reduced-motion' is enabled, the 'transition' will be set to 'none'.
// In this case we have to call the start function, but can finish immediately by emitting a value
// and completing the observable.
// In this case we have to call the start function, but can finish immediately by emitting a value,
// completing the observable and executing both start and end functions synchronously.
const {transitionProperty} = window.getComputedStyle(element);
if (transitionProperty === 'none') {
startFn(element);
(startFn(element) || noopFn)();
return of(undefined);
}

// Starting a new transition
const transition$ = new Subject<any>();
runningTransitions.set(element, transition$);

startFn(element);
const endFn = startFn(element) || noopFn;

const transitionDurationMs = getTransitionDurationMs(element);

Expand All @@ -57,6 +60,7 @@ export const ngbRunTransition =

race(timer$, transitionEnd$).subscribe(() => {
runningTransitions.delete(element);
endFn();
transition$.next();
transition$.complete();
});
Expand Down

0 comments on commit 5ac913d

Please sign in to comment.