Skip to content

Commit

Permalink
✨ add new "Accordion" component with customizable Inputs (#851)
Browse files Browse the repository at this point in the history
* feat: collapse

* test: collpase

* docs: collpase storybook

* docs: collpase custom color

* docs: change exemple custom templete header

* docs: remove data

* refactor: collapse

* refactor: changing the component name to accordion

* Update projects/ion/src/lib/accordion/accordion.component.ts

Co-authored-by: Iury Nogueira <[email protected]>

* Update projects/ion/src/lib/accordion/accordion.component.spec.ts

Co-authored-by: Iury Nogueira <[email protected]>

* style: add cursor point

* refactor: adding error handling when the name and templateHeader properties do not exist

* fix: changing order of icons

---------

Co-authored-by: Alysson Mascarenhas <[email protected]>
Co-authored-by: Iury Nogueira <[email protected]>
  • Loading branch information
3 people authored Oct 6, 2023
1 parent be956f5 commit c79e112
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 0 deletions.
26 changes: 26 additions & 0 deletions projects/ion/src/lib/accordion/accordion.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<section
data-testid="ion-accordion"
[class.open]="show"
[class.close]="!show"
tabindex="0"
>
<header (click)="toggle()">
<div>
<div
*ngIf="!templateHeader; else headerCustom"
data-testid="ion-accordion__header-name"
>
{{ name }}
</div>
<ng-template
#headerCustom
[ngTemplateOutlet]="templateHeader"
></ng-template>
</div>
<ion-icon *ngIf="show" type="semi-up" [size]="iconSize"></ion-icon>
<ion-icon *ngIf="!show" type="semi-down" [size]="iconSize"></ion-icon>
</header>
<main *ngIf="show" data-testid="ion-accordion__main">
<ng-content></ng-content>
</main>
</section>
66 changes: 66 additions & 0 deletions projects/ion/src/lib/accordion/accordion.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
@import '../../styles/index.scss';

@mixin accordion-style($bgColor, $color) {
header {
color: $color;
border-bottom: 1px solid $color;
background-color: $bgColor;

ion-icon {
::ng-deep svg {
fill: $color;
}
}
}
}

section {
@include accordion-style($neutral-1, $neutral-7);
header,
main {
padding: 16px 20px;
}

header {
display: flex;
justify-content: space-between;
align-items: center;
min-height: 64px;
box-sizing: border-box;
cursor: pointer;

div {
div {
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
}
}

main {
background-color: $neutral-1;
}

&:hover {
@include accordion-style($neutral-2, $primary-3);
}

&:active {
@include accordion-style($primary-2, $primary-4);
}

&:focus-visible {
outline: 2px solid $primary-4;
}

&.open {
@include accordion-style($primary-1, $primary-4);
}

&.close {
header {
border-bottom: 1px solid $neutral-4;
}
}
}
132 changes: 132 additions & 0 deletions projects/ion/src/lib/accordion/accordion.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { screen, fireEvent, render } from '@testing-library/angular';
import { Component, NgModule } from '@angular/core';
import { IonAccordionModule } from './accordion.module';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { CommonModule } from '@angular/common';
import { IonIconModule } from '../icon/icon.module';
import { IonAccordionProps } from '../core/types';
import { IonAccordionComponent } from './accordion.component';

@Component({
template: `<ion-accordion [name]="name">
<p data-testID="ion-accordion__main-paragraph">Context Main</p>
</ion-accordion>`,
})
class AccordionTestComponent {
name = 'Name';
}

@Component({
template: `<ion-accordion [templateHeader]="customHeader">
<p data-testID="ion-accordion__main-paragraph">Context Main</p>
</ion-accordion>
<ng-template #customHeader>
<div data-testId="ion-accordion__header-custom">
Custom template header
<ion-icon type="zoom-in"></ion-icon></div
></ng-template>`,
})
class AccordionWithTemplateHeaderTestComponent {}

@NgModule({
declarations: [
AccordionTestComponent,
AccordionWithTemplateHeaderTestComponent,
],
imports: [IonAccordionModule, IonIconModule],
})
class AccordionTestModule {}

describe('IonAccordion', () => {
let accordionTestComponent!: AccordionTestComponent;
let fixture!: ComponentFixture<AccordionTestComponent>;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [AccordionTestModule],
}).compileComponents();
fixture = TestBed.createComponent(AccordionTestComponent);
accordionTestComponent = fixture.componentInstance;
fixture.detectChanges();
});

afterEach(async () => {
fixture.destroy();
});

it('should render ion-accordion', async () => {
expect(screen.getByTestId('ion-accordion')).toBeTruthy();
});

it('should render ion-accordion with name Brisanet', async () => {
const accordionName = 'Brisanet';
accordionTestComponent.name = accordionName;
fixture.detectChanges();
expect(screen.getByTestId('ion-accordion__header-name')).toHaveTextContent(
accordionName
);
});

it('should render main when clicking on header', async () => {
const header = screen.getByTestId('ion-accordion__header-name');
fireEvent.click(header);
fixture.detectChanges();
expect(screen.getByTestId('ion-accordion__main')).toBeTruthy();
expect(
screen.getByTestId('ion-accordion__main-paragraph')
).toHaveTextContent('Context Main');
});

it('should not render main when clicking on header twice', async () => {
const header = screen.getByTestId('ion-accordion__header-name');
fireEvent.click(header);
fireEvent.click(header);
fixture.detectChanges();
expect(screen.queryByTestId('ion-accordion__main')).not.toBeTruthy();
});
});

