diff --git a/projects/ion/src/lib/accordion/accordion.component.html b/projects/ion/src/lib/accordion/accordion.component.html new file mode 100644 index 000000000..dbdef6184 --- /dev/null +++ b/projects/ion/src/lib/accordion/accordion.component.html @@ -0,0 +1,26 @@ +
+
+
+
+ {{ name }} +
+ +
+ + +
+
+ +
+
diff --git a/projects/ion/src/lib/accordion/accordion.component.scss b/projects/ion/src/lib/accordion/accordion.component.scss new file mode 100644 index 000000000..7f05c8af0 --- /dev/null +++ b/projects/ion/src/lib/accordion/accordion.component.scss @@ -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; + } + } +} diff --git a/projects/ion/src/lib/accordion/accordion.component.spec.ts b/projects/ion/src/lib/accordion/accordion.component.spec.ts new file mode 100644 index 000000000..b334bd1ad --- /dev/null +++ b/projects/ion/src/lib/accordion/accordion.component.spec.ts @@ -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: ` +

Context Main

+
`, +}) +class AccordionTestComponent { + name = 'Name'; +} + +@Component({ + template: ` +

Context Main

+
+ +
+ Custom template header +
`, +}) +class AccordionWithTemplateHeaderTestComponent {} + +@NgModule({ + declarations: [ + AccordionTestComponent, + AccordionWithTemplateHeaderTestComponent, + ], + imports: [IonAccordionModule, IonIconModule], +}) +class AccordionTestModule {} + +describe('IonAccordion', () => { + let accordionTestComponent!: AccordionTestComponent; + let fixture!: ComponentFixture; + 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; + 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 => { + 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' + ); + } + }); +}); diff --git a/projects/ion/src/lib/accordion/accordion.component.ts b/projects/ion/src/lib/accordion/accordion.component.ts new file mode 100644 index 000000000..54e061d92 --- /dev/null +++ b/projects/ion/src/lib/accordion/accordion.component.ts @@ -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; + } +} diff --git a/projects/ion/src/lib/accordion/accordion.module.ts b/projects/ion/src/lib/accordion/accordion.module.ts new file mode 100644 index 000000000..429540898 --- /dev/null +++ b/projects/ion/src/lib/accordion/accordion.module.ts @@ -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 {} diff --git a/projects/ion/src/lib/core/types/accordion.ts b/projects/ion/src/lib/core/types/accordion.ts new file mode 100644 index 000000000..bf0f7f87f --- /dev/null +++ b/projects/ion/src/lib/core/types/accordion.ts @@ -0,0 +1,9 @@ +import { TemplateRef } from '@angular/core'; + +export type TemplateHeader = TemplateRef | null; + +export interface IonAccordionProps { + name?: string; + templateHeader?: TemplateHeader; + show?: boolean; +} diff --git a/projects/ion/src/lib/core/types/index.ts b/projects/ion/src/lib/core/types/index.ts index cc881d269..6cd003f00 100644 --- a/projects/ion/src/lib/core/types/index.ts +++ b/projects/ion/src/lib/core/types/index.ts @@ -30,3 +30,4 @@ export * from './tab-group'; export * from './table'; export * from './tag'; export * from './tooltip'; +export * from './accordion'; diff --git a/projects/ion/src/public-api.ts b/projects/ion/src/public-api.ts index f7cd9a051..898c3f322 100644 --- a/projects/ion/src/public-api.ts +++ b/projects/ion/src/public-api.ts @@ -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'; diff --git a/stories/Accordion.stories.ts b/stories/Accordion.stories.ts new file mode 100644 index 000000000..ea2a28c6a --- /dev/null +++ b/stories/Accordion.stories.ts @@ -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: `

The main code should go here

`, + moduleMetadata: { + imports: [CommonModule, IonIconModule, IonAccordionModule], + }, +}); + +export const accordion = TemplateAccordionMainContent.bind({}); + +const TemplateAccordionCustomHeader: Story = (args) => ({ + props: args, + template: ` + +

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.

+
+ + +
+ OLT + +
+
+ `, + moduleMetadata: { + imports: [ + CommonModule, + IonIconModule, + IonAccordionModule, + IonTooltipModule, + ], + }, +}); + +export const AccordionCustomHeader = TemplateAccordionCustomHeader.bind({});