@arbor-education/design-system.components 0.0.3 → 0.0.5

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 (274) hide show
  1. package/README.md +1 -1
  2. package/dist/components/button/Button.d.ts +5 -2
  3. package/dist/components/button/Button.d.ts.map +1 -1
  4. package/dist/components/button/Button.js +3 -1
  5. package/dist/components/button/Button.js.map +1 -1
  6. package/dist/components/card/Card.d.ts +1 -2
  7. package/dist/components/card/Card.d.ts.map +1 -1
  8. package/dist/components/card/Card.js +3 -3
  9. package/dist/components/card/Card.js.map +1 -1
  10. package/dist/components/card/Card.test.js +0 -5
  11. package/dist/components/card/Card.test.js.map +1 -1
  12. package/dist/components/formField/FormField.d.ts +4 -0
  13. package/dist/components/formField/FormField.d.ts.map +1 -1
  14. package/dist/components/formField/FormField.js +2 -1
  15. package/dist/components/formField/FormField.js.map +1 -1
  16. package/dist/components/formField/FormField.stories.d.ts.map +1 -1
  17. package/dist/components/formField/FormField.stories.js +3 -1
  18. package/dist/components/formField/FormField.stories.js.map +1 -1
  19. package/dist/components/formField/FormField.test.js +5 -0
  20. package/dist/components/formField/FormField.test.js.map +1 -1
  21. package/dist/components/formField/inputs/checkbox/CheckboxInput.d.ts +7 -0
  22. package/dist/components/formField/inputs/checkbox/CheckboxInput.d.ts.map +1 -0
  23. package/dist/components/formField/inputs/checkbox/CheckboxInput.js +31 -0
  24. package/dist/components/formField/inputs/checkbox/CheckboxInput.js.map +1 -0
  25. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts +17 -0
  26. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts.map +1 -0
  27. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js +19 -0
  28. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js.map +1 -0
  29. package/dist/components/formField/inputs/checkbox/CheckboxInput.test.d.ts +2 -0
  30. package/dist/components/formField/inputs/checkbox/CheckboxInput.test.d.ts.map +1 -0
  31. package/dist/components/formField/inputs/checkbox/CheckboxInput.test.js +30 -0
  32. package/dist/components/formField/inputs/checkbox/CheckboxInput.test.js.map +1 -0
  33. package/dist/components/formField/inputs/dropdown/Dropdown.d.ts +11 -0
  34. package/dist/components/formField/inputs/dropdown/Dropdown.d.ts.map +1 -0
  35. package/dist/components/formField/inputs/dropdown/Dropdown.js +43 -0
  36. package/dist/components/formField/inputs/dropdown/Dropdown.js.map +1 -0
  37. package/dist/components/formField/inputs/dropdown/Dropdown.stories.d.ts +161 -0
  38. package/dist/components/formField/inputs/dropdown/Dropdown.stories.d.ts.map +1 -0
  39. package/dist/components/formField/inputs/dropdown/Dropdown.stories.js +172 -0
  40. package/dist/components/formField/inputs/dropdown/Dropdown.stories.js.map +1 -0
  41. package/dist/components/formField/inputs/dropdown/Dropdown.test.d.ts +2 -0
  42. package/dist/components/formField/inputs/dropdown/Dropdown.test.d.ts.map +1 -0
  43. package/dist/components/formField/inputs/dropdown/Dropdown.test.js +93 -0
  44. package/dist/components/formField/inputs/dropdown/Dropdown.test.js.map +1 -0
  45. package/dist/components/formField/inputs/dropdown/buttons/dropdownButton/DropdownButton.d.ts +11 -0
  46. package/dist/components/formField/inputs/dropdown/buttons/dropdownButton/DropdownButton.d.ts.map +1 -0
  47. package/dist/components/formField/inputs/dropdown/buttons/dropdownButton/DropdownButton.js +15 -0
  48. package/dist/components/formField/inputs/dropdown/buttons/dropdownButton/DropdownButton.js.map +1 -0
  49. package/dist/components/formField/inputs/dropdown/items/DropdownItemRenderer.d.ts +10 -0
  50. package/dist/components/formField/inputs/dropdown/items/DropdownItemRenderer.d.ts.map +1 -0
  51. package/dist/components/formField/inputs/dropdown/items/DropdownItemRenderer.js +12 -0
  52. package/dist/components/formField/inputs/dropdown/items/DropdownItemRenderer.js.map +1 -0
  53. package/dist/components/formField/inputs/dropdown/items/dropdownItem/DropdownItem.d.ts +9 -0
  54. package/dist/components/formField/inputs/dropdown/items/dropdownItem/DropdownItem.d.ts.map +1 -0
  55. package/dist/components/formField/inputs/dropdown/items/dropdownItem/DropdownItem.js +17 -0
  56. package/dist/components/formField/inputs/dropdown/items/dropdownItem/DropdownItem.js.map +1 -0
  57. package/dist/components/formField/inputs/dropdown/items/dropdownMultiLineItem/DropdownMultiLineItem.d.ts +7 -0
  58. package/dist/components/formField/inputs/dropdown/items/dropdownMultiLineItem/DropdownMultiLineItem.d.ts.map +1 -0
  59. package/dist/components/formField/inputs/dropdown/items/dropdownMultiLineItem/DropdownMultiLineItem.js +16 -0
  60. package/dist/components/formField/inputs/dropdown/items/dropdownMultiLineItem/DropdownMultiLineItem.js.map +1 -0
  61. package/dist/components/formField/inputs/dropdown/wrapper/DropdownWrapper.d.ts +16 -0
  62. package/dist/components/formField/inputs/dropdown/wrapper/DropdownWrapper.d.ts.map +1 -0
  63. package/dist/components/formField/inputs/dropdown/wrapper/DropdownWrapper.js +73 -0
  64. package/dist/components/formField/inputs/dropdown/wrapper/DropdownWrapper.js.map +1 -0
  65. package/dist/components/formField/inputs/number/NumberInput.d.ts +6 -0
  66. package/dist/components/formField/inputs/number/NumberInput.d.ts.map +1 -0
  67. package/dist/components/formField/inputs/number/NumberInput.js +39 -0
  68. package/dist/components/formField/inputs/number/NumberInput.js.map +1 -0
  69. package/dist/components/formField/inputs/number/NumberInput.stories.d.ts +20 -0
  70. package/dist/components/formField/inputs/number/NumberInput.stories.d.ts.map +1 -0
  71. package/dist/components/formField/inputs/number/NumberInput.stories.js +22 -0
  72. package/dist/components/formField/inputs/number/NumberInput.stories.js.map +1 -0
  73. package/dist/components/formField/inputs/number/NumberInput.test.d.ts +2 -0
  74. package/dist/components/formField/inputs/number/NumberInput.test.d.ts.map +1 -0
  75. package/dist/components/formField/inputs/number/NumberInput.test.js +30 -0
  76. package/dist/components/formField/inputs/number/NumberInput.test.js.map +1 -0
  77. package/dist/components/formField/inputs/radio/RadioButtonInput.d.ts +7 -0
  78. package/dist/components/formField/inputs/radio/RadioButtonInput.d.ts.map +1 -0
  79. package/dist/components/formField/inputs/radio/RadioButtonInput.js +9 -0
  80. package/dist/components/formField/inputs/radio/RadioButtonInput.js.map +1 -0
  81. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts +46 -0
  82. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts.map +1 -0
  83. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js +83 -0
  84. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js.map +1 -0
  85. package/dist/components/formField/inputs/radio/RadioButtonInput.test.d.ts +2 -0
  86. package/dist/components/formField/inputs/radio/RadioButtonInput.test.d.ts.map +1 -0
  87. package/dist/components/formField/inputs/radio/RadioButtonInput.test.js +34 -0
  88. package/dist/components/formField/inputs/radio/RadioButtonInput.test.js.map +1 -0
  89. package/dist/components/heading/Heading.d.ts +392 -388
  90. package/dist/components/heading/Heading.d.ts.map +1 -1
  91. package/dist/components/heading/Heading.js +8 -1
  92. package/dist/components/heading/Heading.js.map +1 -1
  93. package/dist/components/heading/Heading.stories.d.ts.map +1 -1
  94. package/dist/components/heading/Heading.stories.js +7 -8
  95. package/dist/components/heading/Heading.stories.js.map +1 -1
  96. package/dist/components/heading/HeadingInnerContainer.d.ts +2 -2
  97. package/dist/components/heading/HeadingInnerContainer.js +4 -4
  98. package/dist/components/icon/Icon.d.ts +2 -2
  99. package/dist/components/icon/Icon.d.ts.map +1 -1
  100. package/dist/components/icon/Icon.js.map +1 -1
  101. package/dist/components/icon/Icon.stories.d.ts +7 -0
  102. package/dist/components/icon/Icon.stories.d.ts.map +1 -1
  103. package/dist/components/icon/Icon.stories.js +8 -0
  104. package/dist/components/icon/Icon.stories.js.map +1 -1
  105. package/dist/components/icon/allowedIcons.d.ts +1 -0
  106. package/dist/components/icon/allowedIcons.d.ts.map +1 -1
  107. package/dist/components/section/Section.d.ts +18 -0
  108. package/dist/components/section/Section.d.ts.map +1 -0
  109. package/dist/components/section/Section.js +36 -0
  110. package/dist/components/section/Section.js.map +1 -0
  111. package/dist/components/section/Section.stories.d.ts +18 -0
  112. package/dist/components/section/Section.stories.d.ts.map +1 -0
  113. package/dist/components/section/Section.stories.js +27 -0
  114. package/dist/components/section/Section.stories.js.map +1 -0
  115. package/dist/components/section/Section.test.d.ts +2 -0
  116. package/dist/components/section/Section.test.d.ts.map +1 -0
  117. package/dist/components/section/Section.test.js +157 -0
  118. package/dist/components/section/Section.test.js.map +1 -0
  119. package/dist/components/slideover/Slideover.d.ts +11 -0
  120. package/dist/components/slideover/Slideover.d.ts.map +1 -0
  121. package/dist/components/slideover/Slideover.js +11 -0
  122. package/dist/components/slideover/Slideover.js.map +1 -0
  123. package/dist/components/slideover/Slideover.test.d.ts +2 -0
  124. package/dist/components/slideover/Slideover.test.d.ts.map +1 -0
  125. package/dist/components/slideover/Slideover.test.js +33 -0
  126. package/dist/components/slideover/Slideover.test.js.map +1 -0
  127. package/dist/components/slideoverManager/SlideoverManager.d.ts +7 -0
  128. package/dist/components/slideoverManager/SlideoverManager.d.ts.map +1 -0
  129. package/dist/components/slideoverManager/SlideoverManager.js +29 -0
  130. package/dist/components/slideoverManager/SlideoverManager.js.map +1 -0
  131. package/dist/components/slideoverManager/SlideoverManager.stories.d.ts +15 -0
  132. package/dist/components/slideoverManager/SlideoverManager.stories.d.ts.map +1 -0
  133. package/dist/components/slideoverManager/SlideoverManager.stories.js +102 -0
  134. package/dist/components/slideoverManager/SlideoverManager.stories.js.map +1 -0
  135. package/dist/components/slideoverManager/SlideoverManager.test.d.ts +2 -0
  136. package/dist/components/slideoverManager/SlideoverManager.test.d.ts.map +1 -0
  137. package/dist/components/slideoverManager/SlideoverManager.test.js +53 -0
  138. package/dist/components/slideoverManager/SlideoverManager.test.js.map +1 -0
  139. package/dist/components/tabs/Tabs.d.ts +14 -18
  140. package/dist/components/tabs/Tabs.d.ts.map +1 -1
  141. package/dist/components/tabs/Tabs.js +6 -39
  142. package/dist/components/tabs/Tabs.js.map +1 -1
  143. package/dist/components/tabs/Tabs.stories.d.ts +35 -6
  144. package/dist/components/tabs/Tabs.stories.d.ts.map +1 -1
  145. package/dist/components/tabs/Tabs.stories.js +17 -45
  146. package/dist/components/tabs/Tabs.stories.js.map +1 -1
  147. package/dist/components/tabs/Tabs.test.d.ts.map +1 -1
  148. package/dist/components/tabs/Tabs.test.js +90 -97
  149. package/dist/components/tabs/Tabs.test.js.map +1 -1
  150. package/dist/components/tabs/TabsItem.d.ts +15 -0
  151. package/dist/components/tabs/TabsItem.d.ts.map +1 -0
  152. package/dist/components/tabs/TabsItem.js +18 -0
  153. package/dist/components/tabs/TabsItem.js.map +1 -0
  154. package/dist/components/tabs/TabsItem.stories.d.ts +618 -0
  155. package/dist/components/tabs/TabsItem.stories.d.ts.map +1 -0
  156. package/dist/components/tabs/TabsItem.stories.js +48 -0
  157. package/dist/components/tabs/TabsItem.stories.js.map +1 -0
  158. package/dist/index.css +1996 -1326
  159. package/dist/index.css.map +1 -1
  160. package/dist/index.d.ts +8 -1
  161. package/dist/index.d.ts.map +1 -1
  162. package/dist/index.js +8 -1
  163. package/dist/index.js.map +1 -1
  164. package/dist/utils/Constants.d.ts +6 -0
  165. package/dist/utils/Constants.d.ts.map +1 -0
  166. package/dist/utils/Constants.js +6 -0
  167. package/dist/utils/Constants.js.map +1 -0
  168. package/dist/utils/PopupParentContext.d.ts +3 -0
  169. package/dist/utils/PopupParentContext.d.ts.map +1 -0
  170. package/dist/utils/PopupParentContext.js +6 -0
  171. package/dist/utils/PopupParentContext.js.map +1 -0
  172. package/dist/utils/PubSub.d.ts +11 -0
  173. package/dist/utils/PubSub.d.ts.map +1 -0
  174. package/dist/utils/PubSub.js +27 -0
  175. package/dist/utils/PubSub.js.map +1 -0
  176. package/dist/utils/PubSub.test.d.ts +2 -0
  177. package/dist/utils/PubSub.test.d.ts.map +1 -0
  178. package/dist/utils/PubSub.test.js +229 -0
  179. package/dist/utils/PubSub.test.js.map +1 -0
  180. package/dist/utils/SlideoverUtils.d.ts +7 -0
  181. package/dist/utils/SlideoverUtils.d.ts.map +1 -0
  182. package/dist/utils/SlideoverUtils.js +8 -0
  183. package/dist/utils/SlideoverUtils.js.map +1 -0
  184. package/dist/utils/getDefaultPopupParent.d.ts +2 -0
  185. package/dist/utils/getDefaultPopupParent.d.ts.map +1 -0
  186. package/dist/utils/getDefaultPopupParent.js +13 -0
  187. package/dist/utils/getDefaultPopupParent.js.map +1 -0
  188. package/dist/utils/hooks/useComponentDidMount.d.ts +3 -0
  189. package/dist/utils/hooks/useComponentDidMount.d.ts.map +1 -0
  190. package/dist/utils/hooks/useComponentDidMount.js +5 -0
  191. package/dist/utils/hooks/useComponentDidMount.js.map +1 -0
  192. package/dist/utils/hooks/usePubSub.d.ts +2 -0
  193. package/dist/utils/hooks/usePubSub.d.ts.map +1 -0
  194. package/dist/utils/hooks/usePubSub.js +12 -0
  195. package/dist/utils/hooks/usePubSub.js.map +1 -0
  196. package/package.json +3 -3
  197. package/src/components/button/Button.story.tsx +9 -0
  198. package/src/components/button/Button.tsx +10 -2
  199. package/src/components/button/button.scss +75 -33
  200. package/src/components/card/Card.test.tsx +0 -6
  201. package/src/components/card/Card.tsx +12 -7
  202. package/src/components/card/card.scss +32 -18
  203. package/src/components/formField/FormField.stories.tsx +9 -1
  204. package/src/components/formField/FormField.test.tsx +6 -0
  205. package/src/components/formField/FormField.tsx +5 -0
  206. package/src/components/formField/formField.scss +20 -8
  207. package/src/components/formField/inputs/checkbox/CheckboxInput.stories.tsx +22 -0
  208. package/src/components/formField/inputs/checkbox/CheckboxInput.test.tsx +35 -0
  209. package/src/components/formField/inputs/checkbox/CheckboxInput.tsx +79 -0
  210. package/src/components/formField/inputs/checkbox/checkboxInput.scss +96 -0
  211. package/src/components/formField/inputs/dropdown/Dropdown.stories.tsx +185 -0
  212. package/src/components/formField/inputs/dropdown/Dropdown.test.tsx +185 -0
  213. package/src/components/formField/inputs/dropdown/Dropdown.tsx +82 -0
  214. package/src/components/formField/inputs/dropdown/buttons/dropdownButton/DropdownButton.tsx +41 -0
  215. package/src/components/formField/inputs/dropdown/buttons/dropdownButton/dropdownButton.scss +12 -0
  216. package/src/components/formField/inputs/dropdown/dropdown.scss +24 -0
  217. package/src/components/formField/inputs/dropdown/items/DropdownItemRenderer.tsx +38 -0
  218. package/src/components/formField/inputs/dropdown/items/dropdownItem/DropdownItem.tsx +49 -0
  219. package/src/components/formField/inputs/dropdown/items/dropdownItem/dropdownItem.scss +62 -0
  220. package/src/components/formField/inputs/dropdown/items/dropdownMultiLineItem/DropdownMultiLineItem.tsx +48 -0
  221. package/src/components/formField/inputs/dropdown/items/dropdownMultiLineItem/dropdownMultiLineItem.scss +52 -0
  222. package/src/components/formField/inputs/dropdown/wrapper/DropdownWrapper.tsx +138 -0
  223. package/src/components/formField/inputs/dropdown/wrapper/dropdownWrapper.scss +32 -0
  224. package/src/components/formField/inputs/input.scss +25 -26
  225. package/src/components/formField/inputs/number/NumberInput.stories.tsx +25 -0
  226. package/src/components/formField/inputs/number/NumberInput.test.tsx +33 -0
  227. package/src/components/formField/inputs/number/NumberInput.tsx +107 -0
  228. package/src/components/formField/inputs/number/numberInput.scss +68 -0
  229. package/src/components/formField/inputs/radio/RadioButtonInput.stories.tsx +97 -0
  230. package/src/components/formField/inputs/radio/RadioButtonInput.test.tsx +37 -0
  231. package/src/components/formField/inputs/radio/RadioButtonInput.tsx +46 -0
  232. package/src/components/formField/inputs/radio/radioButtonInput.scss +100 -0
  233. package/src/components/formField/label/label.scss +5 -1
  234. package/src/components/heading/Heading.stories.tsx +11 -12
  235. package/src/components/heading/Heading.tsx +21 -2
  236. package/src/components/heading/heading.scss +4 -0
  237. package/src/components/icon/Icon.stories.tsx +8 -0
  238. package/src/components/icon/Icon.tsx +2 -2
  239. package/src/components/icon/allowedIcons.tsx +2 -0
  240. package/src/components/pill/pill.scss +7 -7
  241. package/src/components/section/Section.stories.tsx +34 -0
  242. package/src/components/section/Section.test.tsx +308 -0
  243. package/src/components/section/Section.tsx +131 -0
  244. package/src/components/section/section.scss +42 -0
  245. package/src/components/slideover/Slideover.test.tsx +36 -0
  246. package/src/components/slideover/Slideover.tsx +38 -0
  247. package/src/components/slideover/slideover.scss +50 -0
  248. package/src/components/slideoverManager/SlideoverManager.stories.tsx +374 -0
  249. package/src/components/slideoverManager/SlideoverManager.test.tsx +64 -0
  250. package/src/components/slideoverManager/SlideoverManager.tsx +51 -0
  251. package/src/components/slideoverManager/slideoverManager.scss +13 -0
  252. package/src/components/tabs/Tabs.stories.tsx +92 -0
  253. package/src/components/tabs/Tabs.test.tsx +220 -0
  254. package/src/components/tabs/Tabs.tsx +14 -0
  255. package/src/components/tabs/TabsItem.stories.tsx +55 -0
  256. package/src/components/tabs/TabsItem.tsx +42 -0
  257. package/src/components/tabs/tabs.scss +62 -0
  258. package/src/global.scss +10 -1
  259. package/src/index.scss +15 -3
  260. package/src/index.ts +10 -3
  261. package/src/tokens.scss +1321 -1238
  262. package/src/utils/Constants.ts +5 -0
  263. package/src/utils/PopupParentContext.ts +6 -0
  264. package/src/utils/PubSub.test.ts +303 -0
  265. package/src/utils/PubSub.ts +34 -0
  266. package/src/utils/SlideoverUtils.ts +9 -0
  267. package/src/utils/getDefaultPopupParent.ts +14 -0
  268. package/src/utils/hooks/useComponentDidMount.ts +5 -0
  269. package/src/utils/hooks/usePubSub.ts +12 -0
  270. package/tokens/export-config.json +32 -0
  271. package/tokens/json/$metadata.json +5 -0
  272. package/tokens/json/$themes.json +1333 -0
  273. package/tokens/json/Arbor.json +6329 -0
  274. package/src/components/heading/HeadingInnerContainer.tsx +0 -18
