@purpurds/accordion 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ ._purpur-accordion__title_1ower_1{margin-bottom:var(--purpur-spacing-300);color:var(--purpur-color-text-default)}@media screen and (min-width: 600px){._purpur-accordion__title_1ower_1{margin-bottom:var(--purpur-spacing-400)}}._purpur-accordion--negative_1ower_10{background-color:var(--purpur-color-purple-900)}._purpur-accordion--negative_1ower_10 ._purpur-accordion__title_1ower_1{color:var(--purpur-color-text-default-negative)}._purpur-accordion-item_8k8nf_1{overflow:hidden;border-bottom:var(--purpur-border-width-xs) solid var(--purpur-color-border-weak)}._purpur-accordion-item_8k8nf_1:nth-of-type(1){border-top:var(--purpur-border-width-xs) solid var(--purpur-color-border-weak)}._purpur-accordion-item__header_8k8nf_8{display:flex}._purpur-accordion-item__trigger_8k8nf_11{border:none;padding:0;width:100%;font-family:inherit;background-color:transparent;transition:background-color var(--purpur-motion-duration-150) ease;display:flex;align-items:center;justify-content:space-between;padding:var(--purpur-spacing-200) var(--purpur-spacing-200) var(--purpur-spacing-200) 0;cursor:pointer;outline:none}@media screen and (min-width: 600px){._purpur-accordion-item__trigger_8k8nf_11{padding:var(--purpur-spacing-300) var(--purpur-spacing-300) var(--purpur-spacing-300) 0}}@media screen and (min-width: 1024px){._purpur-accordion-item__trigger_8k8nf_11{padding:var(--purpur-spacing-400) var(--purpur-spacing-300) var(--purpur-spacing-400) 0}}._purpur-accordion-item__content_8k8nf_35{overflow:hidden}._purpur-accordion-item__content_8k8nf_35[data-state=open]{animation:_slideDown_8k8nf_1 var(--purpur-motion-duration-150) ease-in-out}._purpur-accordion-item__content_8k8nf_35[data-state=closed]{animation:_slideUp_8k8nf_1 var(--purpur-motion-duration-150) ease-in-out}._purpur-accordion-item__contentText_8k8nf_44{padding:var(--purpur-spacing-200) 0;max-width:37.5rem}._purpur-accordion-item__icon_8k8nf_48{height:var(--purpur-spacing-300);width:var(--purpur-spacing-300);transition:transform var(--purpur-motion-duration-150) ease-in-out}._purpur-accordion-item__trigger_8k8nf_11[data-state=open]>._purpur-accordion-item__icon_8k8nf_48{transform:rotate(180deg)}._purpur-accordion-item_8k8nf_1:has(:focus-visible){outline:none;box-shadow:0 0 0 var(--purpur-border-width-sm) var(--purpur-color-border-interactive-focus)}._purpur-accordion-item__title_8k8nf_60,._purpur-accordion-item__icon_8k8nf_48{color:var(--purpur-color-text-interactive-primary)}._purpur-accordion-item__trigger_8k8nf_11:hover{background-color:var(--purpur-color-background-interactive-transparent-hover)}._purpur-accordion-item__trigger_8k8nf_11:hover__title,._purpur-accordion-item__trigger_8k8nf_11:hover__icon{color:var(--purpur-color-text-interactive-primary-hover)}._purpur-accordion-item__trigger_8k8nf_11:active{background-color:var(--purpur-color-background-interactive-transparent-active)}._purpur-accordion-item__trigger_8k8nf_11:active__title,._purpur-accordion-item__trigger_8k8nf_11:active__icon{color:var(--purpur-color-text-interactive-primary-active)}._purpur-accordion-item__contentText_8k8nf_44 p{color:var(--purpur-color-text-default)}._purpur-accordion-item--negative_8k8nf_78{border-bottom:var(--purpur-border-width-xs) solid var(--purpur-color-border-weak-negative)}._purpur-accordion-item--negative_8k8nf_78:nth-of-type(1){border-top:var(--purpur-border-width-xs) solid var(--purpur-color-border-weak-negative)}._purpur-accordion-item--negative_8k8nf_78 ._purpur-accordion-item__title_8k8nf_60,._purpur-accordion-item--negative_8k8nf_78 ._purpur-accordion-item__icon_8k8nf_48{color:var(--purpur-color-text-interactive-primary-negative)}._purpur-accordion-item--negative_8k8nf_78 ._purpur-accordion-item__trigger_8k8nf_11:hover{background-color:var(--purpur-color-background-interactive-transparent-negative-hover)}._purpur-accordion-item--negative_8k8nf_78 ._purpur-accordion-item__trigger_8k8nf_11:hover ._purpur-accordion-item__title_8k8nf_60,._purpur-accordion-item--negative_8k8nf_78 ._purpur-accordion-item__trigger_8k8nf_11:hover ._purpur-accordion-item__icon_8k8nf_48{color:var(--purpur-color-border-interactive-primary-negative-hover)}._purpur-accordion-item--negative_8k8nf_78 ._purpur-accordion-item__trigger_8k8nf_11:active{background-color:var(--purpur-color-background-interactive-transparent-negative-active)}._purpur-accordion-item--negative_8k8nf_78 ._purpur-accordion-item__trigger_8k8nf_11:active ._purpur-accordion-item__title_8k8nf_60,._purpur-accordion-item--negative_8k8nf_78 ._purpur-accordion-item__trigger_8k8nf_11:active ._purpur-accordion-item__icon_8k8nf_48{color:var(--purpur-color-border-interactive-primary-negative-active)}._purpur-accordion-item--negative_8k8nf_78 ._purpur-accordion-item__contentText_8k8nf_44 p{color:var(--purpur-color-text-default-negative)}@keyframes _slideDown_8k8nf_1{0%{height:0}to{height:var(--radix-accordion-content-height)}}@keyframes _slideUp_8k8nf_1{0%{height:var(--radix-accordion-content-height)}to{height:0}}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@purpurds/accordion",
3
+ "version": "0.0.1",
4
+ "license": "AGPL-3.0-only",
5
+ "main": "./dist/accordion.cjs.js",
6
+ "types": "./dist/accordion.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./dist/accordion.cjs.js",
10
+ "types": "./dist/accordion.d.ts",
11
+ "default": "./dist/accordion.es.js"
12
+ },
13
+ "./styles": "./dist/styles.css"
14
+ },
15
+ "source": "src/accordion.tsx",
16
+ "dependencies": {
17
+ "classnames": "~2.5.0",
18
+ "@radix-ui/react-accordion": "~1.1.2",
19
+ "@purpurds/tokens": "5.0.0",
20
+ "@purpurds/icon": "5.0.0",
21
+ "@purpurds/heading": "5.0.0",
22
+ "@purpurds/paragraph": "5.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@rushstack/eslint-patch": "~1.10.0",
26
+ "@storybook/blocks": "~7.6.0",
27
+ "@storybook/react": "~7.6.0",
28
+ "@telia/base-rig": "~8.2.0",
29
+ "@telia/react-rig": "~3.2.0",
30
+ "@testing-library/dom": "~9.3.3",
31
+ "@testing-library/jest-dom": "~6.4.0",
32
+ "@testing-library/react": "~14.3.0",
33
+ "@types/node": "18",
34
+ "@types/react-dom": "~18.3.0",
35
+ "@types/react": "~18.3.0",
36
+ "eslint-plugin-testing-library": "~6.2.0",
37
+ "eslint": "~8.57.0",
38
+ "jsdom": "~22.1.0",
39
+ "lint-staged": "~10.5.3",
40
+ "prettier": "~2.8.8",
41
+ "react-dom": "~18.3.0",
42
+ "react": "~18.3.0",
43
+ "typescript": "~5.4.2",
44
+ "vite": "~5.2.2",
45
+ "vitest": "~1.5.0",
46
+ "@purpurds/component-rig": "1.0.0"
47
+ },
48
+ "scripts": {
49
+ "build:dev": "vite",
50
+ "build:watch": "vite build --watch",
51
+ "build": "vite build",
52
+ "ci:build": "rushx build",
53
+ "coverage": "vitest run --coverage",
54
+ "lint:fix": "eslint . --fix",
55
+ "lint": "lint-staged --no-stash 2>&1",
56
+ "sbdev": "rush sbdev",
57
+ "test:unit": "vitest run --passWithNoTests",
58
+ "test:watch": "vitest --watch",
59
+ "test": "rushx test:unit",
60
+ "typecheck": "tsc -p ./tsconfig.json"
61
+ }
62
+ }
package/readme.mdx ADDED
@@ -0,0 +1,76 @@
1
+ import { Meta, Stories, ArgTypes, Primary, Subtitle } from "@storybook/blocks";
2
+
3
+ import * as AccordionStories from "./src/accordion.stories";
4
+ import packageInfo from "./package.json";
5
+
6
+ <Meta name="Docs" title="Components/Accordion" of={AccordionStories} />
7
+
8
+ # Accordion
9
+
10
+ <Subtitle>Version {packageInfo.version}</Subtitle>
11
+
12
+ ### Showcase
13
+
14
+ <Primary />
15
+
16
+ ### Properties
17
+
18
+ <ArgTypes />
19
+
20
+ ### Installation
21
+
22
+ #### Via NPM
23
+
24
+ Add the dependency to your consumer app like `"@purpurds/purpur": "^x.y.z"`
25
+
26
+ In MyApp.tsx
27
+
28
+ ```tsx
29
+ import "@purpurds/purpur/styles";
30
+ ```
31
+
32
+ In MyComponent.tsx
33
+
34
+ #### Default
35
+
36
+ ```tsx
37
+ import { Accordion, AccordionItem } from "@purpurds/purpur";
38
+
39
+ export const MyComponent = () => {
40
+ return (
41
+ <Accordion title="Accordion title" titleVariant="title-100">
42
+ <AccordionItem title="Section title" value="1">
43
+ Place body text here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus at
44
+ rutrum nulla.
45
+ </AccordionItem>
46
+
47
+ <AccordionItem title="Section title" value="2">
48
+ Place body text here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus at
49
+ rutrum nulla.
50
+ </AccordionItem>
51
+ </Accordion>
52
+ );
53
+ };
54
+ ```
55
+
56
+ #### Negative with custom content
57
+
58
+ ```tsx
59
+ export const MyComponent = () => {
60
+ return (
61
+ <Accordion negative title="Accordion title">
62
+ <AccordionItem title="Section title1">
63
+ Place body text here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus at
64
+ rutrum nulla.
65
+ <a href="https://telia.se">Link1</a>
66
+ </AccordionItem>
67
+
68
+ <AccordionItem title="Section title2">
69
+ Place body text here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus at
70
+ rutrum nulla.
71
+ <a href="https://telia.se">Link2</a>
72
+ </AccordionItem>
73
+ </Accordion>
74
+ );
75
+ };
76
+ ```
@@ -0,0 +1,150 @@
1
+ @import "@purpurds/tokens/breakpoint/variables";
2
+
3
+ .purpur-accordion-item {
4
+ $root: &;
5
+ overflow: hidden;
6
+ border-bottom: var(--purpur-border-width-xs) solid var(--purpur-color-border-weak);
7
+
8
+ &:nth-of-type(1) {
9
+ border-top: var(--purpur-border-width-xs) solid var(--purpur-color-border-weak);
10
+ }
11
+
12
+ &__header {
13
+ display: flex;
14
+ }
15
+
16
+ &__trigger {
17
+ border: none;
18
+ padding: 0;
19
+ width: 100%;
20
+ font-family: inherit;
21
+ background-color: transparent;
22
+ transition: background-color var(--purpur-motion-duration-150) ease;
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: space-between;
26
+ padding: var(--purpur-spacing-200) var(--purpur-spacing-200) var(--purpur-spacing-200) 0;
27
+ cursor: pointer;
28
+ outline: none;
29
+
30
+ @media screen and (min-width: #{$purpur-breakpoint-md}) {
31
+ padding: var(--purpur-spacing-300) var(--purpur-spacing-300) var(--purpur-spacing-300) 0;
32
+ }
33
+
34
+ @media screen and (min-width: #{$purpur-breakpoint-lg}) {
35
+ padding: var(--purpur-spacing-400) var(--purpur-spacing-300) var(--purpur-spacing-400) 0;
36
+ }
37
+ }
38
+
39
+ &__content {
40
+ overflow: hidden;
41
+ }
42
+
43
+ &__content[data-state="open"] {
44
+ animation: slideDown var(--purpur-motion-duration-150) ease-in-out;
45
+ }
46
+
47
+ &__content[data-state="closed"] {
48
+ animation: slideUp var(--purpur-motion-duration-150) ease-in-out;
49
+ }
50
+
51
+ &__contentText {
52
+ padding: var(--purpur-spacing-200) 0;
53
+ max-width: 37.5rem;
54
+ }
55
+
56
+ &__icon {
57
+ height: var(--purpur-spacing-300);
58
+ width: var(--purpur-spacing-300);
59
+ transition: transform var(--purpur-motion-duration-150) ease-in-out;
60
+ }
61
+
62
+ &__trigger[data-state="open"] > &__icon {
63
+ transform: rotate(180deg);
64
+ }
65
+
66
+ &:has(:focus-visible) {
67
+ outline: none;
68
+ box-shadow: 0 0 0 var(--purpur-border-width-sm) var(--purpur-color-border-interactive-focus);
69
+ }
70
+
71
+ &__title,
72
+ &__icon {
73
+ color: var(--purpur-color-text-interactive-primary);
74
+ }
75
+
76
+ &__trigger:hover {
77
+ background-color: var(--purpur-color-background-interactive-transparent-hover);
78
+
79
+ &__title,
80
+ &__icon {
81
+ color: var(--purpur-color-text-interactive-primary-hover);
82
+ }
83
+ }
84
+
85
+ &__trigger:active {
86
+ background-color: var(--purpur-color-background-interactive-transparent-active);
87
+
88
+ &__title,
89
+ &__icon {
90
+ color: var(--purpur-color-text-interactive-primary-active);
91
+ }
92
+ }
93
+
94
+ &__contentText p {
95
+ color: var(--purpur-color-text-default);
96
+ }
97
+
98
+ &--negative {
99
+ border-bottom: var(--purpur-border-width-xs) solid var(--purpur-color-border-weak-negative);
100
+
101
+ &:nth-of-type(1) {
102
+ border-top: var(--purpur-border-width-xs) solid var(--purpur-color-border-weak-negative);
103
+ }
104
+
105
+ #{$root}__title,
106
+ #{$root}__icon {
107
+ color: var(--purpur-color-text-interactive-primary-negative);
108
+ }
109
+
110
+ #{$root}__trigger:hover {
111
+ background-color: var(--purpur-color-background-interactive-transparent-negative-hover);
112
+
113
+ #{$root}__title,
114
+ #{$root}__icon {
115
+ color: var(--purpur-color-border-interactive-primary-negative-hover);
116
+ }
117
+ }
118
+
119
+ #{$root}__trigger:active {
120
+ background-color: var(--purpur-color-background-interactive-transparent-negative-active);
121
+
122
+ #{$root}__title,
123
+ #{$root}__icon {
124
+ color: var(--purpur-color-border-interactive-primary-negative-active);
125
+ }
126
+ }
127
+
128
+ #{$root}__contentText p {
129
+ color: var(--purpur-color-text-default-negative);
130
+ }
131
+ }
132
+ }
133
+
134
+ @keyframes slideDown {
135
+ from {
136
+ height: 0;
137
+ }
138
+ to {
139
+ height: var(--radix-accordion-content-height);
140
+ }
141
+ }
142
+
143
+ @keyframes slideUp {
144
+ from {
145
+ height: var(--radix-accordion-content-height);
146
+ }
147
+ to {
148
+ height: 0;
149
+ }
150
+ }
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+
4
+ import { Accordion } from "./accordion";
5
+ import { AccordionItem as AccordionItemComponent } from "./accordion-item";
6
+
7
+ const meta: Meta<typeof Accordion> = {
8
+ title: "Components/Accordion",
9
+ component: Accordion,
10
+ parameters: {
11
+ design: [
12
+ {
13
+ name: "Accordion",
14
+ type: "figma",
15
+ url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?type=design&node-id=4218%3A1652&mode=design&t=Xd8xbiGkQgta5BOt-1",
16
+ },
17
+ ],
18
+ },
19
+ };
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof AccordionItemComponent>;
23
+
24
+ export const AccordionItem: Story = {
25
+ render: (args) => (
26
+ <Accordion negative={args.negative}>
27
+ <AccordionItemComponent {...args} />
28
+ </Accordion>
29
+ ),
30
+ args: {
31
+ negative: false,
32
+ title: "Title goes here",
33
+ children:
34
+ "Place body text here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus at rutrum nulla. ",
35
+ },
36
+ };
@@ -0,0 +1,81 @@
1
+ import React, { isValidElement, ReactElement, ReactNode, ReactPortal } from "react";
2
+ import { Heading } from "@purpurds/heading";
3
+ import { IconChevronDown } from "@purpurds/icon";
4
+ import { Paragraph } from "@purpurds/paragraph";
5
+ import * as RadixAccordion from "@radix-ui/react-accordion";
6
+ import c from "classnames/bind";
7
+
8
+ import styles from "./accordion-item.module.scss";
9
+
10
+ const cx = c.bind(styles);
11
+
12
+ const rootClassName = "purpur-accordion-item";
13
+
14
+ export type AccordionItemProps = {
15
+ ["data-testid"]?: string;
16
+ children: ReactNode;
17
+ /**
18
+ * Different accordion styling
19
+ * */
20
+ negative?: boolean;
21
+ className?: string;
22
+ /**
23
+ * Title of the accordion item
24
+ * */
25
+ title: string;
26
+ /**
27
+ * Value is used to identify the accordion. By default title is used for that purpose. If there are identical titles in the accordion, then value should be defined.
28
+ * */
29
+ value?: string;
30
+ };
31
+
32
+ export const AccordionItem = ({
33
+ children,
34
+ className,
35
+ title,
36
+ negative,
37
+ ...props
38
+ }: AccordionItemProps) => {
39
+ const classes = cx([
40
+ className,
41
+ rootClassName,
42
+ {
43
+ [`${rootClassName}--negative`]: negative,
44
+ },
45
+ ]);
46
+
47
+ return (
48
+ <RadixAccordion.Item className={classes} value={title} {...props}>
49
+ <RadixAccordion.Header className={cx(`${rootClassName}__header`)} asChild>
50
+ <RadixAccordion.Trigger className={cx(`${rootClassName}__trigger`, className)}>
51
+ <Heading tag="h3" variant="title-100" className={cx(`${rootClassName}__title`)}>
52
+ {title}
53
+ </Heading>
54
+ <IconChevronDown size="md" className={cx(`${rootClassName}__icon`)} aria-hidden />
55
+ </RadixAccordion.Trigger>
56
+ </RadixAccordion.Header>
57
+ <RadixAccordion.Content className={cx(`${rootClassName}__content`, className)}>
58
+ <div className={cx(`${rootClassName}__contentText`)}>
59
+ {typeof children === "string" ? (
60
+ <Paragraph variant="paragraph-200">{children}</Paragraph>
61
+ ) : (
62
+ children
63
+ )}
64
+ </div>
65
+ </RadixAccordion.Content>
66
+ </RadixAccordion.Item>
67
+ );
68
+ };
69
+
70
+ export const isAccordionItem = (
71
+ child:
72
+ | ReactElement
73
+ | Iterable<ReactNode>
74
+ | ReactPortal
75
+ | string
76
+ | number
77
+ | boolean
78
+ | null
79
+ | undefined
80
+ ): child is ReactElement<AccordionItemProps> =>
81
+ isValidElement<AccordionItemProps>(child) && child?.type === AccordionItem;
@@ -0,0 +1,22 @@
1
+ @import "@purpurds/tokens/breakpoint/variables";
2
+
3
+ .purpur-accordion {
4
+ $root: &;
5
+
6
+ &__title {
7
+ margin-bottom: var(--purpur-spacing-300);
8
+ color: var(--purpur-color-text-default);
9
+
10
+ @media screen and (min-width: #{$purpur-breakpoint-md}) {
11
+ margin-bottom: var(--purpur-spacing-400);
12
+ }
13
+ }
14
+
15
+ &--negative {
16
+ background-color: var(--purpur-color-purple-900);
17
+
18
+ #{$root}__title {
19
+ color: var(--purpur-color-text-default-negative);
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+
4
+ import { Accordion } from "./accordion";
5
+ import { AccordionItem } from "./accordion-item";
6
+
7
+ const meta: Meta<typeof Accordion> = {
8
+ title: "Components/Accordion",
9
+ component: Accordion,
10
+ parameters: {
11
+ design: [
12
+ {
13
+ name: "Accordion",
14
+ type: "figma",
15
+ url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?type=design&node-id=4218%3A1652&mode=design&t=Xd8xbiGkQgta5BOt-1",
16
+ },
17
+ ],
18
+ },
19
+ };
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof Accordion>;
23
+
24
+ export const Showcase: Story = {
25
+ render: (args) => (
26
+ <Accordion {...args}>
27
+ <AccordionItem title="Section title" value="1">
28
+ Place body text here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus at
29
+ rutrum nulla.
30
+ </AccordionItem>
31
+
32
+ <AccordionItem title="Section title" value="2">
33
+ Place body text here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus at
34
+ rutrum nulla.
35
+ </AccordionItem>
36
+
37
+ <AccordionItem title="Section title" value="3">
38
+ Place body text here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus at
39
+ rutrum nulla.
40
+ </AccordionItem>
41
+ </Accordion>
42
+ ),
43
+ args: {
44
+ negative: false,
45
+ title: "Title goes here",
46
+ titleVariant: "title-300",
47
+ },
48
+ };
@@ -0,0 +1,65 @@
1
+ import React from "react";
2
+ import * as matchers from "@testing-library/jest-dom/matchers";
3
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+
6
+ import { Accordion } from "./accordion";
7
+ import { AccordionItem } from "./accordion-item";
8
+
9
+ expect.extend(matchers);
10
+
11
+ const setup = () =>
12
+ render(
13
+ <Accordion title="accordion heading">
14
+ <AccordionItem title="Test heading">item content</AccordionItem>
15
+ </Accordion>
16
+ );
17
+
18
+ describe("Accordion", () => {
19
+ afterEach(cleanup);
20
+
21
+ it("should render accordion heading", () => {
22
+ setup();
23
+
24
+ expect(screen.getByRole("heading", { level: 2, name: "accordion heading" })).toBeDefined();
25
+ });
26
+
27
+ it("should render accordion item heading", () => {
28
+ setup();
29
+
30
+ expect(screen.getByRole("heading", { level: 3, name: "Test heading" })).toBeDefined();
31
+ });
32
+
33
+ it("should not render accordion item content when trigger is not clicked", () => {
34
+ setup();
35
+
36
+ expect(screen.queryByText("item content")).toBeNull();
37
+ });
38
+
39
+ it("should show/hide accordion item content when trigger is clicked", () => {
40
+ setup();
41
+
42
+ const trigger = screen.getByRole("button");
43
+
44
+ fireEvent.click(trigger);
45
+ expect(screen.getByText("item content")).toBeDefined();
46
+
47
+ fireEvent.click(trigger);
48
+ expect(screen.queryByText("item content")).toBeNull();
49
+ });
50
+
51
+ it("should render custom accordion item content", () => {
52
+ render(
53
+ <Accordion title="accordion heading">
54
+ <AccordionItem title="Test heading">
55
+ <a href="/test">Test link</a>
56
+ </AccordionItem>
57
+ </Accordion>
58
+ );
59
+
60
+ const trigger = screen.getByRole("button");
61
+ fireEvent.click(trigger);
62
+
63
+ expect(screen.getByRole("link", { name: "Test link" })).toBeDefined();
64
+ });
65
+ });
@@ -0,0 +1,66 @@
1
+ import React, { Children, cloneElement, ReactNode } from "react";
2
+ import { Heading, TitleVariantType } from "@purpurds/heading";
3
+ import * as RadixAccordion from "@radix-ui/react-accordion";
4
+ import c from "classnames/bind";
5
+
6
+ import styles from "./accordion.module.scss";
7
+ import { isAccordionItem } from "./accordion-item";
8
+
9
+ const cx = c.bind(styles);
10
+
11
+ const rootClassName = "purpur-accordion";
12
+
13
+ export type AccordionProps = {
14
+ ["data-testid"]?: string;
15
+ children: ReactNode;
16
+ className?: string;
17
+ /**
18
+ * Different accordion styling
19
+ * */
20
+ negative?: boolean;
21
+ /**
22
+ * Title of the accordion
23
+ * */
24
+ title?: string;
25
+ /**
26
+ * Different variant of sizing the title
27
+ * */
28
+ titleVariant?: TitleVariantType;
29
+ };
30
+
31
+ export const Accordion = ({
32
+ ["data-testid"]: dataTestId,
33
+ children,
34
+ className,
35
+ negative = false,
36
+ title,
37
+ titleVariant = "title-300",
38
+ }: AccordionProps) => {
39
+ const classes = cx([
40
+ className,
41
+ rootClassName,
42
+ {
43
+ [`${rootClassName}--negative`]: negative,
44
+ },
45
+ ]);
46
+
47
+ const renderChildren = () =>
48
+ Children.map(children, (child) => {
49
+ if (isAccordionItem(child)) {
50
+ return cloneElement(child, { negative });
51
+ }
52
+ });
53
+
54
+ return (
55
+ <RadixAccordion.Root className={classes} type="multiple" data-testid={dataTestId}>
56
+ {title ? (
57
+ <Heading tag="h2" variant={titleVariant} className={cx(`${rootClassName}__title`)}>
58
+ {title}
59
+ </Heading>
60
+ ) : null}
61
+ {renderChildren()}
62
+ </RadixAccordion.Root>
63
+ );
64
+ };
65
+
66
+ Accordion.displayName = "Accordion";
@@ -0,0 +1,4 @@
1
+ declare module "*.scss" {
2
+ const styles: { [className: string]: string };
3
+ export default styles;
4
+ }