@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,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
|
+
<
|
|
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
|
+
>
|
|
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
|
+
}
|