@@ -0,0 +1,308 @@
1
+ import { expect, test, describe, vi } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import { Section } from './Section';
4
+ import '@testing-library/jest-dom/vitest';
5
+
6
+ describe('Section component', () => {
7
+ test('renders basic section with children', () => {
8
+ render(
9
+ <Section>
10
+ <p>Section content</p>
11
+ </Section>,
12
+ );
13
+
14
+ expect(screen.getByText('Section content')).toBeInTheDocument();
15
+ });
16
+
17
+ test('renders section with title', () => {
18
+ render(
19
+ <Section title="Test Section">
20
+ <p>Section content</p>
21
+ </Section>,
22
+ );
23
+
24
+ expect(screen.getByText('Test Section')).toBeInTheDocument();
25
+ });
26
+
27
+ test('renders section with title icon', () => {
28
+ render(
29
+ <Section title="Test Section" titleIconName="info" titleIconScreenReaderText="info icon">
30
+ <p>Section content</p>
31
+ </Section>,
32
+ );
33
+
34
+ expect(screen.getByText('Test Section')).toBeInTheDocument();
35
+ // Icon should be rendered within the heading
36
+ const heading = screen.getByText('info icon');
37
+ expect(heading).toBeInTheDocument();
38
+ });
39
+
40
+ test('renders section with button', () => {
41
+ const mockButtonClick = vi.fn();
42
+
43
+ render(
44
+ <Section
45
+ title="Test Section"
46
+ buttonText="Click Me"
47
+ buttonOnClick={mockButtonClick}
48
+ buttonType="primary"
49
+ buttonSize="M"
50
+ >
51
+ <p>Section content</p>
52
+ </Section>,
53
+ );
54
+
55
+ const button = screen.getByText('Click Me');
56
+ expect(button).toBeInTheDocument();
57
+
58
+ fireEvent.click(button);
59
+ expect(mockButtonClick).toHaveBeenCalledTimes(1);
60
+ });
61
+
62
+ test('applies custom className', () => {
63
+ render(
64
+ <Section className="custom-section">
65
+ <p>Section content</p>
66
+ </Section>,
67
+ );
68
+
69
+ const section = screen.getByRole('region');
70
+ expect(section).toHaveClass('ds-section', 'custom-section');
71
+ });
72
+
73
+ describe('collapsible functionality', () => {
74
+ test('renders collapsible section with chevron icon', () => {
75
+ render(
76
+ <Section title="Collapsible Section" collapsible>
77
+ <p>Section content</p>
78
+ </Section>,
79
+ );
80
+
81
+ const section = screen.getByRole('region');
82
+ expect(section).toHaveClass('ds-section--collapsible');
83
+
84
+ // Should have chevron-up icon when not collapsed
85
+ expect(screen.getByText('Collapse section')).toBeInTheDocument();
86
+ });
87
+
88
+ test('toggles collapsed state when header is clicked', () => {
89
+ render(
90
+ <Section title="Collapsible Section" collapsible>
91
+ <p>Section content</p>
92
+ </Section>,
93
+ );
94
+
95
+ const heading = screen.getByRole('heading');
96
+ const contents = document.querySelector('.ds-section__contents');
97
+
98
+ // Initially not collapsed
99
+ expect(contents).not.toHaveClass('ds-section__contents--collapsed');
100
+
101
+ // Click to collapse
102
+ fireEvent.click(heading);
103
+ expect(contents).toHaveClass('ds-section__contents--collapsed');
104
+
105
+ // Click to expand
106
+ fireEvent.click(heading);
107
+ expect(contents).not.toHaveClass('ds-section__contents--collapsed');
108
+ });
109
+
110
+ test('starts collapsed when initially collapsed prop is true', () => {
111
+ render(
112
+ <Section title="Collapsible Section" collapsible collapsed>
113
+ <p>Section content</p>
114
+ </Section>,
115
+ );
116
+
117
+ const contents = document.querySelector('.ds-section__contents');
118
+ expect(contents).toHaveClass('ds-section__contents--collapsed');
119
+ });
120
+
121
+ test('heading has correct tabIndex for collapsible section', () => {
122
+ render(
123
+ <Section title="Collapsible Section" collapsible>
124
+ <p>Section content</p>
125
+ </Section>,
126
+ );
127
+
128
+ const heading = screen.getByRole('heading');
129
+ expect(heading).toHaveAttribute('tabIndex', '0');
130
+ });
131
+
132
+ test('heading has tabIndex -1 for non-collapsible section', () => {
133
+ render(
134
+ <Section title="Non-collapsible Section">
135
+ <p>Section content</p>
136
+ </Section>,
137
+ );
138
+
139
+ const heading = screen.getByRole('heading');
140
+ expect(heading).toHaveAttribute('tabIndex', '-1');
141
+ });
142
+
143
+ test('does not toggle when non-collapsible section header is clicked', () => {
144
+ render(
145
+ <Section title="Non-collapsible Section">
146
+ <p>Section content</p>
147
+ </Section>,
148
+ );
149
+
150
+ const heading = screen.getByRole('heading');
151
+ const contents = document.querySelector('.ds-section__contents');
152
+
153
+ // Initially not collapsed
154
+ expect(contents).not.toHaveClass('ds-section__contents--collapsed');
155
+
156
+ // Click should not change state
157
+ fireEvent.click(heading);
158
+ expect(contents).not.toHaveClass('ds-section__contents--collapsed');
159
+ });
160
+ });
161
+
162
+ describe('nested sections', () => {
163
+ test('renders nested sections correctly', () => {
164
+ render(
165
+ <Section title="Parent Section" collapsible>
166
+ <p>Parent content</p>
167
+ <Section title="Child Section 1" collapsible>
168
+ <p>Child 1 content</p>
169
+ </Section>
170
+ <Section title="Child Section 2">
171
+ <p>Child 2 content</p>
172
+ <Section title="Grandchild Section">
173
+ <p>Grandchild content</p>
174
+ </Section>
175
+ </Section>
176
+ </Section>,
177
+ );
178
+
179
+ // All titles should be present
180
+ expect(screen.getByText('Parent Section')).toBeInTheDocument();
181
+ expect(screen.getByText('Child Section 1')).toBeInTheDocument();
182
+ expect(screen.getByText('Child Section 2')).toBeInTheDocument();
183
+ expect(screen.getByText('Grandchild Section')).toBeInTheDocument();
184
+
185
+ // All content should be present
186
+ expect(screen.getByText('Parent content')).toBeInTheDocument();
187
+ expect(screen.getByText('Child 1 content')).toBeInTheDocument();
188
+ expect(screen.getByText('Child 2 content')).toBeInTheDocument();
189
+ expect(screen.getByText('Grandchild content')).toBeInTheDocument();
190
+
191
+ // Should have multiple sections
192
+ const sections = document.querySelectorAll('.ds-section');
193
+ expect(sections).toHaveLength(4);
194
+ });
195
+
196
+ test('nested sections can be collapsed independently', () => {
197
+ render(
198
+ <Section title="Parent Section" collapsible>
199
+ <p>Parent content</p>
200
+ <Section title="Child Section" collapsible>
201
+ <p>Child content</p>
202
+ </Section>
203
+ </Section>,
204
+ );
205
+
206
+ const allHeadings = screen.getAllByRole('heading');
207
+ const parentHeading = allHeadings[0];
208
+ const childHeading = allHeadings[1];
209
+
210
+ const allContents = document.querySelectorAll('.ds-section__contents');
211
+ const parentContents = allContents[0];
212
+ const childContents = allContents[1];
213
+
214
+ // Initially both expanded
215
+ expect(parentContents).not.toHaveClass('ds-section__contents--collapsed');
216
+ expect(childContents).not.toHaveClass('ds-section__contents--collapsed');
217
+
218
+ // Collapse child section
219
+ fireEvent.click(childHeading!);
220
+ expect(parentContents).not.toHaveClass('ds-section__contents--collapsed');
221
+ expect(childContents).toHaveClass('ds-section__contents--collapsed');
222
+
223
+ // Collapse parent section
224
+ fireEvent.click(parentHeading!);
225
+ expect(parentContents).toHaveClass('ds-section__contents--collapsed');
226
+ expect(childContents).toHaveClass('ds-section__contents--collapsed');
227
+
228
+ // Expand parent section (child should remain collapsed)
229
+ fireEvent.click(parentHeading!);
230
+ expect(parentContents).not.toHaveClass('ds-section__contents--collapsed');
231
+ expect(childContents).toHaveClass('ds-section__contents--collapsed');
232
+ });
233
+
234
+ test('nested sections with different configurations', () => {
235
+ const parentButtonClick = vi.fn();
236
+ const childButtonClick = vi.fn();
237
+
238
+ render(
239
+ <Section
240
+ title="Parent Section"
241
+ collapsible
242
+ buttonText="Parent Action"
243
+ buttonOnClick={parentButtonClick}
244
+ titleIconName="info"
245
+ >
246
+ <p>Parent content</p>
247
+ <Section
248
+ title="Child Section"
249
+ buttonText="Child Action"
250
+ buttonOnClick={childButtonClick}
251
+ titleIconName="file"
252
+ >
253
+ <p>Child content</p>
254
+ </Section>
255
+ </Section>,
256
+ );
257
+
258
+ // Both buttons should be present and functional
259
+ const parentButton = screen.getByText('Parent Action');
260
+ const childButton = screen.getByText('Child Action');
261
+
262
+ fireEvent.click(parentButton);
263
+ expect(parentButtonClick).toHaveBeenCalledTimes(1);
264
+
265
+ fireEvent.click(childButton);
266
+ expect(childButtonClick).toHaveBeenCalledTimes(1);
267
+
268
+ // Parent should be collapsible, child should not
269
+ const sections = document.querySelectorAll('.ds-section');
270
+ expect(sections[0]).toHaveClass('ds-section--collapsible');
271
+ expect(sections[1]).not.toHaveClass('ds-section--collapsible');
272
+ });
273
+ });
274
+
275
+ describe('accessibility', () => {
276
+ test('section has proper role', () => {
277
+ render(
278
+ <Section title="Test Section">
279
+ <p>Content</p>
280
+ </Section>,
281
+ );
282
+
283
+ expect(screen.getByRole('region')).toBeInTheDocument();
284
+ });
285
+
286
+ test('heading is focusable when collapsible', () => {
287
+ render(
288
+ <Section title="Collapsible Section" collapsible>
289
+ <p>Content</p>
290
+ </Section>,
291
+ );
292
+
293
+ const heading = screen.getByRole('heading');
294
+ expect(heading).toHaveAttribute('tabIndex', '0');
295
+ });
296
+
297
+ test('heading is not focusable when not collapsible', () => {
298
+ render(
299
+ <Section title="Static Section">
300
+ <p>Content</p>
301
+ </Section>,
302
+ );
303
+
304
+ const heading = screen.getByRole('heading');
305
+ expect(heading).toHaveAttribute('tabIndex', '-1');
306
+ });
307
+ });
308
+ });
@@ -0,0 +1,131 @@
1
+ import classNames from 'classnames';
2
+ import { Button, type ButtonSize, type ButtonType } from 'Components/button/Button';
3
+ import { Heading } from 'Components/heading/Heading';
4
+ import type { IconName } from 'Components/icon/allowedIcons';
5
+ import { Icon } from 'Components/icon/Icon';
6
+ import { useState, type HTMLAttributes, type MouseEventHandler } from 'react';
7
+ import { ENTER_KEY, SPACE_KEY } from 'Utils/keyboardConstants';
8
+
9
+ type SectionProps = {
10
+ title?: string;
11
+ titleIconName?: IconName;
12
+ titleIconColor?: string;
13
+ titleIconScreenReaderText?: string;
14
+ collapsible?: boolean;
15
+ collapsed?: boolean;
16
+ buttonText?: string;
17
+ buttonOnClick?: MouseEventHandler<HTMLButtonElement>;
18
+ buttonType?: ButtonType;
19
+ buttonSize?: ButtonSize;
20
+ } & HTMLAttributes<HTMLElement>;
21
+
22
+ export const Section = (props: SectionProps) => {
23
+ const {
24
+ className,
25
+ children,
26
+ title,
27
+ titleIconName,
28
+ titleIconColor,
29
+ titleIconScreenReaderText,
30
+ collapsible = false,
31
+ collapsed: initialCollapsed = false,
32
+ buttonText,
33
+ buttonOnClick,
34
+ buttonType,
35
+ buttonSize,
36
+ } = props;
37
+
38
+ const [collapsed, setCollapsed] = useState(initialCollapsed);
39
+
40
+ const toggleCollapsedState = () => {
41
+ if (collapsible) {
42
+ setCollapsed(!collapsed);
43
+ }
44
+ };
45
+
46
+ return (
47
+ <section
48
+ className={
49
+ classNames(
50
+ 'ds-section',
51
+ {
52
+ 'ds-section--collapsible': collapsible,
53
+ },
54
+ className,
55
+ )
56
+ }
57
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/section#technical_summary
58
+ // We want this element to have a role of "region", but that can't be explicity set, so to get it set
59
+ // implicitly, we slap a label on it and it works.
60
+ aria-label="section"
61
+ >
62
+ <Heading
63
+ role="heading"
64
+ onClick={(e) => {
65
+ e.preventDefault();
66
+ e.stopPropagation();
67
+ toggleCollapsedState();
68
+ }}
69
+ onKeyDown={(e) => {
70
+ if ([SPACE_KEY, ENTER_KEY].includes(e.key)) {
71
+ e.preventDefault();
72
+ e.stopPropagation();
73
+ toggleCollapsedState();
74
+ }
75
+ }}
76
+ tabIndex={collapsible ? 0 : -1}
77
+ >
78
+ <Heading.InnerContainer>
79
+ {title}
80
+ {titleIconName && (
81
+ <Icon
82
+ name={titleIconName}
83
+ color={titleIconColor}
84
+ screenReaderText={titleIconScreenReaderText}
85
+ size={24}
86
+ />
87
+ )}
88
+ </Heading.InnerContainer>
89
+ <Heading.InnerContainer>
90
+ {buttonText && (
91
+ <Button
92
+ onClick={buttonOnClick}
93
+ type={buttonType}
94
+ size={buttonSize}
95
+ >
96
+ {buttonText}
97
+ </Button>
98
+ )}
99
+ {collapsible && (
100
+ <button
101
+ className="remove-default-button-styles"
102
+ onClick={(e) => {
103
+ e.preventDefault();
104
+ e.stopPropagation();
105
+ toggleCollapsedState();
106
+ }}
107
+ aria-expanded={!collapsed}
108
+ >
109
+ <Icon
110
+ name={collapsed ? 'chevron-down' : 'chevron-up'}
111
+ size={24}
112
+ screenReaderText={collapsed ? 'Expand section' : 'Collapse section'}
113
+ />
114
+ </button>
115
+ )}
116
+ </Heading.InnerContainer>
117
+ </Heading>
118
+ <div className={
119
+ classNames(
120
+ 'ds-section__contents',
121
+ {
122
+ 'ds-section__contents--collapsed': collapsed,
123
+ },
124
+ )
125
+ }
126
+ >
127
+ {children}
128
+ </div>
129
+ </section>
130
+ );
131
+ };
@@ -0,0 +1,42 @@
1
+ .ds-section {
2
+ border-radius: var(--section-container-radius);
3
+ padding: var(--section-container-spacing-vertical) var(--section-container-spacing-horizontal);
4
+ background-color: var(--section-container-color-background);
5
+
6
+ .ds-section {
7
+ padding-right: 0;
8
+ }
9
+
10
+ .ds-heading {
11
+ padding: var(--section-heading-spacing-top) var(--section-heading-spacing-right) var(--section-heading-spacing-bottom) var(--section-heading-spacing-left);
12
+ border-bottom: var(--border-weight) solid var(--section-heading-color-border);
13
+ margin: 0;
14
+ border-radius: var(--section-container-radius);
15
+ color: var(--section-heading-color-text);
16
+ }
17
+
18
+ &--collapsible {
19
+ .ds-heading {
20
+ cursor: pointer;
21
+ background-color: var(--section-heading-interactive-default-color-background);
22
+
23
+ &:hover {
24
+ background-color: var(--section-heading-interactive-hover-color-background);
25
+ }
26
+
27
+ &:active {
28
+ background-color: var(--section-heading-interactive-pressed-color-background);
29
+ }
30
+
31
+ &:focus {
32
+ border: var(--border-weight) solid var(--section-heading-interactive-focus-color-border);
33
+ }
34
+ }
35
+
36
+ .ds-section__contents {
37
+ &--collapsed {
38
+ display: none;
39
+ }
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,36 @@
1
+ import { expect, test, describe, vi } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import { Slideover } from './Slideover';
4
+ import '@testing-library/jest-dom/vitest';
5
+ import { SlideoverUtils } from 'Utils/SlideoverUtils';
6
+
7
+ vi.mock('Utils/SlideoverUtils', () => ({
8
+ SlideoverUtils: {
9
+ removeSlideover: vi.fn(),
10
+ },
11
+ }));
12
+
13
+ describe('Slideover', () => {
14
+ test('renders the title and children', () => {
15
+ render(<Slideover title="My Test Slideover"><div>My Content</div></Slideover>);
16
+ expect(screen.getByText('My Test Slideover')).toBeInTheDocument();
17
+ expect(screen.getByText('My Content')).toBeInTheDocument();
18
+ });
19
+
20
+ test('renders footer contents when provided', () => {
21
+ render(<Slideover footerContents={<div>Footer Stuff</div>} />);
22
+ expect(screen.getByText('Footer Stuff')).toBeInTheDocument();
23
+ });
24
+
25
+ test('does not render footer when footerContents is not provided', () => {
26
+ const { container } = render(<Slideover />);
27
+ expect(container.querySelector('.ds-slideover__footer')).not.toBeInTheDocument();
28
+ });
29
+
30
+ test('calls removeSlideover when the back button is clicked', () => {
31
+ render(<Slideover />);
32
+ const backButton = screen.getByRole('button', { name: /Back/i });
33
+ fireEvent.click(backButton);
34
+ expect(SlideoverUtils.removeSlideover).toHaveBeenCalled();
35
+ });
36
+ });
@@ -0,0 +1,38 @@
1
+ import classNames from 'classnames';
2
+ import { Button } from 'Components/button/Button';
3
+ import { Heading } from 'Components/heading/Heading';
4
+ import type { IconName } from 'Components/icon/allowedIcons';
5
+ import { Icon } from 'Components/icon/Icon';
6
+ import type { ReactNode } from 'react';
7
+ import { SlideoverUtils } from 'Utils/SlideoverUtils';
8
+
9
+ export type SlideoverProps = {
10
+ title?: string;
11
+ children?: ReactNode;
12
+ footerContents?: ReactNode;
13
+ headerIcon?: IconName;
14
+ centerHeaderText?: boolean;
15
+ };
16
+
17
+ export const Slideover = (props: SlideoverProps) => {
18
+ const { title, children, footerContents, headerIcon, centerHeaderText = true } = props;
19
+
20
+ return (
21
+ <aside className="ds-slideover">
22
+ <div className={classNames('ds-slideover__header', { 'ds-slideover__header--center': centerHeaderText })}>
23
+ <Button type="tertiary" onClick={SlideoverUtils.removeSlideover}>
24
+ <Icon name="chevrons-left" />
25
+ Back
26
+ </Button>
27
+ <Heading level={2}>
28
+ {title}
29
+ {headerIcon && <Icon name={headerIcon} />}
30
+ </Heading>
31
+ </div>
32
+
33
+ <div className="ds-slideover__contents">{children}</div>
34
+
35
+ {footerContents && <div className="ds-slideover__footer">{footerContents}</div>}
36
+ </aside>
37
+ );
38
+ };
@@ -0,0 +1,50 @@
1
+ .ds-slideover {
2
+ background-color: var(--slideover-color-background);
3
+ position: fixed;
4
+ right: 0;
5
+ height: 100%;
6
+ width: 580px;
7
+ display: flex;
8
+ flex-direction: column;
9
+ align-items: stretch;
10
+
11
+ &__header {
12
+ background-color: var(--slideover-header-color-background);
13
+ display: flex;
14
+ align-items: center;
15
+ padding: var(--slideover-header-spacing-padding);
16
+
17
+ .ds-heading {
18
+ flex-grow: 1;
19
+ font-size: var(--type-headings-h2-size);
20
+ padding: 0;
21
+ margin: 0;
22
+ }
23
+
24
+ &--center {
25
+ .ds-heading {
26
+ justify-content: center;
27
+ gap: var(--slideover-header-spacing-padding);
28
+ }
29
+ }
30
+ }
31
+
32
+ &__contents {
33
+ flex-grow: 1;
34
+ overflow-y: scroll;
35
+ display: flex;
36
+ flex-direction: column;
37
+ align-items: stretch;
38
+ gap: var(--slideover-spacing-padding);
39
+ padding: var(--slideover-spacing-padding);
40
+ }
41
+
42
+ &__footer {
43
+ padding: var(--slideover-footer-spacing-vertical) var(--slideover-footer-spacing-horizontal);
44
+ background-color: var(--slideover-footer-color-background);
45
+ display: flex;
46
+ align-items: center;
47
+ align-self: stretch;
48
+ justify-content: flex-end;
49
+ }
50
+ }