@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.
Files changed (227) hide show
  1. package/.babelrc +12 -0
  2. package/.storybook/main.ts +35 -0
  3. package/.storybook/preview.ts +4 -0
  4. package/BAKpostcss.config.jsBAK +15 -0
  5. package/BAKtailwind.config.mjsBAK +99 -0
  6. package/README.md +464 -16
  7. package/coverage/storybook/coverage-storybook.json +32411 -0
  8. package/coverage/storybook/lcov-report/Accordion.tsx.html +805 -0
  9. package/coverage/storybook/lcov-report/Badge.tsx.html +346 -0
  10. package/coverage/storybook/lcov-report/Breadcrumbs.tsx.html +742 -0
  11. package/coverage/storybook/lcov-report/Button.tsx.html +448 -0
  12. package/coverage/storybook/lcov-report/ButtonGroup.tsx.html +403 -0
  13. package/coverage/storybook/lcov-report/Card.tsx.html +292 -0
  14. package/coverage/storybook/lcov-report/CharacterCounter.tsx.html +253 -0
  15. package/coverage/storybook/lcov-report/CheckBox.tsx.html +1555 -0
  16. package/coverage/storybook/lcov-report/DatePicker.tsx.html +826 -0
  17. package/coverage/storybook/lcov-report/Input.tsx.html +1012 -0
  18. package/coverage/storybook/lcov-report/List.tsx.html +364 -0
  19. package/coverage/storybook/lcov-report/Modal.tsx.html +745 -0
  20. package/coverage/storybook/lcov-report/Pill.tsx.html +358 -0
  21. package/coverage/storybook/lcov-report/Search.tsx.html +997 -0
  22. package/coverage/storybook/lcov-report/SearchContent.tsx.html +235 -0
  23. package/coverage/storybook/lcov-report/SectionHeader.tsx.html +358 -0
  24. package/coverage/storybook/lcov-report/Select.tsx.html +1012 -0
  25. package/coverage/storybook/lcov-report/Shield.tsx.html +802 -0
  26. package/coverage/storybook/lcov-report/SideBarNav.tsx.html +490 -0
  27. package/coverage/storybook/lcov-report/Skeleton.tsx.html +394 -0
  28. package/coverage/storybook/lcov-report/Slider.tsx.html +385 -0
  29. package/coverage/storybook/lcov-report/Status.tsx.html +322 -0
  30. package/coverage/storybook/lcov-report/Tabs.tsx.html +610 -0
  31. package/coverage/storybook/lcov-report/Toggle.tsx.html +373 -0
  32. package/coverage/storybook/lcov-report/Tooltip.tsx.html +496 -0
  33. package/coverage/storybook/lcov-report/base.css +224 -0
  34. package/coverage/storybook/lcov-report/block-navigation.js +87 -0
  35. package/coverage/storybook/lcov-report/favicon.png +0 -0
  36. package/coverage/storybook/lcov-report/index.html +476 -0
  37. package/coverage/storybook/lcov-report/prettify.css +1 -0
  38. package/coverage/storybook/lcov-report/prettify.js +2 -0
  39. package/coverage/storybook/lcov-report/sort-arrow-sprite.png +0 -0
  40. package/coverage/storybook/lcov-report/sorter.js +196 -0
  41. package/coverage/storybook/lcov.info +2312 -0
  42. package/dist/README.md +1815 -0
  43. package/eslint.config.mjs +13 -0
  44. package/package.json +6 -7
  45. package/project.json +11 -0
  46. package/src/assets/img/Frame.svg +5 -0
  47. package/src/assets/img/backArrowRight.svg +10 -0
  48. package/src/assets/img/bc-separator.png +0 -0
  49. package/src/assets/img/calendar.png +0 -0
  50. package/src/assets/img/calendar.svg +4 -0
  51. package/src/assets/img/check.svg +5 -0
  52. package/src/assets/img/check_box.svg +10 -0
  53. package/src/assets/img/check_box_empty.svg +10 -0
  54. package/src/assets/img/check_box_fill.svg +10 -0
  55. package/src/assets/img/check_box_fill_empty.svg +10 -0
  56. package/src/assets/img/chevron-down-white.svg +2 -0
  57. package/src/assets/img/chevron-down.svg +2 -0
  58. package/src/assets/img/chevron-left.svg +1 -0
  59. package/src/assets/img/chevron-right-light.svg +4 -0
  60. package/src/assets/img/chevron-right.svg +3 -0
  61. package/src/assets/img/chevron-up-white.svg +1 -0
  62. package/src/assets/img/chevron-up.svg +1 -0
  63. package/src/assets/img/clock.svg +6 -0
  64. package/src/assets/img/close.svg +1 -0
  65. package/src/assets/img/close2.svg +6 -0
  66. package/src/assets/img/closeModal.svg +10 -0
  67. package/src/assets/img/close_icon_dark.svg +10 -0
  68. package/src/assets/img/close_small.svg +3 -0
  69. package/src/assets/img/emergency_home.svg +10 -0
  70. package/src/assets/img/first-aid-kit.svg +7 -0
  71. package/src/assets/img/heartbeat.svg +4 -0
  72. package/src/assets/img/home-gray.svg +3 -0
  73. package/src/assets/img/home.svg +3 -0
  74. package/src/assets/img/hospital.jpg +0 -0
  75. package/src/assets/img/indeterminate_check_box.svg +10 -0
  76. package/src/assets/img/indeterminate_check_box_fill.svg +10 -0
  77. package/src/assets/img/info_24_ 1d4ed8.svg +3 -0
  78. package/src/assets/img/info_24_ 2c6441.svg +3 -0
  79. package/src/assets/img/marker_check_by_default.svg +10 -0
  80. package/src/assets/img/marker_check_by_default_fill.svg +10 -0
  81. package/src/assets/img/minus-accordion.svg +5 -0
  82. package/src/assets/img/minus.svg +3 -0
  83. package/src/assets/img/open.svg +1 -0
  84. package/src/assets/img/pill-white.svg +7 -0
  85. package/src/assets/img/pill.svg +5 -0
  86. package/src/assets/img/plus-accordion.svg +5 -0
  87. package/src/assets/img/plus.svg +4 -0
  88. package/src/assets/img/prescription.svg +6 -0
  89. package/src/assets/img/search.svg +10 -0
  90. package/src/assets/img/search_icon_light.svg +10 -0
  91. package/src/assets/img/separator.svg +3 -0
  92. package/src/assets/img/stethoscope-white.svg +8 -0
  93. package/src/assets/img/stethoscope.svg +8 -0
  94. package/src/assets/img/thumb_up.svg +10 -0
  95. package/src/assets/img/vector.svg +3 -0
  96. package/src/assets/img/warning-badge-disabled.svg +11 -0
  97. package/src/assets/img/warning-badge-green.svg +11 -0
  98. package/src/assets/img/warning-badge-red.svg +11 -0
  99. package/src/assets/img/warning-badge-yellow.svg +11 -0
  100. package/src/assets/img/warning.svg +10 -0
  101. package/src/global.d.ts +13 -0
  102. package/{index.d.ts → src/index.ts} +13 -5
  103. package/src/lib/Accordian--Accordian.stories.tsx +312 -0
  104. package/src/lib/Accordion.spec.tsx +384 -0
  105. package/src/lib/Accordion.tsx +240 -0
  106. package/src/lib/AppointmentPicker.spec.tsx +138 -0
  107. package/src/lib/AppointmentPicker.tsx +97 -0
  108. package/src/lib/Badge--Badge.stories.tsx +60 -0
  109. package/src/lib/Badge.spec.tsx +70 -0
  110. package/src/lib/Badge.tsx +87 -0
  111. package/src/lib/Breadcrumbs-Breadcrumbs.stories.tsx +114 -0
  112. package/src/lib/Breadcrumbs.spec.tsx +218 -0
  113. package/src/lib/Breadcrumbs.tsx +219 -0
  114. package/src/lib/Button--Button.stories.tsx +220 -0
  115. package/src/lib/Button.spec.tsx +241 -0
  116. package/src/lib/Button.tsx +121 -0
  117. package/src/lib/ButtonGroup--ButtonGroup.stories.tsx +129 -0
  118. package/src/lib/ButtonGroup.spec.tsx +89 -0
  119. package/src/lib/ButtonGroup.tsx +107 -0
  120. package/src/lib/Card--Card.stories.tsx +113 -0
  121. package/src/lib/Card.spec.tsx +112 -0
  122. package/src/lib/Card.tsx +69 -0
  123. package/src/lib/CharacterCounter--CharacterCounter.stories.tsx +169 -0
  124. package/src/lib/CharacterCounter.spec.tsx +123 -0
  125. package/src/lib/CharacterCounter.tsx +56 -0
  126. package/src/lib/CheckBox--CheckBox.stories.tsx +107 -0
  127. package/src/lib/CheckBox.spec.tsx +412 -0
  128. package/src/lib/CheckBox.tsx +491 -0
  129. package/src/lib/DatePicker--DatePicker.stories.tsx +228 -0
  130. package/src/lib/DatePicker.spec.tsx +424 -0
  131. package/src/lib/DatePicker.tsx +247 -0
  132. package/src/lib/Input--Input.stories.tsx +449 -0
  133. package/src/lib/Input.spec.tsx +281 -0
  134. package/src/lib/Input.tsx +309 -0
  135. package/src/lib/List--List.stories.tsx +157 -0
  136. package/src/lib/List.spec.tsx +211 -0
  137. package/src/lib/List.tsx +93 -0
  138. package/src/lib/Modal--Modal.stories.tsx +454 -0
  139. package/src/lib/Modal.spec.tsx +202 -0
  140. package/src/lib/Modal.tsx +220 -0
  141. package/src/lib/Pill--Pill.stories.tsx +98 -0
  142. package/src/lib/Pill.spec.tsx +103 -0
  143. package/src/lib/Pill.tsx +91 -0
  144. package/src/lib/ProgressBar.spec.tsx +106 -0
  145. package/src/lib/ProgressBar.tsx +112 -0
  146. package/src/lib/RadioGroup.spec.tsx +84 -0
  147. package/src/lib/RadioGroup.tsx +74 -0
  148. package/src/lib/RadioIcon.tsx +13 -0
  149. package/src/lib/Search--Search.stories.tsx +67 -0
  150. package/src/lib/Search.spec.tsx +182 -0
  151. package/src/lib/Search.tsx +304 -0
  152. package/src/lib/SearchContent.tsx +51 -0
  153. package/src/lib/SectionHeader--SectionHeader.stories.tsx +98 -0
  154. package/src/lib/SectionHeader.spec.tsx +60 -0
  155. package/src/lib/SectionHeader.tsx +91 -0
  156. package/src/lib/Select--Select.stories.tsx +387 -0
  157. package/src/lib/Select.spec.tsx +493 -0
  158. package/src/lib/Select.tsx +311 -0
  159. package/src/lib/Shield--Shield.stories.tsx +196 -0
  160. package/src/lib/Shield.spec.tsx +275 -0
  161. package/src/lib/Shield.tsx +239 -0
  162. package/src/lib/SideBarNav--SideBarNav.stories.tsx +136 -0
  163. package/src/lib/SideBarNav.spec.tsx +178 -0
  164. package/src/lib/SideBarNav.tsx +135 -0
  165. package/src/lib/Skeleton--Skeleton.stories.tsx +77 -0
  166. package/src/lib/Skeleton.module.css +16 -0
  167. package/src/lib/Skeleton.spec.tsx +83 -0
  168. package/src/lib/Skeleton.tsx +103 -0
  169. package/src/lib/SkipLink.spec.tsx +76 -0
  170. package/src/lib/SkipLink.tsx +48 -0
  171. package/src/lib/Slider--Slider.stories.tsx +108 -0
  172. package/src/lib/Slider.module.css +109 -0
  173. package/src/lib/Slider.spec.tsx +67 -0
  174. package/src/lib/Slider.tsx +101 -0
  175. package/src/lib/Status--Status.stories.tsx +93 -0
  176. package/src/lib/Status.spec.tsx +118 -0
  177. package/src/lib/Status.tsx +79 -0
  178. package/src/lib/Tabs--Tabs.stories.tsx +294 -0
  179. package/src/lib/Tabs.spec.tsx +249 -0
  180. package/src/lib/Tabs.tsx +188 -0
  181. package/src/lib/Tester.spec.tsx +17 -0
  182. package/src/lib/Toggle--Toggle.stories.tsx +162 -0
  183. package/src/lib/Toggle.spec.tsx +122 -0
  184. package/src/lib/Toggle.tsx +96 -0
  185. package/src/lib/Tooltip--Tooltip.stories.tsx +315 -0
  186. package/src/lib/Tooltip.spec.tsx +307 -0
  187. package/src/lib/Tooltip.tsx +137 -0
  188. package/src/lib/bak-simple-ui.stories.tsx-bak +24 -0
  189. package/src/styles.css +190 -0
  190. package/tsconfig.json +25 -0
  191. package/tsconfig.lib.json +42 -0
  192. package/tsconfig.spec.json +29 -0
  193. package/tsconfig.storybook.json +36 -0
  194. package/vite.config.mts +87 -0
  195. package/vitest.setup.ts +12 -0
  196. package/index.css +0 -1
  197. package/index.js +0 -35
  198. package/index.mjs +0 -4981
  199. package/lib/Accordion.d.ts +0 -36
  200. package/lib/AppointmentPicker.d.ts +0 -21
  201. package/lib/Badge.d.ts +0 -11
  202. package/lib/Breadcrumbs.d.ts +0 -13
  203. package/lib/Button.d.ts +0 -15
  204. package/lib/ButtonGroup.d.ts +0 -8
  205. package/lib/Card.d.ts +0 -11
  206. package/lib/CharacterCounter.d.ts +0 -11
  207. package/lib/CheckBox.d.ts +0 -30
  208. package/lib/DatePicker.d.ts +0 -7
  209. package/lib/Input.d.ts +0 -16
  210. package/lib/List.d.ts +0 -22
  211. package/lib/Modal.d.ts +0 -18
  212. package/lib/Pill.d.ts +0 -13
  213. package/lib/ProgressBar.d.ts +0 -19
  214. package/lib/RadioGroup.d.ts +0 -15
  215. package/lib/Search.d.ts +0 -26
  216. package/lib/SearchContent.d.ts +0 -6
  217. package/lib/SectionHeader.d.ts +0 -18
  218. package/lib/Select.d.ts +0 -19
  219. package/lib/Shield.d.ts +0 -12
  220. package/lib/SideBarNav.d.ts +0 -21
  221. package/lib/Skeleton.d.ts +0 -15
  222. package/lib/SkipLink.d.ts +0 -22
  223. package/lib/Slider.d.ts +0 -14
  224. package/lib/Status.d.ts +0 -10
  225. package/lib/Tabs.d.ts +0 -23
  226. package/lib/Toggle.d.ts +0 -11
  227. 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
+ );