@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,247 @@
1
+ import React, { useState, useRef, useEffect } from "react";
2
+ import { Dayjs } from "dayjs";
3
+ import dayjs from "dayjs";
4
+ import Calendar from '../assets/img/calendar.svg';
5
+
6
+ export interface DatePickerProps {
7
+ id: string;
8
+ label: string;
9
+ value?: string;
10
+ onChange: (date: string) => void | null;
11
+ }
12
+
13
+ export const DatePicker = ({ id, label, value,
14
+ onChange }: DatePickerProps) => {
15
+ const [isOpen, setIsOpen] = useState(false);
16
+ const [inputValue, setInputValue] = useState(value);
17
+ const [isValidDateFormat, setIsValidDateFormat] = useState(true);
18
+ const [valueEntered, setValueEntered] = useState<boolean>(!!value);
19
+ const [focusedDate, setFocusedDate] = useState<Dayjs | null>(
20
+ dayjs(value, "MM-DD-YYYY", true).isValid()
21
+ ? dayjs(value, "MM-DD-YYYY", true)
22
+ : null
23
+ );
24
+
25
+ // const currentDate =
26
+ // console.log(currentDate); // Output: e.g., 10-27-2023
27
+ const containerRef = useRef<HTMLDivElement | null>(null); // reference the entire DatePicker container
28
+ const inputRef = useRef<HTMLInputElement | null>(null);
29
+ const dialogRef = useRef<HTMLDivElement | null>(null);
30
+
31
+ const daysInMonth = focusedDate?.daysInMonth() || 30;
32
+ const startOfMonth = focusedDate?.startOf("month").day() || 0;
33
+
34
+ useEffect(() => {
35
+ if (!inputValue) {
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps
37
+ value = dayjs().format('MM-DD-YYYY');
38
+ setValueEntered(false)
39
+ }
40
+
41
+ setInputValue(value);
42
+ const maybeDate = dayjs(value, "MM-DD-YYYY", true);
43
+ setFocusedDate(maybeDate.isValid() ? maybeDate : null);
44
+ setIsValidDateFormat(maybeDate.isValid() && value !== "");
45
+
46
+ }, [value]);
47
+
48
+ useEffect(() => {
49
+ function handleClickOutside(e: MouseEvent) {
50
+ // If no containerRef or the user clicks inside the container, do nothing
51
+ if (!containerRef.current) return;
52
+ // e.target can be typed as Node
53
+ if (!containerRef.current.contains(e.target as Node)) {
54
+ setIsOpen(false);
55
+ }
56
+ }
57
+
58
+ if (isOpen && dialogRef.current) {
59
+ const firstFocusable = dialogRef.current.querySelector(
60
+ '[role="button"]:not([disabled])'
61
+ ) as HTMLElement;
62
+ firstFocusable?.focus();
63
+ document.addEventListener('mousedown', handleClickOutside);
64
+ }
65
+
66
+ return () => {
67
+ document.removeEventListener('mousedown', handleClickOutside);
68
+ };
69
+ }, [isOpen]);
70
+
71
+ const handleDateSelect = (date: Dayjs) => {
72
+ const formatted = date.format("MM-DD-YYYY");
73
+ setValueEntered(true);
74
+ setFocusedDate(date);
75
+ setInputValue(formatted);
76
+ setIsValidDateFormat(true);
77
+ onChange(formatted);
78
+ setIsOpen(false);
79
+ inputRef.current?.focus();
80
+ };
81
+
82
+ const handleKeyDown = (event: React.KeyboardEvent, date: Dayjs) => {
83
+ if (event.key === "Enter") {
84
+ handleDateSelect(date);
85
+ }
86
+ };
87
+
88
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
89
+ const newValue = e.target.value;
90
+ setInputValue(newValue);
91
+ setValueEntered(true);
92
+
93
+ // Regex to ensure the string is strictly MM-DD-YYYY (2 digits - 2 digits - 4 digits)
94
+ // const dateRegex = /^\d{2}-\d{2}-\d{4}$/;
95
+ const dateRegex = /^\d{1,2}-\d{2}-\d{4}$/;
96
+
97
+ // 1) Check if the string matches the pattern exactly.
98
+ if (!dateRegex.test(newValue)) {
99
+ setIsValidDateFormat(false);
100
+ onChange(newValue); // Still let the parent know about the change
101
+ return;
102
+ }
103
+
104
+ // 2) If it matches the pattern, check if it's a valid date via Day.js strict parse
105
+ const maybeDate = dayjs(newValue, "MM-DD-YYYY", true);
106
+ if (maybeDate.isValid()) {
107
+ console.log("MAYBEDATE IS VALID");
108
+ setFocusedDate(maybeDate);
109
+ setIsValidDateFormat(true);
110
+ onChange(newValue);
111
+ } else {
112
+ console.log("MAYBEDATE IS --NOT-- VALID");
113
+ setIsValidDateFormat(false);
114
+ onChange(newValue);
115
+ }
116
+ };
117
+
118
+ const toggleCalendar = () => {
119
+ setIsOpen((prev) => !prev);
120
+ };
121
+
122
+ const goToPreviousMonth = () => {
123
+ if (focusedDate) {
124
+ setFocusedDate(focusedDate.subtract(1, "month"));
125
+ }
126
+ };
127
+
128
+ const goToNextMonth = () => {
129
+ if (focusedDate) {
130
+ setFocusedDate(focusedDate.add(1, "month"));
131
+ }
132
+ };
133
+
134
+ const renderDays = () => {
135
+ if (!focusedDate) return null;
136
+
137
+ const dates = [];
138
+ for (let i = 0; i < startOfMonth; i++) {
139
+ dates.push(<div key={`empty-${i}`} className="w-8 h-8"></div>);
140
+ }
141
+ for (let i = 1; i <= daysInMonth; i++) {
142
+ const date = focusedDate.date(i);
143
+ dates.push(
144
+ <button
145
+ key={i}
146
+ tabIndex={0}
147
+ onClick={() => handleDateSelect(date)}
148
+ onKeyDown={(e) => handleKeyDown(e, date)}
149
+ className={`w-8 h-8 ${
150
+ date.isSame(focusedDate, "date") ? "bg-blue-600 text-white" : ""
151
+ } hover:bg-blue-100 focus:ring`}
152
+ aria-label={date.format("MMMM D, YYYY")}
153
+ >
154
+ {i}
155
+ </button>
156
+ );
157
+ }
158
+
159
+ return dates;
160
+ };
161
+
162
+
163
+ return (
164
+ <div className="relative" ref={containerRef} >
165
+ <label htmlFor={id} id='date-picker-input' className="block text-sm font-medium text-gray-700">
166
+ {label}
167
+ </label>
168
+ <div className="flex items-center mt-1">
169
+ <input
170
+ id={id}
171
+ type="text"
172
+ aria-labelledby="date-picker-input"
173
+ ref={inputRef}
174
+ value={valueEntered ? inputValue : ''}
175
+ onChange={handleInputChange}
176
+ placeholder="MM-DD-YYYY"
177
+ className="block w-full border-gray-300 border-2 rounded-l-md rounded-r-none py-2
178
+ focus:border-blue-500 pl-1 focus:outline-hidden"
179
+ />
180
+ <button
181
+ type="button"
182
+ onClick={toggleCalendar}
183
+ className="px-3 py-2 bg-gray-100 border-2 border-gray-300 rounded-r-md rounded-l-none hover:bg-gray-200
184
+ focus:outline-hidden focus:border-blue-500"
185
+ aria-label="Open calendar"
186
+ aria-haspopup="dialog"
187
+ aria-expanded={isOpen}
188
+ aria-controls={`${id}-dialog`}
189
+ >
190
+ <img
191
+ src={Calendar}
192
+ alt="calendar icon"
193
+ className="size-6"
194
+ />
195
+
196
+ </button>
197
+ </div>
198
+ {isOpen && (
199
+ <div
200
+ ref={dialogRef}
201
+ id={`${id}-dialog`}
202
+ role="dialog"
203
+ aria-label="Calendar"
204
+ className="absolute z-10 bg-white shadow-lg rounded-md p-1"
205
+ >
206
+ {/* Calendar Header */}
207
+ <div className="flex items-center justify-between mb-4">
208
+ <button
209
+ onClick={goToPreviousMonth}
210
+ className="px-3 py-2 bg-gray-100 border border-gray-300 rounded-md hover:bg-gray-200 focus:outline-hidden focus:ring"
211
+ aria-label="Previous month"
212
+ >
213
+ &lt;
214
+ </button>
215
+ <div className="text-lg font-semibold">
216
+ {/* Display Month name of entered value, or name of current month if no 'value' passed in */}
217
+ {focusedDate?.format("MMMM YYYY")}
218
+ </div>
219
+ <button
220
+ onClick={goToNextMonth}
221
+ className="px-3 py-2 bg-gray-100 border border-gray-300 rounded-md hover:bg-gray-200 focus:outline-hidden focus:ring"
222
+ aria-label="Next month"
223
+ >
224
+ &gt;
225
+ </button>
226
+ </div>
227
+ {/* Calendar Days */}
228
+ <div className="grid grid-cols-7 gap-1 p-1">
229
+ <p className="pl-1">Su</p>
230
+ <p className="pl-1">Mo</p>
231
+ <p className="pl-1">Tu</p>
232
+ <p className="pl-1">We</p>
233
+ <p className="pl-1">Th</p>
234
+ <p className="pl-2">Fr</p>
235
+ <p className="pl-1">Sa</p>
236
+ {renderDays()}
237
+ </div>
238
+ </div>
239
+ )}
240
+ {!isValidDateFormat && (
241
+ <p className="text-red-600">
242
+ Please enter a valid date 'MM-DD-YYYY'
243
+ </p>
244
+ )}
245
+ </div>
246
+ );
247
+ };
@@ -0,0 +1,449 @@
1
+ import { Meta, StoryContext } from '@storybook/react';
2
+ import { Input, InputProps } from './Input';
3
+ import { userEvent, within, waitFor } from 'storybook/test';
4
+ import { expect } from 'storybook/test';
5
+ import { Component, FC, ReactNode, useRef, useState } from 'react';
6
+
7
+
8
+ export default {
9
+ title: 'Components/Input',
10
+ component: Input,
11
+ parameters: {
12
+ layout: 'centered',
13
+ backgrounds: {
14
+ default: 'white',
15
+ values: [
16
+ { name: 'white', value: '#ffffff' },
17
+ { name: 'light', value: '#f0f0f0' },
18
+ ],
19
+ },
20
+ },
21
+ argTypes: {
22
+ className: { control: 'text' },
23
+ classNameLabel: { control: 'text' },
24
+ label: { control: 'text' },
25
+ placeholder: { control: 'text' },
26
+ },
27
+ args: {
28
+ label: 'Default Input',
29
+ placeholder: 'placeholder text here'
30
+ },
31
+ } as Meta<typeof Input>;
32
+
33
+ // ERROR
34
+ // Default Variant Input Test
35
+ export const DefaultVariantTest = {
36
+ args: {
37
+ label: 'Username',
38
+ placeholder: 'Enter your username',
39
+ },
40
+ play: async ({ canvasElement }: StoryContext) => {
41
+ // await new Promise((resolve) => setTimeout(resolve, 2000));
42
+ const canvas = within(canvasElement);
43
+ const label = canvas.getByText('Username');
44
+ expect(label).toBeInTheDocument();
45
+
46
+ // await new Promise((resolve) => setTimeout(resolve, 2500));
47
+ const input = canvas.getByPlaceholderText('Enter your username');
48
+ await userEvent.click(input);
49
+ await userEvent.type(input, 'testuser');
50
+ // await new Promise((resolve) => setTimeout(resolve, 5000));
51
+ expect(input).toHaveValue('testuser');
52
+ },
53
+ };
54
+
55
+ export const DefaultWithInsetLabel = {
56
+ args: {
57
+ label: 'Username',
58
+ insetLabel: true,
59
+ placeholder: 'Enter your username',
60
+ },
61
+ }
62
+
63
+
64
+ export const PhoneMask = {
65
+ args: {
66
+ label: "Phone",
67
+ placeholder: "(###) ###-####",
68
+ mask: "(###) ###-####"
69
+ },
70
+ play:async (
71
+ {
72
+ canvasElement
73
+ }: StoryContext
74
+ ) => {
75
+ const canvas = within(canvasElement);
76
+
77
+ // capture input
78
+ const input = canvas.getByPlaceholderText("(###) ###-####");
79
+ // focus on input
80
+ await userEvent.click(input);
81
+ // type value
82
+ await userEvent.type(input, "a4b2b5b1b2b3b4b5n6n7");
83
+ expect(input).toHaveValue("(425) 123-4567");
84
+ }
85
+ };
86
+
87
+ export const ModelNumberMask = {
88
+ args: {
89
+ label: "Enter Model #",
90
+ placeholder: "@@##",
91
+ mask: "@@##"
92
+ },
93
+ play:async (
94
+ {
95
+ canvasElement
96
+ }: StoryContext
97
+ ) => {
98
+ const canvas = within(canvasElement);
99
+
100
+ // capture input
101
+ const input = canvas.getByPlaceholderText("@@##");
102
+ // focus on input
103
+ await userEvent.click(input);
104
+ // type value
105
+ await userEvent.type(input, "12Abav12d");
106
+ expect(input).toHaveValue("Ab12");
107
+ }
108
+ };
109
+
110
+ export const MaskTestSSN = (props: InputProps) => {
111
+
112
+ return (
113
+ <>
114
+ <p className='mb-5 mx-2'>This tests the <strong>mask</strong> prop to correct for social
115
+ security number input. If the user types a character other than a number
116
+ or a minus sign, it is ignored. If the user types numbers only, it is
117
+ formatted as required to match a ssn.</p>
118
+ <Input
119
+ label="SSN"
120
+ mask='###-##-####'
121
+ placeholder='###-##-###'
122
+ />
123
+ </>
124
+ );
125
+ };
126
+
127
+ export const MaskTestPhone = (props: InputProps) => {
128
+
129
+ return (
130
+ <>
131
+ <p className='mb-5 mx-2'>This tests the <strong>mask</strong> prop to correct for
132
+ phone number input. If the user types an invalid character it is ignored unless a
133
+ non-numeric or non-letter character has been specified, in which case that is
134
+ automatically applied. If the user types numbers only, it is
135
+ formatted as required to match the example phone number mask.</p>
136
+ <Input
137
+ label="Phone Number"
138
+ mask='(###) ###-####'
139
+ placeholder='(###) ###-####'
140
+ />
141
+ </>
142
+ );
143
+ };
144
+
145
+ // ERROR
146
+ export const DefaultVariantTextShadow = {
147
+ args: {
148
+ label: 'Username',
149
+ placeholder: 'Enter your username',
150
+ textShadow: true,
151
+ },
152
+ }
153
+
154
+ // Error Variant Test
155
+ // export const ErrorVariant = {
156
+ // args: {
157
+ // variant: 'bogus',
158
+ // label: 'Username',
159
+ // placeholder: 'Enter your username',
160
+ // },
161
+ // };
162
+
163
+
164
+
165
+ export const ErrorVariant = (props: InputProps) => {
166
+
167
+ return (
168
+ <ErrorBoundary>
169
+ <Input
170
+ {...props}
171
+ variant='bogus'
172
+ label="Incorrect Variant Attempt"
173
+ data-testid="input"
174
+ />
175
+ </ErrorBoundary>
176
+ );
177
+ };
178
+
179
+
180
+ // Outline Variant Test
181
+ export const OutlineVariantTest = {
182
+ args: {
183
+ label: 'Email',
184
+ placeholder: 'Enter your email',
185
+ variant: 'outline',
186
+ },
187
+ play: async ({ canvasElement }: StoryContext) => {
188
+ const canvas = within(canvasElement);
189
+ const input = canvas.getByPlaceholderText('Enter your email');
190
+ await userEvent.click(input);
191
+ await userEvent.type(input, 'user@example.com');
192
+ expect(input).toHaveValue('user@example.com');
193
+ },
194
+ };
195
+
196
+ // ERROR
197
+ // NonHover Variant Test
198
+ export const NonHoverVariantTest = {
199
+ args: {
200
+ label: 'Password',
201
+ placeholder: 'Enter your password',
202
+ variant: 'nonHover',
203
+ type: 'password',
204
+ },
205
+ play: async ({ canvasElement }: StoryContext) => {
206
+ const canvas = within(canvasElement);
207
+ const input = canvas.getByPlaceholderText('Enter your password');
208
+ await userEvent.click(input);
209
+ await userEvent.type(input, 'mysecretpassword');
210
+ expect(input).toHaveValue('mysecretpassword');
211
+ },
212
+ };
213
+
214
+ // ERROR
215
+ // Required Field Test
216
+ export const RequiredFieldTest = {
217
+ args: {
218
+ label: 'First Name',
219
+ placeholder: 'Enter your first name',
220
+ required: true,
221
+ },
222
+ play: async ({ canvasElement }: StoryContext) => {
223
+ const canvas = within(canvasElement);
224
+ const requiredSymbol = canvas.getByText('*');
225
+ expect(requiredSymbol).toBeInTheDocument();
226
+
227
+ const input = canvas.getByPlaceholderText('Enter your first name');
228
+ await userEvent.click(input);
229
+ await userEvent.type(input, 'John');
230
+ expect(input).toHaveValue('John');
231
+ },
232
+ };
233
+
234
+
235
+ const RefCallbackComponent = (props: InputProps) => {
236
+ const [refCalled, setRefCalled] = useState(false);
237
+
238
+ return (
239
+ <>
240
+ <Input
241
+ {...props}
242
+ label="Ref Callback Input"
243
+ data-testid="input"
244
+ // Pass a function ref to trigger the branch on line 138 in Input
245
+ ref={(node) => {
246
+ if (node) {
247
+ setRefCalled(true);
248
+ }
249
+ }}
250
+ />
251
+ {refCalled && (
252
+ <span data-testid="ref-called">
253
+ Ref callback executed
254
+ </span>
255
+ )}
256
+ </>
257
+ );
258
+ };
259
+
260
+ // ERROR
261
+ export const RefCallback = {
262
+ render: () => <RefCallbackComponent />,
263
+ args: {
264
+ variant: 'default',
265
+ },
266
+ play: async ({ canvasElement }: StoryContext) => {
267
+ const canvas = within(canvasElement);
268
+ // Wait for the ref-called element to appear, confirming that the function ref was invoked.
269
+ await waitFor(() =>
270
+ expect(canvas.getByTestId('ref-called')).toBeInTheDocument()
271
+ );
272
+ },
273
+ };
274
+
275
+ // ERROR
276
+ //Invalid variant test
277
+ // export const InvalidVariantTest = {
278
+ // args: {
279
+ // label: 'Invalid Variant',
280
+ // placeholder: 'Enter your email address',
281
+ // variant:'red',
282
+ // },
283
+ // };
284
+
285
+ // ERROR
286
+ // Disabled Input Test
287
+ export const DisabledInputTest = {
288
+ args: {
289
+ label: 'Disabled Input',
290
+ placeholder: 'Cannot type here',
291
+ disabled: true,
292
+ },
293
+ play: async ({ canvasElement }: StoryContext ) => {
294
+ const canvas = within(canvasElement);
295
+ const input = canvas.getByPlaceholderText('Cannot type here');
296
+ expect(input).toBeDisabled();
297
+ },
298
+ };
299
+
300
+ // Large Input Size Test
301
+ export const LargeInputTest = {
302
+ args: {
303
+ label: 'Large Input',
304
+ placeholder: 'Large input',
305
+ inputSize: 'lg',
306
+ },
307
+ play: async ({ canvasElement }: StoryContext) => {
308
+ const canvas = within(canvasElement);
309
+ const input = canvas.getByPlaceholderText('Large input');
310
+ expect(input).toBeInTheDocument();
311
+ },
312
+ };
313
+
314
+ export const Default = {
315
+ play: async ({ canvasElement }: StoryContext) => {
316
+ const canvas = within(canvasElement);
317
+ canvas.getByRole('textbox').innerText = 'You\'ve clicked me';
318
+ // const input = canvas.getByRole('textbox');
319
+ // await userEvent.click(input);
320
+ expect(canvas.getByRole('textbox').innerText).toBe(
321
+ ""
322
+ );
323
+ // await userEvent.click(input);
324
+ // expect(canvas.getByRole('textbox').innerText).toBe(
325
+ // "You've clicked me 2 times"
326
+ // );
327
+ },
328
+ };
329
+
330
+ export const AlternateClasses = {
331
+ args: {
332
+ className: "bg-blue-100",
333
+ labelInputColor: "#dbeafe"
334
+ },
335
+ argTypes:{
336
+ className: { control: 'text' },
337
+ classNameLabel: { control: 'text' },
338
+ label: { control: 'text' },
339
+ placeholder: { control: 'text' },
340
+ labelInputColor: { control: 'text' },
341
+ }
342
+ };
343
+
344
+ export const Required = {
345
+ args: {
346
+ required: true,
347
+ }
348
+ };
349
+
350
+ export const DifferingBackgrounds = {
351
+ args: {
352
+ className: "rounded-lg border-[#0256ab] bg-[#caecf5] placeholder-gray-500",
353
+ labelBaseColor: '#ffffff',
354
+ labelInputColor: '#caecf5',
355
+ classNameLabel: 'text-[#0256ab]',
356
+ required: true,
357
+ textShadow: true
358
+ }
359
+ }
360
+
361
+ export const Username = {
362
+ args: {
363
+ label: "Username",
364
+ placeholder: "Enter your username",
365
+ },
366
+ play:async (
367
+ {
368
+ canvasElement
369
+ }: StoryContext
370
+ ) => {
371
+ const canvas = within(canvasElement);
372
+ const label = canvas.getByText("Username");
373
+ expect(label).toBeInTheDocument();
374
+
375
+ const input = canvas.getByPlaceholderText("Enter your username");
376
+ await userEvent.click(input);
377
+ await userEvent.type(input, "testuser");
378
+ expect(input).toHaveValue("testuser");
379
+ }
380
+ };
381
+
382
+ // --------------
383
+ // Test w/ ref
384
+ const WithRefComponent: FC = () => {
385
+ const inputRef = useRef<HTMLInputElement>(null);
386
+
387
+ return (
388
+ <div>
389
+ <Input
390
+ ref={inputRef}
391
+ label="Test Input with Ref"
392
+ variant="default"
393
+ onFocus={() => console.log('Focused!')}
394
+ onBlur={() => console.log('Blurred!')}
395
+ />
396
+ <button
397
+ className="border bg-slate-200 rounded-md mt-4 ms-2 p-2 border-black hover:bg-slate-300"
398
+ onClick={() => {
399
+ if (inputRef.current) {
400
+ inputRef.current.focus();
401
+ }
402
+ }}
403
+ >
404
+ Focus Input
405
+ </button>
406
+ </div>
407
+ );
408
+ };
409
+
410
+ export const WithRef: Meta<InputProps> = {
411
+ render: () => <WithRefComponent />,
412
+ };
413
+
414
+
415
+ // Error Boundary definition for Error Checking
416
+
417
+ interface ErrorBoundaryProps {
418
+ children: ReactNode;
419
+ }
420
+
421
+ interface ErrorBoundaryState {
422
+ hasError: boolean;
423
+ error: Error | null;
424
+ }
425
+
426
+ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
427
+ constructor(props: ErrorBoundaryProps) {
428
+ super(props);
429
+ this.state = { hasError: false, error: null };
430
+ }
431
+
432
+ static getDerivedStateFromError(error: Error) {
433
+ // Update state so the next render shows the fallback UI.
434
+ return { hasError: true, error };
435
+ }
436
+
437
+ componentDidCatch(error: Error, errorInfo: any) {
438
+ // You can log the error to an error reporting service here.
439
+ console.error("Error caught in ErrorBoundary:", error, errorInfo);
440
+ }
441
+
442
+ render() {
443
+ if (this.state.hasError) {
444
+ return <div>Error: {this.state.error?.message}</div>;
445
+ }
446
+
447
+ return this.props.children;
448
+ }
449
+ }