@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.
- package/.babelrc +12 -0
- package/.storybook/main.ts +35 -0
- package/.storybook/preview.ts +4 -0
- package/BAKpostcss.config.jsBAK +15 -0
- package/BAKtailwind.config.mjsBAK +99 -0
- package/README.md +464 -16
- package/coverage/storybook/coverage-storybook.json +32411 -0
- package/coverage/storybook/lcov-report/Accordion.tsx.html +805 -0
- package/coverage/storybook/lcov-report/Badge.tsx.html +346 -0
- package/coverage/storybook/lcov-report/Breadcrumbs.tsx.html +742 -0
- package/coverage/storybook/lcov-report/Button.tsx.html +448 -0
- package/coverage/storybook/lcov-report/ButtonGroup.tsx.html +403 -0
- package/coverage/storybook/lcov-report/Card.tsx.html +292 -0
- package/coverage/storybook/lcov-report/CharacterCounter.tsx.html +253 -0
- package/coverage/storybook/lcov-report/CheckBox.tsx.html +1555 -0
- package/coverage/storybook/lcov-report/DatePicker.tsx.html +826 -0
- package/coverage/storybook/lcov-report/Input.tsx.html +1012 -0
- package/coverage/storybook/lcov-report/List.tsx.html +364 -0
- package/coverage/storybook/lcov-report/Modal.tsx.html +745 -0
- package/coverage/storybook/lcov-report/Pill.tsx.html +358 -0
- package/coverage/storybook/lcov-report/Search.tsx.html +997 -0
- package/coverage/storybook/lcov-report/SearchContent.tsx.html +235 -0
- package/coverage/storybook/lcov-report/SectionHeader.tsx.html +358 -0
- package/coverage/storybook/lcov-report/Select.tsx.html +1012 -0
- package/coverage/storybook/lcov-report/Shield.tsx.html +802 -0
- package/coverage/storybook/lcov-report/SideBarNav.tsx.html +490 -0
- package/coverage/storybook/lcov-report/Skeleton.tsx.html +394 -0
- package/coverage/storybook/lcov-report/Slider.tsx.html +385 -0
- package/coverage/storybook/lcov-report/Status.tsx.html +322 -0
- package/coverage/storybook/lcov-report/Tabs.tsx.html +610 -0
- package/coverage/storybook/lcov-report/Toggle.tsx.html +373 -0
- package/coverage/storybook/lcov-report/Tooltip.tsx.html +496 -0
- package/coverage/storybook/lcov-report/base.css +224 -0
- package/coverage/storybook/lcov-report/block-navigation.js +87 -0
- package/coverage/storybook/lcov-report/favicon.png +0 -0
- package/coverage/storybook/lcov-report/index.html +476 -0
- package/coverage/storybook/lcov-report/prettify.css +1 -0
- package/coverage/storybook/lcov-report/prettify.js +2 -0
- package/coverage/storybook/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/storybook/lcov-report/sorter.js +196 -0
- package/coverage/storybook/lcov.info +2312 -0
- package/dist/README.md +1815 -0
- package/eslint.config.mjs +13 -0
- package/package.json +6 -7
- package/project.json +11 -0
- package/src/assets/img/Frame.svg +5 -0
- package/src/assets/img/backArrowRight.svg +10 -0
- package/src/assets/img/bc-separator.png +0 -0
- package/src/assets/img/calendar.png +0 -0
- package/src/assets/img/calendar.svg +4 -0
- package/src/assets/img/check.svg +5 -0
- package/src/assets/img/check_box.svg +10 -0
- package/src/assets/img/check_box_empty.svg +10 -0
- package/src/assets/img/check_box_fill.svg +10 -0
- package/src/assets/img/check_box_fill_empty.svg +10 -0
- package/src/assets/img/chevron-down-white.svg +2 -0
- package/src/assets/img/chevron-down.svg +2 -0
- package/src/assets/img/chevron-left.svg +1 -0
- package/src/assets/img/chevron-right-light.svg +4 -0
- package/src/assets/img/chevron-right.svg +3 -0
- package/src/assets/img/chevron-up-white.svg +1 -0
- package/src/assets/img/chevron-up.svg +1 -0
- package/src/assets/img/clock.svg +6 -0
- package/src/assets/img/close.svg +1 -0
- package/src/assets/img/close2.svg +6 -0
- package/src/assets/img/closeModal.svg +10 -0
- package/src/assets/img/close_icon_dark.svg +10 -0
- package/src/assets/img/close_small.svg +3 -0
- package/src/assets/img/emergency_home.svg +10 -0
- package/src/assets/img/first-aid-kit.svg +7 -0
- package/src/assets/img/heartbeat.svg +4 -0
- package/src/assets/img/home-gray.svg +3 -0
- package/src/assets/img/home.svg +3 -0
- package/src/assets/img/hospital.jpg +0 -0
- package/src/assets/img/indeterminate_check_box.svg +10 -0
- package/src/assets/img/indeterminate_check_box_fill.svg +10 -0
- package/src/assets/img/info_24_ 1d4ed8.svg +3 -0
- package/src/assets/img/info_24_ 2c6441.svg +3 -0
- package/src/assets/img/marker_check_by_default.svg +10 -0
- package/src/assets/img/marker_check_by_default_fill.svg +10 -0
- package/src/assets/img/minus-accordion.svg +5 -0
- package/src/assets/img/minus.svg +3 -0
- package/src/assets/img/open.svg +1 -0
- package/src/assets/img/pill-white.svg +7 -0
- package/src/assets/img/pill.svg +5 -0
- package/src/assets/img/plus-accordion.svg +5 -0
- package/src/assets/img/plus.svg +4 -0
- package/src/assets/img/prescription.svg +6 -0
- package/src/assets/img/search.svg +10 -0
- package/src/assets/img/search_icon_light.svg +10 -0
- package/src/assets/img/separator.svg +3 -0
- package/src/assets/img/stethoscope-white.svg +8 -0
- package/src/assets/img/stethoscope.svg +8 -0
- package/src/assets/img/thumb_up.svg +10 -0
- package/src/assets/img/vector.svg +3 -0
- package/src/assets/img/warning-badge-disabled.svg +11 -0
- package/src/assets/img/warning-badge-green.svg +11 -0
- package/src/assets/img/warning-badge-red.svg +11 -0
- package/src/assets/img/warning-badge-yellow.svg +11 -0
- package/src/assets/img/warning.svg +10 -0
- package/src/global.d.ts +13 -0
- package/{index.d.ts → src/index.ts} +13 -5
- package/src/lib/Accordian--Accordian.stories.tsx +312 -0
- package/src/lib/Accordion.spec.tsx +384 -0
- package/src/lib/Accordion.tsx +240 -0
- package/src/lib/AppointmentPicker.spec.tsx +138 -0
- package/src/lib/AppointmentPicker.tsx +97 -0
- package/src/lib/Badge--Badge.stories.tsx +60 -0
- package/src/lib/Badge.spec.tsx +70 -0
- package/src/lib/Badge.tsx +87 -0
- package/src/lib/Breadcrumbs-Breadcrumbs.stories.tsx +114 -0
- package/src/lib/Breadcrumbs.spec.tsx +218 -0
- package/src/lib/Breadcrumbs.tsx +219 -0
- package/src/lib/Button--Button.stories.tsx +220 -0
- package/src/lib/Button.spec.tsx +241 -0
- package/src/lib/Button.tsx +121 -0
- package/src/lib/ButtonGroup--ButtonGroup.stories.tsx +129 -0
- package/src/lib/ButtonGroup.spec.tsx +89 -0
- package/src/lib/ButtonGroup.tsx +107 -0
- package/src/lib/Card--Card.stories.tsx +113 -0
- package/src/lib/Card.spec.tsx +112 -0
- package/src/lib/Card.tsx +69 -0
- package/src/lib/CharacterCounter--CharacterCounter.stories.tsx +169 -0
- package/src/lib/CharacterCounter.spec.tsx +123 -0
- package/src/lib/CharacterCounter.tsx +56 -0
- package/src/lib/CheckBox--CheckBox.stories.tsx +107 -0
- package/src/lib/CheckBox.spec.tsx +412 -0
- package/src/lib/CheckBox.tsx +491 -0
- package/src/lib/DatePicker--DatePicker.stories.tsx +228 -0
- package/src/lib/DatePicker.spec.tsx +424 -0
- package/src/lib/DatePicker.tsx +247 -0
- package/src/lib/Input--Input.stories.tsx +449 -0
- package/src/lib/Input.spec.tsx +281 -0
- package/src/lib/Input.tsx +309 -0
- package/src/lib/List--List.stories.tsx +157 -0
- package/src/lib/List.spec.tsx +211 -0
- package/src/lib/List.tsx +93 -0
- package/src/lib/Modal--Modal.stories.tsx +454 -0
- package/src/lib/Modal.spec.tsx +202 -0
- package/src/lib/Modal.tsx +220 -0
- package/src/lib/Pill--Pill.stories.tsx +98 -0
- package/src/lib/Pill.spec.tsx +103 -0
- package/src/lib/Pill.tsx +91 -0
- package/src/lib/ProgressBar.spec.tsx +106 -0
- package/src/lib/ProgressBar.tsx +112 -0
- package/src/lib/RadioGroup.spec.tsx +84 -0
- package/src/lib/RadioGroup.tsx +74 -0
- package/src/lib/RadioIcon.tsx +13 -0
- package/src/lib/Search--Search.stories.tsx +67 -0
- package/src/lib/Search.spec.tsx +182 -0
- package/src/lib/Search.tsx +304 -0
- package/src/lib/SearchContent.tsx +51 -0
- package/src/lib/SectionHeader--SectionHeader.stories.tsx +98 -0
- package/src/lib/SectionHeader.spec.tsx +60 -0
- package/src/lib/SectionHeader.tsx +91 -0
- package/src/lib/Select--Select.stories.tsx +387 -0
- package/src/lib/Select.spec.tsx +493 -0
- package/src/lib/Select.tsx +311 -0
- package/src/lib/Shield--Shield.stories.tsx +196 -0
- package/src/lib/Shield.spec.tsx +275 -0
- package/src/lib/Shield.tsx +239 -0
- package/src/lib/SideBarNav--SideBarNav.stories.tsx +136 -0
- package/src/lib/SideBarNav.spec.tsx +178 -0
- package/src/lib/SideBarNav.tsx +135 -0
- package/src/lib/Skeleton--Skeleton.stories.tsx +77 -0
- package/src/lib/Skeleton.module.css +16 -0
- package/src/lib/Skeleton.spec.tsx +83 -0
- package/src/lib/Skeleton.tsx +103 -0
- package/src/lib/SkipLink.spec.tsx +76 -0
- package/src/lib/SkipLink.tsx +48 -0
- package/src/lib/Slider--Slider.stories.tsx +108 -0
- package/src/lib/Slider.module.css +109 -0
- package/src/lib/Slider.spec.tsx +67 -0
- package/src/lib/Slider.tsx +101 -0
- package/src/lib/Status--Status.stories.tsx +93 -0
- package/src/lib/Status.spec.tsx +118 -0
- package/src/lib/Status.tsx +79 -0
- package/src/lib/Tabs--Tabs.stories.tsx +294 -0
- package/src/lib/Tabs.spec.tsx +249 -0
- package/src/lib/Tabs.tsx +188 -0
- package/src/lib/Tester.spec.tsx +17 -0
- package/src/lib/Toggle--Toggle.stories.tsx +162 -0
- package/src/lib/Toggle.spec.tsx +122 -0
- package/src/lib/Toggle.tsx +96 -0
- package/src/lib/Tooltip--Tooltip.stories.tsx +315 -0
- package/src/lib/Tooltip.spec.tsx +307 -0
- package/src/lib/Tooltip.tsx +137 -0
- package/src/lib/bak-simple-ui.stories.tsx-bak +24 -0
- package/src/styles.css +190 -0
- package/tsconfig.json +25 -0
- package/tsconfig.lib.json +42 -0
- package/tsconfig.spec.json +29 -0
- package/tsconfig.storybook.json +36 -0
- package/vite.config.mts +87 -0
- package/vitest.setup.ts +12 -0
- package/index.css +0 -1
- package/index.js +0 -35
- package/index.mjs +0 -4981
- package/lib/Accordion.d.ts +0 -36
- package/lib/AppointmentPicker.d.ts +0 -21
- package/lib/Badge.d.ts +0 -11
- package/lib/Breadcrumbs.d.ts +0 -13
- package/lib/Button.d.ts +0 -15
- package/lib/ButtonGroup.d.ts +0 -8
- package/lib/Card.d.ts +0 -11
- package/lib/CharacterCounter.d.ts +0 -11
- package/lib/CheckBox.d.ts +0 -30
- package/lib/DatePicker.d.ts +0 -7
- package/lib/Input.d.ts +0 -16
- package/lib/List.d.ts +0 -22
- package/lib/Modal.d.ts +0 -18
- package/lib/Pill.d.ts +0 -13
- package/lib/ProgressBar.d.ts +0 -19
- package/lib/RadioGroup.d.ts +0 -15
- package/lib/Search.d.ts +0 -26
- package/lib/SearchContent.d.ts +0 -6
- package/lib/SectionHeader.d.ts +0 -18
- package/lib/Select.d.ts +0 -19
- package/lib/Shield.d.ts +0 -12
- package/lib/SideBarNav.d.ts +0 -21
- package/lib/Skeleton.d.ts +0 -15
- package/lib/SkipLink.d.ts +0 -22
- package/lib/Slider.d.ts +0 -14
- package/lib/Status.d.ts +0 -10
- package/lib/Tabs.d.ts +0 -23
- package/lib/Toggle.d.ts +0 -11
- 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
|
+
};
|