describe('IonAccordion - template header', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let accordionWithTemplateHeaderTestComponent!: AccordionWithTemplateHeaderTestComponent;
let fixture!: ComponentFixture<AccordionWithTemplateHeaderTestComponent>;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [CommonModule, AccordionTestModule],
}).compileComponents();
fixture = TestBed.createComponent(AccordionWithTemplateHeaderTestComponent);
accordionWithTemplateHeaderTestComponent = fixture.componentInstance;
fixture.detectChanges();
});

afterEach(async () => {
fixture.destroy();
});

it('should render template header', async () => {
const headerCustom = await screen.getByTestId(
'ion-accordion__header-custom'
);
expect(headerCustom).toBeTruthy();
expect(headerCustom).toHaveTextContent('Custom template header');
expect(document.getElementById('ion-icon-zoom-in')).toBeTruthy();
});
});

describe('IonAccordion - throw error', () => {
const sut = async (customProps?: IonAccordionProps): Promise<void> => {
await render(IonAccordionComponent, {
componentProperties: customProps,
imports: [CommonModule, IonIconModule],
});
};

it('should throw an error when name and templateHeader properties do not exist', async () => {
try {
await sut();
} catch (error) {
expect(error.message).toBe(
'The name or templateHeader properties were not set correctly'
);
}
});
});
27 changes: 27 additions & 0 deletions projects/ion/src/lib/accordion/accordion.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Component, Input, OnInit } from '@angular/core';
import { TemplateHeader } from '../core/types/accordion';

@Component({
selector: 'ion-accordion',
templateUrl: './accordion.component.html',
styleUrls: ['./accordion.component.scss'],
})
export class IonAccordionComponent implements OnInit {
@Input() name?: string;
@Input() templateHeader?: TemplateHeader;
@Input() show? = false;

iconSize = 24;

ngOnInit(): void {
if (!this.name && !this.templateHeader) {
throw new Error(
'The name or templateHeader properties were not set correctly'
);
}
}

toggle(): void {
this.show = !this.show;
}
}
11 changes: 11 additions & 0 deletions projects/ion/src/lib/accordion/accordion.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonAccordionComponent } from './accordion.component';
import { IonIconModule } from '../icon/icon.module';

@NgModule({
declarations: [IonAccordionComponent],
imports: [CommonModule, IonIconModule],
exports: [IonAccordionComponent],
})
export class IonAccordionModule {}
9 changes: 9 additions & 0 deletions projects/ion/src/lib/core/types/accordion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { TemplateRef } from '@angular/core';

export type TemplateHeader = TemplateRef<HTMLElement> | null;

export interface IonAccordionProps {
name?: string;
templateHeader?: TemplateHeader;
show?: boolean;
}
1 change: 1 addition & 0 deletions projects/ion/src/lib/core/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ export * from './tab-group';
export * from './table';
export * from './tag';
export * from './tooltip';
export * from './accordion';
1 change: 1 addition & 0 deletions projects/ion/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ export * from './lib/typography/';
export { default as debounce } from './lib/utils/debounce';
export { default as BnTable } from './core/bn-table/bn-table';
export * from './lib/table/utilsTable';
export * from './lib/accordion/accordion.module';
61 changes: 61 additions & 0 deletions stories/Accordion.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { CommonModule } from '@angular/common';
import { Meta, Story } from '@storybook/angular/types-6-0';
import { IonIconModule } from '../projects/ion/src/lib/icon/icon.module';
import { IonTooltipModule } from '../projects/ion/src/lib/tooltip/tooltip.module';
import { IonAccordionComponent } from '../projects/ion/src/lib/accordion/accordion.component';
import { IonAccordionModule } from '../projects/ion/src/lib/accordion/accordion.module';

export default {
title: 'Ion/Data Display/Accordion',
component: IonAccordionComponent,
} as Meta;

const TemplateAccordionMainContent: Story = (args) => ({
props: args,
template: `<ion-accordion name="Accordion name example"><p style="margin:0">The main code should go here</p></ion-accordion>`,
moduleMetadata: {
imports: [CommonModule, IonIconModule, IonAccordionModule],
},
});

export const accordion = TemplateAccordionMainContent.bind({});

const TemplateAccordionCustomHeader: Story = (args) => ({
props: args,
template: `
<ion-accordion
[templateHeader]="customHeader"
color="burlywood"
>
<p>Uma terminação de linha óptica, também chamada de terminal de linha óptica, é um dispositivo que serve como ponto final do provedor de serviços de uma rede óptica passiva. </p>
</ion-accordion>
<ng-template #customHeader>
<div style="display:flex; align-items:center; gap: 8px;">
<ion-icon type="olt"></ion-icon><b>OLT</b>
<ion-icon
type="information"
[size]=14
color="#6868ff"
ionTooltip
ionTooltipTitle="Terminação de Linha Óptica"
ionTooltipPosition="topCenter"
[ionTooltipArrowPointAtCenter]="true"
ionTooltipColorScheme="dark"
ionTooltipTrigger="hover"
ionTooltipShowDelay="0"
></ion-icon>
</div>
</ng-template>
`,
moduleMetadata: {
imports: [
CommonModule,
IonIconModule,
IonAccordionModule,
IonTooltipModule,
],
},
});

export const AccordionCustomHeader = TemplateAccordionCustomHeader.bind({});

0 comments on commit c79e112

Please sign in to comment.