Skip to content

Commit

Permalink
feat(accordion): add expand / collapse methods
Browse files Browse the repository at this point in the history
Closes #1970
Closes #1978

Closes #2595
  • Loading branch information
pkozlowski-opensource committed Aug 16, 2018
1 parent 3d8c08a commit e64f2ff
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 18 deletions.
182 changes: 178 additions & 4 deletions src/accordion/accordion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import {createGenericTestComponent} from '../test/common';

import {Component} from '@angular/core';

import {NgbAccordionModule} from './accordion.module';
import {NgbAccordionConfig} from './accordion-config';
import {NgbAccordion} from './accordion';
import {NgbAccordionModule, NgbPanelChangeEvent, NgbAccordionConfig, NgbAccordion} from './accordion.module';

const createTestComponent = (html: string) =>
createGenericTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
Expand Down Expand Up @@ -584,6 +582,181 @@ describe('ngb-accordion', () => {
expect(accordion.type).toBe(config.type);
});
});

describe('imperative API', () => {

function createTestImperativeAccordion(testHtml: string) {
const fixture = createTestComponent(testHtml);
const accordion = fixture.debugElement.query(By.directive(NgbAccordion)).componentInstance;
const nativeElement = fixture.nativeElement;
return {fixture, accordion, nativeElement};
}

it('should check if a panel with a given id is expanded', () => {
const testHtml = `
<ngb-accordion activeIds="first">
<ngb-panel id="first"></ngb-panel>
<ngb-panel id="second"></ngb-panel>
</ngb-accordion>`;

const {accordion, nativeElement} = createTestImperativeAccordion(testHtml);

expectOpenPanels(nativeElement, [true, false]);
expect(accordion.isExpanded('first')).toBe(true);
expect(accordion.isExpanded('second')).toBe(false);
});

it('should expanded and collapse individual panels', () => {
const testHtml = `
<ngb-accordion>
<ngb-panel id="first"></ngb-panel>
<ngb-panel id="second"></ngb-panel>
</ngb-accordion>`;

const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml);

expectOpenPanels(nativeElement, [false, false]);

accordion.expand('first');
fixture.detectChanges();
expectOpenPanels(nativeElement, [true, false]);

accordion.expand('second');
fixture.detectChanges();
expectOpenPanels(nativeElement, [true, true]);

accordion.collapse('second');
fixture.detectChanges();
expectOpenPanels(nativeElement, [true, false]);
});

it('should not expand / collapse if already expanded / collapsed', () => {
const testHtml = `
<ngb-accordion activeIds="first" (panelChange)="changeCallback()">
<ngb-panel id="first"></ngb-panel>
<ngb-panel id="second"></ngb-panel>
</ngb-accordion>`;

const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml);

expectOpenPanels(nativeElement, [true, false]);

spyOn(fixture.componentInstance, 'changeCallback');

accordion.expand('first');
fixture.detectChanges();
expectOpenPanels(nativeElement, [true, false]);

accordion.collapse('second');
fixture.detectChanges();
expectOpenPanels(nativeElement, [true, false]);

expect(fixture.componentInstance.changeCallback).not.toHaveBeenCalled();
});

it('should not expand disabled panels', () => {
const testHtml = `
<ngb-accordion (panelChange)="changeCallback()">
<ngb-panel id="first" [disabled]="true"></ngb-panel>
</ngb-accordion>`;

const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml);

expectOpenPanels(nativeElement, [false]);

spyOn(fixture.componentInstance, 'changeCallback');

accordion.expand('first');
fixture.detectChanges();
expectOpenPanels(nativeElement, [false]);
expect(fixture.componentInstance.changeCallback).not.toHaveBeenCalled();
});

it('should not expand / collapse when preventDefault called on the panelChange event', () => {
const testHtml = `
<ngb-accordion activeIds="first" (panelChange)="preventDefaultCallback($event)">
<ngb-panel id="first"></ngb-panel>
<ngb-panel id="second"></ngb-panel>
</ngb-accordion>`;

const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml);

expectOpenPanels(nativeElement, [true, false]);

accordion.collapse('first');
fixture.detectChanges();
expectOpenPanels(nativeElement, [true, false]);

accordion.expand('second');
fixture.detectChanges();
expectOpenPanels(nativeElement, [true, false]);
});

it('should expandAll when closeOthers is false', () => {

const testHtml = `
<ngb-accordion [closeOthers]="false">
<ngb-panel id="first"></ngb-panel>
<ngb-panel id="second"></ngb-panel>
</ngb-accordion>`;

const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml);

expectOpenPanels(nativeElement, [false, false]);

accordion.expandAll();
fixture.detectChanges();
expectOpenPanels(nativeElement, [true, true]);
});

