Skip to content

Commit

Permalink
feat(animations): always run the start function in ngbRunTransition (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
fbasso committed Jun 29, 2020
1 parent 0e2120e commit 99e7e8c
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 106 deletions.
239 changes: 155 additions & 84 deletions src/collapse/collapse.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {createGenericTestComponent, isBrowserVisible} from '../test/common';

import {Component} from '@angular/core';
Expand Down Expand Up @@ -38,6 +38,7 @@ describe('ngb-collapse', () => {

it('should toggle collapsed content based on bound model change', () => {
const fixture = createTestComponent(`<div [ngbCollapse]="collapsed">Some content</div>`);
fixture.detectChanges();

const tc = fixture.componentInstance;
const collapseEl = getCollapsibleContent(fixture.nativeElement);
Expand All @@ -54,8 +55,26 @@ describe('ngb-collapse', () => {

it('should allow toggling collapse from outside', () => {
const fixture = createTestComponent(`
<button (click)="collapse.collapsed = !collapse.collapsed">Collapse</button>
<div [ngbCollapse] #collapse="ngbCollapse"></div>`);
<button (click)="collapse.toggle()">Collapse</button>
<div [ngbCollapse]="collapsed" #collapse="ngbCollapse"></div>`);

const compiled = fixture.nativeElement;
const collapseEl = getCollapsibleContent(compiled);
const buttonEl = compiled.querySelector('button');

buttonEl.click();
fixture.detectChanges();
expect(collapseEl).not.toHaveCssClass('show');

buttonEl.click();
fixture.detectChanges();
expect(collapseEl).toHaveCssClass('show');
});

it('should work with no binding', () => {
const fixture = createTestComponent(`
<button (click)="collapse.toggle()">Collapse</button>
<div ngbCollapse #collapse="ngbCollapse"></div>`);

const compiled = fixture.nativeElement;
const collapseEl = getCollapsibleContent(compiled);
Expand All @@ -77,14 +96,17 @@ if (isBrowserVisible('ngb-collapse animations')) {
@Component({
template: `
<button (click)="c.toggle()">Collapse!</button>
<div [(ngbCollapse)]="collapsed" #c="ngbCollapse" (ngbCollapseChange)="onCollapse()"></div>
<div [(ngbCollapse)]="collapsed" #c="ngbCollapse" (ngbCollapseChange)="onCollapse()"
(shown)="onShown()" (hidden)="onHidden()"></div>
`,
host: {'[class.ngb-reduce-motion]': 'reduceMotion'}
})
class TestAnimationComponent {
collapsed = false;
reduceMotion = true;
onCollapse = () => {};
onShown = () => {};
onHidden = () => {};
}

beforeEach(() => {
Expand All @@ -95,87 +117,136 @@ if (isBrowserVisible('ngb-collapse animations')) {
});
});

[true, false].forEach(reduceMotion => {

it(`should run collapsing transition (force-reduced-motion = ${reduceMotion})`, async(() => {
const fixture = TestBed.createComponent(TestAnimationComponent);
fixture.componentInstance.reduceMotion = reduceMotion;
fixture.detectChanges();

const buttonEl = fixture.nativeElement.querySelector('button');
const content = getCollapsibleContent(fixture.nativeElement);

const onCollapseSpy = spyOn(fixture.componentInstance, 'onCollapse');

// First we're going to collapse, then expand
onCollapseSpy.and.callFake(() => {
if (fixture.componentInstance.collapsed) {
expect(content).toHaveClass('collapse');
expect(content).not.toHaveClass('show');
expect(content).not.toHaveClass('collapsing');

// Expanding
buttonEl.click();
} else {
expect(content).toHaveClass('collapse');
expect(content).toHaveClass('show');
expect(content).not.toHaveClass('collapsing');
}
});

expect(content).toHaveClass('collapse');
expect(content).toHaveClass('show');
expect(content).not.toHaveClass('collapsing');
expect(fixture.componentInstance.collapsed).toBe(false);

// Collapsing
buttonEl.click();
expect(content).not.toHaveClass('collapse');
expect(content).not.toHaveClass('show');
expect(content).toHaveClass('collapsing');
}));

it(`should run revert collapsing transition (force-reduced-motion = ${reduceMotion})`, async(() => {
const fixture = TestBed.createComponent(TestAnimationComponent);
fixture.componentInstance.reduceMotion = reduceMotion;
fixture.detectChanges();

const buttonEl = fixture.nativeElement.querySelector('button');
const content = getCollapsibleContent(fixture.nativeElement);

const onCollapseSpy = spyOn(fixture.componentInstance, 'onCollapse');

onCollapseSpy.and.callFake(() => {
expect(fixture.componentInstance.collapsed).toBe(false);
expect(content).toHaveClass('collapse');
expect(content).toHaveClass('show');
expect(content).not.toHaveClass('collapsing');
});

expect(content).toHaveClass('collapse');
expect(content).toHaveClass('show');
expect(content).not.toHaveClass('collapsing');
expect(fixture.componentInstance.collapsed).toBe(false);

// Collapsing
buttonEl.click();
expect(content).not.toHaveClass('collapse');
expect(content).not.toHaveClass('show');
expect(content).toHaveClass('collapsing');

// Expanding
buttonEl.click();
if (reduceMotion) {
expect(content).toHaveClass('collapse');
expect(content).toHaveClass('show');
expect(content).not.toHaveClass('collapsing');
} else {
expect(content).not.toHaveClass('collapse');
expect(content).not.toHaveClass('show');
expect(content).toHaveClass('collapsing');
}
}));
it(`should run collapsing transition (force-reduced-motion = false)`, (done) => {
const fixture = TestBed.createComponent(TestAnimationComponent);
fixture.componentInstance.reduceMotion = false;
fixture.detectChanges();

const buttonEl = fixture.nativeElement.querySelector('button');
const content = getCollapsibleContent(fixture.nativeElement);

const onCollapseSpy = spyOn(fixture.componentInstance, 'onCollapse');
const onShownSpy = spyOn(fixture.componentInstance, 'onShown');
const onHiddenSpy = spyOn(fixture.componentInstance, 'onHidden');

// First we're going to collapse, then expand
onHiddenSpy.and.callFake(() => {
expect(content).toHaveClass('collapse');
expect(content).not.toHaveClass('show');
expect(content).not.toHaveClass('collapsing');

// Expanding
buttonEl.click();
fixture.detectChanges();
expect(onShownSpy).not.toHaveBeenCalled();
expect(content).not.toHaveClass('collapse');
expect(content).not.toHaveClass('show');
expect(content).toHaveClass('collapsing');
});

onShownSpy.and.callFake(() => {
expect(onCollapseSpy).toHaveBeenCalledTimes(2);
expect(content).toHaveClass('collapse');
expect(content).toHaveClass('show');
expect(content).not.toHaveClass('collapsing');

done();
});

expect(content).toHaveClass('collapse');
expect(content).toHaveClass('show');
expect(content).not.toHaveClass('collapsing');
expect(fixture.componentInstance.collapsed).toBe(false);

// Collapsing
buttonEl.click();
fixture.detectChanges();
expect(onHiddenSpy).not.toHaveBeenCalled();
expect(onCollapseSpy).toHaveBeenCalledTimes(1);
expect(content).not.toHaveClass('collapse');
expect(content).not.toHaveClass('show');
expect(content).toHaveClass('collapsing');
});

it(`should run collapsing transition (force-reduced-motion = true)`, () => {
const fixture = TestBed.createComponent(TestAnimationComponent);
fixture.componentInstance.reduceMotion = true;
fixture.detectChanges();

const buttonEl = fixture.nativeElement.querySelector('button');
const content = getCollapsibleContent(fixture.nativeElement);

const onCollapseSpy = spyOn(fixture.componentInstance, 'onCollapse');
const onShownSpy = spyOn(fixture.componentInstance, 'onShown');
const onHiddenSpy = spyOn(fixture.componentInstance, 'onHidden');

expect(content).toHaveClass('collapse');
expect(content).toHaveClass('show');
expect(content).not.toHaveClass('collapsing');
expect(fixture.componentInstance.collapsed).toBe(false);

// Collapsing
buttonEl.click();
fixture.detectChanges();
expect(onHiddenSpy).toHaveBeenCalled();
expect(onCollapseSpy).toHaveBeenCalledTimes(1);
expect(content).toHaveClass('collapse');
expect(content).not.toHaveClass('show');
expect(content).not.toHaveClass('collapsing');

// Expanding
buttonEl.click();
fixture.detectChanges();
expect(onShownSpy).toHaveBeenCalled();
expect(onCollapseSpy).toHaveBeenCalledTimes(2);
expect(content).toHaveClass('collapse');
expect(content).toHaveClass('show');
expect(content).not.toHaveClass('collapsing');
});

it(`should run revert collapsing transition (force-reduced-motion = false)`, (done) => {
const fixture = TestBed.createComponent(TestAnimationComponent);
fixture.componentInstance.reduceMotion = false;
fixture.detectChanges();

const buttonEl = fixture.nativeElement.querySelector('button');
const content = getCollapsibleContent(fixture.nativeElement);

const onCollapseSpy = spyOn(fixture.componentInstance, 'onCollapse');
const onShownSpy = spyOn(fixture.componentInstance, 'onShown');
const onHiddenSpy = spyOn(fixture.componentInstance, 'onHidden');

onShownSpy.and.callFake(() => {
expect(onHiddenSpy).not.toHaveBeenCalled();
expect(fixture.componentInstance.collapsed).toBe(false);
expect(content).toHaveClass('collapse');
expect(content).toHaveClass('show');
expect(content).not.toHaveClass('collapsing');
done();
});

expect(content).toHaveClass('collapse');
expect(content).toHaveClass('show');
expect(content).not.toHaveClass('collapsing');
expect(fixture.componentInstance.collapsed).toBe(false);

// Collapsing
buttonEl.click();
fixture.detectChanges();
expect(onCollapseSpy).toHaveBeenCalledTimes(1);
expect(content).not.toHaveClass('collapse');
expect(content).not.toHaveClass('show');
expect(content).toHaveClass('collapsing');

// Expanding before hidden
buttonEl.click();
fixture.detectChanges();
expect(onCollapseSpy).toHaveBeenCalledTimes(2);
expect(content).not.toHaveClass('collapse');
expect(content).not.toHaveClass('show');
expect(content).toHaveClass('collapsing');
});

});
}

Expand Down
60 changes: 50 additions & 10 deletions src/collapse/collapse.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import {Directive, Input, ElementRef, Output, EventEmitter} from '@angular/core';
import {
Directive,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
} from '@angular/core';
import {ngbRunTransition} from '../util/transition/ngbTransition';
import {ngbCollapsingTransition} from '../util/transition/ngbCollapseTransition';
import {NgbCollapseConfig} from './collapse-config';

/**
* A directive to provide a simple way of hiding and showing elements on the page.
*/
@Directive({
selector: '[ngbCollapse]',
exportAs: 'ngbCollapse',
host: {'[class.collapse]': 'true', '[class.show]': '!collapsed'}
})
export class NgbCollapse {
@Directive({selector: '[ngbCollapse]', exportAs: 'ngbCollapse'})
export class NgbCollapse implements OnInit, OnChanges {
/**
* If `true`, collapse will be animated.
*
Expand All @@ -27,8 +32,30 @@ export class NgbCollapse {

@Output() ngbCollapseChange = new EventEmitter<boolean>();

/**
* An event emitted when the collapse element is shown, after the transition. It has no payload.
*/
@Output() shown = new EventEmitter<void>();

/**
* An event emitted when the collapse element is hidden, after the transition. It has no payload.
*/
@Output() hidden = new EventEmitter<void>();


constructor(private _element: ElementRef, config: NgbCollapseConfig) { this.animation = config.animation; }

ngOnInit() {
this._element.nativeElement.classList.add('collapse');
this._runTransition(this.collapsed, false, false);
}

ngOnChanges({collapsed}: SimpleChanges) {
if (!collapsed.firstChange) {
this._runTransition(this.collapsed, this.animation);
}
}

/**
* Triggers collapsing programmatically.
*
Expand All @@ -37,10 +64,23 @@ export class NgbCollapse {
*/
toggle(open: boolean = this.collapsed) {
this.collapsed = !open;
this.ngbCollapseChange.next(this.collapsed);
this._runTransition(this.collapsed, this.animation);
}

private _runTransition(collapsed: boolean, animation: boolean, emitEvent = true) {
ngbRunTransition(this._element.nativeElement, ngbCollapsingTransition, {
animation: this.animation,
animation,
runningTransition: 'stop',
context: {direction: open ? 'show' : 'hide'}
}).subscribe(() => this.ngbCollapseChange.next(this.collapsed));
context: {direction: collapsed ? 'hide' : 'show'}
}).subscribe(() => {
if (emitEvent) {
if (collapsed) {
this.hidden.emit();
} else {
this.shown.emit();
}
}
});
}
}

0 comments on commit 99e7e8c

Please sign in to comment.