@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,281 @@
1
+ import React, { createRef } from "react";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import { userEvent } from "@testing-library/user-event"; // userEvent.type functionality
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import { axe } from "vitest-axe";
6
+ import { Input } from "./Input";
7
+
8
+ describe("Input Component", () => {
9
+ it("renders the input with default styles", () => {
10
+ render(<Input label="Default Input" />);
11
+ const input = screen.getByLabelText("Default Input");
12
+ // console.log('input.className: ', input.className);
13
+ expect(input).toBeInTheDocument();
14
+ expect(input).toHaveClass("px-4 py-3 arial rounded-lg border border-[#B3B3B3] mx-2 bg-white");
15
+ });
16
+
17
+ it("applies custom classes and merges with default classes", () => {
18
+ render(<Input label="Custom Input" className="custom-class" />);
19
+ const input = screen.getByLabelText("Custom Input");
20
+
21
+ expect(input).toHaveClass("custom-class");
22
+ expect(input).toHaveClass("px-4 py-3 arial rounded-lg border border-[#B3B3B3] mx-2 bg-white");
23
+ });
24
+
25
+ it("applies variant-specific styles", () => {
26
+ render(<Input label="Outline Input" variant="outline" />);
27
+ const input = screen.getByLabelText("Outline Input");
28
+
29
+ expect(input).toHaveClass("border-dha-mc-true-blue bg-white border text-dha-mc-true-blue");
30
+ });
31
+
32
+ it("renders a required indicator in the label", () => {
33
+ render(<Input label="Required Input" required />);
34
+ const label = screen.getByText("*");
35
+
36
+ expect(label).toBeInTheDocument();
37
+ expect(label).toHaveClass("text-red-500");
38
+ });
39
+
40
+ it("applies label styles with gradient background", () => {
41
+ render(
42
+ <Input
43
+ label="Styled Label"
44
+ labelBaseColor="#ffffff"
45
+ textShadow={true}
46
+ />
47
+ );
48
+ const label = screen.getByText("Styled Label");
49
+
50
+ expect(label).toHaveStyle("background: linear-gradient");
51
+ });
52
+
53
+ it("sets the correct type for the input", () => {
54
+ render(<Input label="Password Input" type="password" />);
55
+ const input = screen.getByLabelText("Password Input");
56
+
57
+ expect(input).toHaveAttribute("type", "password");
58
+ });
59
+
60
+ it("should forward the ref to the input element", () => {
61
+ const ref = createRef<HTMLInputElement>();
62
+ render(<Input label="Input with Ref" ref={ref} />);
63
+
64
+ expect(ref.current).toBeInstanceOf(HTMLInputElement);
65
+ expect(ref.current?.getAttribute("aria-label")).toBe("Input with Ref");
66
+ });
67
+
68
+ it("renders a disabled input with appropriate styles", () => {
69
+ render(
70
+ <Input label="Disabled Input" disabled variant="nonHover" />
71
+ );
72
+ const input = screen.getByLabelText("Disabled Input");
73
+
74
+ expect(input).toBeDisabled();
75
+ expect(input).toHaveClass("disabled:border-[#F2F2F2]");
76
+ });
77
+
78
+
79
+ it("throws an error for an invalid variant", () => {
80
+ // Temporarily silence the console.error output
81
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
82
+ const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
83
+
84
+ expect(() => {
85
+ render(<Input label="Default Input" variant="incorrect" />);
86
+ }).toThrowError(
87
+ 'Invalid variant: "incorrect". Valid variants are: "default", "outline", and "nonHover"'
88
+ );
89
+
90
+ consoleErrorSpy.mockRestore();
91
+ });
92
+
93
+ it('returns early if inputRef is null', () => {
94
+ // Mock the useRef hook to return null
95
+ vitest.spyOn(React, 'useRef').mockReturnValueOnce({ current: null });
96
+
97
+ // Render the component
98
+ const { container } = render(<Input label="Test Label" />);
99
+
100
+ // Assert that the component renders without throwing any errors
101
+ expect(container).toBeDefined();
102
+
103
+ // Clean up the mock
104
+ vitest.restoreAllMocks();
105
+ });
106
+
107
+
108
+ it('covers the early return scenario (null ref)', () => {
109
+ // 1) Spy on React's useRef
110
+ const useRefSpy = vitest.spyOn(React, 'useRef');
111
+ // 2) Mock once to return null
112
+ useRefSpy.mockReturnValueOnce({ current: null });
113
+
114
+ // 3) Render -> triggers early return in useEffect
115
+ render(<Input label="Some label" />);
116
+
117
+ // Cleanup
118
+ useRefSpy.mockRestore();
119
+ // No assertion needed, just ensure no crash
120
+ });
121
+
122
+ it('covers the scenario where inputRef is not null', () => {
123
+ // Normal render: React will attach an actual input element to ref
124
+ const { getByLabelText } = render(
125
+ <Input label="Another label" data-testid="inputEl" />
126
+ );
127
+
128
+ // Assert that the element is found (and hence not null)
129
+ const input = getByLabelText('Another label');
130
+ expect(input).toBeInTheDocument();
131
+ // This ensures code *after* the `if(!inputEl) return;` runs
132
+ });
133
+
134
+ it('calls the ref function if ref is a function', () => {
135
+ // Mock a ref function
136
+ const mockRefFunction = vitest.fn();
137
+
138
+ // Render the Input component with the mock ref
139
+ const { getByRole } = render(<Input label="Test Label" ref={mockRefFunction} />);
140
+
141
+ // Get the input element
142
+ const inputElement = getByRole('textbox');
143
+
144
+ // Assert that the ref function was called with the input element
145
+ expect(mockRefFunction).toHaveBeenCalledWith(inputElement);
146
+ });
147
+
148
+ it("applies phone mask correctly", async () => {
149
+ render(<Input label="Phone Input" mask="(###) ###-####" />);
150
+ const input = screen.getByLabelText("Phone Input") as HTMLInputElement;
151
+ await userEvent.type(input, "a1b2c3d4e5f6g7h8i9j0k");
152
+ expect(input.value).toBe("(123) 456-7890");
153
+ });
154
+
155
+
156
+ it("applies letter mask correctly", () => {
157
+ render(<Input label="Name Code" mask="@@@-@@" />);
158
+ const input = screen.getByLabelText("Name Code") as HTMLInputElement;
159
+ fireEvent.change(input, { target: { value: "abcDE" } });
160
+ // Expected output: "abc-de"
161
+ expect(input.value).toBe("abc-DE");
162
+ });
163
+
164
+
165
+ it("ignores invalid characters for a numeric mask", () => {
166
+ render(<Input label="Number Mask" mask="###" />);
167
+ const input = screen.getByLabelText("Number Mask") as HTMLInputElement;
168
+ // Providing letters when numbers are expected should result in an empty string.
169
+ fireEvent.change(input, { target: { value: "abc" } });
170
+ expect(input.value).toBe("");
171
+ });
172
+
173
+ it("ignores extra characters beyond the mask length", () => {
174
+ render(<Input label="Extra Characters" mask="###-###" />);
175
+ const input = screen.getByLabelText("Extra Characters") as HTMLInputElement;
176
+ // Input has more digits than required. Only the first 6 digits should be applied.
177
+ fireEvent.change(input, { target: { value: "1234567" } });
178
+ expect(input.value).toBe("123-456");
179
+ });
180
+
181
+ it("takes input without mask", () => {
182
+ render(<Input label="Extra Characters" />);
183
+ const input = screen.getByLabelText("Extra Characters") as HTMLInputElement;
184
+ // Input has more digits than required. Only the first 6 digits should be applied.
185
+ fireEvent.change(input, { target: { value: "1234567abc" } });
186
+ expect(input.value).toBe("1234567abc");
187
+ });
188
+
189
+
190
+ it("applies a complex mask with mixed literals correctly", () => {
191
+ render(<Input label="Complex Mask" mask="(@@) ###-####" />);
192
+ const input = screen.getByLabelText("Complex Mask") as HTMLInputElement;
193
+ // With input "ab1234567890", the mask should yield "(ab) 123-4567"
194
+ fireEvent.change(input, { target: { value: "ab1234567890" } });
195
+ expect(input.value).toBe("(ab) 123-4567");
196
+ });
197
+
198
+ it("applies a complex mask with mixed literals and mask input correctly", () => {
199
+ render(<Input label="Complex Mask" mask="(@@) ###-####" />);
200
+ const input = screen.getByLabelText("Complex Mask") as HTMLInputElement;
201
+ // With input "(ab)1234567890", the mask should yield "(ab) 123-4567"
202
+ fireEvent.change(input, { target: { value: "(ab)1234567890" } });
203
+ expect(input.value).toBe("(ab) 123-4567");
204
+ });
205
+ });
206
+
207
+
208
+ describe('Input Component Accessibility', () => {
209
+ it('should have no accessibility violations with default props', async () => {
210
+ const { container } = render(<Input label="Accessible Input" />);
211
+ const results = await axe(container);
212
+ expect(results).toHaveNoViolations();
213
+ });
214
+
215
+ it('should have no accessibility violations when required', async () => {
216
+ const { container } = render(<Input label="Required Input" required />);
217
+ const results = await axe(container);
218
+ expect(results).toHaveNoViolations();
219
+ });
220
+
221
+ it('should have no accessibility violations for each variant', async () => {
222
+ const variants = ['default', 'outline', 'nonHover'];
223
+ for (const variant of variants) {
224
+ const { container } = render(<Input label={`Input ${variant}`} variant={variant} />);
225
+ const results = await axe(container);
226
+ expect(results).toHaveNoViolations();
227
+ }
228
+ });
229
+
230
+ it('should associate the input with its label', () => {
231
+ render(<Input label="Accessible Label" id="input-id" />);
232
+ const input = screen.getByLabelText('Accessible Label');
233
+ expect(input).toBeInTheDocument();
234
+ expect(input).toHaveAttribute('id', 'input-id');
235
+ });
236
+
237
+ it('should render required indicator for required inputs', () => {
238
+ render(<Input label="Required Input" required />);
239
+ const label = screen.getByText('Required Input');
240
+ const requiredIndicator = screen.getByText('*');
241
+ expect(label).toBeInTheDocument();
242
+ expect(requiredIndicator).toBeInTheDocument();
243
+ expect(requiredIndicator).toHaveClass('text-red-500');
244
+ });
245
+
246
+ it('should apply custom classes without accessibility violations', async () => {
247
+ const { container } = render(
248
+ <Input label="Custom Class Input" className="custom-class" classNameLabel="custom-label-class" />
249
+ );
250
+ const input = screen.getByLabelText('Custom Class Input');
251
+ expect(input).toHaveClass('custom-class');
252
+ const label = screen.getByText('Custom Class Input');
253
+ expect(label).toHaveClass('custom-label-class');
254
+ const results = await axe(container);
255
+ expect(results).toHaveNoViolations();
256
+ });
257
+
258
+ it('should handle text shadow without accessibility violations', async () => {
259
+ const { container } = render(<Input label="Text Shadow Input" textShadow={true} />);
260
+ const label = screen.getByText('Text Shadow Input');
261
+ const computedStyle = window.getComputedStyle(label);
262
+ expect(computedStyle.textShadow).toContain('1px 1px 2px');
263
+ // expect(label).toHaveStyle({
264
+ // textShadow: expect.stringContaining('1px 1px 2px'),
265
+ // });
266
+ const results = await axe(container);
267
+ expect(results).toHaveNoViolations();
268
+ });
269
+
270
+ it('should handle text shadow + insetLabel without accessibility violations', async () => {
271
+ const { container } = render(<Input label="Text Shadow Input" insetLabel={true} textShadow={true} />);
272
+ const label = screen.getByText('Text Shadow Input');
273
+ const computedStyle = window.getComputedStyle(label);
274
+ expect(computedStyle.textShadow).toContain('1px 1px 2px');
275
+ // expect(label).toHaveStyle({
276
+ // textShadow: expect.stringContaining('1px 1px 2px'),
277
+ // });
278
+ const results = await axe(container);
279
+ expect(results).toHaveNoViolations();
280
+ });
281
+ });
@@ -0,0 +1,309 @@
1
+ import { ChangeEvent, forwardRef, InputHTMLAttributes, useEffect, useRef, useState } from 'react';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ // return true if a number, false otherwise
5
+ function isNumber(c: string): boolean {
6
+ if ('0123456789'.includes(c))
7
+ return true;
8
+ else
9
+ return false;
10
+ }
11
+
12
+ // return true if a letter, i.e. a...z or A...Z, false otherwise
13
+ function isLetter(char: string): boolean {
14
+ // if (char.length !== 1) {
15
+ // return false; // Not a single character
16
+ // }
17
+
18
+ const charCode = char.charCodeAt(0);
19
+
20
+ // Check if it's an uppercase letter (A-Z)
21
+ if (charCode >= 65 && charCode <= 90) {
22
+ return true;
23
+ }
24
+
25
+ // Check if it's a lowercase letter (a-z)
26
+ if (charCode >= 97 && charCode <= 122) {
27
+ return true;
28
+ }
29
+
30
+ return false; // Not a letter
31
+ }
32
+
33
+ // given an input string & number, return the next type that is a letter or number
34
+ function nextMaskCharType(input: string, startIndex: number): string {
35
+ for (let i = startIndex; i < input.length; i++) {
36
+ if (input[i] === '@')
37
+ return '@';
38
+ else if (input[i] === '#')
39
+ return '#';
40
+ }
41
+
42
+ // return default empty string if there is no following letter or number
43
+ return '';
44
+ }
45
+
46
+
47
+ // NOTE: If updated, please update error message below as well re invalid VariantType
48
+ type LegalVariantTypes = "default" | "outline" | "nonHover";
49
+
50
+ interface VariantType {
51
+ variant: LegalVariantTypes;
52
+ classes: string;
53
+ labelClasses: string;
54
+ insetLabelClasses: string;
55
+ }
56
+
57
+ const baseClasses = 'px-4 py-3 arial rounded-lg border border-[#B3B3B3] mx-2 bg-white ' +
58
+ 'hover:outline-[#c6c6c6] active:outline-[#0256ab] focus:outline-4 focus:outline-[#fa89f1] text-base md:text-lg';
59
+
60
+ const variants: VariantType[] = [
61
+ {
62
+ variant: 'default',
63
+ classes: 'hover:bg-dha-mc-pale-blue text-black hover:dha-mc-primary-text ' +
64
+ 'hover:border-dha-mc-secondary-border disabled:bg-dha-mc-bottom-nav-background ' +
65
+ 'disabled:text-dha-mc-checkbox-inactive disabled:border-[#F2F2F2] ' +
66
+ 'disabled:border w-[90vw] lg:max-w-[25em]',
67
+ labelClasses: 'ms-2.5 text-base',
68
+ insetLabelClasses: 'absolute ms-5 -translate-y-1/2 px-2 text-[0.8em] w-auto h-auto',
69
+ },
70
+ {
71
+ variant: 'outline',
72
+ classes: 'border-dha-mc-true-blue bg-white border text-dha-mc-true-blue ' +
73
+ 'disabled:border-[#F2F2F2] disabled:text-dha-mc-checkbox-inactive w-[35vw] ' +
74
+ 'min-w-min sm:max-w-[15em]',
75
+ labelClasses: 'ms-2.5 text-base',
76
+ insetLabelClasses: 'absolute ms-5 -translate-y-1/2 px-2 text-[0.8em] w-auto h-auto',
77
+ },
78
+ {
79
+ variant: 'nonHover',
80
+ classes: 'text-black disabled:bg-dha-mc-bottom-nav-background ' +
81
+ 'disabled:text-dha-mc-checkbox-inactive disabled:border-[#F2F2F2] ' +
82
+ 'disabled:border w-[90vw] lg:max-w-[25em]',
83
+ labelClasses: 'ms-2.5 text-base',
84
+ insetLabelClasses: 'absolute ms-5 -translate-y-1/2 px-2 text-[0.8em] w-auto h-auto',
85
+ }
86
+ ]
87
+
88
+ export interface InputProps
89
+ extends InputHTMLAttributes<HTMLInputElement> {
90
+ className?: string;
91
+ classNameLabel?: string;
92
+ classNameRequired?: string
93
+ variant?: string;
94
+ label?: string;
95
+ insetLabel?: boolean;
96
+ labelBaseColor?: string;
97
+ // labelInputColor?: string;
98
+ textShadow?: boolean;
99
+ error?: boolean;
100
+ mask?: string;
101
+ disabled?: boolean;
102
+ }
103
+
104
+ const Input = forwardRef<HTMLInputElement, InputProps>(
105
+ (
106
+ {
107
+ className,
108
+ variant = 'default',
109
+ label,
110
+ insetLabel = false,
111
+ classNameLabel = '',
112
+ classNameRequired = '',
113
+ labelBaseColor = '#fff',
114
+ error = false,
115
+ // labelInputColor = '#fff',
116
+ textShadow = false,
117
+ mask,
118
+ disabled,
119
+ ...props
120
+ },
121
+ ref
122
+ ) => {
123
+ const [labelBackground, setLabelBackground] = useState<string | undefined>();
124
+ const [inputClasses, setInputClasses] = useState(baseClasses);
125
+ const [labelClasses, setLabelClasses] = useState('');
126
+ const [hover, setHover] = useState<boolean>(false);
127
+ const inputRef = useRef<HTMLInputElement | null>(null);
128
+ const [value, setValue] = useState('');
129
+
130
+ const errorClasses = 'border-[#D54309] border';
131
+
132
+ const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
133
+ const newValue = event.target.value;
134
+ if (mask) {
135
+ setValue(applyMask(newValue));
136
+ } else {
137
+ setValue(newValue);
138
+ }
139
+ };
140
+
141
+ /**
142
+ * Takes the input string and validates it against the mask prop. If validation
143
+ * is good, it returns a string built from the input formatted to match the mask.
144
+ *
145
+ * @param input
146
+ */
147
+ function applyMask(input: string): string {
148
+
149
+ let output = "";
150
+ let maskPosition = 0;
151
+
152
+ if (mask) {
153
+ while (input.length > 0 && maskPosition < mask.length) {
154
+
155
+ // let logString = '';
156
+
157
+ if (mask[maskPosition] !== '#' && mask[maskPosition] !== '@') {
158
+ // logString += 'mask is not letter or number, ';
159
+ // if char at mask & input match
160
+ if (mask[maskPosition] === input[0]) {
161
+ // logString += 'mask === input, assigning input[0] to output';
162
+ output += input[0];
163
+ input = input.slice(1);
164
+ maskPosition++;
165
+ // add mask char to output if input[0] chartype matches mask
166
+ // char for maskPosition + 1
167
+ } else
168
+ if (
169
+ ( (isLetter(input[0]) && nextMaskCharType(mask, maskPosition) === '@') ||
170
+ (isNumber(input[0]) && nextMaskCharType(mask, maskPosition) === '#') )
171
+ )
172
+ {
173
+ // logString += 'mask !== input && following charType is correct, ' +
174
+ // 'assigning mask char to output - ' + mask[maskPosition];
175
+ output += mask[maskPosition];
176
+ maskPosition++;
177
+ }
178
+ else {
179
+ // logString += ' incrementing maskPosition'
180
+ maskPosition++;
181
+ }
182
+ // we have a num/alpha character
183
+ } else {
184
+ // logString += 'mask is letter/number, '
185
+ // if mask & input are both numbers
186
+ if (mask[maskPosition] === '#' && isNumber(input[0])) {
187
+ // logString += 'is number, assigning input[0] to output';
188
+ output += input[0];
189
+ input = input.slice(1);
190
+ maskPosition++;
191
+ // if mask & input are both letters
192
+ } else if (mask[maskPosition] === '@' && isLetter(input[0])) {
193
+ // logString += 'is letter, assigning input[0] to output';
194
+ output += input[0];
195
+ input = input.slice(1);
196
+ maskPosition++;
197
+ } else {
198
+ // throw away this character - incorrect char type per mask
199
+ // logString += 'does not match mask type, ignoring'
200
+ input = input.slice(1);
201
+ }
202
+ }
203
+
204
+ // console.log(logString);
205
+ // console.log('output: ' + output);
206
+ // console.log('maskPosition: ' + (maskPosition - 1));
207
+ // console.log('input.length: ' + input.length);
208
+ // console.log('input: ' + input);
209
+ // console.log('------------------------------');
210
+ // console.log('\n\n');
211
+ }
212
+
213
+ }
214
+
215
+ return output;
216
+
217
+ }
218
+
219
+
220
+ useEffect(() => {
221
+ if (variant) {
222
+ setInputClasses(twMerge(baseClasses, variants.find((v) => v.variant === variant)?.classes,
223
+ error && errorClasses, className)); // display error state if error state boolean value true
224
+ if (insetLabel)
225
+ setLabelClasses(twMerge(variants.find((v) => v.variant === variant)?.insetLabelClasses, classNameLabel));
226
+ else
227
+ setLabelClasses(twMerge(variants.find((v) => v.variant === variant)?.labelClasses, classNameLabel));
228
+ }
229
+ }, [className, classNameLabel, variant, hover, insetLabel, error]);
230
+
231
+ // Update label background to match input's default and hover state
232
+ useEffect(() => {
233
+ if (insetLabel) {
234
+ const inputEl = inputRef.current;
235
+
236
+ if (!inputEl) return;
237
+
238
+ // (boolean) --> setHover(bool-value)
239
+ const updateLabelBackground = (isHover: boolean) => {
240
+ const computedStyle = getComputedStyle(inputEl);
241
+ setLabelBackground(computedStyle.backgroundColor);
242
+ setHover(isHover);
243
+ };
244
+
245
+ // Update label background initially and on hover
246
+ updateLabelBackground(true);
247
+
248
+ // event listeners
249
+ inputEl.addEventListener('mouseover', () => updateLabelBackground(true));
250
+ inputEl.addEventListener('mouseout', () => updateLabelBackground(false));
251
+
252
+ return () => {
253
+ inputEl.removeEventListener('mouseover', () => updateLabelBackground);
254
+ inputEl.removeEventListener('mouseout', () => updateLabelBackground);
255
+ };
256
+
257
+ }
258
+ }, [labelBaseColor, insetLabel, hover, disabled]);
259
+
260
+ // Ensure the variant is valid
261
+ if (!["default", "outline", "nonHover"].includes(variant)) {
262
+ // return (<div>invalid variant on Input</div>)
263
+ throw new Error(
264
+ `Invalid variant: "${variant}". Valid variants are: "default", "outline", and "nonHover".`
265
+ );
266
+ }
267
+
268
+ return (
269
+ <span className="group relative">
270
+ {/* Label */}
271
+
272
+ <div
273
+ className={twMerge(`${!insetLabel && 'pb-2'}`, labelClasses)}
274
+ style={insetLabel ? {
275
+ background: `linear-gradient(to bottom, ${labelBaseColor} 0%, ${labelBaseColor} 50%, ${labelBackground} 60%, ${labelBackground} 100%)`,
276
+ textShadow: textShadow ?
277
+ `1px 1px 2px ${labelBaseColor}, 0 0 1em ${labelBaseColor}, 0 0 0.2em ${labelBaseColor}`
278
+ : '',
279
+ } : {textShadow: textShadow ?
280
+ `1px 1px 2px ${labelBaseColor}, 0 0 1em ${labelBaseColor}, 0 0 0.2em ${labelBaseColor}`
281
+ : '' }}
282
+ >
283
+ {label}
284
+ {props.required && <span className={twMerge("absolute text-red-500 ms-0.5 -mt-1", classNameRequired)}>*</span>}
285
+ </div>
286
+
287
+ {/* Input */}
288
+ <input
289
+ className={inputClasses}
290
+ aria-label={label}
291
+ value={value}
292
+ onChange={handleInputChange}
293
+ disabled={disabled}
294
+ // className={cn(inputVariants({variant, inputSize, className}))}
295
+ ref={(node) => {
296
+ if (typeof ref === 'function') ref(node);
297
+ else if (ref) ref.current = node;
298
+ inputRef.current = node; // Maintain local reference
299
+ }}
300
+ {...props}
301
+ />
302
+ </span>
303
+ );
304
+ }
305
+ );
306
+
307
+ Input.displayName = 'Input';
308
+
309
+ export { Input };