@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,493 @@
1
+ import React from "react";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import { vi } from "vitest";
4
+ import { axe } from "vitest-axe";
5
+ import { Select, SelectProps } from "./Select";
6
+
7
+ describe("Select Component", () => {
8
+ const options = [
9
+ { name: "Option 1", value: "1" },
10
+ { name: "Option 2", value: "2" },
11
+ { name: "Option 3", value: "3" },
12
+ ];
13
+
14
+ it("renders the select dropdown with the default state, error, and variant fill", () => {
15
+ render(<Select variant="fill" error options={options} setSelectedOption={vi.fn()} />);
16
+ const button = screen.getByRole("button", { name: /Options/i });
17
+
18
+ //clicks the button twice to open and close the menu, verifying correct chevrons render
19
+ fireEvent.click(button);
20
+ fireEvent.click(button);
21
+
22
+ expect(button).toBeInTheDocument();
23
+ expect(button).toHaveTextContent("Options");
24
+ expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
25
+ });
26
+
27
+ it("renders the dropdown label when provided", () => {
28
+ render(<Select label="Dropdown Label" options={options} setSelectedOption={vi.fn()} />);
29
+ const label = screen.getByText("Dropdown Label");
30
+
31
+ expect(label).toBeInTheDocument();
32
+ expect(label).toHaveClass("text-black");
33
+ });
34
+
35
+ it("toggles the dropdown menu on button click", () => {
36
+ render(<Select options={options} setSelectedOption={vi.fn()} />);
37
+ const button = screen.getByRole("button");
38
+
39
+ // Menu should not be present initially
40
+ expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
41
+
42
+ // Open the dropdown
43
+ fireEvent.click(button);
44
+ expect(screen.getByRole("listbox")).toBeInTheDocument();
45
+
46
+ // Close the dropdown
47
+ fireEvent.click(button);
48
+ expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
49
+ });
50
+
51
+
52
+ it("selects an option and calls the callback", () => {
53
+ const setSelectedOptionMock = vi.fn();
54
+ render(<Select options={options} setSelectedOption={setSelectedOptionMock} />);
55
+
56
+ const button = screen.getByRole("button");
57
+ fireEvent.click(button);
58
+
59
+ const option = screen.getByText("Option 1");
60
+ fireEvent.click(option);
61
+
62
+ expect(setSelectedOptionMock).toHaveBeenCalledWith("1");
63
+ expect(button).toHaveTextContent("Option 1");
64
+ expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
65
+ });
66
+
67
+ it("falls back to the option name if no value is provided", () => {
68
+ const customOptions = [{ name: "No Value Option" }];
69
+ const setSelectedOptionMock = vi.fn();
70
+
71
+ render(<Select options={customOptions} setSelectedOption={setSelectedOptionMock} />);
72
+ const button = screen.getByRole("button");
73
+
74
+ fireEvent.click(button);
75
+
76
+ const option = screen.getByText("No Value Option");
77
+ fireEvent.click(option);
78
+
79
+ expect(setSelectedOptionMock).toHaveBeenCalledWith("No Value Option");
80
+ expect(button).toHaveTextContent("No Value Option");
81
+ });
82
+
83
+ it("closes the dropdown when clicking outside", () => {
84
+ render(<Select options={options} setSelectedOption={vi.fn()} />);
85
+ const button = screen.getByRole("button");
86
+
87
+ // Open the dropdown
88
+ fireEvent.click(button);
89
+ expect(screen.getByRole("listbox")).toBeInTheDocument();
90
+
91
+ // Click outside
92
+ fireEvent.mouseDown(document.body);
93
+ expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
94
+ });
95
+
96
+
97
+ it("applies custom classes and merges them with default styles", () => {
98
+ render(
99
+ <Select
100
+ className="custom-class"
101
+ options={options}
102
+ setSelectedOption={vi.fn()}
103
+ />
104
+ );
105
+
106
+ const button = screen.getByRole("button");
107
+ expect(button).toHaveClass("custom-class");
108
+ });
109
+
110
+ it("applies variant-specific styles", () => {
111
+ render(<Select variant="outline" options={options} setSelectedOption={vi.fn()} />);
112
+ const button = screen.getByRole("button");
113
+
114
+ expect(button).toHaveClass("border-[#b3b3b3] bg-white border");
115
+ });
116
+
117
+ it("renders a disabled dropdown", () => {
118
+ render(
119
+ <Select
120
+ options={options}
121
+ setSelectedOption={vi.fn()}
122
+ disabled
123
+ />
124
+ );
125
+
126
+ const button = screen.getByRole("button");
127
+ expect(button).toBeDisabled();
128
+ expect(button).toHaveClass("disabled:bg-dha-mc-bottom-nav-background disabled:text-dha-mc-checkbox-inactive");
129
+ });
130
+
131
+
132
+ it('falls back to the default variant when an invalid variant is provided', () => {
133
+ const { container } = render(
134
+ <Select
135
+ options={options}
136
+ variant="InvalidVariant"
137
+ setSelectedOption={vitest.fn()}
138
+ />
139
+ );
140
+
141
+ // Verify that the classes for the "default" variant are applied
142
+ const button = container.querySelector('button');
143
+ expect(button).toHaveClass('hover:bg-gray-200');
144
+ });
145
+
146
+ });
147
+
148
+
149
+ describe('Select Accessibility Tests', () => {
150
+ const mockSetSelectedOption = vi.fn();
151
+ const options = [
152
+ { name: 'Option 1', value: '1' },
153
+ { name: 'Option 2', value: '2' },
154
+ { name: 'Option 3', value: '3' },
155
+ ];
156
+
157
+ const renderComponent = (props: Partial<SelectProps> = {}) =>
158
+ render(
159
+ <Select
160
+ label="Choose an option"
161
+ options={options}
162
+ setSelectedOption={mockSetSelectedOption}
163
+ {...props}
164
+ />
165
+ );
166
+
167
+ it('should have no accessibility violations in the default state', async () => {
168
+ const { container } = renderComponent();
169
+ const results = await axe(container);
170
+ expect(results).toHaveNoViolations();
171
+ });
172
+
173
+ it('should have no accessibility violations when open', async () => {
174
+ const { container, getByRole } = renderComponent();
175
+ const button = getByRole('button', { name: /Select options/i });
176
+ fireEvent.click(button);
177
+ const results = await axe(container);
178
+ expect(results).toHaveNoViolations();
179
+ });
180
+
181
+ it('should include a label if provided', () => {
182
+ const { getByText } = renderComponent({ label: 'Accessible Label' });
183
+ expect(getByText('Accessible Label')).toBeInTheDocument();
184
+ });
185
+
186
+ it('should render options correctly', () => {
187
+ const { getByRole, getByLabelText } = renderComponent();
188
+ const button = getByRole('button', { name: /Select options/i });
189
+ fireEvent.click(button);
190
+
191
+ options.forEach((option) => {
192
+ const optionElement = getByLabelText(`option ${option.name}`);
193
+ expect(optionElement).toBeInTheDocument();
194
+ });
195
+ });
196
+
197
+ it('should allow selecting an option', () => {
198
+ const { getByRole, getByLabelText } = renderComponent();
199
+ const button = getByRole('button', { name: /Select options/i });
200
+ fireEvent.click(button);
201
+
202
+ const optionToSelect = getByLabelText('option Option 2');
203
+ fireEvent.click(optionToSelect);
204
+
205
+ expect(mockSetSelectedOption).toHaveBeenCalledWith('2');
206
+ expect(button).toHaveTextContent('Option 2');
207
+ });
208
+
209
+ it('should support disabled state', () => {
210
+ const { getByRole } = renderComponent({ disabled: true });
211
+ const button = getByRole('button', { name: /Select options/i });
212
+ expect(button).toBeDisabled();
213
+ });
214
+
215
+ it('should handle custom classes without affecting accessibility', async () => {
216
+ const { container } = renderComponent({ className: 'custom-class' });
217
+ const results = await axe(container);
218
+ expect(results).toHaveNoViolations();
219
+ });
220
+
221
+ it('should handle an empty options array gracefully', () => {
222
+ const { queryByRole } = renderComponent({ options: [] });
223
+ const dropdown = queryByRole('listbox');
224
+ expect(dropdown).not.toBeInTheDocument();
225
+ });
226
+
227
+ it('should close the dropdown when clicking outside', () => {
228
+ const { getByRole, container } = renderComponent();
229
+ const button = getByRole('button', { name: /Options/i });
230
+ fireEvent.click(button);
231
+
232
+ fireEvent.mouseDown(container);
233
+ expect(button).toHaveAttribute('aria-expanded', 'false');
234
+ });
235
+
236
+
237
+ it('should maintain focus management for keyboard users', () => {
238
+ const options = [
239
+ { name: 'Option 1', value: '1' },
240
+ { name: 'Option 2', value: '2' },
241
+ { name: 'Option 3', value: '3' },
242
+ ];
243
+
244
+ const { getByRole, getByLabelText } = renderComponent({
245
+ options,
246
+ optionsLabel: 'Choose an option',
247
+ });
248
+
249
+ const button = getByRole('button', { name: /Choose an option/i });
250
+ fireEvent.click(button); // Open the dropdown
251
+
252
+ const firstOption = getByLabelText('option Option 1');
253
+ fireEvent.keyDown(firstOption, { key: 'Enter', code: 'Enter' }); // Simulate Enter key press
254
+ // fireEvent.click(firstOption); // works, meaning keyboard is NOT. Need to fix ...
255
+
256
+ expect(mockSetSelectedOption).toHaveBeenCalledWith('1'); // Verify correct value is selected
257
+ expect(button).toHaveTextContent('Option 1'); // Verify button displays the selected option
258
+ });
259
+
260
+ it('should move focus to the next option with ArrowDown key', () => {
261
+ renderComponent();
262
+ const button = screen.getByRole('button', { name: /Select options/i });
263
+ fireEvent.click(button);
264
+
265
+ const listbox = screen.getByRole('listbox');
266
+
267
+ // Press ArrowDown
268
+ fireEvent.keyDown(listbox, { key: 'ArrowDown' });
269
+
270
+ // Second option should be focused (because the first key press advances the focus to first option)
271
+ expect(screen.getByRole('option', { name: 'option Option 2' })).toHaveFocus();
272
+
273
+ // Press ArrowDown again
274
+ fireEvent.keyDown(listbox, { key: 'ArrowDown' });
275
+
276
+ // Third option should now be focused
277
+ expect(screen.getByRole('option', { name: 'option Option 3' })).toHaveFocus();
278
+ });
279
+
280
+ it('should move focus to the previous option with ArrowUp key', () => {
281
+ renderComponent();
282
+ const button = screen.getByRole('button', { name: /Select options/i });
283
+ fireEvent.click(button);
284
+
285
+ const listbox = screen.getByRole('listbox');
286
+
287
+ // Press ArrowUp
288
+ fireEvent.keyDown(listbox, { key: 'ArrowUp' });
289
+
290
+ // Wraps around to the last option
291
+ expect(screen.getByRole('option', { name: 'option Option 3' })).toHaveFocus();
292
+
293
+ // Press ArrowUp again
294
+ fireEvent.keyDown(listbox, { key: 'ArrowUp' });
295
+
296
+ // Second option should be focused
297
+ expect(screen.getByRole('option', { name: 'option Option 2' })).toHaveFocus();
298
+ });
299
+
300
+
301
+ it('should move focus to the first option with Home key', () => {
302
+ renderComponent();
303
+ const button = screen.getByRole('button', { name: /Select options/i });
304
+ fireEvent.click(button);
305
+
306
+ const listbox = screen.getByRole('listbox');
307
+
308
+ // Press Home
309
+ fireEvent.keyDown(listbox, { key: 'Home' });
310
+
311
+ // First option should be focused
312
+ expect(screen.getByRole('option', { name: 'option Option 1' })).toHaveFocus();
313
+ });
314
+
315
+ it('should move focus to the last option with End key', () => {
316
+ renderComponent();
317
+ const button = screen.getByRole('button', { name: /Select options/i });
318
+ fireEvent.click(button);
319
+
320
+ const listbox = screen.getByRole('listbox');
321
+
322
+ // Press End
323
+ fireEvent.keyDown(listbox, { key: 'End' });
324
+
325
+ // Last option should be focused
326
+ expect(screen.getByRole('option', { name: 'option Option 3' })).toHaveFocus();
327
+ });
328
+
329
+ // it('should call preventDefault for navigation keys', () => {
330
+ // renderComponent();
331
+ // const button = screen.getByRole('button', { name: /Select options/i });
332
+ // fireEvent.click(button);
333
+
334
+ // const listbox = screen.getByRole('listbox');
335
+
336
+ // // Spy on preventDefault
337
+ // const preventDefaultSpy = vi.fn();
338
+
339
+ // // Simulate the keydown event
340
+ // fireEvent.keyDown(listbox, {
341
+ // key: 'ArrowDown',
342
+ // preventDefault: preventDefaultSpy,
343
+ // });
344
+
345
+ // // Ensure preventDefault was called
346
+ // expect(preventDefaultSpy).toHaveBeenCalled();
347
+ // });
348
+
349
+ it('should close the select box when Escape key is pressed', () => {
350
+ // Render the component
351
+ renderComponent();
352
+
353
+ // Open the dropdown
354
+ const button = screen.getByRole('button', { name: /Select options/i });
355
+ fireEvent.click(button);
356
+
357
+ // Verify that the dropdown is open
358
+ expect(screen.getByRole('listbox')).toBeInTheDocument();
359
+
360
+ // Press the Escape key
361
+ fireEvent.keyDown(screen.getByRole('listbox'), { key: 'Escape' });
362
+
363
+ // Verify that the dropdown is closed
364
+ expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
365
+ });
366
+
367
+ it('should close the dropdown when focus moves outside', () => {
368
+ renderComponent();
369
+
370
+ // Open the dropdown
371
+ const button = screen.getByRole('button', { name: /Select options/i });
372
+ fireEvent.click(button);
373
+ expect(screen.getByRole('listbox')).toBeInTheDocument();
374
+
375
+ // Simulate focus moving outside
376
+ fireEvent.focusIn(document.body);
377
+
378
+ // Verify the dropdown is closed
379
+ expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
380
+ });
381
+
382
+ it('should select an option when Enter is pressed (using event.code)', () => {
383
+ renderComponent();
384
+
385
+ // Open the dropdown
386
+ const button = screen.getByRole('button', { name: /Select options/i });
387
+ fireEvent.click(button);
388
+
389
+ // Verify that the dropdown is open
390
+ expect(screen.getByRole('listbox')).toBeInTheDocument();
391
+
392
+ // Press Enter (using event.code)
393
+ fireEvent.keyDown(screen.getByRole('option', { name: 'option Option 1' }), {
394
+ code: 'Enter',
395
+ });
396
+
397
+ // Verify that the dropdown is closed
398
+ expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
399
+ });
400
+
401
+ it('should select an option when Space key is pressed', () => {
402
+ renderComponent();
403
+
404
+ // Open the dropdown
405
+ const button = screen.getByRole('button', { name: /Select options/i });
406
+ fireEvent.click(button);
407
+
408
+ // Verify that the dropdown is open
409
+ expect(screen.getByRole('listbox')).toBeInTheDocument();
410
+
411
+ // Press Space key
412
+ fireEvent.keyDown(screen.getByRole('option', { name: 'option Option 1' }), {
413
+ key: ' ',
414
+ });
415
+
416
+ // Verify that the dropdown is closed
417
+ expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
418
+ });
419
+
420
+ it('should close the select box when Escape key is pressed without selecting an option', () => {
421
+ renderComponent();
422
+
423
+ // Open the dropdown
424
+ const button = screen.getByRole('button', { name: /Select options/i });
425
+ fireEvent.click(button);
426
+
427
+ // Verify that the dropdown is open
428
+ expect(screen.getByRole('listbox')).toBeInTheDocument();
429
+
430
+ // Press Escape key on an option
431
+ fireEvent.keyDown(screen.getByRole('option', { name: 'option Option 1' }), {
432
+ key: 'Escape',
433
+ });
434
+
435
+ // Verify that the dropdown is closed
436
+ expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
437
+
438
+ // Ensure no option was selected
439
+ expect(button).toHaveTextContent('Options');
440
+ });
441
+
442
+ it("positions dropdown above the button when there is insufficient space below", () => {
443
+ // Set global dimensions so that containerHeight is 600.
444
+ Object.defineProperty(window, "innerHeight", {
445
+ writable: true,
446
+ configurable: true,
447
+ value: 600,
448
+ });
449
+ Object.defineProperty(document.body, "offsetHeight", {
450
+ writable: true,
451
+ configurable: true,
452
+ value: 600,
453
+ });
454
+
455
+ const setSelectedOptionMock = vi.fn();
456
+ const { container } = render(<Select options={options} setSelectedOption={setSelectedOptionMock} />);
457
+
458
+ // Override container's bounding rect to simulate a button with top:200 and bottom:500.
459
+ const outerContainer = container.firstChild as HTMLElement;
460
+ outerContainer.getBoundingClientRect = () => ({
461
+ top: 200,
462
+ bottom: 500, // available space below = 600 - 500 = 100
463
+ left: 0,
464
+ right: 600,
465
+ width: 600,
466
+ height: 300,
467
+ x: 0,
468
+ y: 0,
469
+ toJSON: () => {('')}
470
+ });
471
+
472
+ // Save the original descriptor for offsetHeight.
473
+ const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLDivElement.prototype, 'offsetHeight');
474
+ // Override offsetHeight so that any element with role "listbox" reports a height of 150.
475
+ Object.defineProperty(HTMLDivElement.prototype, 'offsetHeight', {
476
+ configurable: true,
477
+ get: function () {
478
+ if (this.getAttribute && this.getAttribute('role') === 'listbox') {
479
+ return 150;
480
+ }
481
+ return originalOffsetHeight && originalOffsetHeight.get ? originalOffsetHeight.get.call(this) : 0;
482
+ },
483
+ });
484
+
485
+ // Open the dropdown so that updateDropdownPosition runs.
486
+ const button = screen.getByRole("button");
487
+ fireEvent.click(button);
488
+
489
+ // With spaceBelow (100) < dropdownHeight (150) and container top (200) > 150,
490
+ // updateDropdownPosition should set isAbove to true, causing the button to have "rounded-b-lg".
491
+ expect(button.className).toMatch(/rounded-b-lg/);
492
+ });
493
+ });