Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(accordion): introduce NgbAccordionButton directive #4478

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
(hidden)="logEvent($event)"
>
<h2 ngbAccordionHeader>
<button ngbAccordionToggle>Simple</button>
<button ngbAccordionButton>Simple</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
Expand All @@ -31,7 +31,7 @@ <h2 ngbAccordionHeader>
</div>
<div ngbAccordionItem>
<h2 ngbAccordionHeader>
<button ngbAccordionToggle>
<button ngbAccordionButton>
<span>&#9733; <b>Fancy</b> title &#9733;</span>
</button>
</h2>
Expand All @@ -50,7 +50,7 @@ <h2 ngbAccordionHeader>
</div>
<div ngbAccordionItem [disabled]="true">
<h2 ngbAccordionHeader>
<button ngbAccordionToggle>Disabled</button>
<button ngbAccordionButton>Disabled</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
Expand Down
155 changes: 106 additions & 49 deletions src/accordion/accordion.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ describe('ngb-accordion directive', () => {
(shown) = "itemShownCallback($event)"
(hidden) = "itemHiddenCallback($event)"
>
<div ngbAccordionHeader>
<button ngbAccordionToggle>{{item.header}}</button>
</div>
<h2 ngbAccordionHeader>
<button ngbAccordionButton>{{item.header}}</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody><ng-template>{{item.body}}</ng-template></div>
</div>
Expand Down Expand Up @@ -201,8 +201,8 @@ describe('ngb-accordion directive', () => {
const fixture = createTestComponent(
`<div ngbAccordion>
<div ngbAccordionItem>
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
</div>`,
);
Expand Down Expand Up @@ -233,6 +233,61 @@ describe('ngb-accordion directive', () => {
});
});

it(`should allow customizing headers with 'NgbAccordionToggle'`, () => {
const fixture = createTestComponent(
`<div ngbAccordion>
<div [ngbAccordionItem]="'custom-' + index" *ngFor="let item of items; let index = index;">
<div ngbAccordionHeader class='accordion-button'>
<button type='button' ngbAccordionToggle>{{item.header}}</button>
</div>
<div ngbAccordionCollapse>
<div ngbAccordionBody><ng-template>{{item.body}}</ng-template></div>
</div>
</div>
</div>`,
);

fixture.detectChanges();

const el = fixture.nativeElement;
expectOpenPanels(el, [false, false, false]);

// open second
getButton(el, 1).click();
fixture.detectChanges();
expectOpenPanels(el, [false, true, false]);

// close second
getButton(el, 1).click();
fixture.detectChanges();
expectOpenPanels(el, [false, false, false]);
});

it(`should no allow clicking on disabled headers with 'NgbAcdordionToggle'`, () => {
const fixture = createTestComponent(
`<div ngbAccordion>
<div ngbAccordionItem [disabled]="true">
<div ngbAccordionHeader class="accordion-button">
<button type='button' ngbAccordionToggle>Toggle</button>
</div>
<div ngbAccordionCollapse>
<div ngbAccordionBody><ng-template>Body</ng-template></div>
</div>
</div>
</div>`,
);

fixture.detectChanges();

const el = fixture.nativeElement;
expectOpenPanels(el, [false]);

// user click should not open the panel
getButton(el, 0).click();
fixture.detectChanges();
expectOpenPanels(el, [false]);
});

it('should remove body content from DOM only for the first collapse', () => {
const fixture = TestBed.createComponent(TestComponent);
const originalItems = fixture.componentInstance.items;
Expand All @@ -250,9 +305,9 @@ describe('ngb-accordion directive', () => {
const fixture = createTestComponent(
`<div ngbAccordion>
<div [ngbAccordionItem]="'custom-' + index" *ngFor="let item of items; let index = index;">
<div ngbAccordionHeader>
<button ngbAccordionToggle>{{item.header}}</button>
</div>
<h2 ngbAccordionHeader>
<button ngbAccordionButton>{{item.header}}</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody><ng-template>{{item.body}}</ng-template></div>
</div>
Expand All @@ -266,9 +321,9 @@ describe('ngb-accordion directive', () => {
const fixture = createTestComponent(
`<div ngbAccordion>
<div ngbAccordionItem *ngFor="let item of items">
<div ngbAccordionHeader>
<button ngbAccordionToggle>{{item.header}}</button>
</div>
<h2 ngbAccordionHeader>
<button ngbAccordionButton>{{item.header}}</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody><ng-template>{{item.body}}</ng-template></div>
</div>
Expand Down Expand Up @@ -383,12 +438,12 @@ describe('ngb-accordion directive', () => {
const html = `
<div ngbAccordion>
<div ngbAccordionItem [collapsed]="false">
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
<div ngbAccordionItem>
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
</div>`;
const { accordionDirective, nativeElement } = createTestImperativeAccordion(html);
Expand All @@ -397,16 +452,17 @@ describe('ngb-accordion directive', () => {
expect(accordionDirective.isExpanded(ids[0])).toBe(true);
expect(accordionDirective.isExpanded(ids[1])).toBe(false);
});

it('should expanded and collapse individual panels', () => {
const html = `
<div ngbAccordion>
<div ngbAccordionItem>
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
<div ngbAccordionItem>
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
</div>
`;
Expand All @@ -427,16 +483,17 @@ describe('ngb-accordion directive', () => {
fixture.detectChanges();
expectOpenPanels(nativeElement, [true, false]);
});

it('should not expand / collapse if already expanded / collapsed', () => {
const testHtml = `
<div ngbAccordion (hidden)="hiddenCallback($event)" (shown)="shownCallback($event)">
<div ngbAccordionItem [collapsed]="false">
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
<div ngbAccordionItem>
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
</div>`;

Expand All @@ -462,12 +519,12 @@ describe('ngb-accordion directive', () => {
const testHtml = `
<div ngbAccordion [closeOthers]="true">
<div ngbAccordionItem>
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
<div ngbAccordionItem [collapsed]="false">
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
</div>`;
const { accordionDirective, nativeElement, fixture } = createTestImperativeAccordion(testHtml);
Expand All @@ -482,8 +539,8 @@ describe('ngb-accordion directive', () => {
const testHtml = `
<div ngbAccordion (shown)="shownCallback($event)">
<div ngbAccordionItem [disabled]="true">
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
</div>`;
const { accordionDirective, nativeElement, fixture } = createTestImperativeAccordion(testHtml);
Expand All @@ -503,12 +560,12 @@ describe('ngb-accordion directive', () => {
const testHtml = `
<div ngbAccordion [closeOthers]="false">
<div ngbAccordionItem>
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
<div ngbAccordionItem>
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
</div>`;

Expand All @@ -525,12 +582,12 @@ describe('ngb-accordion directive', () => {
const testHtml = `
<div ngbAccordion [closeOthers]="true">
<div ngbAccordionItem>
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
<div ngbAccordionItem>
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
</div>`;

Expand All @@ -547,12 +604,12 @@ describe('ngb-accordion directive', () => {
const testHtml = `
<div ngbAccordion [closeOthers]="true">
<div ngbAccordionItem>
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
<div ngbAccordionItem [collapsed]="false">
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
</div>`;

Expand All @@ -569,12 +626,12 @@ describe('ngb-accordion directive', () => {
const testHtml = `
<div ngbAccordion>
<div ngbAccordionItem>
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse> <div ngbAccordionBody></div></div>
</div>
<div ngbAccordionItem [collapsed]="false">
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse> <div ngbAccordionBody></div></div>
</div>
</div>`;

Expand Down Expand Up @@ -634,12 +691,12 @@ if (isBrowserVisible('ngb-accordion-directive animations')) {
template: `
<div ngbAccordion (shown)="onShown($event)" (hidden)="onHidden($event)">
<div ngbAccordionItem [collapsed]="false" (shown)="onCollapseShown()" (hidden)="onCollapseHidden()">
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
<div ngbAccordionItem>
<div ngbAccordionHeader> <button ngbAccordionToggle>Toggle</button></div>
<div ngbAccordionCollapse> <div ngbAccordionBody></div> </div>
<h2 ngbAccordionHeader><button ngbAccordionButton>Toggle</button></h2>
<div ngbAccordionCollapse><div ngbAccordionBody></div></div>
</div>
</div>
`,
Expand Down
35 changes: 30 additions & 5 deletions src/accordion/accordion.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ let nextId = 0;
standalone: true,
imports: [NgTemplateOutlet],
host: { '[class.accordion-body]': 'true' },
template: ` <ng-template [ngTemplateOutlet]="template()" ]></ng-template> `,
template: ` <ng-template [ngTemplateOutlet]="template()"></ng-template> `,
})
export class NgbAccordionBody {
constructor(@Inject(forwardRef(() => NgbAccordionItem)) private _item: NgbAccordionItem) {}
Expand Down Expand Up @@ -61,18 +61,21 @@ export class NgbAccordionCollapse {
) {}
}

/**
* A directive to put on a toggling element inside the accordion header.
* It will register click handlers that toggle the associated panel and will handle accessibility attributes.
*
* This directive is used internally by the `NgbAccordionButton` directive.
*/
@Directive({
selector: '[ngbAccordionToggle]',
standalone: true,
host: {
'[id]': 'item.toggleId',
'[disabled]': 'item.disabled',
'[class.accordion-button]': 'true',
'[class.collapsed]': 'item.collapsed',
'[attr.aria-controls]': 'item.collapseId',
'[attr.aria-expanded]': '!item.collapsed',
type: 'button',
'(click)': '!item.disable ? accordion.toggle(item.id): undefined',
'(click)': '!item.disabled && accordion.toggle(item.id)',
},
})
export class NgbAccordionToggle {
Expand All @@ -82,6 +85,28 @@ export class NgbAccordionToggle {
) {}
}

/**
* A directive to put on a button element inside the accordion header.
* If you want a custom markup for the header, you can also use the `NgbAccordionToggle` directive.
*/
@Directive({
selector: 'button[ngbAccordionButton]',
standalone: true,
host: {
'[disabled]': 'item.disabled',
'[class.accordion-button]': 'true',
type: 'button',
},
hostDirectives: [
{
directive: NgbAccordionToggle,
},
],
})
export class NgbAccordionButton {
constructor(@Inject(forwardRef(() => NgbAccordionItem)) public item: NgbAccordionItem) {}
}

@Directive({
selector: '[ngbAccordionHeader]',
standalone: true,
Expand Down