@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,275 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { describe, it, expect, vi } from "vitest";
3
+ import { Shield, ShieldProps } from './Shield';
4
+ import React, { createRef } from 'react';
5
+ import { axe } from "vitest-axe";
6
+ import closeIcon from '../assets/img/close_small.svg';
7
+ import warning from '../assets/img/warning.svg';
8
+ import emergency from '../assets/img/emergency_home.svg';
9
+ import thumbUp from '../assets/img/thumb_up.svg';
10
+
11
+
12
+ const imagePath = new URL('/src/assets/pill.svg', import.meta.url).href;
13
+
14
+ describe('Shield', () => {
15
+
16
+ it('renders the correct icon for variant "warning"', () => {
17
+ render(<Shield variant="warning" subVariant="half">Warning Shield</Shield>);
18
+ const icon = screen.getByRole('img', { name: /warning icon/i });
19
+ expect(icon).toHaveAttribute('src', warning);
20
+ });
21
+
22
+ it('renders the correct icon for variant "hazard"', () => {
23
+ render(<Shield variant="hazard" subVariant="half">Hazard Shield</Shield>);
24
+ const icon = screen.getByRole('img', { name: /hazard icon/i });
25
+ expect(icon).toHaveAttribute('src', emergency);
26
+ });
27
+
28
+ it('renders the correct icon for variant "thumbUp"', () => {
29
+ render(<Shield variant="go" subVariant="half">Thumb Up Shield</Shield>);
30
+ const icon = screen.getByRole('img', { name: /go icon/i });
31
+ expect(icon).toHaveAttribute('src', thumbUp);
32
+ });
33
+
34
+ it('renders children inside the Shield', () => {
35
+ render(<Shield variant="warning" subVariant="half">This is a warning</Shield>);
36
+ expect(screen.getByText('This is a warning')).toBeInTheDocument();
37
+ });
38
+
39
+ it('closes the Shield when the close button is clicked', () => {
40
+ render(<Shield variant="warning" subVariant="half">Closable Shield</Shield>);
41
+
42
+ const shieldElement = screen.getByText('Closable Shield').parentElement;
43
+ const closeButton = screen.getByRole('button', { name: /close modal/i });
44
+
45
+ expect(shieldElement).toBeVisible();
46
+
47
+ fireEvent.click(closeButton);
48
+
49
+ expect(shieldElement).toHaveClass('hidden');
50
+ });
51
+
52
+ it('renders the close button with the correct icon', () => {
53
+ render(<Shield variant="warning" subVariant="half">Shield with Close</Shield>);
54
+
55
+ const closeIconImg = screen.getByRole('img', { name: /close alert icon/i });
56
+
57
+ expect(closeIconImg).toHaveAttribute('src', closeIcon);
58
+ });
59
+
60
+ // ---
61
+
62
+
63
+ // Test basic render - default variant / default subvariant
64
+ it('should render successfully', () => {
65
+ const { baseElement } = render(<Shield variant={'default'} subVariant={'default'}>
66
+ <h1>Heading</h1>
67
+ <p>Content Body</p>
68
+ </Shield>);
69
+ expect(baseElement).toBeTruthy();
70
+ });
71
+
72
+ // Test basic render - icon variant / blue subvariant
73
+ it('should render successfully', () => {
74
+ const { baseElement } = render(<Shield variant={'icon'} subVariant={'blue'}>
75
+ I'm a Shield!
76
+ </Shield>);
77
+ expect(baseElement).toBeTruthy();
78
+ });
79
+
80
+ // Test basic render - media variant / default subvariant
81
+ it('should render successfully', () => {
82
+ const { baseElement } = render(<Shield variant={'media'} subVariant={'default'} imagePath={imagePath} imageAlt='Pill icon'>
83
+ <h1>Heading</h1>
84
+ <p>Content Body</p>
85
+ </Shield>);
86
+ expect(baseElement).toBeTruthy();
87
+ });
88
+
89
+ // Test diff variant & size props
90
+ it('should apply the correct class for the default variant', () => {
91
+ const { container } = render(<Shield variant="default" subVariant={'default'} >I'm a Shield!</Shield>);
92
+ const shieldElement = container.firstChild as HTMLElement;
93
+ expect(shieldElement.className).toContain('bg-gray-500/10');
94
+ });
95
+
96
+
97
+ it('should apply the correct class for the icon variant', () => {
98
+ const { container } = render(<Shield variant="icon" subVariant={'default'} >I'm a Shield!</Shield>);
99
+ const shieldElement = container.firstChild as HTMLElement;
100
+ expect(shieldElement.className).toContain('bg-gray-500');
101
+ });
102
+
103
+
104
+ it('should apply the correct class for media variant', () => {
105
+ const { container } = render(<Shield variant={'media'} subVariant={'default'} >
106
+ <h1>Heading</h1>
107
+ <p>Body content</p>
108
+ </Shield>);
109
+ const shieldElement = container.firstChild as HTMLElement;
110
+ expect(shieldElement.className).toContain('w-[250px] mx-2 border-l-8 border-black grid grid-cols-6 rounded-md bg-gray-400/10 pr-2 text-xs font-medium text-gray-400 ring-1 ring-inset ring-gray-400/20 overflow-hidden');
111
+ });
112
+
113
+ // Test image prop and accessibility
114
+ it('should render an image with the correct alt text', () => {
115
+ const { container } = render(<Shield variant={'media'} subVariant={'default'} imagePath={imagePath} imageAlt='Test Image'>
116
+ <h1>Heading</h1>
117
+ <p>Body Content</p>
118
+ </Shield>);
119
+ const image = container.querySelector('img');
120
+ expect(image).toBeTruthy();
121
+ // expect(image?.getAttribute('src')).toBe('path/to/image.jpg');
122
+ expect(image?.getAttribute('alt')).toBe('Test Image');
123
+ });
124
+
125
+ it('renders without children if none are provided', () => {
126
+ const { container } = render(<Shield variant="default" subVariant="gray" />);
127
+ const shield = container.firstChild as HTMLElement;
128
+ expect(shield).toBeInTheDocument();
129
+ expect(shield).toHaveClass('mx-2 inline-flex items-center rounded-md bg-gray-500/10 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-400/20');
130
+ });
131
+
132
+ it('renders a custom media shield with additional image classes', () => {
133
+ render(
134
+ <Shield
135
+ variant="media"
136
+ subVariant="green"
137
+ imagePath="/path/to/image.jpg"
138
+ imageAlt="Custom Image"
139
+ classNameImage="rounded-lg"
140
+ >
141
+ Custom Media Shield
142
+ </Shield>
143
+ );
144
+ const img = screen.getByAltText('Custom Image');
145
+ expect(img).toHaveClass('h-full rounded-lg');
146
+ });
147
+
148
+ // handles empty props gracefully
149
+ it('handles empty props gracefully', () => {
150
+ const { container } = render(<Shield variant="default" subVariant="default" />);
151
+ const shield = container.firstChild as HTMLElement;
152
+ expect(shield).toBeInTheDocument();
153
+ });
154
+
155
+ it('should forward the ref to the correct DOM element', () => {
156
+ const ref = createRef<HTMLDivElement>();
157
+ render(
158
+ <Shield ref={ref} variant="default" subVariant="default">
159
+ Shield with Ref
160
+ </Shield>
161
+ );
162
+
163
+ // Confirm the ref is attached to the span element
164
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
165
+ expect(ref.current?.textContent).toBe('Shield with Ref');
166
+ });
167
+
168
+ it('should forward the ref to the correct element for media variant', () => {
169
+ const ref = createRef<HTMLDivElement>();
170
+ render(
171
+ <Shield
172
+ ref={ref}
173
+ variant="media"
174
+ subVariant="default"
175
+ imagePath="/path/to/image.jpg"
176
+ imageAlt="Media Image"
177
+ >
178
+ Media Shield with Ref
179
+ </Shield>
180
+ );
181
+
182
+ // Confirm the ref is attached to the div element
183
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
184
+ expect(ref.current?.textContent).toContain('Media Shield with Ref');
185
+ });
186
+
187
+
188
+ it('merges custom classNameSvg with default classNameSvg for "icon" variant and "custom" subVariant', () => {
189
+ const customSvgClasses = 'custom-size custom-fill';
190
+
191
+ // Render the Shield component with the "icon" variant and "custom" subVariant
192
+ render(
193
+ <Shield variant="icon" subVariant="custom" classNameSvg={customSvgClasses}>
194
+ Icon Shield
195
+ </Shield>
196
+ );
197
+
198
+ // Use act to ensure updates triggered by useEffect are applied
199
+ const svgElement = screen.getByRole('img', { hidden: true }); // Finds the SVG element
200
+
201
+ // Verify that the final svgClassValue is merged correctly
202
+ expect(svgElement).toHaveClass('size-1.5 fill-gray-400'); // From "default" subVariant in "icon" variant
203
+ expect(svgElement).toHaveClass('custom-size custom-fill'); // Custom svgClasses
204
+ });
205
+
206
+ });
207
+
208
+ describe('Shield Accessibility Tests', () => {
209
+ const renderComponent = (props: ShieldProps) => render(<Shield {...props}>Accessible Shield</Shield>);
210
+
211
+ it('should have no accessibility violations for default variant', async () => {
212
+ const { container } = renderComponent({ variant: 'default', subVariant: 'default' });
213
+ const results = await axe(container);
214
+ expect(results).toHaveNoViolations();
215
+ });
216
+
217
+ it('should have no accessibility violations for icon variant', async () => {
218
+ const { container } = renderComponent({ variant: 'icon', subVariant: 'blue' });
219
+ const results = await axe(container);
220
+ expect(results).toHaveNoViolations();
221
+ });
222
+
223
+ it('should have no accessibility violations for media variant with image', async () => {
224
+ const { container } = renderComponent({
225
+ variant: 'media',
226
+ subVariant: 'green',
227
+ imagePath: 'https://via.placeholder.com/50',
228
+ imageAlt: 'Sample Image',
229
+ });
230
+ const results = await axe(container);
231
+ expect(results).toHaveNoViolations();
232
+ });
233
+
234
+ it('should include alt text for images in media variant', () => {
235
+ const { getByAltText } = renderComponent({
236
+ variant: 'media',
237
+ subVariant: 'green',
238
+ imagePath: 'https://via.placeholder.com/50',
239
+ imageAlt: 'Accessible Image',
240
+ });
241
+ expect(getByAltText('Accessible Image')).toBeTruthy();
242
+ });
243
+
244
+ it('should not have duplicate or missing roles in icon variant', async () => {
245
+ const { container } = renderComponent({ variant: 'icon', subVariant: 'red' });
246
+ const svg = container.querySelector('svg');
247
+ expect(svg).toHaveAttribute('role', 'img');
248
+ expect(svg).toHaveAttribute('aria-hidden', 'true');
249
+ });
250
+
251
+ it('should support custom class merging without accessibility issues', async () => {
252
+ const { container } = renderComponent({
253
+ variant: 'default',
254
+ subVariant: 'custom',
255
+ className: 'custom-class',
256
+ });
257
+ const results = await axe(container);
258
+ expect(results).toHaveNoViolations();
259
+ });
260
+
261
+ it('should handle empty or missing children gracefully', async () => {
262
+ const { container } = renderComponent({ variant: 'default', subVariant: 'default', children: null });
263
+ const results = await axe(container);
264
+ expect(results).toHaveNoViolations();
265
+ });
266
+
267
+ it('should support ARIA attributes for assistive technologies', () => {
268
+ const { container } = renderComponent({
269
+ variant: 'icon',
270
+ subVariant: 'yellow',
271
+ 'aria-label': 'Yellow Shield',
272
+ });
273
+ expect(container.querySelector('div')).toHaveAttribute('aria-label', 'Yellow Shield');
274
+ });
275
+ });
@@ -0,0 +1,239 @@
1
+ import { forwardRef, HTMLAttributes, ReactNode, useEffect, useState } from 'react';
2
+ import { twMerge } from 'tailwind-merge';
3
+ import thumbUp from '../assets/img/thumb_up.svg';
4
+ import warning from '../assets/img/warning.svg';
5
+ import emergency from '../assets/img/emergency_home.svg';
6
+ import closeIcon from '../assets/img/close_small.svg'
7
+
8
+ interface VariantType {
9
+ variant: string; // type of shield
10
+ subVariant: string; // variant of shield
11
+ classes?: string; // class property for this variant
12
+ classNameSvg?: string; // classNameSvg if relevant
13
+ }
14
+
15
+ const variants: VariantType[] = [
16
+ {
17
+ variant: 'default',
18
+ subVariant: 'default',
19
+ classes: 'mx-2 inline-flex items-center rounded-md bg-gray-500/10 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-400/20'
20
+ },
21
+ {
22
+ variant: 'default',
23
+ subVariant: 'gray',
24
+ classes: 'mx-2 inline-flex items-center rounded-md bg-gray-500/10 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-400/20'
25
+ },
26
+ {
27
+ variant: 'default',
28
+ subVariant: 'red',
29
+ classes: 'mx-2 inline-flex items-center rounded-md bg-red-500/10 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-400/20'
30
+ },
31
+ {
32
+ variant: 'default',
33
+ subVariant: 'yellow',
34
+ classes: 'mx-2 inline-flex items-center rounded-md bg-yellow-500/10 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-yellow-400/20'
35
+ },
36
+ {
37
+ variant: 'default',
38
+ subVariant: 'green',
39
+ classes: 'mx-2 inline-flex items-center rounded-md bg-green-500/10 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-500/20'
40
+ },
41
+ {
42
+ variant: 'default',
43
+ subVariant: 'blue',
44
+ classes: 'mx-2 inline-flex items-center rounded-md bg-blue-400/10 px-2 py-1 text-xs font-medium text-blue-400 ring-1 ring-inset ring-blue-400/30'
45
+ },
46
+
47
+ {
48
+ variant: 'warning',
49
+ subVariant: 'half',
50
+ classes: 'flex flex-row w-1/2 bg-[#fff1be] border-[#efbd1f] p-2 rounded-md border justify-center items-start gap-2.5 inline-flex">'
51
+ },
52
+ {
53
+ variant: 'warning',
54
+ subVariant: 'full',
55
+ classes: 'flex flex-row w-full bg-[#fff1be] border-[#efbd1f] p-2 rounded-md border justify-center items-start gap-2.5 inline-flex'
56
+ },
57
+
58
+ {
59
+ variant: 'hazard',
60
+ subVariant: 'half',
61
+ classes: 'flex flex-row w-1/2 bg-[#f4c2c2] border-[#efbd1f] p-2 rounded-md border justify-center items-start gap-2.5 inline-flex">'
62
+ },
63
+ {
64
+ variant: 'hazard',
65
+ subVariant: 'full',
66
+ classes: 'flex flex-row w-full bg-[#f4c2c2] border-[#efbd1f] p-2 rounded-md border justify-center items-start gap-2.5 inline-flex">'
67
+ },
68
+
69
+ {
70
+ variant: 'go',
71
+ subVariant: 'half',
72
+ classes: 'flex flex-row w-1/2 bg-[#d6f4d5] border-[#40bf40] p-2 rounded-md border justify-center items-start gap-2.5 inline-flex">'
73
+ },
74
+ {
75
+ variant: 'go',
76
+ subVariant: 'full',
77
+ classes: 'flex flex-row w-full bg-[#d6f4d5] border-[#40bf40] p-2 rounded-md border justify-center items-start gap-2.5 inline-flex">'
78
+ },
79
+
80
+
81
+ {
82
+ variant: 'icon',
83
+ subVariant: 'default',
84
+ classes: 'inline-flex items-center gap-x-1.5 rounded-full bg-gray-500 px-2 py-1 text-xs font-medium text-gray-700',
85
+ classNameSvg: 'size-1.5 fill-gray-400'
86
+ },
87
+ {
88
+ variant: 'icon',
89
+ subVariant: 'gray',
90
+ classes: 'inline-flex items-center gap-x-1.5 rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700',
91
+ classNameSvg: 'size-1.5 fill-gray-400'
92
+ },
93
+ {
94
+ variant: 'icon',
95
+ subVariant: 'red',
96
+ classes: 'inline-flex items-center gap-x-1.5 rounded-full bg-red-100 px-2 py-1 text-xs font-medium text-red-700',
97
+ classNameSvg: 'size-1.5 fill-red-400'
98
+ },
99
+ {
100
+ variant: 'icon',
101
+ subVariant: 'yellow',
102
+ classes: 'inline-flex items-center gap-x-1.5 rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-700',
103
+ classNameSvg: 'size-1.5 fill-yellow-400'
104
+ },
105
+ {
106
+ variant: 'icon',
107
+ subVariant: 'green',
108
+ classes: 'inline-flex items-center gap-x-1.5 rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700',
109
+ classNameSvg: 'size-1.5 fill-green-400'
110
+ },
111
+ {
112
+ variant: 'icon',
113
+ subVariant: 'blue',
114
+ classes: 'inline-flex items-center gap-x-1.5 rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700',
115
+ classNameSvg: 'size-1.5 fill-blue-400'
116
+ },
117
+ {
118
+ variant: 'media',
119
+ subVariant: 'default',
120
+ classes: 'w-[250px] mx-2 border-l-8 border-black grid grid-cols-6 rounded-md bg-gray-400/10 pr-2 text-xs font-medium text-gray-400 ring-1 ring-inset ring-gray-400/20 overflow-hidden',
121
+ },
122
+ {
123
+ variant: 'media',
124
+ subVariant: 'gray',
125
+ classes: 'w-[250px] mx-2 border-l-8 border-black grid grid-cols-6 rounded-md bg-gray-400/10 pr-2 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-400/20 overflow-hidden',
126
+ },
127
+ {
128
+ variant: 'media',
129
+ subVariant: 'green',
130
+ classes: 'w-[250px] mx-2 border-l-8 border-black grid grid-cols-6 rounded-md bg-gray-400/10 pr-2 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-400/20 overflow-hidden',
131
+ },
132
+ ]
133
+
134
+ export interface ShieldProps extends HTMLAttributes<HTMLDivElement> {
135
+ variant: string;
136
+ subVariant: string;
137
+ className?: string;
138
+ classNameSvg?: string;
139
+ imagePath?: string;
140
+ imageAlt?: string;
141
+ classNameImage?: string;
142
+ children?: ReactNode;
143
+ }
144
+
145
+ export const Shield = forwardRef<HTMLDivElement, ShieldProps>(
146
+ ({ variant = 'default', subVariant = 'default', className,
147
+ classNameSvg = 'size-1.5 fill-yellow-400', imagePath, imageAlt,
148
+ classNameImage, children, ...props }, ref) => {
149
+
150
+ const [classValue, setClassValue] = useState<string>();
151
+ const [svgClassValue, setSvgClassValue] = useState<string>();
152
+ const [svgSource, setSvgSource] = useState(warning);
153
+
154
+ const onClose = () => {
155
+ setClassValue(twMerge(classValue, 'hidden'));
156
+ }
157
+
158
+ // set selected value for classValue and svgClassValue
159
+ useEffect(() => {
160
+
161
+ // If dev specifies 'custom' for subVariant, use 'default' classes as the base
162
+ // so we can merge their custom classes later using twMerge
163
+ const tempSubVariant = (subVariant === 'custom') ? 'default' : subVariant;
164
+
165
+ const tempClasses = variants.find(
166
+ (v) => v.variant === variant && v.subVariant === tempSubVariant
167
+ )?.classes;
168
+
169
+ // assign classNameSvg after finding variant & subVariant
170
+ if (variant === 'icon') {
171
+ const tempSvgClasses = variants.find(
172
+ (v) => v.variant === variant && v.subVariant === tempSubVariant
173
+ )?.classNameSvg;
174
+
175
+ if (subVariant === 'custom') {
176
+ setSvgClassValue(twMerge(tempSvgClasses, classNameSvg));
177
+ } else {
178
+ setSvgClassValue(tempSvgClasses);
179
+ }
180
+ }
181
+
182
+ // set proper icon path if variant warning, hazard, or thumbUp
183
+ if (variant === 'hazard') {
184
+ setSvgSource(emergency);
185
+ } else if (variant === 'go') {
186
+ setSvgSource(thumbUp);
187
+ }
188
+
189
+ // merge custom classes (dev can set custom classes whether using custom component or not)
190
+ setClassValue(twMerge(tempClasses, className));
191
+
192
+ }, [subVariant, className, variant, classNameSvg]);
193
+
194
+ // console.log(imagePath);
195
+
196
+ return (
197
+ <>
198
+ {variant === 'default' &&
199
+ <div className={classValue} {...props} ref={ref}>
200
+ {children}
201
+ </div>
202
+ }
203
+ { (variant === 'warning' || variant === 'hazard' || variant ==='go') &&
204
+ <div className={classValue}>
205
+ <div className="size-6 flex-col justify-center items-center gap-2.5 inline-flex overflow-hidden relative">
206
+ <div className="left-0 top-0 absolute"><img className='size-6' src={svgSource} alt={`${variant} icon`} /></div>
207
+ </div>
208
+ <div className="grow shrink basis-0 self-stretch text-black text-sm font-normal font-['Arial'] leading-[19px]">{children}</div>
209
+ <button
210
+ // ref={closeButtonRef}
211
+ onClick={onClose} // adds 'hidden' to class list for parent div
212
+ className="size-3 py-px justify-center items-center gap-2.5 flex"
213
+ aria-label="Close modal"
214
+ >
215
+ <img src={closeIcon} alt='close alert icon' />
216
+ </button>
217
+ </div>
218
+ }
219
+ {variant === 'icon' &&
220
+ <div className={classValue} {...props} ref={ref}>
221
+ <svg viewBox="0 0 6 6" aria-hidden="true" className={svgClassValue} role="img">
222
+ <circle r={3} cx={3} cy={3} />
223
+ </svg>
224
+ {children}
225
+ </div>
226
+ }
227
+ {variant === 'media' &&
228
+ <div className={classValue} {...props} ref={ref}>
229
+ <div className="col-start-1 col-span-2">
230
+ <img className={twMerge('h-full',classNameImage)} src={imagePath} alt={imageAlt} />
231
+ </div>
232
+ <div className="w-full ml-2 col-start-3 col-span-3">
233
+ {children}
234
+ </div>
235
+ </div>
236
+ }
237
+ </>
238
+ );
239
+ });
@@ -0,0 +1,136 @@
1
+ import { Meta, StoryContext } from '@storybook/react';
2
+ import { SideBarNav, SideBarNavProps } from './SideBarNav';
3
+ // import List from './List';
4
+ import { Link } from 'react-router-dom';
5
+
6
+ import hospital from '../assets/img/hospital.jpg';
7
+ import home from '../assets/img/home.svg';
8
+ import pill from '../assets/img/pill.svg';
9
+ import prescription from '../assets/img/prescription.svg';
10
+ import { userEvent, waitFor, within } from 'storybook/test';
11
+ import { expect } from 'storybook/test';
12
+ import { useState } from 'react';
13
+
14
+ const imagePath = 'src/assets/img/hospital.jpg';
15
+
16
+ /*
17
+ className?: string;
18
+ variant?: string;
19
+ image?: ReactNode;
20
+ classNameImage?: string;
21
+ version: string;
22
+ */
23
+ const menuItems = [
24
+ {
25
+ children: <>
26
+ <img className='max-h-[2em] max-w-[2em] pr-1 inline' src={pill} alt='home link icon' />
27
+ <p className='inline'>TEST Link 1</p>
28
+ {/* <Link to='https://google.com' target='_blank'>Link 1</Link> */}
29
+ </>
30
+ },
31
+ {
32
+ children: <>
33
+ <img className='max-h-[2em] max-w-[2em] pr-1 inline' src={pill} alt='link icon 2' />
34
+ <p className='inline'>Link 2</p>
35
+ {/* <Link to='https://google.com' target='_blank'>Link 2</Link> */}
36
+ </>
37
+ },
38
+ {
39
+ children: <>
40
+ <img className="max-h-[2em] max-w-[2em] pr-1 inline" src={prescription} alt='link icon 3' />
41
+ <p className='inline'>TEST Link 3</p>
42
+ {/* <Link to='https://google.com' target='_blank'>Link 3</Link> */}
43
+ </>
44
+ }
45
+ ]
46
+
47
+ // A wrapper that lets you unmount the SideBarNav component, used in the test to cover line 40
48
+ const UnmountWrapper = (props: SideBarNavProps) => {
49
+ const [mounted, setMounted] = useState(true);
50
+ return (
51
+ <div>
52
+ {mounted && <SideBarNav {...props} />}
53
+ <button style={{ marginTop: '1rem' }} onClick={() => setMounted(false)}>
54
+ Unmount
55
+ </button>
56
+ </div>
57
+ );
58
+ };
59
+
60
+
61
+ // Meta object - defines basic storybook options for this story
62
+ export default {
63
+ title: 'Components/SideBarNav',
64
+ component: SideBarNav,
65
+ argTypes: {
66
+ // variant: {
67
+ // control: 'select',
68
+ // options: ['','']
69
+ // },
70
+ },
71
+ args: {
72
+ image: hospital,
73
+ menuItems: menuItems,
74
+ // label: 'Button', // set default argument values
75
+ },
76
+ parameters: {
77
+ layout: 'centered', // options are 'centered', 'fullscreen', and 'padded' (default value)
78
+ backgrounds: { default: 'dark' }, // options are light, medium, or dark
79
+ },
80
+ } as Meta<SideBarNavProps>;
81
+
82
+ // Define "Default" story
83
+ export const Default = {
84
+ args: {
85
+
86
+ }
87
+ };
88
+
89
+ // Story to test the hamburger menu, toggle behavior, and click-outside close
90
+ export const HamburgerMenuToggle = {
91
+ args: {
92
+ menu: true,
93
+ clickOutsideCloses: true
94
+ },
95
+ play: async ({ canvasElement }: StoryContext) => {
96
+ const canvas = within(canvasElement);
97
+ // Find the hamburger menu button (rendered only when menu=true)
98
+ const button = await canvas.findByTestId('hamburgerMenu');
99
+ // Click the button to toggle (open) the sidebar
100
+ await userEvent.click(button);
101
+
102
+ // Wait for the sidebar to slide in (i.e. have class 'translate-x-0')
103
+ await waitFor(() => {
104
+ // Query for the sidebar element by its unique width class
105
+ const sidebar = canvasElement.querySelector('div.w-56');
106
+ expect(sidebar?.className).toContain('translate-x-0');
107
+ });
108
+
109
+ // Simulate a click outside the sidebar to trigger clickOutsideCloses logic
110
+ await userEvent.click(document.body);
111
+ await waitFor(() => {
112
+ const sidebar = canvasElement.querySelector('div.w-56');
113
+ expect(sidebar?.className).not.toContain('translate-x-0');
114
+ });
115
+ }
116
+ };
117
+
118
+
119
+ // Story to cover the unmount (cleanup) logic (line 40 of the component)
120
+ export const UnmountCleanup = {
121
+ render: (args: SideBarNavProps) => <UnmountWrapper {...args} menu={true} clickOutsideCloses={true} />,
122
+ play: async ({ canvasElement }: StoryContext) => {
123
+ const canvas = within(canvasElement);
124
+
125
+ // Open the sidebar by clicking the hamburger button
126
+ const toggleButton = await canvas.findByRole('button', { name: /menu/i }).catch(() => canvas.getAllByRole('button')[0]);
127
+ await userEvent.click(toggleButton);
128
+
129
+ // Click the "Unmount" button to remove the component (which triggers cleanup)
130
+ const unmountButton = await canvas.findByRole('button', { name: /unmount/i });
131
+ await userEvent.click(unmountButton);
132
+
133
+ // Optionally, click outside and verify no error occurs (since the listener is removed)
134
+ await userEvent.click(document.body);
135
+ }
136
+ };