it('should expand first panel when closeOthers is true and none of panels is expanded', () => {
const testHtml = `
<ngb-accordion [closeOthers]="true">
<ngb-panel id="first"></ngb-panel>
<ngb-panel id="second"></ngb-panel>
</ngb-accordion>`;

const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml);

expectOpenPanels(nativeElement, [false, false]);

accordion.expandAll();
fixture.detectChanges();
expectOpenPanels(nativeElement, [true, false]);
});

it('should do nothing if closeOthers is true and one panel is expanded', () => {
const testHtml = `
<ngb-accordion [closeOthers]="true" activeIds="second">
<ngb-panel id="first"></ngb-panel>
<ngb-panel id="second"></ngb-panel>
</ngb-accordion>`;

const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml);

expectOpenPanels(nativeElement, [false, true]);

accordion.expandAll();
fixture.detectChanges();
expectOpenPanels(nativeElement, [false, true]);
});

it('should collapse all panels', () => {
const testHtml = `
<ngb-accordion activeIds="second">
<ngb-panel id="first"></ngb-panel>
<ngb-panel id="second"></ngb-panel>
</ngb-accordion>`;

const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml);

expectOpenPanels(nativeElement, [false, true]);

accordion.collapseAll();
fixture.detectChanges();
expectOpenPanels(nativeElement, [false, false]);
});
});
});

@Component({selector: 'test-cmp', template: ''})
Expand All @@ -596,5 +769,6 @@ class TestComponent {
{id: 'two', disabled: false, title: 'Panel 2', content: 'bar', type: ''},
{id: 'three', disabled: false, title: 'Panel 3', content: 'baz', type: ''}
];
changeCallback = (event: any) => {};
changeCallback = (event: NgbPanelChangeEvent) => {};
preventDefaultCallback = (event: NgbPanelChangeEvent) => { event.preventDefault(); };
}
71 changes: 57 additions & 14 deletions src/accordion/accordion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,25 +168,48 @@ export class NgbAccordion implements AfterContentChecked {
}

/**
* Programmatically toggle a panel with a given id.
* Checks if a panel with a given id is expanded or not.
*/
toggle(panelId: string) {
const panel = this.panels.find(p => p.id === panelId);
isExpanded(panelId: string): boolean { return this.activeIds.indexOf(panelId) > -1; }

if (panel && !panel.disabled) {
let defaultPrevented = false;
/**
* Expands a panel with a given id. Has no effect if the panel is already expanded or disabled.
*/
expand(panelId: string): void { this._changeOpenState(this._findPanelById(panelId), true); }

this.panelChange.emit(
{panelId: panelId, nextState: !panel.isOpen, preventDefault: () => { defaultPrevented = true; }});
/**
* Expands all panels if [closeOthers]="false". For the [closeOthers]="true" case will have no effect if there is an
* open panel, otherwise the first panel will be expanded.
*/
expandAll(): void {
if (this.closeOtherPanels) {
if (this.activeIds.length === 0 && this.panels.length) {
this._changeOpenState(this.panels.first, true);
}
} else {
this.panels.forEach(panel => this._changeOpenState(panel, true));
}
}

if (!defaultPrevented) {
panel.isOpen = !panel.isOpen;
/**
* Collapses a panel with a given id. Has no effect if the panel is already collapsed or disabled.
*/
collapse(panelId: string) { this._changeOpenState(this._findPanelById(panelId), false); }

if (this.closeOtherPanels) {
this._closeOthers(panelId);
}
this._updateActiveIds();
}
/**
* Collapses all open panels.
*/
collapseAll() {
this.panels.forEach((panel) => { this._changeOpenState(panel, false); });
}

/**
* Programmatically toggle a panel with a given id. Has no effect if the panel is disabled.
*/
toggle(panelId: string) {
const panel = this._findPanelById(panelId);
if (panel) {
this._changeOpenState(panel, !panel.isOpen);
}
}

Expand All @@ -206,6 +229,24 @@ export class NgbAccordion implements AfterContentChecked {
}
}

private _changeOpenState(panel: NgbPanel, nextState: boolean) {
if (panel && !panel.disabled && panel.isOpen !== nextState) {
let defaultPrevented = false;

this.panelChange.emit(
{panelId: panel.id, nextState: nextState, preventDefault: () => { defaultPrevented = true; }});

if (!defaultPrevented) {
panel.isOpen = nextState;

if (nextState && this.closeOtherPanels) {
this._closeOthers(panel.id);
}
this._updateActiveIds();
}
}
}

private _closeOthers(panelId: string) {
this.panels.forEach(panel => {
if (panel.id !== panelId) {
Expand All @@ -214,6 +255,8 @@ export class NgbAccordion implements AfterContentChecked {
});
}

private _findPanelById(panelId: string): NgbPanel | null { return this.panels.find(p => p.id === panelId); }

private _updateActiveIds() {
this.activeIds = this.panels.filter(panel => panel.isOpen && !panel.disabled).map(panel => panel.id);
}
Expand Down

0 comments on commit e64f2ff

Please sign in to comment.