@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,491 @@
1
+
2
+ import React, { InputHTMLAttributes, ReactElement, ReactNode, cloneElement, isValidElement, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
3
+ import { twMerge } from 'tailwind-merge';
4
+ import CheckBoxChecked from '../assets/img/check_box.svg';
5
+ import CheckBoxEmpty from '../assets/img/check_box_empty.svg';
6
+ import CheckBoxIndeterminate from '../assets/img/indeterminate_check_box.svg';
7
+ import CheckBoxCheckedFill from '../assets/img/check_box_fill.svg'
8
+ import CheckBoxEmptyFill from '../assets/img/check_box_fill_empty.svg';
9
+ import CheckBoxIndeterminateFill from '../assets/img/indeterminate_check_box_fill.svg';
10
+ import CheckBoxMarker from '../assets/img/marker_check_by_default.svg';
11
+ import CheckBoxMarkerFill from '../assets/img/marker_check_by_default_fill.svg';
12
+
13
+ export interface CheckBoxGroupProps {
14
+ children: ReactNode;
15
+ bridgeParent?: boolean;
16
+ fill?: boolean; // pass to CheckBox - fill or std variant
17
+ icon?: boolean; // pass to CheckBox -use icons to represent ux variants (vs default browser styles)
18
+ marker?: boolean; // pass to CheckBox -use x-marker instead of check
19
+ showBranch?: boolean;
20
+ }
21
+
22
+ type StatusType = 'checked' | 'unchecked' | 'indeterminate';
23
+
24
+ type IconType = 'sibling' | 'sibling-child' | 'child' | 'blank';
25
+
26
+ export interface CheckBoxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
27
+ ariaLabel?: string;
28
+ value?: string;
29
+ fill?: boolean; // fill or std variant
30
+ icon?: boolean; // use icons to represent ux variants (vs default browser styles)
31
+ marker?: boolean; // use x-marker instead of check
32
+ level?: number;
33
+ classNameSvg?: string;
34
+ classNameInput?: string;
35
+ iconType?: IconType[]; // iterate each level for every CheckBox, blank, sibling, etc.
36
+ children?: ReactNode;
37
+ index?: number;
38
+ showBranch?: boolean;
39
+ status?: StatusType;
40
+ setStatusUpdate?: ( status: StatusType, index: number ) => void;
41
+ }
42
+ //
43
+ /*
44
+ * siblingsExist
45
+ * returns true if a sibling exists later in the list at the same level w/o
46
+ * interediary elements in between that exist at a higher level. e.g., if
47
+ * a following item not at the currentLevel contains a higher level value,
48
+ * an automatic false is returned.
49
+ */
50
+ function siblingExists(checkBoxArray: ReactElement<CheckBoxProps>[], currentIndex: number, bridgeParent: boolean): boolean {
51
+
52
+ // must determine if there is another sibling at the current level w/ index > currentIndex
53
+ const currentLevel = checkBoxArray[currentIndex].props.level ?? 0;
54
+
55
+ if (currentIndex === checkBoxArray.length - 1) {
56
+ return false;
57
+ }
58
+
59
+ for (let i = currentIndex + 1; i < checkBoxArray.length; i++) {
60
+
61
+ const nextLevel = checkBoxArray[i].props.level;
62
+ if (nextLevel === undefined) {
63
+ continue;
64
+ }
65
+
66
+ // can be no sibling if we have reached another element at a lower level
67
+ if (nextLevel < currentLevel) {
68
+
69
+ if (bridgeParent && (nextLevel + 1) === currentLevel) {
70
+ return true;
71
+ } else {
72
+ return false;
73
+ }
74
+ }
75
+ // we have found a direct sibling
76
+ if (nextLevel === currentLevel) {
77
+ return true;
78
+ }
79
+ }
80
+
81
+ return false;
82
+ }
83
+
84
+ /*
85
+ * leveExistsBelow
86
+ * Return true if the given level item exists later in the list without going to a level lower
87
+ * than that given, meaning something higher in the heirarchy before finding the desired level
88
+ */
89
+ function levelExistsLater(checkBoxArray: ReactElement<CheckBoxProps>[], currentIndex: number, targetLevel: number): boolean {
90
+
91
+ if (currentIndex === checkBoxArray.length - 1) {
92
+ return false;
93
+ }
94
+
95
+ for (let i = currentIndex + 1; i < checkBoxArray.length; i++) {
96
+ const newLevel = checkBoxArray[i].props.level || 0;
97
+
98
+ if (newLevel < targetLevel)
99
+ return false;
100
+
101
+ if (newLevel === targetLevel)
102
+ return true;
103
+ }
104
+
105
+ return false;
106
+ }
107
+
108
+ /*
109
+ * buildElementIconTypes
110
+ * Build out the iconType array per element
111
+ * if level 0 return undefined
112
+ * on current level, is element a sibling-child or child.
113
+ * if current level === level 1, return this single-element array
114
+ * cycle through remaining levels, determining icon to represent relationship for current element
115
+ */
116
+ function buildElementIconTypes(checkBoxArray: ReactElement<CheckBoxProps>[],
117
+ currentIndex: number, bridgeParent: boolean): IconType[] | undefined {
118
+
119
+ const currentLevel = checkBoxArray[currentIndex].props.level ?? 0;
120
+ const icons: IconType[] = [];
121
+
122
+ // if currentLevel === 0, return
123
+ if (currentLevel === 0) {
124
+ return undefined;
125
+ }
126
+
127
+ if (currentLevel > 0 && currentIndex + 1 < checkBoxArray.length
128
+ && checkBoxArray[currentIndex + 1].props.level === currentLevel ) {
129
+ icons.unshift('sibling-child');
130
+ } else if (siblingExists(checkBoxArray, currentIndex, bridgeParent)) {
131
+ icons.unshift('sibling-child');
132
+ } else {
133
+ icons.unshift('child');
134
+ }
135
+
136
+ // we've taken care of 1st level - return if we are only at level 1 already
137
+ if (currentLevel === 1) {
138
+ return icons;
139
+ }
140
+
141
+ if (checkBoxArray.length > currentIndex) {
142
+ // next level we are looking for, place an icon at each level
143
+ for (let level = currentLevel - 1; level > 0; level--) {
144
+ let workingIcon:IconType = 'blank'; // assume blank, re-assign if found differently below
145
+ if (levelExistsLater(checkBoxArray, currentIndex, level)
146
+ || (bridgeParent && levelExistsLater(checkBoxArray, currentIndex, level - 1))) {
147
+ workingIcon = 'sibling'
148
+ }
149
+ icons.unshift(workingIcon);
150
+ }
151
+ }
152
+
153
+ return icons;
154
+ }
155
+
156
+ /*
157
+ * function elementIndeterminate
158
+ * Determines the determinate status of a given element at a given level.
159
+ * Does this by cycling through children elements, looking for all checked,
160
+ * all unchecked, or mix (indeterminate)
161
+ */
162
+ function elementIndeterminate(checkBoxArray: ReactElement<CheckBoxProps>[],
163
+ elements: StatusType[], targetLevel: number, index: number): StatusType {
164
+
165
+ const safeLevel = targetLevel + 1; // one level below target for all children
166
+
167
+ // Array of StatusType: type StatusType = 'checked' | 'unchecked' | 'indeterminate';
168
+ let allChecked = true;
169
+ let allUnChecked = true;
170
+
171
+ // loop through children if exist, checking for checked/unchecked status
172
+ for (let i = index + 1; i < elements.length; i++) {
173
+
174
+ const currentLevel = checkBoxArray[i].props.level ?? 0;
175
+
176
+ // if we are no longer under intended parent, break
177
+ if (currentLevel < safeLevel) {
178
+ break;
179
+ }
180
+
181
+ if (elements[i] === 'checked') {
182
+ allUnChecked = false;
183
+ } else if (elements[i] === 'unchecked') {
184
+ allChecked = false;
185
+ } else if (elements[i] === 'indeterminate') {
186
+ allUnChecked = false;
187
+ allChecked = false;
188
+ break; // we are indeterminate - break
189
+ }
190
+ }
191
+
192
+ let status:StatusType;
193
+ if (allChecked)
194
+ status = 'checked';
195
+ else if (allUnChecked)
196
+ status = 'unchecked';
197
+ else {
198
+ status = 'indeterminate';
199
+ }
200
+
201
+ return status;
202
+ }
203
+
204
+ // fill?: boolean; // fill or std variant
205
+ // icon?: boolean; // use icons to represent ux variants (vs default browser styles)
206
+ // marker?: boolean; // use x-marker instead of check
207
+
208
+ // CheckBoxGroup component - wraps CheckBoxes for the purpose of displaying their relationship icons
209
+ export function CheckBoxGroup({ children, bridgeParent = false, fill = false,
210
+ icon = true, marker = false, showBranch = true }: CheckBoxGroupProps) {
211
+
212
+ const [updateStatus, setUpdateStatus] = useState<{status: StatusType, index: number}>();
213
+
214
+ // memoize / build the raw checkbox array only when “children” changes
215
+ const checkBoxArray = useMemo(
216
+ () =>
217
+ React.Children
218
+ .toArray(children)
219
+ .filter(
220
+ (c): c is ReactElement<CheckBoxProps> =>
221
+ isValidElement<CheckBoxProps>(c) && c.type === CheckBox
222
+ ),
223
+ [children] // execute when children changes
224
+ );
225
+
226
+ const [statusArray, setStatusArray] =
227
+ useState<StatusType[]>(() => checkBoxArray.map(() => 'unchecked'));
228
+
229
+ // IF UPDATESTATUS IS CHANGED, UPDATE STATUS ARRAY. UPDATING THE STATUSARRAY
230
+ // WILL PROMPT AN UPDATE TO THE BELOW USEMEMO
231
+ useEffect(() => {
232
+ if (!updateStatus) return;
233
+ const { index, status } = updateStatus;
234
+
235
+ const currentLevel = checkBoxArray[index].props.level ?? 0;
236
+
237
+ // make copy of statusArray
238
+ const statusArrayCopy:StatusType[] = [...statusArray];
239
+
240
+ // update new status in array copy to use when forwarded
241
+ statusArrayCopy[index] = status;
242
+
243
+ // CYCLE DOWN UPDATING TO STATUS VALUE
244
+ if (status !== 'indeterminate') {
245
+ for (let i = index + 1; i < statusArrayCopy.length; i++) {
246
+ const level = checkBoxArray[i].props.level ?? 0
247
+
248
+ // if we've passed any children of updated checkbox, exit
249
+ if (level <= currentLevel) {
250
+ break;
251
+ }
252
+ // else update item inside array
253
+ else {
254
+ statusArrayCopy[i] = status;
255
+ }
256
+ }
257
+ }
258
+
259
+ // CYCLE UP TO CHECK INDETERMINATE STATUS - immediate parent/level
260
+ for (let targetLevel = currentLevel - 1; targetLevel >= 0; targetLevel--) {
261
+
262
+ // cycle through indexes in reverse, looking for 1st instance of targetLevel
263
+ // this will be our next parent
264
+ for (let i = index - 1; i >= 0; i--) {
265
+
266
+ // determine if parent should be unchecked, checked, or indeterminate
267
+ // call helper function for this
268
+ if (checkBoxArray[i].props.level === targetLevel) {
269
+ statusArrayCopy[i] = elementIndeterminate(checkBoxArray, statusArrayCopy, targetLevel, i);
270
+ break; // continue to next level up / parent
271
+ }
272
+ }
273
+ }
274
+
275
+ // updateStatusArray w/ new information, which updates child components
276
+ setStatusArray(statusArrayCopy);
277
+
278
+ // clear it out if you want, so you don’t re‐run on the same update
279
+ setUpdateStatus(undefined);
280
+
281
+ // eslint-disable-next-line react-hooks/exhaustive-deps
282
+ }, [updateStatus]);
283
+
284
+
285
+ // build the icons, index, and setStatusUpdate function on top of that array
286
+ const enhanced = useMemo(
287
+ () =>
288
+ checkBoxArray.map((child, idx) => {
289
+ const iconType = buildElementIconTypes(checkBoxArray, idx, bridgeParent);
290
+ // wrap the state‐setter so it matches (status, index) => void
291
+ const handleStatusUpdate = (status: StatusType, index: number) => {
292
+ setUpdateStatus({ status, index });
293
+ };
294
+
295
+ return cloneElement(child, {
296
+ iconType,
297
+ index: idx,
298
+ fill: fill,
299
+ icon: icon,
300
+ marker: marker,
301
+ showBranch: showBranch,
302
+ status: statusArray[idx],
303
+ setStatusUpdate: handleStatusUpdate,
304
+ });
305
+ }),
306
+ [checkBoxArray, bridgeParent, fill, icon, marker, showBranch, statusArray] // execute when one of these change
307
+ );
308
+
309
+ return <div>{enhanced}</div>;
310
+ }
311
+
312
+ /*
313
+ * CheckBox
314
+ * This is the CheckBox component, it represents an individual CheckBox item
315
+ * on the page.
316
+ */
317
+ export const CheckBox = React.forwardRef<HTMLInputElement, CheckBoxProps>(
318
+ ({ className = '', icon = true, classNameInput = '', ariaLabel='CheckBox Component',
319
+ value = 'on', fill=false, level = 0, classNameSvg='', iconType = [], marker = false,
320
+ index, setStatusUpdate, status, children, showBranch = true, ...props }, ref) => {
321
+
322
+ const [localStatus, setLocalStatus] = useState<StatusType>('unchecked');
323
+ const innerRef = useRef<HTMLInputElement>(null);
324
+ const [checkIcon, setCheckIcon] = useState<string>(CheckBoxEmpty);
325
+
326
+ // forward innerRef.current to outer ref
327
+ useImperativeHandle(ref, () => innerRef.current as HTMLInputElement);
328
+
329
+ // if status is changed, update localStatus
330
+ useEffect(() => {
331
+ if (status)
332
+ setLocalStatus(status);
333
+ }, [status])
334
+
335
+ // if localStatus is changed, update parent
336
+ useEffect(() => {
337
+ // guard against calling function when localStatus is set to status
338
+ if (setStatusUpdate && index !== undefined) {
339
+ setStatusUpdate(localStatus, index);
340
+ }
341
+
342
+ // set appropriate indeterminate state
343
+ if (innerRef.current) {
344
+ innerRef.current.indeterminate = localStatus === 'indeterminate';
345
+ }
346
+
347
+ // eslint-disable-next-line react-hooks/exhaustive-deps
348
+ }, [localStatus]);
349
+
350
+ useEffect(() => {
351
+ if (icon) {
352
+ if (localStatus === 'checked') {
353
+ if (fill) {
354
+ if (marker) {
355
+ setCheckIcon(CheckBoxMarkerFill);
356
+ } else {
357
+ setCheckIcon(CheckBoxCheckedFill);
358
+ }
359
+ } else {
360
+ if (marker) {
361
+ setCheckIcon(CheckBoxMarker);
362
+ } else {
363
+ setCheckIcon(CheckBoxChecked);
364
+ }
365
+ }
366
+ } else if (localStatus === 'unchecked') {
367
+ if (fill) {
368
+ setCheckIcon(CheckBoxEmptyFill);
369
+ } else {
370
+ setCheckIcon(CheckBoxEmpty);
371
+ }
372
+
373
+ } else if (localStatus === 'indeterminate') {
374
+ if (fill) {
375
+ setCheckIcon(CheckBoxIndeterminateFill)
376
+ } else {
377
+ setCheckIcon(CheckBoxIndeterminate)
378
+ }
379
+ }
380
+ }
381
+ }, [fill, icon, localStatus, marker]);
382
+
383
+
384
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
385
+ if (e.target.checked) {
386
+ setLocalStatus('checked');
387
+ } else {
388
+ setLocalStatus('unchecked');
389
+ }
390
+ };
391
+
392
+ return (
393
+ <div className={twMerge('flex items-center', className)}>
394
+ {iconType.map((type, idx) => (
395
+ <CheckBoxIcon
396
+ key={idx}
397
+ type={showBranch ? type : 'blank'}
398
+ icon={icon} // changes starting margin to fix alignment w/ svg checkboxes
399
+ classNameLine={twMerge('h-full border',classNameSvg)}
400
+ />
401
+ ))}
402
+
403
+ <label className={twMerge("inline-flex items-center gap-1 text-base md:text-lg", className)}>
404
+ <input
405
+ ref={innerRef}
406
+ type="checkbox"
407
+ value={value}
408
+ checked={localStatus !== 'indeterminate' && localStatus === 'checked' }
409
+ aria-label={ariaLabel}
410
+ className={twMerge('', classNameInput, icon && 'hidden')}
411
+ onChange={handleChange}
412
+ {...props}
413
+ />
414
+ {icon && <img alt={localStatus} src={checkIcon} className='' />}
415
+ {children}
416
+ </label>
417
+ </div>
418
+ );
419
+ }
420
+ );
421
+
422
+ CheckBox.displayName = 'CheckBox';
423
+
424
+ /*
425
+ * type
426
+ * sibling --> a vertical bar leading to the next sibling when there are children between
427
+ * sibling-child --> sibling w/ a branch that leads to a child
428
+ * child (only) --> like sibling-child, but does not include bottom portion of vertical bar that leads to sibling
429
+ */
430
+ interface CheckBoxIconProps {
431
+ type?: 'sibling' | 'sibling-child' | 'child' | 'blank';
432
+ color?: string;
433
+ styles?: string;
434
+ className?: string;
435
+ classNameLine?: string;
436
+ icon?: boolean;
437
+ }
438
+ // https://mediamodifier.com/svg-editor#
439
+ const CheckBoxIcon = ({ color, styles, type = 'sibling', icon = true,
440
+ className=twMerge('h-7', icon && 'ms-1'), classNameLine='' }: CheckBoxIconProps) => {
441
+
442
+ if (type === 'sibling') { // vertical bar
443
+ return (
444
+ <svg version="1.1"
445
+ className={twMerge('size-4', className)}
446
+ xmlns="http://www.w3.org/2000/svg">
447
+
448
+ <line x1="40%" y1="0%" x2="40%" y2="100%"
449
+ className={twMerge('stroke-[#a1a6a8]', classNameLine)} />
450
+ </svg>
451
+ );
452
+ }
453
+ else if (type === 'sibling-child') {
454
+ return (
455
+ <svg version="1.1"
456
+ className={twMerge('size-4', className)}
457
+ xmlns="http://www.w3.org/2000/svg">
458
+
459
+ <line x1="40%" y1="0%" x2="40%" y2="100%"
460
+ className={twMerge('stroke-[#a1a6a8]', classNameLine)} />
461
+
462
+ <line x1="40%" y1="50%" x2="100%" y2="50%"
463
+ className={twMerge('stroke-[#a1a6a8]', classNameLine)} />
464
+
465
+ </svg>
466
+ );
467
+ }
468
+ else if (type === 'child') {
469
+ return (
470
+ <svg version="1.1"
471
+ className={twMerge('size-4', className)}
472
+ xmlns="http://www.w3.org/2000/svg">
473
+
474
+ <line x1="40%" y1="0%" x2="40%" y2="52%"
475
+ className={twMerge('stroke-[#a1a6a8]', classNameLine)} />
476
+
477
+ <line x1="38%" y1="50%" x2="100%" y2="50%"
478
+ className={twMerge('stroke-[#a1a6a8]', classNameLine)} />
479
+
480
+ </svg>
481
+ );
482
+ }
483
+ else if (type === 'blank') {
484
+ return (
485
+ <svg version="1.1"
486
+ className={twMerge('size-4', className)}
487
+ xmlns="http://www.w3.org/2000/svg">
488
+ </svg>
489
+ );
490
+ }
491
+ };