Skip to content

Commit

Permalink
feat(animations): support 'runningTransaction: stop'
Browse files Browse the repository at this point in the history
  • Loading branch information
maxokorokov committed May 18, 2020
1 parent 5ac913d commit 2373de3
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 16 deletions.
92 changes: 84 additions & 8 deletions src/util/transition/ngbTransition.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,35 +90,54 @@ if (isBrowserVisible('ngbRunTransition')) {
expect(completeSpy).toHaveBeenCalled();
});

it(`should complete new transition if one is already running with 'runningTransition: continue'`, (done) => {
element.classList.add('ngb-test-fade');
it(`should complete new transition and continue one already running with 'runningTransition: continue'`, (done) => {
let startCalls = 0;
let endCalls = 0;
const startFn = ({classList}: HTMLElement) => {
startCalls++;
classList.add('ngb-test-during');
return () => {
endCalls++;
classList.remove('ngb-test-during');
classList.add('ngb-test-after');
};
};

// first
// starting first
const nextSpy1 = createSpy();
const errorSpy1 = createSpy();

ngbRunTransition(element, fadeFn, {animation: true, runningTransition: 'continue'})
ngbRunTransition(element, startFn, {animation: true, runningTransition: 'continue'})
.subscribe(nextSpy1, errorSpy1, async() => {
expect(startCalls).toBe(1);
expect(endCalls).toBe(1);
expect(component.componentInstance.onTransitionEnd).toHaveBeenCalledTimes(1);
expect(nextSpy1).toHaveBeenCalledWith(undefined);
expect(element.classList.contains('ngb-test-show')).toBe(false);
expect(errorSpy1).not.toHaveBeenCalled();
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');
done();
});

// first transition is on-going, start function was called
expect(nextSpy1).not.toHaveBeenCalled();
expect(element.classList.contains('ngb-test-during')).toBe(true);
expect(element.classList.contains('ngb-test-after')).toBe(false);
expect(window.getComputedStyle(element).opacity).toBe('1');

// second
// starting second
const nextSpy2 = createSpy();
const errorSpy2 = createSpy();
const completeSpy2 = createSpy();

ngbRunTransition(element, fadeFn, {animation: true, runningTransition: 'continue'})
ngbRunTransition(element, startFn, {animation: true, runningTransition: 'continue'})
.subscribe(nextSpy2, errorSpy2, completeSpy2);

// first transition is on-going
// first transition is still on-going
expect(nextSpy1).not.toHaveBeenCalled();
expect(element.classList.contains('ngb-test-during')).toBe(true);
expect(element.classList.contains('ngb-test-after')).toBe(false);
expect(window.getComputedStyle(element).opacity).toBe('1');

// second transition was completed and no value was emitted
Expand All @@ -127,6 +146,63 @@ if (isBrowserVisible('ngbRunTransition')) {
expect(completeSpy2).toHaveBeenCalled();
});

it(`should run new transition and stop one already running with 'runningTransition: stop'`, (done) => {
let startCalls = 0;
let endCalls = 0;
const startFn = ({classList}: HTMLElement) => {
startCalls++;
classList.add('ngb-test-during');
return () => {
endCalls++;
classList.remove('ngb-test-during');
classList.add('ngb-test-after');
};
};

// starting first
const nextSpy1 = createSpy();
const errorSpy1 = createSpy();
const completeSpy1 = createSpy();

ngbRunTransition(element, startFn, {animation: true, runningTransition: 'stop'})
.subscribe(nextSpy1, errorSpy1, completeSpy1);

// first transition is on-going, start function was called
expect(nextSpy1).not.toHaveBeenCalled();
expect(completeSpy1).not.toHaveBeenCalled();
expect(element.classList.contains('ngb-test-during')).toBe(true);
expect(element.classList.contains('ngb-test-after')).toBe(false);
expect(window.getComputedStyle(element).opacity).toBe('1');

// starting second
const nextSpy2 = createSpy();
const errorSpy2 = createSpy();

ngbRunTransition(element, startFn, {animation: true, runningTransition: 'stop'})
.subscribe(nextSpy2, errorSpy2, async() => {
expect(startCalls).toBe(2);
expect(endCalls).toBe(1);
expect(component.componentInstance.onTransitionEnd).toHaveBeenCalledTimes(1);
expect(nextSpy2).toHaveBeenCalledWith(undefined);
expect(errorSpy2).not.toHaveBeenCalled();
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');
done();
});

// second transition should have started
expect(nextSpy2).not.toHaveBeenCalled();
expect(element.classList.contains('ngb-test-during')).toBe(true);
expect(element.classList.contains('ngb-test-after')).toBe(false);
expect(window.getComputedStyle(element).opacity).toBe('1');

// first transition was completed and no value was emitted
expect(nextSpy1).not.toHaveBeenCalled();
expect(errorSpy1).not.toHaveBeenCalled();
expect(completeSpy1).toHaveBeenCalled();
});

it(`should complete and release the DOM element even if transition end is not fired`, (done) => {
element.classList.add('ngb-test-fade');

Expand Down
23 changes: 15 additions & 8 deletions src/util/transition/ngbTransition.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {EMPTY, fromEvent, Observable, of, race, Subject, timer} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
import {endWith, takeUntil} from 'rxjs/operators';
import {getTransitionDurationMs} from './util';
import {environment} from '../../environment';

Expand Down Expand Up @@ -27,10 +27,16 @@ export const ngbRunTransition =
// Checking if there are already running transitions on the given element.
const runningTransition$ = runningTransitions.get(element);
if (runningTransition$) {
// If there is one running and we want for it to continue to run, we have to cancel the current one.
// We're not emitting any values, but simply completing the observable (EMPTY).
if (options.runningTransition === 'continue') {
return EMPTY;
switch (options.runningTransition) {
// If there is one running and we want for it to 'continue' to run, we have to cancel the new one.
// We're not emitting any values, but simply completing the observable (EMPTY).
case 'continue':
return EMPTY;
// If there is one running and we want for it to 'stop', we have to complete the running one.
// We're simply completing the running one and not emitting any values.
case 'stop':
runningTransition$.complete();
runningTransitions.delete(element);
}
}

Expand All @@ -45,6 +51,7 @@ export const ngbRunTransition =

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

const endFn = startFn(element) || noopFn;
Expand All @@ -55,10 +62,10 @@ export const ngbRunTransition =
// because 'transitionend' event might not be fired in some browsers, if the transitioning
// element becomes invisible (ex. when scrolling, making browser tab inactive, etc.). The timer
// guarantees, that we'll release the DOM element and complete 'ngbRunTransition'.
const transitionEnd$ = fromEvent(element, 'transitionend').pipe(takeUntil(transition$));
const timer$ = timer(transitionDurationMs + transitionTimerDelayMs).pipe(takeUntil(transition$));
const transitionEnd$ = fromEvent(element, 'transitionend').pipe(takeUntil(stop$));
const timer$ = timer(transitionDurationMs + transitionTimerDelayMs).pipe(takeUntil(stop$));

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

0 comments on commit 2373de3

Please sign in to comment.