Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions lib/components/Accordion/base/__tests__/accordion-base.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { render, screen, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Base from "../index";
import { StandaloneAndroidIcon } from "@deriv/quill-icons/Standalone";
import H2 from "@components/Typography/heading/h2";

const title = "Accordion Title";
const subtitle = "Accordion Subtitle";
const content = () => <H2>Content</H2>;
const icon = <StandaloneAndroidIcon />;

describe("Accordion - Base", () => {
beforeEach(() => {
render(
<Base
title={title}
subtitle={subtitle}
content={content}
icon={icon}
/>,
);
});

it("should render title correctly", () => {
expect(screen.getByText(title)).toBeInTheDocument();
});

it("should render subtitle correctly", () => {
expect(screen.getByText(subtitle)).toBeInTheDocument();
});

it("should render content correctly", () => {
const contentElement = screen.getByRole("heading", {
name: "Content",
level: 2,
});
expect(contentElement).toBeInTheDocument();
});

it("should render icon correctly", () => {
expect(screen.getByTestId("accordion-icon")).toBeInTheDocument();
});

it("should be in a collapsed state initially", () => {
expect(screen.getByTestId("expanded-content"));
});

it("should expand the accordion on click", async () => {
const toggleButton = screen.getByTestId("toggle-expand");

await act(async () => {
await userEvent.click(toggleButton);
});

expect(screen.getByTestId("expanded-content")).toBeInTheDocument();
});
});
145 changes: 145 additions & 0 deletions lib/components/Accordion/base/accordion-base.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
.quill-accordion {
&-container {
overflow: hidden;

&-divider {
&-both {
border: var(--core-borderWidth-100) solid var(--semantic-color-monochrome-border-normal-mid);

}
&-bottom {
border-block: var(--core-borderWidth-100) solid var(--semantic-color-monochrome-border-normal-mid);

}
&-none {
border: none;
}
}
&-disabled {
pointer-events: none;
fill: var(--component-accordion-icon-disabled);
}
&-expanded-color {
background-color: var(--component-accordion-bg-expand);
}
}
&-base {

display: flex;
cursor: pointer;
align-items: center;
justify-content: space-between;
gap: var(--component-accordion-spacing-lg);
padding: var(--component-accordion-spacing-lg);
border-color: var(--semantic-color-monochrome-border-normal-mid);

&:hover {
background-color: var(--component-accordion-bg-hover);
}

&:active {
background-color: var(--component-accordion-bg-active);
}

&-disabled {
pointer-events: none;
}
}

&-icon {

&-disabled {
fill: var(--component-accordion-icon-disabled);
}
}

&-header {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
gap: var(--component-accordion-spacing-xs);

&-title {
&-color {
color: var(--component-textIcon-normal-prominent);
}

&-sm {
font-size: var(--semantic-typography-body-md-regular-default);
}
&-md {
font-size: var(--semantic-typography-body-lg-regular-default);
}
&-lg {
font-size: var(--semantic-typography-body-xl-regular-default);
}

&-disabled {
color: var(--component-textIcon-normal-disabled);
}
}

&-subtitle {
&-color {
color: var(--component-textIcon-normal-subtle);
}

&-sm {
font-size: var(--semantic-typography-body-sm-regular-default);
}
&-md {
font-size: var(--semantic-typography-body-md-regular-default);
}
&-lg {
font-size: var(--semantic-typography-body-lg-regular-default);
}

&-disabled {
color: var(--component-textIcon-normal-disabled);
}
}
}
&-icon-rotate {
transition-property: transform;
transition-timing-function: var(--core-motion-ease-400);
transition-duration: var(--core-motion-duration-200);
transform: rotate(0);

&[data-state="open"] {
transform: rotate(180deg);
}
}
&-disabled {
background-color: var(--component-textIcon-normal-disabled);
}
&-transition {
cursor: pointer;
transform-origin: top;
transform: scaleY(0);
transition: transform var(--core-motion-duration-200)
var(--core-motion-ease-400);

&--open {
opacity: 1;
transform: scaleY(1);
}

&--close {
opacity: 0;
transform: scaleY(0);
}

}
&-expanded {
&-visible {
max-height: 0px;
}
&-content {
display: flex;
height: fit-content;
padding: var(--component-accordion-spacing-lg);
}
}

}
181 changes: 181 additions & 0 deletions lib/components/Accordion/base/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { useCallback, useEffect, useRef, useState } from "react";
import clsx from "clsx";
import { Text } from "@components/Typography";
import { StandaloneChevronDownRegularIcon } from "@deriv/quill-icons/Standalone";
import { AccordionProps } from "../types";
import "./accordion-base.scss";

export const Base = ({
id,
className,
title = "",
subtitle,
content: Content,
expanded = false,
textSize = "md",
icon,
divider = "none",
disabled = false,
customContent: CustomContent,
contentClassname,
onExpand,
}: AccordionProps) => {
const [isExpanded, setExpanded] = useState(expanded);
const [isAutoExpand, setAutoExpand] = useState(false);

const accordionElement = useRef<HTMLDivElement>(null);

const toggleCollapse = useCallback(() => {
setExpanded((current) => !current);
setAutoExpand(false);
scrollToExpanded(500);

if (onExpand) {
onExpand(!isExpanded, title);
}
}, [isExpanded, onExpand, title]);

const handleKeyUp = useCallback(
(e: KeyboardEvent) => {
if (e.code === "Enter" || e.key === "Enter") {
if (accordionElement.current === document.activeElement) {
toggleCollapse();
}
}
},
[toggleCollapse],
);

const scrollToExpanded = (delay = 1000) => {
const accElement = accordionElement.current;

if (accElement) {
setTimeout(() => {
accElement.scrollIntoView({
block: "center",
behavior: "smooth",
});
}, delay);
}
};

useEffect(() => {
const hashWithoutSymbol =
typeof window !== "undefined" && window.location.hash.slice(1);

if (hashWithoutSymbol === id) {
setAutoExpand(true);
scrollToExpanded();
}
}, [id]);

// Key handlers
useEffect(() => {
document.addEventListener("keyup", handleKeyUp);
return () => {
document.removeEventListener("keyup", handleKeyUp);
};
}, [handleKeyUp]);

useEffect(() => {
setExpanded(expanded);
}, [expanded]);

return (
<div
data-id={id}
ref={accordionElement}
tabIndex={0}
className={clsx(
"quill-accordion-container",
`quill-accordion-container-divider-${divider}`,
disabled && `quill-accordion-container-${disabled}`,
className,
isExpanded && "quill-accordion-container-expanded-color",
)}
>
<div
className={clsx(
"quill-accordion-base",
disabled && "quill-accordion-base-disabled",
contentClassname,
)}
onClick={() => toggleCollapse()}
data-testid="toggle-expand"
>
{CustomContent ? (
<CustomContent />
) : (
<>
{icon && (
<div
className={clsx(
disabled && "quill-accordion-icon-disabled",
)}
data-testid="accordion-icon"
>
{icon}
</div>
)}
<div className={"quill-accordion-header"}>
<Text
className={clsx(
"quill-accordion-base-header-title-color",
`quill-accordion-base-header-title-${textSize}`,
disabled &&
"quill-accordion-header-title-disabled",
)}
>
{title}
</Text>
{subtitle && (
<Text
className={clsx(
"quill-accordion-header-subtitle-color",
`quill-accordion-base-header-subtitle-${textSize}`,
disabled &&
"quill-accordion-header-subtitle-disabled",
)}
>
{subtitle}
</Text>
)}
</div>
</>
)}
<div
className="quill-accordion-icon-rotate"
data-state={isExpanded || isAutoExpand ? "open" : "close"}
data-testid="chevron"
>
<StandaloneChevronDownRegularIcon
iconSize="sm"
fill={
disabled
? "var(--component-accordion-icon-disabled)"
: "var(--component-textIcon-normal-prominent)"
}
/>
</div>
</div>

<div
className={clsx(
disabled && "quill-accordion-disabled",
isAutoExpand || isExpanded
? "quill-accordion-expanded"
: "quill-accordion-expanded-visible",
)}
data-testid="expanded-content"
>
<div className={clsx("quill-accordion-expanded-content")}>
{Content && <Content />}
</div>
</div>
</div>
);
};

Base.displayName = "AccordionBase";

export default Base;
Loading