@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,112 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
//needs work, will need additional props for sure
|
|
5
|
+
//idea here is that the currentStep is held outside of the component as a piece of state
|
|
6
|
+
export interface ProgressBarProps {
|
|
7
|
+
isStep?: boolean;
|
|
8
|
+
classNameFillColor?: string;
|
|
9
|
+
classNameBorderColor?: string;
|
|
10
|
+
classNameGradient?: string;
|
|
11
|
+
classNameArrowColor?: string;
|
|
12
|
+
/** Total number of steps in the progress bar */
|
|
13
|
+
totalSteps: number;
|
|
14
|
+
/** Current active step (1-based index) */
|
|
15
|
+
currentStep: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A progress bar component that shows a series of steps as circles with connecting lines.
|
|
20
|
+
* When steps are completed that step as well as the line up to the next circle are filled
|
|
21
|
+
* in with the specified color
|
|
22
|
+
*/
|
|
23
|
+
export const ProgressBar: React.FC<ProgressBarProps> = (
|
|
24
|
+
{
|
|
25
|
+
totalSteps,
|
|
26
|
+
currentStep,
|
|
27
|
+
classNameArrowColor='fill-[#305B25]',
|
|
28
|
+
classNameFillColor='bg-green-500',
|
|
29
|
+
classNameBorderColor='border-[#305B25]',
|
|
30
|
+
classNameGradient='from-[#305B25] to-[#5AAB46]',
|
|
31
|
+
isStep=true
|
|
32
|
+
}
|
|
33
|
+
) => {
|
|
34
|
+
// Generate an array of step indices up to totalSteps
|
|
35
|
+
const steps = Array.from({ length: totalSteps }, (_, i) => i + 1);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="flex items-center w-full">
|
|
39
|
+
{steps.map((step, index) => {
|
|
40
|
+
const isActive = step <= currentStep-1;
|
|
41
|
+
const isLast = index === steps.length - 1;
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
{/* Circle for each step */}
|
|
45
|
+
<div
|
|
46
|
+
className={twMerge(`flex items-center justify-center size-8 rounded-full border-[3px] border-gray-300 text-gray-500`,
|
|
47
|
+
`${isActive && `${classNameFillColor} ${classNameBorderColor}`}`)}
|
|
48
|
+
>
|
|
49
|
+
{isStep ? <div className={`${!isActive ? 'text-gray-400' : 'text-white'}`}>{step}</div>
|
|
50
|
+
: isActive ? (
|
|
51
|
+
<svg width="24" height="25" viewBox="0 0 24 25" className={`${!isActive ? 'fill-gray-400' : 'fill-white'}`} xmlns="http://www.w3.org/2000/svg">
|
|
52
|
+
<g id="check_box">
|
|
53
|
+
<mask id="mask0_4855_19004" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="25">
|
|
54
|
+
<rect id="Bounding box" y="0.5" width="24" height="24"/>
|
|
55
|
+
</mask>
|
|
56
|
+
<g mask="url(#mask0_4855_19004)">
|
|
57
|
+
<path id="Icon" d="M10.6001 14.2465L8.27711 11.9232C8.13861 11.7849 7.96453 11.7141 7.75486
|
|
58
|
+
11.7107C7.54536 11.7076 7.36811 11.7784 7.22311 11.9232C7.07828 12.0682 7.00586 12.2439 7.00586
|
|
59
|
+
12.4502C7.00586 12.6566 7.07828 12.8322 7.22311 12.9772L9.96736 15.7215C10.1482 15.9021 10.3591
|
|
60
|
+
15.9925 10.6001 15.9925C10.8411 15.9925 11.052 15.9021 11.2329 15.7215L16.7964 10.158C16.9347
|
|
61
|
+
10.0195 17.0055 9.84538 17.0089 9.63572C17.012 9.42622 16.9412 9.24897 16.7964 9.10397C16.6514
|
|
62
|
+
8.95914 16.4757 8.88672 16.2694 8.88672C16.063 8.88672 15.8874 8.95914 15.7424 9.10397L10.6001 14.2465Z"/>
|
|
63
|
+
</g>
|
|
64
|
+
</g>
|
|
65
|
+
</svg>
|
|
66
|
+
) : (
|
|
67
|
+
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
68
|
+
<g id="check_box">
|
|
69
|
+
<mask id="mask0_4855_17150" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="25">
|
|
70
|
+
<rect id="Bounding box" y="0.5" width="24" height="24" fill="#D9D9D9"/>
|
|
71
|
+
</mask>
|
|
72
|
+
<g mask="url(#mask0_4855_17150)">
|
|
73
|
+
<path id="Icon" d="M15.75 13H7.75C7.53717 13 7.359 12.9282 7.2155 12.7845C7.07183 12.641 7 12.4628 7
|
|
74
|
+
12.25C7 12.0372 7.07183 11.859 7.2155 11.7155C7.359 11.5718 7.53717 11.5 7.75 11.5H15.75C15.9628 11.5
|
|
75
|
+
16.141 11.5718 16.2845 11.7155C16.4282 11.859 16.5 12.0372 16.5 12.25C16.5 12.4628 16.4282 12.641 16.2845
|
|
76
|
+
12.7845C16.141 12.9282 15.9628 13 15.75 13Z" fill="#939194"/>
|
|
77
|
+
</g>
|
|
78
|
+
</g>
|
|
79
|
+
</svg>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Line between circles, except after the last step */}
|
|
84
|
+
{!isLast && (
|
|
85
|
+
<div
|
|
86
|
+
className={twMerge(`relative -z-10 -mx-[0.1em] flex-1 h-1 bg-gray-300`,
|
|
87
|
+
`${isActive && `bg-linear-to-r ${classNameGradient} from-50%`}`
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
{/* Arrow in the middle of the completed segment */}
|
|
91
|
+
{step < currentStep && step > currentStep - 2 &&(
|
|
92
|
+
<div className="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2" >
|
|
93
|
+
<svg viewBox="0 0 23 18" className="size-8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
94
|
+
<g id="Arrow">
|
|
95
|
+
<rect width="22.48" height="18" />
|
|
96
|
+
<path id="Polygon 1" className={classNameArrowColor} d="M13.452 9.79161C13.9706 9.39128 13.9706 8.60871 13.452
|
|
97
|
+
8.20839L9.61096 5.24354C8.95357 4.73611 7.99993 5.2047 7.99993 6.03514L7.99993
|
|
98
|
+
11.9649C7.99993 12.7953 8.95357 13.2639 9.61096 12.7565L13.452 9.79161Z"/>
|
|
99
|
+
</g>
|
|
100
|
+
</svg>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default ProgressBar;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { axe } from "vitest-axe";
|
|
4
|
+
import { RadioGroup, RadioOption } from "./RadioGroup";
|
|
5
|
+
import { vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
describe("RadioGroup Component", () => {
|
|
8
|
+
const options: RadioOption[] = [
|
|
9
|
+
{ label: "Option A", value: "a" },
|
|
10
|
+
{ label: "Option B", value: "b", disabled: true },
|
|
11
|
+
{ label: "Option C", value: "c" },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
it("renders all radio options with proper labels and role", () => {
|
|
15
|
+
render(<RadioGroup options={options} />);
|
|
16
|
+
const radios = screen.getAllByRole("radio");
|
|
17
|
+
expect(radios).toHaveLength(3);
|
|
18
|
+
options.forEach(({ label }) => {
|
|
19
|
+
const radio = screen.getByRole("radio", { name: label });
|
|
20
|
+
expect(radio).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("no option is selected by default", () => {
|
|
25
|
+
render(<RadioGroup options={options} />);
|
|
26
|
+
options.forEach(({ label }) => {
|
|
27
|
+
const radio = screen.getByRole("radio", { name: label });
|
|
28
|
+
expect(radio).not.toBeChecked();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("selects an option when clicked and expects it to be checked", () => {
|
|
33
|
+
render(<RadioGroup options={options} />);
|
|
34
|
+
const radioA = screen.getByRole("radio", { name: "Option A" });
|
|
35
|
+
|
|
36
|
+
fireEvent.click(radioA);
|
|
37
|
+
|
|
38
|
+
expect(radioA).toBeChecked();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("properly renders a disabled radio button", () => {
|
|
42
|
+
render(<RadioGroup options={options} />);
|
|
43
|
+
const radioB = screen.getByRole("radio", { name: "Option B" });
|
|
44
|
+
|
|
45
|
+
expect(radioB).toBeDisabled();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("applies custom classes to container, label, and input elements", () => {
|
|
49
|
+
render(
|
|
50
|
+
<RadioGroup
|
|
51
|
+
options={options}
|
|
52
|
+
classNameContainer="container-class"
|
|
53
|
+
classNameLabel="label-class"
|
|
54
|
+
classNameInput="input-class"
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const group = screen.getByRole("radiogroup");
|
|
59
|
+
expect(group).toHaveClass("container-class");
|
|
60
|
+
|
|
61
|
+
const labelA = screen.getByText("Option A").closest("label");
|
|
62
|
+
expect(labelA).toHaveClass("label-class");
|
|
63
|
+
|
|
64
|
+
const inputA = screen.getByRole("radio", { name: "Option A" });
|
|
65
|
+
expect(inputA).toHaveClass("input-class");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("RadioGroup Accessibility Tests", () => {
|
|
70
|
+
it("should have no accessibility violations by default", async () => {
|
|
71
|
+
const { container } = render(<RadioGroup options={[]} />);
|
|
72
|
+
const results = await axe(container);
|
|
73
|
+
expect(results).toHaveNoViolations();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should have no accessibility violations with disabled options", async () => {
|
|
77
|
+
const opts: RadioOption[] = [
|
|
78
|
+
{ label: "Disabled Option", value: "d", disabled: true },
|
|
79
|
+
];
|
|
80
|
+
const { container } = render(<RadioGroup options={opts} />);
|
|
81
|
+
const results = await axe(container);
|
|
82
|
+
expect(results).toHaveNoViolations();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
// import circleBorder from '../assets/img/circle-border.svg';
|
|
4
|
+
import { RadioIcon } from './RadioIcon';
|
|
5
|
+
|
|
6
|
+
export type RadioOption = {
|
|
7
|
+
label: string;
|
|
8
|
+
value: string;
|
|
9
|
+
id: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type RadioGroupProps = {
|
|
14
|
+
title?: string;
|
|
15
|
+
classNameLabel?: string;
|
|
16
|
+
classNameInput?: string;
|
|
17
|
+
classNameContainer?: string;
|
|
18
|
+
options: RadioOption[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const RadioGroup: React.FC<RadioGroupProps> = ({
|
|
22
|
+
title,
|
|
23
|
+
classNameLabel,
|
|
24
|
+
classNameInput,
|
|
25
|
+
classNameContainer,
|
|
26
|
+
options,
|
|
27
|
+
}) => {
|
|
28
|
+
const [internalValue, setInternalValue] = useState('');
|
|
29
|
+
|
|
30
|
+
const handleChange = (val: string) => {
|
|
31
|
+
setInternalValue(val);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const selectedValue = internalValue;
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<fieldset role="radiogroup" className={classNameContainer}>
|
|
39
|
+
<p className="mb-2">{title}</p>
|
|
40
|
+
{options.map(({ label, value: val, id, disabled }) => (
|
|
41
|
+
// const isSelected = internalValue === val;
|
|
42
|
+
|
|
43
|
+
<label
|
|
44
|
+
key={val}
|
|
45
|
+
className={twMerge(`flex mb-2 items-center gap-1 cursor-pointer ${
|
|
46
|
+
disabled ? "opacity-50 cursor-not-allowed" : ""
|
|
47
|
+
}`, classNameLabel)}
|
|
48
|
+
>
|
|
49
|
+
|
|
50
|
+
<span className="rounded-full size-4 border border-black flex items-center justify-center">
|
|
51
|
+
{val === internalValue &&
|
|
52
|
+
// <img className='fill-red-500'
|
|
53
|
+
// src={circleCenter}
|
|
54
|
+
// alt={"radio button selected"}
|
|
55
|
+
// />
|
|
56
|
+
<RadioIcon className='' />
|
|
57
|
+
}
|
|
58
|
+
</span>
|
|
59
|
+
|
|
60
|
+
<input
|
|
61
|
+
id={id}
|
|
62
|
+
className='hidden'
|
|
63
|
+
type="radio"
|
|
64
|
+
value={val}
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
checked={selectedValue === val}
|
|
67
|
+
onChange={() => handleChange(val)}
|
|
68
|
+
/>
|
|
69
|
+
{label}
|
|
70
|
+
</label>
|
|
71
|
+
))}
|
|
72
|
+
</fieldset>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// RadioIcon.tsx
|
|
2
|
+
export function RadioIcon({ className }: { className?: string }) {
|
|
3
|
+
console.log('RadioIcon: className = ' + className);
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
viewBox="0 0 100 100"
|
|
8
|
+
className={className}
|
|
9
|
+
>
|
|
10
|
+
<circle cx="50" cy="50" r="35" fill="currentColor" />
|
|
11
|
+
</svg>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Meta, StoryContext } from '@storybook/react';
|
|
2
|
+
import { Search, SearchProps, DataSearchResults } from './Search';
|
|
3
|
+
import { SearchableData } from './SearchContent';
|
|
4
|
+
import { userEvent, within, waitFor } from 'storybook/test';
|
|
5
|
+
import { expect } from 'storybook/test';
|
|
6
|
+
import { Component, FC, ReactNode, useRef, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
title: 'Components/Search',
|
|
11
|
+
component: Search,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'centered',
|
|
14
|
+
backgrounds: {
|
|
15
|
+
default: 'light',
|
|
16
|
+
values: [
|
|
17
|
+
{ name: 'light', value: '#f0f0f0' },
|
|
18
|
+
{ name: 'dark', value: '#444849' },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
argTypes: {
|
|
23
|
+
className: { control: 'text' },
|
|
24
|
+
classNameLabel: { control: 'text' },
|
|
25
|
+
ariaLabel: { control: 'text' },
|
|
26
|
+
placeholder: { control: 'text' },
|
|
27
|
+
},
|
|
28
|
+
args: {
|
|
29
|
+
|
|
30
|
+
},
|
|
31
|
+
} as Meta<typeof Search>;
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
export const DefaultSearch = (props: SearchProps) => {
|
|
35
|
+
const [searchResults, setSearchResults] = useState<DataSearchResults>();
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
|
|
40
|
+
<div className='block md:hidden mb-4 text-blue-500'>small</div>
|
|
41
|
+
<div className="hidden md:block lg:hidden mb-4 text-blue-500">medium</div>
|
|
42
|
+
<div className="hidden lg:block mb-4 text-blue-500">large</div>
|
|
43
|
+
<Search
|
|
44
|
+
searchableData={SearchableData}
|
|
45
|
+
setSearchResults={setSearchResults}
|
|
46
|
+
/>
|
|
47
|
+
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// export const XClears = (props: SearchProps) => {
|
|
53
|
+
// const [searchResults, setSearchResults] = useState<DataSearchResults>();
|
|
54
|
+
|
|
55
|
+
// return (
|
|
56
|
+
// <>
|
|
57
|
+
|
|
58
|
+
// <div className='block md:hidden mb-4 text-blue-500'>small</div>
|
|
59
|
+
// <div className="hidden md:block lg:hidden mb-4 text-blue-500">medium</div>
|
|
60
|
+
// <div className="hidden lg:block mb-4 text-blue-500">large</div>
|
|
61
|
+
// <Search
|
|
62
|
+
// searchableData={SearchableData}
|
|
63
|
+
// setSearchResults={setSearchResults}
|
|
64
|
+
// />
|
|
65
|
+
// </>
|
|
66
|
+
// );
|
|
67
|
+
// };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import React, { createRef } from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
+
import { axe } from 'vitest-axe';
|
|
5
|
+
import { Search, DataSearchResults } from './Search';
|
|
6
|
+
|
|
7
|
+
// Mock SearchableData for predictable search behavior
|
|
8
|
+
const data = [
|
|
9
|
+
{ path: '/page1', title: 'Page One', content: 'Alpha bravo charlie delta alpha bravo' },
|
|
10
|
+
{ path: '/page2', title: 'Page Two', content: 'Echo foxtrot golf hotel echo foxtrot' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
describe('Search Component', () => {
|
|
15
|
+
let setSearchResults: vi.Mock;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
setSearchResults = vi.fn();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('renders an input with default classes and forwarded props', () => {
|
|
22
|
+
render(<Search searchableData={data} ariaLabel="Search Label" placeholder="Search..." setSearchResults={setSearchResults} data-testid="search-input" />);
|
|
23
|
+
const input = screen.getByTestId('search-input');
|
|
24
|
+
expect(input).toBeInTheDocument();
|
|
25
|
+
// Should have baseClasses from implementation
|
|
26
|
+
expect(input).toHaveClass('h-8', 'py-2', 'border-[#07192d]');
|
|
27
|
+
expect(input).toHaveAttribute('placeholder', 'Search...');
|
|
28
|
+
// aria-label should match label prop
|
|
29
|
+
expect(input).toHaveAttribute('aria-label', 'Search Label');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('forwards ref to the input element', () => {
|
|
33
|
+
const ref = createRef<HTMLInputElement>();
|
|
34
|
+
render(<Search searchableData={data} ref={ref} setSearchResults={setSearchResults} />);
|
|
35
|
+
expect(ref.current).toBeInstanceOf(HTMLInputElement);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('has no accessibility violations by default', async () => {
|
|
39
|
+
const { container } = render(<Search searchableData={data} setSearchResults={setSearchResults} />);
|
|
40
|
+
const results = await axe(container);
|
|
41
|
+
expect(results).toHaveNoViolations();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('has no accessibility violations with search results', async () => {
|
|
45
|
+
const { container } = render(<Search searchableData={data} setSearchResults={setSearchResults} />);
|
|
46
|
+
const input = screen.getByRole('textbox');
|
|
47
|
+
fireEvent.change(input, { target: { value: 'echo' } });
|
|
48
|
+
const results = await axe(container);
|
|
49
|
+
expect(results).toHaveNoViolations();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('expands from icon-only view when iconLink is true', () => {
|
|
53
|
+
render(
|
|
54
|
+
<Search
|
|
55
|
+
searchableData={data}
|
|
56
|
+
setSearchResults={setSearchResults}
|
|
57
|
+
iconLink
|
|
58
|
+
ariaLabel="Search Label"
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
// initially there's no textbox
|
|
62
|
+
expect(screen.queryByRole('textbox')).toBeNull();
|
|
63
|
+
|
|
64
|
+
// click the lone search-icon button
|
|
65
|
+
const iconButton = screen.getByRole('button');
|
|
66
|
+
fireEvent.click(iconButton);
|
|
67
|
+
|
|
68
|
+
// now the input should be rendered
|
|
69
|
+
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('calls setSearchResults on clicking the search button with non-empty input', () => {
|
|
73
|
+
render(<Search searchableData={data} setSearchResults={setSearchResults} />);
|
|
74
|
+
|
|
75
|
+
// type a query
|
|
76
|
+
const input = screen.getByRole('textbox');
|
|
77
|
+
fireEvent.change(input, { target: { value: 'alpha' } });
|
|
78
|
+
|
|
79
|
+
// click the first search button (small-screen icon)
|
|
80
|
+
const [smallSearchButton] = screen.getAllByRole('button');
|
|
81
|
+
fireEvent.click(smallSearchButton);
|
|
82
|
+
|
|
83
|
+
// should have been called with the filtered results object
|
|
84
|
+
expect(setSearchResults).toHaveBeenCalledWith(
|
|
85
|
+
expect.objectContaining({
|
|
86
|
+
input: 'alpha',
|
|
87
|
+
pages: expect.any(Array),
|
|
88
|
+
}) as DataSearchResults
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('calls setSearchResults with empty pages when clicking search on an empty input', () => {
|
|
93
|
+
render(<Search searchableData={data} setSearchResults={setSearchResults} />);
|
|
94
|
+
|
|
95
|
+
// immediately click the search button
|
|
96
|
+
const [smallSearchButton] = screen.getAllByRole('button');
|
|
97
|
+
fireEvent.click(smallSearchButton);
|
|
98
|
+
|
|
99
|
+
expect(setSearchResults).toHaveBeenCalledWith({
|
|
100
|
+
input: '',
|
|
101
|
+
pages: [{ results: [] }],
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('clears the input and hides results when clicking the clear (X) button', () => {
|
|
106
|
+
render(<Search searchableData={data} setSearchResults={setSearchResults} />);
|
|
107
|
+
|
|
108
|
+
// type something that yields results
|
|
109
|
+
const input = screen.getByRole('textbox');
|
|
110
|
+
fireEvent.change(input, { target: { value: 'echo' } });
|
|
111
|
+
|
|
112
|
+
// the clear button is the last <button> in the DOM
|
|
113
|
+
const buttons = screen.getAllByRole('button');
|
|
114
|
+
const clearButton = buttons[buttons.length - 1];
|
|
115
|
+
fireEvent.click(clearButton);
|
|
116
|
+
|
|
117
|
+
// input should be reset
|
|
118
|
+
expect(input).toHaveValue('');
|
|
119
|
+
|
|
120
|
+
// results list should be gone
|
|
121
|
+
expect(screen.queryByText('echo')).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('navigates to the first result path when pressing Enter without setSearchResults', () => {
|
|
125
|
+
// stub out window.location so we can observe the href assignment
|
|
126
|
+
// @ts-expect-error expecting an error
|
|
127
|
+
delete window.location;
|
|
128
|
+
// @ts-expect-error expecting an error
|
|
129
|
+
window.location = { href: '' };
|
|
130
|
+
|
|
131
|
+
render(<Search searchableData={data} />);
|
|
132
|
+
|
|
133
|
+
const input = screen.getByRole('textbox');
|
|
134
|
+
fireEvent.change(input, { target: { value: 'echo' } });
|
|
135
|
+
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
|
|
136
|
+
|
|
137
|
+
expect(window.location.href).toBe('/page2');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('shows "no search results" message when there are no matches', () => {
|
|
141
|
+
render(<Search searchableData={data} />);
|
|
142
|
+
|
|
143
|
+
const input = screen.getByRole('textbox');
|
|
144
|
+
fireEvent.change(input, { target: { value: 'zebra' } });
|
|
145
|
+
|
|
146
|
+
expect(screen.getByText('no search results')).toBeInTheDocument();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('calls handleSearchClick (setSearchResults) on Enter when setSearchResults is provided', () => {
|
|
150
|
+
render(<Search searchableData={data} setSearchResults={setSearchResults} />);
|
|
151
|
+
|
|
152
|
+
// type a query so that value is non-empty
|
|
153
|
+
const input = screen.getByRole('textbox');
|
|
154
|
+
fireEvent.change(input, { target: { value: 'bravo' } });
|
|
155
|
+
|
|
156
|
+
// press Enter
|
|
157
|
+
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
|
|
158
|
+
|
|
159
|
+
// should have delegated to handleSearchClick, i.e. called setSearchResults
|
|
160
|
+
expect(setSearchResults).toHaveBeenCalledWith(
|
|
161
|
+
expect.objectContaining({
|
|
162
|
+
input: 'bravo',
|
|
163
|
+
pages: expect.any(Array),
|
|
164
|
+
}) as DataSearchResults
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('clears out results when the user manually deletes all text', () => {
|
|
169
|
+
render(<Search searchableData={data} />);
|
|
170
|
+
|
|
171
|
+
// type something to show real‐time results
|
|
172
|
+
const input = screen.getByRole('textbox');
|
|
173
|
+
fireEvent.change(input, { target: { value: 'charlie' } });
|
|
174
|
+
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
175
|
+
|
|
176
|
+
// now clear via typing
|
|
177
|
+
fireEvent.change(input, { target: { value: '' } });
|
|
178
|
+
|
|
179
|
+
// the listbox should be gone
|
|
180
|
+
expect(screen.queryByRole('listbox')).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
});
|