@dhasdk/simple-ui 1.0.7 → 1.0.8
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.
- package/.babelrc +12 -0
- package/.storybook/main.ts +35 -0
- package/.storybook/preview.ts +4 -0
- package/BAKpostcss.config.jsBAK +15 -0
- package/BAKtailwind.config.mjsBAK +99 -0
- package/README.md +464 -16
- package/coverage/storybook/coverage-storybook.json +32411 -0
- package/coverage/storybook/lcov-report/Accordion.tsx.html +805 -0
- package/coverage/storybook/lcov-report/Badge.tsx.html +346 -0
- package/coverage/storybook/lcov-report/Breadcrumbs.tsx.html +742 -0
- package/coverage/storybook/lcov-report/Button.tsx.html +448 -0
- package/coverage/storybook/lcov-report/ButtonGroup.tsx.html +403 -0
- package/coverage/storybook/lcov-report/Card.tsx.html +292 -0
- package/coverage/storybook/lcov-report/CharacterCounter.tsx.html +253 -0
- package/coverage/storybook/lcov-report/CheckBox.tsx.html +1555 -0
- package/coverage/storybook/lcov-report/DatePicker.tsx.html +826 -0
- package/coverage/storybook/lcov-report/Input.tsx.html +1012 -0
- package/coverage/storybook/lcov-report/List.tsx.html +364 -0
- package/coverage/storybook/lcov-report/Modal.tsx.html +745 -0
- package/coverage/storybook/lcov-report/Pill.tsx.html +358 -0
- package/coverage/storybook/lcov-report/Search.tsx.html +997 -0
- package/coverage/storybook/lcov-report/SearchContent.tsx.html +235 -0
- package/coverage/storybook/lcov-report/SectionHeader.tsx.html +358 -0
- package/coverage/storybook/lcov-report/Select.tsx.html +1012 -0
- package/coverage/storybook/lcov-report/Shield.tsx.html +802 -0
- package/coverage/storybook/lcov-report/SideBarNav.tsx.html +490 -0
- package/coverage/storybook/lcov-report/Skeleton.tsx.html +394 -0
- package/coverage/storybook/lcov-report/Slider.tsx.html +385 -0
- package/coverage/storybook/lcov-report/Status.tsx.html +322 -0
- package/coverage/storybook/lcov-report/Tabs.tsx.html +610 -0
- package/coverage/storybook/lcov-report/Toggle.tsx.html +373 -0
- package/coverage/storybook/lcov-report/Tooltip.tsx.html +496 -0
- package/coverage/storybook/lcov-report/base.css +224 -0
- package/coverage/storybook/lcov-report/block-navigation.js +87 -0
- package/coverage/storybook/lcov-report/favicon.png +0 -0
- package/coverage/storybook/lcov-report/index.html +476 -0
- package/coverage/storybook/lcov-report/prettify.css +1 -0
- package/coverage/storybook/lcov-report/prettify.js +2 -0
- package/coverage/storybook/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/storybook/lcov-report/sorter.js +196 -0
- package/coverage/storybook/lcov.info +2312 -0
- package/dist/README.md +1815 -0
- package/eslint.config.mjs +13 -0
- package/package.json +6 -7
- package/project.json +11 -0
- package/src/assets/img/Frame.svg +5 -0
- package/src/assets/img/backArrowRight.svg +10 -0
- package/src/assets/img/bc-separator.png +0 -0
- package/src/assets/img/calendar.png +0 -0
- package/src/assets/img/calendar.svg +4 -0
- package/src/assets/img/check.svg +5 -0
- package/src/assets/img/check_box.svg +10 -0
- package/src/assets/img/check_box_empty.svg +10 -0
- package/src/assets/img/check_box_fill.svg +10 -0
- package/src/assets/img/check_box_fill_empty.svg +10 -0
- package/src/assets/img/chevron-down-white.svg +2 -0
- package/src/assets/img/chevron-down.svg +2 -0
- package/src/assets/img/chevron-left.svg +1 -0
- package/src/assets/img/chevron-right-light.svg +4 -0
- package/src/assets/img/chevron-right.svg +3 -0
- package/src/assets/img/chevron-up-white.svg +1 -0
- package/src/assets/img/chevron-up.svg +1 -0
- package/src/assets/img/clock.svg +6 -0
- package/src/assets/img/close.svg +1 -0
- package/src/assets/img/close2.svg +6 -0
- package/src/assets/img/closeModal.svg +10 -0
- package/src/assets/img/close_icon_dark.svg +10 -0
- package/src/assets/img/close_small.svg +3 -0
- package/src/assets/img/emergency_home.svg +10 -0
- package/src/assets/img/first-aid-kit.svg +7 -0
- package/src/assets/img/heartbeat.svg +4 -0
- package/src/assets/img/home-gray.svg +3 -0
- package/src/assets/img/home.svg +3 -0
- package/src/assets/img/hospital.jpg +0 -0
- package/src/assets/img/indeterminate_check_box.svg +10 -0
- package/src/assets/img/indeterminate_check_box_fill.svg +10 -0
- package/src/assets/img/info_24_ 1d4ed8.svg +3 -0
- package/src/assets/img/info_24_ 2c6441.svg +3 -0
- package/src/assets/img/marker_check_by_default.svg +10 -0
- package/src/assets/img/marker_check_by_default_fill.svg +10 -0
- package/src/assets/img/minus-accordion.svg +5 -0
- package/src/assets/img/minus.svg +3 -0
- package/src/assets/img/open.svg +1 -0
- package/src/assets/img/pill-white.svg +7 -0
- package/src/assets/img/pill.svg +5 -0
- package/src/assets/img/plus-accordion.svg +5 -0
- package/src/assets/img/plus.svg +4 -0
- package/src/assets/img/prescription.svg +6 -0
- package/src/assets/img/search.svg +10 -0
- package/src/assets/img/search_icon_light.svg +10 -0
- package/src/assets/img/separator.svg +3 -0
- package/src/assets/img/stethoscope-white.svg +8 -0
- package/src/assets/img/stethoscope.svg +8 -0
- package/src/assets/img/thumb_up.svg +10 -0
- package/src/assets/img/vector.svg +3 -0
- package/src/assets/img/warning-badge-disabled.svg +11 -0
- package/src/assets/img/warning-badge-green.svg +11 -0
- package/src/assets/img/warning-badge-red.svg +11 -0
- package/src/assets/img/warning-badge-yellow.svg +11 -0
- package/src/assets/img/warning.svg +10 -0
- package/src/global.d.ts +13 -0
- package/{index.d.ts → src/index.ts} +13 -5
- package/src/lib/Accordian--Accordian.stories.tsx +312 -0
- package/src/lib/Accordion.spec.tsx +384 -0
- package/src/lib/Accordion.tsx +240 -0
- package/src/lib/AppointmentPicker.spec.tsx +138 -0
- package/src/lib/AppointmentPicker.tsx +97 -0
- package/src/lib/Badge--Badge.stories.tsx +60 -0
- package/src/lib/Badge.spec.tsx +70 -0
- package/src/lib/Badge.tsx +87 -0
- package/src/lib/Breadcrumbs-Breadcrumbs.stories.tsx +114 -0
- package/src/lib/Breadcrumbs.spec.tsx +218 -0
- package/src/lib/Breadcrumbs.tsx +219 -0
- package/src/lib/Button--Button.stories.tsx +220 -0
- package/src/lib/Button.spec.tsx +241 -0
- package/src/lib/Button.tsx +121 -0
- package/src/lib/ButtonGroup--ButtonGroup.stories.tsx +129 -0
- package/src/lib/ButtonGroup.spec.tsx +89 -0
- package/src/lib/ButtonGroup.tsx +107 -0
- package/src/lib/Card--Card.stories.tsx +113 -0
- package/src/lib/Card.spec.tsx +112 -0
- package/src/lib/Card.tsx +69 -0
- package/src/lib/CharacterCounter--CharacterCounter.stories.tsx +169 -0
- package/src/lib/CharacterCounter.spec.tsx +123 -0
- package/src/lib/CharacterCounter.tsx +56 -0
- package/src/lib/CheckBox--CheckBox.stories.tsx +107 -0
- package/src/lib/CheckBox.spec.tsx +412 -0
- package/src/lib/CheckBox.tsx +491 -0
- package/src/lib/DatePicker--DatePicker.stories.tsx +228 -0
- package/src/lib/DatePicker.spec.tsx +424 -0
- package/src/lib/DatePicker.tsx +247 -0
- package/src/lib/Input--Input.stories.tsx +449 -0
- package/src/lib/Input.spec.tsx +281 -0
- package/src/lib/Input.tsx +309 -0
- package/src/lib/List--List.stories.tsx +157 -0
- package/src/lib/List.spec.tsx +211 -0
- package/src/lib/List.tsx +93 -0
- package/src/lib/Modal--Modal.stories.tsx +454 -0
- package/src/lib/Modal.spec.tsx +202 -0
- package/src/lib/Modal.tsx +220 -0
- package/src/lib/Pill--Pill.stories.tsx +98 -0
- package/src/lib/Pill.spec.tsx +103 -0
- package/src/lib/Pill.tsx +91 -0
- package/src/lib/ProgressBar.spec.tsx +106 -0
- package/src/lib/ProgressBar.tsx +112 -0
- package/src/lib/RadioGroup.spec.tsx +84 -0
- package/src/lib/RadioGroup.tsx +74 -0
- package/src/lib/RadioIcon.tsx +13 -0
- package/src/lib/Search--Search.stories.tsx +67 -0
- package/src/lib/Search.spec.tsx +182 -0
- package/src/lib/Search.tsx +304 -0
- package/src/lib/SearchContent.tsx +51 -0
- package/src/lib/SectionHeader--SectionHeader.stories.tsx +98 -0
- package/src/lib/SectionHeader.spec.tsx +60 -0
- package/src/lib/SectionHeader.tsx +91 -0
- package/src/lib/Select--Select.stories.tsx +387 -0
- package/src/lib/Select.spec.tsx +493 -0
- package/src/lib/Select.tsx +311 -0
- package/src/lib/Shield--Shield.stories.tsx +196 -0
- package/src/lib/Shield.spec.tsx +275 -0
- package/src/lib/Shield.tsx +239 -0
- package/src/lib/SideBarNav--SideBarNav.stories.tsx +136 -0
- package/src/lib/SideBarNav.spec.tsx +178 -0
- package/src/lib/SideBarNav.tsx +135 -0
- package/src/lib/Skeleton--Skeleton.stories.tsx +77 -0
- package/src/lib/Skeleton.module.css +16 -0
- package/src/lib/Skeleton.spec.tsx +83 -0
- package/src/lib/Skeleton.tsx +103 -0
- package/src/lib/SkipLink.spec.tsx +76 -0
- package/src/lib/SkipLink.tsx +48 -0
- package/src/lib/Slider--Slider.stories.tsx +108 -0
- package/src/lib/Slider.module.css +109 -0
- package/src/lib/Slider.spec.tsx +67 -0
- package/src/lib/Slider.tsx +101 -0
- package/src/lib/Status--Status.stories.tsx +93 -0
- package/src/lib/Status.spec.tsx +118 -0
- package/src/lib/Status.tsx +79 -0
- package/src/lib/Tabs--Tabs.stories.tsx +294 -0
- package/src/lib/Tabs.spec.tsx +249 -0
- package/src/lib/Tabs.tsx +188 -0
- package/src/lib/Tester.spec.tsx +17 -0
- package/src/lib/Toggle--Toggle.stories.tsx +162 -0
- package/src/lib/Toggle.spec.tsx +122 -0
- package/src/lib/Toggle.tsx +96 -0
- package/src/lib/Tooltip--Tooltip.stories.tsx +315 -0
- package/src/lib/Tooltip.spec.tsx +307 -0
- package/src/lib/Tooltip.tsx +137 -0
- package/src/lib/bak-simple-ui.stories.tsx-bak +24 -0
- package/src/styles.css +190 -0
- package/tsconfig.json +25 -0
- package/tsconfig.lib.json +42 -0
- package/tsconfig.spec.json +29 -0
- package/tsconfig.storybook.json +36 -0
- package/vite.config.mts +87 -0
- package/vitest.setup.ts +12 -0
- package/index.css +0 -1
- package/index.js +0 -35
- package/index.mjs +0 -4981
- package/lib/Accordion.d.ts +0 -36
- package/lib/AppointmentPicker.d.ts +0 -21
- package/lib/Badge.d.ts +0 -11
- package/lib/Breadcrumbs.d.ts +0 -13
- package/lib/Button.d.ts +0 -15
- package/lib/ButtonGroup.d.ts +0 -8
- package/lib/Card.d.ts +0 -11
- package/lib/CharacterCounter.d.ts +0 -11
- package/lib/CheckBox.d.ts +0 -30
- package/lib/DatePicker.d.ts +0 -7
- package/lib/Input.d.ts +0 -16
- package/lib/List.d.ts +0 -22
- package/lib/Modal.d.ts +0 -18
- package/lib/Pill.d.ts +0 -13
- package/lib/ProgressBar.d.ts +0 -19
- package/lib/RadioGroup.d.ts +0 -15
- package/lib/Search.d.ts +0 -26
- package/lib/SearchContent.d.ts +0 -6
- package/lib/SectionHeader.d.ts +0 -18
- package/lib/Select.d.ts +0 -19
- package/lib/Shield.d.ts +0 -12
- package/lib/SideBarNav.d.ts +0 -21
- package/lib/Skeleton.d.ts +0 -15
- package/lib/SkipLink.d.ts +0 -22
- package/lib/Slider.d.ts +0 -14
- package/lib/Status.d.ts +0 -10
- package/lib/Tabs.d.ts +0 -23
- package/lib/Toggle.d.ts +0 -11
- package/lib/Tooltip.d.ts +0 -14
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
import { Accordion, AccordionParent, AccordionParentProps, AccordionProps } from './Accordion';
|
|
4
|
+
import React, { createRef } from 'react';
|
|
5
|
+
import { axe } from "vitest-axe";
|
|
6
|
+
|
|
7
|
+
const imagePath = new URL('/src/assets/pill.svg', import.meta.url).href;
|
|
8
|
+
|
|
9
|
+
describe('Accordion', () => {
|
|
10
|
+
|
|
11
|
+
it('renders the Accordion component with the given label', () => {
|
|
12
|
+
render(<Accordion label="Test Accordion" />);
|
|
13
|
+
expect(screen.getByText('Test Accordion')).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('renders an accordion with chevron icons instead of plus/minus', () => {
|
|
17
|
+
render(<Accordion chevron label="Test Accordion" useBackground={false} />);
|
|
18
|
+
|
|
19
|
+
const closedImg = screen.getByAltText('Open Icon');
|
|
20
|
+
expect(closedImg).toHaveAttribute('src', expect.stringContaining('chevron-down'));
|
|
21
|
+
|
|
22
|
+
const button = screen.getByText('Test Accordion');
|
|
23
|
+
fireEvent.click(button);
|
|
24
|
+
|
|
25
|
+
const openImg = screen.getByAltText('Close Icon');
|
|
26
|
+
expect(openImg).toHaveAttribute('src', expect.stringContaining('chevron-up'));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders an accordion with useBackground set to false and ensures conditional class is not applied', () => {
|
|
30
|
+
render(<Accordion label="Test Accordion" useBackground={false} />);
|
|
31
|
+
|
|
32
|
+
const button = screen.getByText('Test Accordion');
|
|
33
|
+
fireEvent.click(button);
|
|
34
|
+
|
|
35
|
+
const content = screen.getByRole('region');
|
|
36
|
+
expect(content).not.toHaveClass('rounded-b-md');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('toggles content visibility when clicked', () => {
|
|
40
|
+
render(
|
|
41
|
+
<Accordion label="Toggle Accordion">
|
|
42
|
+
<p>Accordion content</p>
|
|
43
|
+
</Accordion>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const button = screen.getByText('Toggle Accordion');
|
|
47
|
+
expect(screen.queryByText('Accordion content')).not.toBeInTheDocument();
|
|
48
|
+
|
|
49
|
+
fireEvent.click(button);
|
|
50
|
+
expect(screen.getByText('Accordion content')).toBeInTheDocument();
|
|
51
|
+
|
|
52
|
+
fireEvent.click(button);
|
|
53
|
+
expect(screen.queryByText('Accordion content')).not.toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('displays custom icons when provided', () => {
|
|
57
|
+
const customOpenIcon = <span data-testid="custom-open-icon">Open</span>;
|
|
58
|
+
const customCloseIcon = <span data-testid="custom-close-icon">Close</span>;
|
|
59
|
+
|
|
60
|
+
render(
|
|
61
|
+
<Accordion
|
|
62
|
+
label="Custom Icon Accordion"
|
|
63
|
+
iconAccordionOpened={customCloseIcon}
|
|
64
|
+
iconAccordionClosed={customOpenIcon}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(screen.getByTestId('custom-open-icon')).toBeInTheDocument();
|
|
69
|
+
|
|
70
|
+
fireEvent.click(screen.getByText('Custom Icon Accordion'));
|
|
71
|
+
expect(screen.getByTestId('custom-close-icon')).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('applies custom classes correctly', () => {
|
|
75
|
+
render(<Accordion label="Styled Accordion" classNameContainer="custom-class" />);
|
|
76
|
+
const accordionElement = screen.getByText('Styled Accordion').closest('div');
|
|
77
|
+
expect(accordionElement).toHaveClass('custom-class');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('passes axe accessibility tests', async () => {
|
|
81
|
+
const { container } = render(<Accordion label="Accessible Accordion" />);
|
|
82
|
+
expect(await axe(container)).toHaveNoViolations();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('AccordionParent', () => {
|
|
87
|
+
it('renders multiple Accordion components inside AccordionParent', () => {
|
|
88
|
+
render(
|
|
89
|
+
<AccordionParent variant="default">
|
|
90
|
+
<Accordion label="Accordion 1" />
|
|
91
|
+
<Accordion label="Accordion 2" />
|
|
92
|
+
</AccordionParent>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(screen.getByText('Accordion 1')).toBeInTheDocument();
|
|
96
|
+
expect(screen.getByText('Accordion 2')).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('allows only one Accordion to be open at a time when singleOpen is true', () => {
|
|
100
|
+
render(
|
|
101
|
+
<AccordionParent singleOpen>
|
|
102
|
+
<Accordion label="Accordion 1">
|
|
103
|
+
<p>Content 1</p>
|
|
104
|
+
</Accordion>
|
|
105
|
+
<Accordion label="Accordion 2">
|
|
106
|
+
<p>Content 2</p>
|
|
107
|
+
</Accordion>
|
|
108
|
+
</AccordionParent>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const accordion1 = screen.getByText('Accordion 1');
|
|
112
|
+
const accordion2 = screen.getByText('Accordion 2');
|
|
113
|
+
|
|
114
|
+
fireEvent.click(accordion1);
|
|
115
|
+
expect(screen.getByText('Content 1')).toBeInTheDocument();
|
|
116
|
+
expect(screen.queryByText('Content 2')).not.toBeInTheDocument();
|
|
117
|
+
|
|
118
|
+
fireEvent.click(accordion2);
|
|
119
|
+
expect(screen.queryByText('Content 1')).not.toBeInTheDocument();
|
|
120
|
+
expect(screen.getByText('Content 2')).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('allows multiple Accordions to be open at the same time when singleOpen is false', () => {
|
|
124
|
+
render(
|
|
125
|
+
<AccordionParent singleOpen={false}>
|
|
126
|
+
<Accordion label="Accordion 1">
|
|
127
|
+
<p>Content 1</p>
|
|
128
|
+
</Accordion>
|
|
129
|
+
<Accordion label="Accordion 2">
|
|
130
|
+
<p>Content 2</p>
|
|
131
|
+
</Accordion>
|
|
132
|
+
</AccordionParent>
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const accordion1 = screen.getByText('Accordion 1');
|
|
136
|
+
const accordion2 = screen.getByText('Accordion 2');
|
|
137
|
+
|
|
138
|
+
fireEvent.click(accordion1);
|
|
139
|
+
expect(screen.getByText('Content 1')).toBeInTheDocument();
|
|
140
|
+
|
|
141
|
+
fireEvent.click(accordion2);
|
|
142
|
+
expect(screen.getByText('Content 2')).toBeInTheDocument();
|
|
143
|
+
expect(screen.getByText('Content 1')).toBeInTheDocument();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('applies common classes to child Accordions when specified', () => {
|
|
147
|
+
render(
|
|
148
|
+
<AccordionParent classNameChildHeading="common-heading">
|
|
149
|
+
<Accordion label="Accordion 1" />
|
|
150
|
+
<Accordion label="Accordion 2" />
|
|
151
|
+
</AccordionParent>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const accordion1Button = screen.getByText('Accordion 1').closest('button');
|
|
155
|
+
const accordion2Button = screen.getByText('Accordion 2').closest('button');
|
|
156
|
+
|
|
157
|
+
expect(accordion1Button).toHaveClass('common-heading');
|
|
158
|
+
expect(accordion2Button).toHaveClass('common-heading');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('passes axe accessibility tests', async () => {
|
|
162
|
+
const { container } = render(
|
|
163
|
+
<AccordionParent variant="default">
|
|
164
|
+
<Accordion label="Accessible Accordion 1" />
|
|
165
|
+
<Accordion label="Accessible Accordion 2" />
|
|
166
|
+
</AccordionParent>
|
|
167
|
+
);
|
|
168
|
+
expect(await axe(container)).toHaveNoViolations();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('renders non-Accordion children without modification', () => {
|
|
172
|
+
render(
|
|
173
|
+
<AccordionParent>
|
|
174
|
+
{'Plain text child'}
|
|
175
|
+
</AccordionParent>
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(screen.getByText('Plain text child')).toBeInTheDocument();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('Accordion Accessibility Tests', () => {
|
|
185
|
+
const renderAccordion = (props = {}) =>
|
|
186
|
+
render(<Accordion label="Accessible Accordion" {...props}>Accordion Content</Accordion>);
|
|
187
|
+
|
|
188
|
+
it('should have no accessibility violations for default Accordion', async () => {
|
|
189
|
+
const { container } = renderAccordion();
|
|
190
|
+
const results = await axe(container);
|
|
191
|
+
expect(results).toHaveNoViolations();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should render without accessibility violations', async () => {
|
|
195
|
+
const { container } = render(
|
|
196
|
+
<Accordion label="Accordion Label">
|
|
197
|
+
<p>Accordion Content</p>
|
|
198
|
+
</Accordion>
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const results = await axe(container);
|
|
202
|
+
expect(results).toHaveNoViolations();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should toggle aria-expanded when clicked', () => {
|
|
206
|
+
render(
|
|
207
|
+
<Accordion label="Accordion Label">
|
|
208
|
+
<p>Accordion Content</p>
|
|
209
|
+
</Accordion>
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const button = screen.getByRole('button', { name: /accordion label/i });
|
|
213
|
+
|
|
214
|
+
// Initial state
|
|
215
|
+
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
216
|
+
|
|
217
|
+
// Toggle open
|
|
218
|
+
fireEvent.click(button);
|
|
219
|
+
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
220
|
+
|
|
221
|
+
// Toggle closed
|
|
222
|
+
fireEvent.click(button);
|
|
223
|
+
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should be keyboard navigable', () => {
|
|
227
|
+
render(
|
|
228
|
+
<Accordion label="Accordion Label">
|
|
229
|
+
<p>Accordion Content</p>
|
|
230
|
+
</Accordion>
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const button = screen.getByRole('button', { name: /accordion label/i });
|
|
234
|
+
|
|
235
|
+
// Press "Enter" key
|
|
236
|
+
fireEvent.keyDown(button, { key: 'Enter' });
|
|
237
|
+
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
238
|
+
|
|
239
|
+
// Press "Space" key
|
|
240
|
+
fireEvent.keyDown(button, { key: ' ' });
|
|
241
|
+
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('AccordionParent Accessibility Tests', () => {
|
|
247
|
+
const renderAccordionParent = (props = {}) =>
|
|
248
|
+
render(
|
|
249
|
+
<AccordionParent {...props}>
|
|
250
|
+
<Accordion label="Accordion 1">Content 1</Accordion>
|
|
251
|
+
<Accordion label="Accordion 2">Content 2</Accordion>
|
|
252
|
+
</AccordionParent>
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
it('should have no accessibility violations for AccordionParent', async () => {
|
|
256
|
+
const { container } = renderAccordionParent();
|
|
257
|
+
const results = await axe(container, {
|
|
258
|
+
rules: {
|
|
259
|
+
'color-contrast': { enabled: false },
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
expect(results).toHaveNoViolations();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should render multiple accordions without accessibility violations', async () => {
|
|
266
|
+
const { container } = render(
|
|
267
|
+
<AccordionParent>
|
|
268
|
+
<Accordion label="Accordion 1">
|
|
269
|
+
<p>Content 1</p>
|
|
270
|
+
</Accordion>
|
|
271
|
+
<Accordion label="Accordion 2">
|
|
272
|
+
<p>Content 2</p>
|
|
273
|
+
</Accordion>
|
|
274
|
+
</AccordionParent>
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const results = await axe(container);
|
|
278
|
+
expect(results).toHaveNoViolations();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should only allow one accordion to be open at a time when singleOpen is true', () => {
|
|
282
|
+
render(
|
|
283
|
+
<AccordionParent singleOpen>
|
|
284
|
+
<Accordion label="Accordion 1">
|
|
285
|
+
<p>Content 1</p>
|
|
286
|
+
</Accordion>
|
|
287
|
+
<Accordion label="Accordion 2">
|
|
288
|
+
<p>Content 2</p>
|
|
289
|
+
</Accordion>
|
|
290
|
+
</AccordionParent>
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const buttons = screen.getAllByRole('button');
|
|
294
|
+
|
|
295
|
+
// Open the first accordion
|
|
296
|
+
fireEvent.click(buttons[0]);
|
|
297
|
+
expect(buttons[0]).toHaveAttribute('aria-expanded', 'true');
|
|
298
|
+
expect(buttons[1]).toHaveAttribute('aria-expanded', 'false');
|
|
299
|
+
|
|
300
|
+
// Open the second accordion, the first one should close
|
|
301
|
+
fireEvent.click(buttons[1]);
|
|
302
|
+
expect(buttons[0]).toHaveAttribute('aria-expanded', 'false');
|
|
303
|
+
expect(buttons[1]).toHaveAttribute('aria-expanded', 'true');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should allow multiple accordions to be open when singleOpen is false', () => {
|
|
307
|
+
render(
|
|
308
|
+
<AccordionParent singleOpen={false}>
|
|
309
|
+
<Accordion label="Accordion 1">
|
|
310
|
+
<p>Content 1</p>
|
|
311
|
+
</Accordion>
|
|
312
|
+
<Accordion label="Accordion 2">
|
|
313
|
+
<p>Content 2</p>
|
|
314
|
+
</Accordion>
|
|
315
|
+
</AccordionParent>
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const buttons = screen.getAllByRole('button');
|
|
319
|
+
|
|
320
|
+
// Open the first accordion
|
|
321
|
+
fireEvent.click(buttons[0]);
|
|
322
|
+
expect(buttons[0]).toHaveAttribute('aria-expanded', 'true');
|
|
323
|
+
|
|
324
|
+
// Open the second accordion, the first one should stay open
|
|
325
|
+
fireEvent.click(buttons[1]);
|
|
326
|
+
expect(buttons[0]).toHaveAttribute('aria-expanded', 'true');
|
|
327
|
+
expect(buttons[1]).toHaveAttribute('aria-expanded', 'true');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe('AccordionParent cloning logic', () => {
|
|
334
|
+
it('honors child.props.hr === false (no <hr>) and index=0', () => {
|
|
335
|
+
render(
|
|
336
|
+
<AccordionParent>
|
|
337
|
+
<Accordion label="No HR" hr={false}>
|
|
338
|
+
<p>Content</p>
|
|
339
|
+
</Accordion>
|
|
340
|
+
</AccordionParent>
|
|
341
|
+
);
|
|
342
|
+
// open the first (and only) accordion
|
|
343
|
+
fireEvent.click(screen.getByText('No HR'));
|
|
344
|
+
|
|
345
|
+
// The <hr> should not render
|
|
346
|
+
expect(screen.queryByRole('separator')).toBeNull();
|
|
347
|
+
|
|
348
|
+
// Check index made it into the IDs
|
|
349
|
+
const region = screen.getByRole('region');
|
|
350
|
+
expect(region).toHaveAttribute('id', 'accordion-0-content');
|
|
351
|
+
expect(screen.getByRole('button')).toHaveAttribute('id', 'accordion-0-header');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('merges parent + child classNameHr into the <hr>', () => {
|
|
355
|
+
render(
|
|
356
|
+
<AccordionParent classNameHr="parent-hr">
|
|
357
|
+
<Accordion label="Has HR" classNameHr="child-hr">
|
|
358
|
+
<p>Content</p>
|
|
359
|
+
</Accordion>
|
|
360
|
+
</AccordionParent>
|
|
361
|
+
);
|
|
362
|
+
fireEvent.click(screen.getByText('Has HR'));
|
|
363
|
+
|
|
364
|
+
// Now an <hr> MUST be present
|
|
365
|
+
const hr = screen.getByRole('separator');
|
|
366
|
+
// The default border classes plus parent-hr and child-hr
|
|
367
|
+
expect(hr).toHaveClass('border-[#dfe1e2]', 'parent-hr', 'child-hr');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('passes useBackground=false to child (bg-transparent + no borders)', () => {
|
|
371
|
+
render(
|
|
372
|
+
<AccordionParent>
|
|
373
|
+
<Accordion label="No BG" useBackground={false}>
|
|
374
|
+
<p>Content</p>
|
|
375
|
+
</Accordion>
|
|
376
|
+
</AccordionParent>
|
|
377
|
+
);
|
|
378
|
+
fireEvent.click(screen.getByText('No BG'));
|
|
379
|
+
|
|
380
|
+
const region = screen.getByRole('region');
|
|
381
|
+
// The contentClasses path for `useBackground===false` includes bg-transparent and border-0
|
|
382
|
+
expect(region).toHaveClass('bg-transparent', 'border-0');
|
|
383
|
+
});
|
|
384
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// Currently Exports Accordion, AccordionProps
|
|
2
|
+
import { Children, cloneElement, forwardRef, HTMLAttributes, isValidElement, ReactNode, useEffect, useState } from 'react';
|
|
3
|
+
import { twMerge } from 'tailwind-merge';
|
|
4
|
+
import plus from '../assets/img/plus-accordion.svg';
|
|
5
|
+
import minus from '../assets/img/minus-accordion.svg';
|
|
6
|
+
import chevronDown from '../assets/img/chevron-down.svg';
|
|
7
|
+
import chevronUp from '../assets/img/chevron-up.svg';
|
|
8
|
+
|
|
9
|
+
const defaultParentMarginTop = '-mt-2';
|
|
10
|
+
|
|
11
|
+
interface VariantType {
|
|
12
|
+
[key: string]: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const baseClassesButton = 'flex justify-between items-center w-full py-4 px-5 text-left bg-white ' +
|
|
16
|
+
' rounded-xs border border-[#dfe1e2] mt-2 text-[#71767a] text-lg font-bold font-["Arial"]';
|
|
17
|
+
|
|
18
|
+
const baseClassesContent = 'px-6 py-4 border border-t-0 border-[#dfe1e2] text-[#71767a] bg-white ' +
|
|
19
|
+
'text-lg font-normal font-["Arial"]';
|
|
20
|
+
|
|
21
|
+
const variantsButton: VariantType = {
|
|
22
|
+
default: '',
|
|
23
|
+
blank: '',
|
|
24
|
+
outline: '',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const variantsContent: VariantType = {
|
|
28
|
+
default: '',
|
|
29
|
+
blank: '',
|
|
30
|
+
outline: '',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type CallbackFunction = (open: boolean) => void;
|
|
34
|
+
|
|
35
|
+
export interface AccordionProps extends HTMLAttributes<HTMLButtonElement> {
|
|
36
|
+
label: string;
|
|
37
|
+
index?: number; // index of specific accordion in stack
|
|
38
|
+
onExpand?: (setOpen: CallbackFunction) => void; // onExpand callback
|
|
39
|
+
variant?: string;
|
|
40
|
+
chevron?: boolean;
|
|
41
|
+
iconAccordionOpened?: ReactNode;
|
|
42
|
+
iconAccordionClosed?: ReactNode;
|
|
43
|
+
// rounded?: boolean;
|
|
44
|
+
classNameContainer?: string;
|
|
45
|
+
classNameContent?: string;
|
|
46
|
+
classNameHeading?: string;
|
|
47
|
+
children?: ReactNode;
|
|
48
|
+
singleOpen?: boolean;
|
|
49
|
+
hr?: boolean; // uses hr when sharing background? default true
|
|
50
|
+
classNameHr?: string; // custom css for hr element
|
|
51
|
+
useBackground?: boolean; // content shares background w/ heading
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const Accordion = forwardRef<HTMLDivElement, AccordionProps>(
|
|
55
|
+
({ variant = 'default', label, classNameContainer = '', chevron = false,
|
|
56
|
+
iconAccordionOpened, iconAccordionClosed, hr = true,
|
|
57
|
+
classNameHeading = '', classNameContent = '', onExpand, singleOpen,
|
|
58
|
+
classNameHr = '', useBackground = true,
|
|
59
|
+
index, children, ...props }, ref) => {
|
|
60
|
+
|
|
61
|
+
const [open, setOpen] = useState(false);
|
|
62
|
+
const [contentClasses, setContentClasses] = useState(baseClassesContent);
|
|
63
|
+
const [headingClasses, setHeadingClasses] = useState(baseClassesButton);
|
|
64
|
+
const [hrClasses, setHrClasses] = useState('')
|
|
65
|
+
const id = `accordion-${index ?? label.replace(/\s+/g, '-').toLowerCase()}`;
|
|
66
|
+
|
|
67
|
+
const onClick = () => {
|
|
68
|
+
// Invoke the parent's onExpand handler with the close function
|
|
69
|
+
if (onExpand && singleOpen) {
|
|
70
|
+
onExpand(setOpen);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Toggle the current accordion's open state
|
|
74
|
+
setOpen(!open);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const onKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
|
78
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
onClick();
|
|
81
|
+
//console.log('key pressed, event.key === \'' + event.key + '\', index: ' + index + ', label: ' + label);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
setHrClasses(twMerge('border-[#dfe1e2] -mt-4 pt-0 mb-3', classNameHr));
|
|
87
|
+
}, [classNameHr]);
|
|
88
|
+
|
|
89
|
+
useEffect (() => {
|
|
90
|
+
if (useBackground) {
|
|
91
|
+
// setHeadingClasses(baseClassesButton);
|
|
92
|
+
setContentClasses(baseClassesContent);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// setHeadingClasses(twMerge(baseClassesButton, ''))
|
|
96
|
+
setContentClasses(twMerge(baseClassesContent, 'bg-transparent border-0'));
|
|
97
|
+
}
|
|
98
|
+
}, [useBackground]);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (open && useBackground) {
|
|
102
|
+
setHeadingClasses(twMerge(baseClassesButton, 'border-b-0 rounded-t rounded-b-none'))
|
|
103
|
+
} else { // !open && !useBackground
|
|
104
|
+
setHeadingClasses(baseClassesButton);
|
|
105
|
+
}
|
|
106
|
+
}, [open, useBackground]);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div ref={ref} className={classNameContainer}>
|
|
110
|
+
<button
|
|
111
|
+
className={twMerge(headingClasses, variantsButton.variant, classNameHeading)}
|
|
112
|
+
// rounded && (open ? 'rounded-t-md' : 'rounded-md'), '')}
|
|
113
|
+
aria-controls={`${id}-content`}
|
|
114
|
+
aria-expanded={open}
|
|
115
|
+
id={`${id}-header`}
|
|
116
|
+
onClick={onClick}
|
|
117
|
+
onKeyDown={onKeyDown}
|
|
118
|
+
{...props}
|
|
119
|
+
>
|
|
120
|
+
<span>{label}</span>
|
|
121
|
+
<span>
|
|
122
|
+
{open ?
|
|
123
|
+
iconAccordionOpened || <img src={chevron ? chevronUp : minus} alt="Close Icon" /> :
|
|
124
|
+
iconAccordionClosed || <img src={chevron ? chevronDown : plus} alt="Open Icon" />
|
|
125
|
+
}
|
|
126
|
+
</span>
|
|
127
|
+
</button>
|
|
128
|
+
|
|
129
|
+
{ open &&
|
|
130
|
+
<>
|
|
131
|
+
{/* use HR only when useBackground true */}
|
|
132
|
+
|
|
133
|
+
<div
|
|
134
|
+
className={twMerge(contentClasses, variantsContent.variant, classNameContent,
|
|
135
|
+
// rounded && useBackground && 'rounded-b-md', 'relative'
|
|
136
|
+
useBackground && 'rounded-b-md', 'relative'
|
|
137
|
+
)}
|
|
138
|
+
id={`${id}-content`}
|
|
139
|
+
role="region"
|
|
140
|
+
aria-labelledby={`${id}-header`}
|
|
141
|
+
>
|
|
142
|
+
{ useBackground && hr && <hr className={hrClasses} /> }
|
|
143
|
+
{children}
|
|
144
|
+
</div>
|
|
145
|
+
</>
|
|
146
|
+
}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
export interface AccordionParentProps extends HTMLAttributes<HTMLDivElement> {
|
|
153
|
+
children: ReactNode;
|
|
154
|
+
variant?: string;
|
|
155
|
+
chevron?: boolean;
|
|
156
|
+
iconAccordionOpened?: ReactNode;
|
|
157
|
+
iconAccordionClosed?: ReactNode;
|
|
158
|
+
// rounded?: boolean;
|
|
159
|
+
// classNameHeading?: string;
|
|
160
|
+
// classNameContent?: string;
|
|
161
|
+
classNameContainer?: string;
|
|
162
|
+
classNameChildHeading?: string;
|
|
163
|
+
classNameChildContent?: string;
|
|
164
|
+
singleOpen?: boolean; // open one accordion at a time, default is true
|
|
165
|
+
hr?: boolean;
|
|
166
|
+
classNameHr?: string;
|
|
167
|
+
useBackground?: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/*
|
|
171
|
+
* This is the parent accordion component, and does the following:
|
|
172
|
+
* 1. manages state for opened and closed accordion elements, so by default only one
|
|
173
|
+
* element is opened at a time. If a different accordion element is opened, the
|
|
174
|
+
* previously opened one is closed.
|
|
175
|
+
* 2. when multiple accordions are grouped together, takes in common state values
|
|
176
|
+
* so that the same state value does not have to be specified over and over for each
|
|
177
|
+
* separate accordion component
|
|
178
|
+
*/
|
|
179
|
+
export const AccordionParent = forwardRef<HTMLDivElement, AccordionParentProps>(
|
|
180
|
+
(
|
|
181
|
+
{
|
|
182
|
+
children, variant = 'default', chevron, iconAccordionOpened,
|
|
183
|
+
iconAccordionClosed,
|
|
184
|
+
// rounded,
|
|
185
|
+
classNameContainer,
|
|
186
|
+
singleOpen = true,
|
|
187
|
+
classNameChildHeading,
|
|
188
|
+
classNameChildContent,
|
|
189
|
+
hr,
|
|
190
|
+
classNameHr,
|
|
191
|
+
useBackground,
|
|
192
|
+
...props
|
|
193
|
+
},
|
|
194
|
+
ref
|
|
195
|
+
) => {
|
|
196
|
+
|
|
197
|
+
const [closePreviousAccordion, setClosePreviousAccordion] = useState<CallbackFunction | null>(
|
|
198
|
+
null
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Expansion handler to manage accordion open/close
|
|
202
|
+
const accordionExpansionHandler = (newSetOpen: CallbackFunction) => {
|
|
203
|
+
// Close the previously opened accordion, if any
|
|
204
|
+
if (closePreviousAccordion) {
|
|
205
|
+
closePreviousAccordion(false);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Save the new accordion's setOpen reference
|
|
209
|
+
setClosePreviousAccordion(() => newSetOpen);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div ref={ref} {...props} className={twMerge(defaultParentMarginTop, classNameContainer)}>
|
|
214
|
+
{Children.map(children, (child, index) => {
|
|
215
|
+
if (isValidElement<AccordionProps>(child)) {
|
|
216
|
+
return cloneElement<AccordionProps>(child, { // <AccordionProps> to ensure child component sees parent specification in code
|
|
217
|
+
...child.props, // Merge existing props from the child
|
|
218
|
+
variant,
|
|
219
|
+
chevron,
|
|
220
|
+
iconAccordionOpened,
|
|
221
|
+
iconAccordionClosed,
|
|
222
|
+
// rounded,
|
|
223
|
+
classNameHeading: twMerge(classNameChildHeading, child.props.classNameHeading), // className for child component
|
|
224
|
+
classNameContent: twMerge(classNameChildContent, child.props.classNameContent),
|
|
225
|
+
singleOpen,
|
|
226
|
+
onExpand: (newSetOpen: CallbackFunction) =>
|
|
227
|
+
accordionExpansionHandler(newSetOpen), // Pass expansion handler to child
|
|
228
|
+
// child defaults to true, so if false, use child/false value
|
|
229
|
+
hr: child.props.hr === false ? false : hr,
|
|
230
|
+
classNameHr: twMerge(classNameHr, child.props.classNameHr),
|
|
231
|
+
useBackground: child.props.useBackground === false ? false : useBackground,
|
|
232
|
+
index,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return child;
|
|
236
|
+
})}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
);
|