Skip to content

Commit

Permalink
feat: accordion component (#818)
Browse files Browse the repository at this point in the history
Closes #739

---------

Co-authored-by: Levent Anil Ozen <[email protected]>
Co-authored-by: Enes Yıldırım <[email protected]>
Co-authored-by: Erbil <[email protected]>
  • Loading branch information
4 people authored Apr 25, 2024
1 parent af8bef0 commit c21191d
Show file tree
Hide file tree
Showing 10 changed files with 676 additions and 0 deletions.
2 changes: 2 additions & 0 deletions commitlint.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ module.exports = {
"deps",
"deps-dev",
// Components as scopes listed below
"accordion",
"accordion-group",
"button",
"icon",
"input",
Expand Down
2 changes: 2 additions & 0 deletions src/baklava.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { default as BlAccordionGroup } from "./components/accordion-group/bl-accordion-group";
export { default as BlAccordion } from "./components/accordion-group/accordion/bl-accordion";
export { default as BlAlert } from "./components/alert/bl-alert";
export { default as BlBadge } from "./components/badge/bl-badge";
export { default as BlButton } from "./components/button/bl-button";
Expand Down
81 changes: 81 additions & 0 deletions src/components/accordion-group/accordion/bl-accordion.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
:host {
display: block;
}

.accordion {
--border: 1px solid var(--bl-color-neutral-lighter);
--default-radius: var(--bl-size-2xs);
--radius-top-left: var(--bl-accordion-radius-top-left, var(--default-radius));
--radius-top-right: var(--bl-accordion-radius-top-right, var(--default-radius));
--radius-bottom-right: var(--bl-accordion-radius-bottom-right, var(--default-radius));
--radius-bottom-left: var(--bl-accordion-radius-bottom-left, var(--default-radius));

width: 100%;
}

.summary {
list-style: none;
user-select: none;
cursor: pointer;
font: var(--bl-font-title-3-medium);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--bl-size-2xs);
padding: var(--bl-size-m);
background: var(--bl-color-neutral-full);
color: var(--bl-color-neutral-darker);
border: var(--border);
border-bottom: var(--bl-accordion-border-bottom, var(--border));
border-radius: var(--radius-top-left) var(--radius-top-right) var(--radius-bottom-right)
var(--radius-bottom-left);
transition: background-color 200ms;
}

.summary::-webkit-details-marker {
display: none;
}

.summary:hover {
background: var(--bl-color-neutral-lightest);
}

.summary:focus-visible {
outline: 2px solid var(--bl-color-primary);
outline-offset: -1px;
}

.indicator {
transition: transform 200ms;
}

.accordion[open] .indicator {
transform: rotate(180deg);
}

.accordion[open] .summary {
border-bottom: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}

.caption {
flex: 1;
}

.accordion-content {
padding: var(--bl-size-m);
background: var(--bl-color-neutral-full);
border: var(--border);
border-top: 0;
border-bottom: var(--bl-accordion-border-bottom, var(--border));
border-bottom-left-radius: var(--radius-bottom-left);
border-bottom-right-radius: var(--radius-bottom-right);
font: var(--bl-font-body-text-2-regular);
}

.disabled .summary {
cursor: not-allowed;
background: var(--bl-color-neutral-lightest);
color: var(--bl-color-neutral-light);
}
92 changes: 92 additions & 0 deletions src/components/accordion-group/accordion/bl-accordion.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { ArgsTable, Canvas, Meta, Story } from "@storybook/addon-docs";
import { html } from "lit";
import { ifDefined } from 'lit/directives/if-defined.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';

<Meta
title="Components/Accordion/Accordion"
component="bl-accordion"
argTypes={{
icon: { control: "text" },
disabled: { control: "boolean" },
caption: {
control: 'text'
},
}}
/>

export const AccordionTemplate = (args) => html`
<bl-accordion icon="${ifDefined(args.icon)}" caption="${ifDefined(args.caption)}"
disabled="${ifDefined(args.disabled)}">${unsafeHTML(args.content)}
</bl-accordion>`;

# Accordion

<bl-badge icon="document">[ADR](https://github.com/Trendyol/baklava/issues/739)</bl-badge>
<bl-badge
icon="puzzle">[Figma](https://www.figma.com/file/RrcLH0mWpIUy4vwuTlDeKN/Baklava-Design-Guide?type=design&node-id=15548%3A20910)</bl-badge>

Accordion is a component that allows the user to show and hide sections of related content on a page.

### Usage


* The accordion component includes a free content area.
* The caption can be set either via **attribute** or **slot**.
* An icon can be added to the beginning of the caption.
* Accordion can be disabled.
* The accordion group component combines accordions and only allows one accordion to be open by default

<Canvas>
<Story name="Basic Usage" args={{
caption: "Toggle Me",
content: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Adipisci architecto debitis earum fuga iusto modi molestias necessitatibus provident quam! Nisi!"
}}>
{AccordionTemplate.bind({})}
</Story>
</Canvas>


## With Icon

Add icon to beginning of caption with `icon` attribute.

<Canvas>
<Story name="With Icon" args={{
caption: "Toggle Me",
content: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Adipisci architecto debitis earum fuga iusto modi molestias necessitatibus provident quam! Nisi!",
icon: "info"
}}>
{AccordionTemplate.bind({})}
</Story>
</Canvas>

## Disabled

Use the `disable` attribute to prevent the details from expanding.

<Canvas>
<Story name="Disabled" args={{
caption: "Toggle Me",
content: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Adipisci architecto debitis earum fuga iusto modi molestias necessitatibus provident quam! Nisi!",
disabled: true
}}>
{AccordionTemplate.bind({})}
</Story>
</Canvas>

## Reference

<ArgsTable of="bl-accordion" />

## Public Functions

* `expand()`: Can be used to expand accordion.
* `collapse()`: Can be used to collapse accordion.

Example usage;

```js
document.querySelector('bl-accordion').expand();
document.querySelector('bl-accordion').collapse();
```
184 changes: 184 additions & 0 deletions src/components/accordion-group/accordion/bl-accordion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { assert, fixture, html, expect, waitUntil, aTimeout } from "@open-wc/testing";
import { spy } from "sinon";
import BlAccordion from "./bl-accordion";

describe("bl-accordion", () => {
it("is defined", () => {
const el = document.createElement("bl-accordion");

assert.instanceOf(el, BlAccordion);
});

it("renders with default values", async () => {
const el = await fixture<BlAccordion>(html`
<bl-accordion></bl-accordion>`);

assert.shadowDom.equal(
el,
`<details class="accordion">
<summary aria-controls="content" aria-disabled="false" aria-expanded="false" class="summary" tabindex="0">
<slot name="caption">
<span class="caption">
</span>
</slot>
<bl-icon
class="indicator"
name="arrow_down"
>
</bl-icon>
</summary>
<div aria-labelledby="header" class="accordion-content" id="content" role="region">
<slot>
</slot>
</div>
</details>`
);
});

it("should set icon", async () => {
const el = await fixture<BlAccordion>(html`<bl-accordion icon="eye_on"></bl-accordion>`);

await el.updateComplete;
const iconEl = el.shadowRoot!.querySelector(".icon")!;

expect(iconEl.getAttribute("name")).to.eq("eye_on");
});

it("should set info icon when icon is boolean", async () => {
const el = await fixture<BlAccordion>(html`<bl-accordion icon></bl-accordion>`);

const iconEl = el.shadowRoot!.querySelector(".icon")!;

expect(iconEl.getAttribute("name")).to.eq("info");
});

it("should set caption", async () => {
const captionText = "Best caption";
const el = await fixture<BlAccordion>(html`<bl-accordion caption="${captionText}"></bl-accordion>`);
const captionEl = el.shadowRoot?.querySelector<HTMLSpanElement>(".caption");

expect(captionEl).to.exist;
expect(captionEl!.innerText).to.eq(captionText);
});

it("should set caption via slot", async () => {
const captionText = "Best caption";
const el = await fixture<BlAccordion>(html`<bl-accordion><div slot="caption">${captionText}</div></bl-accordion>`);
const captionSlot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name="caption"]');
const captionSlotContent = captionSlot!.assignedNodes()[0] as HTMLDivElement;

expect(captionSlotContent.innerText).to.eq(captionText);
});

it("should be visible with the open attribute", async () => {
const el = await fixture<BlAccordion>(html`
<bl-accordion open>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Molestie at elementum eu facilisis. Morbi quis commodo odio aenean sed adipiscing diam
donec.
</bl-accordion>`);

const body = el.shadowRoot!.querySelector(".accordion-content")!;

expect(body.clientHeight).greaterThan(0);
});

it("should not be visible without the open attribute", async () => {
const el = await fixture<BlAccordion>(html`
<bl-accordion>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Molestie at elementum eu facilisis. Morbi quis commodo odio aenean sed adipiscing diam
donec.
</bl-accordion>`);

const body = el.shadowRoot!.querySelector(".accordion-content")!;

expect(body.clientHeight).to.eq(0);
});

it("should emit bl-toggle when click on collapsed accordion", async () => {
const el = await fixture<BlAccordion>(html`
<bl-accordion>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Molestie at elementum eu facilisis. Morbi quis commodo odio aenean sed adipiscing diam
donec.
</bl-accordion>`);

const toggleHandler = spy();

el.addEventListener("bl-toggle", (e) => toggleHandler((e as CustomEvent).detail));

const summary = el.shadowRoot!.querySelector("summary")!;

summary.click();

await waitUntil(() => toggleHandler.calledOnce);

expect(toggleHandler).to.have.been.calledWith(true);
});

it("should emit bl-toggle when click on expanded accordion", async () => {
const el = await fixture<BlAccordion>(html`
<bl-accordion open>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Molestie at elementum eu facilisis. Morbi quis commodo odio aenean sed adipiscing diam
donec.
</bl-accordion>`);

const toggleHandler = spy();

el.addEventListener("bl-toggle", (e) => toggleHandler((e as CustomEvent).detail));

const summary = el.shadowRoot!.querySelector("summary")!;

summary.click();

await waitUntil(() => toggleHandler.calledOnce);

expect(toggleHandler).to.have.been.calledWith(false);
});

it("should not open when disabled", async () => {
const el = await fixture<BlAccordion>(html`
<bl-accordion disabled>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Molestie at elementum eu facilisis. Morbi quis commodo odio aenean sed adipiscing diam
donec.
</bl-accordion>`);

const summary = el.shadowRoot!.querySelector("summary")!;

summary.click();

expect(el.open).to.eq(false);
});

it("should not change open attribute when disabled", async () => {
const el = await fixture<BlAccordion>(html`
<bl-accordion disabled open>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Molestie at elementum eu facilisis. Morbi quis commodo odio aenean sed adipiscing diam
donec.
</bl-accordion>`);

expect(el.open).to.eq(false);
});

it("should not affect animation", async () => {
const el = await fixture<BlAccordion>(html`
<bl-accordion>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Molestie at elementum eu facilisis. Morbi quis commodo odio aenean sed adipiscing diam
donec.
</bl-accordion>`);

const summary = el.shadowRoot!.querySelector("summary")!;

summary.click();
summary.click();
summary.click();

await aTimeout(200);
expect(el.open).to.eq(true);
});
});
Loading

0 comments on commit c21191d

Please sign in to comment.