@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
package/src/lib/Tabs.tsx
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabs component lets the developer split the UI into independent, reusable pieces, and think about each piece in isolation
|
|
3
|
+
* @returns JSX.Element
|
|
4
|
+
*/
|
|
5
|
+
import React, { useState, useRef, useCallback, ReactNode } from "react";
|
|
6
|
+
import { twMerge } from "tailwind-merge";
|
|
7
|
+
|
|
8
|
+
interface VariantType {
|
|
9
|
+
[key: string]: {
|
|
10
|
+
container: string;
|
|
11
|
+
tab: string;
|
|
12
|
+
title: string;
|
|
13
|
+
active: string;
|
|
14
|
+
inactive: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// variant styling definition, both size and layout
|
|
19
|
+
const variants: VariantType = {
|
|
20
|
+
default: {
|
|
21
|
+
container: 'flex border-b border-gray-200',
|
|
22
|
+
tab: 'text-[#092068]',
|
|
23
|
+
title: '',
|
|
24
|
+
active: 'border-b-2 border-[#092068] hover:border-b-2 hover:border-[#7392f3] hover:font-bold active:bg-[#9fc5f0] focus:shadow-[0px_0px_0px_3px_rgba(251,137,241,1.00)] focus:rounded-md',
|
|
25
|
+
inactive: 'hover:border-b-2 hover:border-[#7392f3] hover:font-bold',
|
|
26
|
+
},
|
|
27
|
+
outline: {
|
|
28
|
+
container: 'flex border-b border-gray-200',
|
|
29
|
+
tab: 'border-2 border-blue-500 text-blue-500',
|
|
30
|
+
title: '',
|
|
31
|
+
active: 'border-2 bg-[#092068] text-white hover:bg-[#d1dbfb] hover:text-[#092068] hover:font-bold active:bg-[#9fc5f0] focus:shadow-[0px_0px_0px_3px_rgba(251,137,241,1)]',
|
|
32
|
+
inactive: 'text-[#092068] hover:bg-[#d1dbfb] hover:font-bold',
|
|
33
|
+
},
|
|
34
|
+
transparent: {
|
|
35
|
+
container: 'flex border-b border-gray-200',
|
|
36
|
+
tab: 'text-[#092068]',
|
|
37
|
+
title: '',
|
|
38
|
+
active: 'border-b-2 border-[#092068] hover:border-b-2 hover:border-[#7392f3] hover:font-bold active:bg-[#9fc5f0] focus:shadow-[0px_0px_0px_3px_rgba(251,137,241,1.00)] focus:rounded-md',
|
|
39
|
+
inactive: 'hover:border-b-2 hover:border-[#7392f3] hover:font-bold',
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export enum IconPosition {
|
|
44
|
+
Right = "right",
|
|
45
|
+
Left = "left",
|
|
46
|
+
IconOnly = "iconOnly",
|
|
47
|
+
None = "undefined"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface Tab {
|
|
51
|
+
id: string;
|
|
52
|
+
label: string;
|
|
53
|
+
onClick?: () => void;
|
|
54
|
+
content?: ReactNode;
|
|
55
|
+
activeIcon?: ReactNode | undefined;
|
|
56
|
+
inactiveIcon?: ReactNode | undefined; // Accepts any valid React element (e.g., image, icon component)
|
|
57
|
+
iconPosition?: IconPosition;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TabsProps {
|
|
61
|
+
variant?: string;
|
|
62
|
+
className?: string;
|
|
63
|
+
classNameContainer?: string;
|
|
64
|
+
customActiveClass?: string;
|
|
65
|
+
customInactiveClass?: string;
|
|
66
|
+
tabs: Tab[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const Tabs = ({ variant = 'default', tabs, className, classNameContainer, customActiveClass, customInactiveClass}: TabsProps) => {
|
|
70
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
71
|
+
const [hover, setHover] = useState(false);
|
|
72
|
+
const [currentTitle, setCurrentTitle] = useState('');
|
|
73
|
+
const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
74
|
+
|
|
75
|
+
if (variant === '') variant = 'default'; // empty variant, assign it value of 'default'
|
|
76
|
+
|
|
77
|
+
// Move focus to the active tab
|
|
78
|
+
const focusTab = useCallback((index: number) => {
|
|
79
|
+
tabRefs.current[index]?.focus();
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const handleTabClick = (index: number, onClick?: () => void) => {
|
|
83
|
+
if (onClick) {
|
|
84
|
+
onClick();
|
|
85
|
+
}
|
|
86
|
+
setActiveTab(index);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const makeTabRef = (i: number) => (el: HTMLButtonElement | null) => {
|
|
90
|
+
tabRefs.current[i] = el
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const handleKeyDown = useCallback(
|
|
94
|
+
(event: React.KeyboardEvent<HTMLButtonElement>, index: number) => {
|
|
95
|
+
if (event.key === "ArrowRight") {
|
|
96
|
+
const nextIndex = (index + 1) % tabs.length;
|
|
97
|
+
setActiveTab(nextIndex);
|
|
98
|
+
focusTab(nextIndex);
|
|
99
|
+
} else if (event.key === "ArrowLeft") {
|
|
100
|
+
const prevIndex = (index - 1 + tabs.length) % tabs.length;
|
|
101
|
+
setActiveTab(prevIndex);
|
|
102
|
+
focusTab(prevIndex);
|
|
103
|
+
} else if (event.key === "Home") {
|
|
104
|
+
setActiveTab(0);
|
|
105
|
+
focusTab(0);
|
|
106
|
+
} else if (event.key === "End") {
|
|
107
|
+
setActiveTab(tabs.length - 1);
|
|
108
|
+
focusTab(tabs.length - 1);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
[tabs.length, focusTab]
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const handleMouseOver = (title: string) => {
|
|
115
|
+
setHover(true);
|
|
116
|
+
setCurrentTitle(title);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className={twMerge('w-full', classNameContainer)}>
|
|
121
|
+
{/* Tab List */}
|
|
122
|
+
<div
|
|
123
|
+
role="tablist"
|
|
124
|
+
aria-label="Tabs"
|
|
125
|
+
className={twMerge(variants[variant].container, className)}
|
|
126
|
+
>
|
|
127
|
+
{tabs.map((tab, index) => (
|
|
128
|
+
<button
|
|
129
|
+
key={tab.id}
|
|
130
|
+
role="tab"
|
|
131
|
+
id={`tab-${tab.id}`}
|
|
132
|
+
onMouseEnter={() => handleMouseOver(tab.id)}
|
|
133
|
+
onMouseLeave={() => setHover(false)}
|
|
134
|
+
aria-selected={activeTab === index}
|
|
135
|
+
aria-controls={`panel-${tab.id}`}
|
|
136
|
+
tabIndex={activeTab === index ? 0 : -1}
|
|
137
|
+
ref={makeTabRef(index)}
|
|
138
|
+
className={`flex px-6 py-3 md:py-[14px] lg:py-4 focus:outline-hidden ${(variant === "outline") ? (`border-[#092068] ${
|
|
139
|
+
index === 0
|
|
140
|
+
? "border-l-2 border-t-2 border-b-2 rounded-l-md focus:rounded-l-md" // Left border for the first element
|
|
141
|
+
: index === tabs.length - 1
|
|
142
|
+
? "border-r-2 border-t-2 border-b-2 rounded-r-md focus:rounded-r-md" // Right border for the last element
|
|
143
|
+
: "border-t-2 border-b-2" // Top and bottom borders for others
|
|
144
|
+
}`): ''} ${
|
|
145
|
+
activeTab === index
|
|
146
|
+
? twMerge(variants[variant].active, customActiveClass)
|
|
147
|
+
: twMerge(variants[variant].inactive, customInactiveClass)
|
|
148
|
+
}`}
|
|
149
|
+
onClick={() => handleTabClick(index, tab.onClick)}
|
|
150
|
+
onKeyDown={(event) => handleKeyDown(event, index)}
|
|
151
|
+
>
|
|
152
|
+
{tab.iconPosition === IconPosition.Left && tab.activeIcon && (
|
|
153
|
+
<span className="icon-left mr-3 size-6 lg:mt-0.5">{activeTab === index ? (hover && currentTitle === tab.id) ? tab.inactiveIcon : tab.activeIcon : tab.inactiveIcon}</span>
|
|
154
|
+
)}
|
|
155
|
+
{
|
|
156
|
+
tab.iconPosition === "iconOnly" && tab.activeIcon ?
|
|
157
|
+
(
|
|
158
|
+
<span className="size-6 lg:mt-0.5">{activeTab === index ? (hover && currentTitle === tab.id) ? tab.inactiveIcon : tab.activeIcon : tab.inactiveIcon}</span>
|
|
159
|
+
)
|
|
160
|
+
:
|
|
161
|
+
(
|
|
162
|
+
<span className='text-sm md:text-base lg:text-lg mt-0.5 md:mt-0'>{tab.label}</span>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
{tab.iconPosition === "right" && tab.activeIcon && (
|
|
166
|
+
<span className="icon-right ml-3 size-6 lg:mt-0.5">{activeTab === index ? (hover && currentTitle === tab.id) ? tab.inactiveIcon : tab.activeIcon : tab.inactiveIcon}</span>
|
|
167
|
+
)}
|
|
168
|
+
</button>
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{/* Tab Panels */}
|
|
173
|
+
{tabs.map((tab, index) => (
|
|
174
|
+
<div
|
|
175
|
+
key={tab.id}
|
|
176
|
+
role="tabpanel"
|
|
177
|
+
id={`panel-${tab.id}`}
|
|
178
|
+
aria-labelledby={`tab-${tab.id}`}
|
|
179
|
+
hidden={activeTab !== index}
|
|
180
|
+
// className={`p-4 ${activeTab !== index ? 'hidden' : ''}`}
|
|
181
|
+
className="p-4"
|
|
182
|
+
>
|
|
183
|
+
{tab.content}
|
|
184
|
+
</div>
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { axe } from "vitest-axe";
|
|
2
|
+
|
|
3
|
+
it("should demonstrate this matcher's usage", async () => {
|
|
4
|
+
const render = () => `<html>
|
|
5
|
+
<body>
|
|
6
|
+
<main>
|
|
7
|
+
<p aria-label="paragraph with image">
|
|
8
|
+
<img src="#" alt="image alt text" />
|
|
9
|
+
</p>
|
|
10
|
+
</main>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
13
|
+
`;
|
|
14
|
+
// pass anything that outputs html to axe
|
|
15
|
+
const html = render();
|
|
16
|
+
expect(await axe(html)).toHaveNoViolations();
|
|
17
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Meta, StoryObj, StoryFn } from '@storybook/react';
|
|
2
|
+
import { expect, fn, userEvent, within } from 'storybook/test';
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { Toggle, ToggleProps } from './Toggle';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: 'Components/Toggle',
|
|
9
|
+
component: Toggle,
|
|
10
|
+
parameters: {
|
|
11
|
+
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
|
12
|
+
layout: 'centered',
|
|
13
|
+
},
|
|
14
|
+
argTypes: {
|
|
15
|
+
className: { control: 'text' },
|
|
16
|
+
thumbClassName: { control: 'text' },
|
|
17
|
+
defaultChecked: { control: 'boolean' },
|
|
18
|
+
disabled: { control: 'boolean' },
|
|
19
|
+
},
|
|
20
|
+
} as Meta<ToggleProps>
|
|
21
|
+
|
|
22
|
+
// Default story
|
|
23
|
+
export const Default = {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
// Default classes story
|
|
27
|
+
export const Outlined = {
|
|
28
|
+
args: {
|
|
29
|
+
variant: 'Outlined'
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// MedCard classes story
|
|
34
|
+
export const MedCard = {
|
|
35
|
+
args: {
|
|
36
|
+
variant: 'MedCard'
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Alternate classes story
|
|
41
|
+
export const AlternateClasses = {
|
|
42
|
+
args: {
|
|
43
|
+
className: 'border-4 border-black',
|
|
44
|
+
thumbClassName: 'bg-red-600 border-4 border-black',
|
|
45
|
+
defaultChecked: false,
|
|
46
|
+
disabled: false,
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Disabled toggle story
|
|
51
|
+
export const DisabledToggle = {
|
|
52
|
+
args: {
|
|
53
|
+
disabled: true
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Toggle Handler
|
|
58
|
+
export const ToggleHandler: StoryFn = () => {
|
|
59
|
+
const [selectUpdates, setSelectUpdates] = useState(false);
|
|
60
|
+
return (
|
|
61
|
+
<div className="grid place-items-center">
|
|
62
|
+
<Toggle
|
|
63
|
+
className="my-2"
|
|
64
|
+
defaultChecked={false}
|
|
65
|
+
onCheckedChange={(checked: boolean) => {
|
|
66
|
+
setSelectUpdates(checked);
|
|
67
|
+
console.log('checked: ', checked);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
disabled={false}
|
|
71
|
+
/>
|
|
72
|
+
<p className="" role="status">Checked: {selectUpdates ? 'true' : 'false'} </p>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const ToggleTestRunner: StoryFn = () => {
|
|
78
|
+
const [selectUpdates, setSelectUpdates] = useState(false);
|
|
79
|
+
return (
|
|
80
|
+
<div className="grid place-items-center">
|
|
81
|
+
<Toggle
|
|
82
|
+
className="my-2"
|
|
83
|
+
defaultChecked={false}
|
|
84
|
+
onCheckedChange={(checked: boolean) => {
|
|
85
|
+
setSelectUpdates(checked);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
disabled={false}
|
|
89
|
+
/>
|
|
90
|
+
<p role="status">Checked: {selectUpdates ? 'true' : 'false'} </p>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
ToggleTestRunner.play = async ({ canvasElement }) => {
|
|
96
|
+
const canvas = within(canvasElement);
|
|
97
|
+
|
|
98
|
+
// Find the Toggle component and the Checked status text
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
100
|
+
const toggleButton = canvas.getByRole('switch'); // Adjust if Toggle uses a different role
|
|
101
|
+
const statusText = canvas.getByRole('status'); // get paragraph displaying status
|
|
102
|
+
|
|
103
|
+
// Initial state should be unchecked
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
105
|
+
expect(statusText).toHaveTextContent('Checked: false');
|
|
106
|
+
|
|
107
|
+
// Click the toggle to check it
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
109
|
+
await userEvent.click(toggleButton);
|
|
110
|
+
expect(statusText).toHaveTextContent('Checked: true');
|
|
111
|
+
|
|
112
|
+
// Click the toggle again to uncheck it
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
114
|
+
await userEvent.click(toggleButton);
|
|
115
|
+
expect(statusText).toHaveTextContent('Checked: false');
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
export const ToggleTestRunnerSlow: StoryFn = () => {
|
|
120
|
+
const [selectUpdates, setSelectUpdates] = useState(false);
|
|
121
|
+
return (
|
|
122
|
+
<div className="grid place-items-center">
|
|
123
|
+
<Toggle
|
|
124
|
+
className="my-2"
|
|
125
|
+
defaultChecked={false}
|
|
126
|
+
onCheckedChange={(checked: boolean) => {
|
|
127
|
+
setSelectUpdates(checked);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
disabled={false}
|
|
131
|
+
/>
|
|
132
|
+
<p role="status">Checked: {selectUpdates ? 'true' : 'false'} </p>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/*
|
|
138
|
+
ToggleTestRunnerSlow.play = async ({ canvasElement }) => {
|
|
139
|
+
const canvas = within(canvasElement);
|
|
140
|
+
|
|
141
|
+
// Find the Toggle component and the Checked status text
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
143
|
+
const toggleButton = canvas.getByRole('switch'); // Adjust if Toggle uses a different role
|
|
144
|
+
const statusText = canvas.getByRole('status'); // get paragraph displaying status
|
|
145
|
+
|
|
146
|
+
// Initial state should be unchecked
|
|
147
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
148
|
+
expect(statusText).toHaveTextContent('Checked: false');
|
|
149
|
+
|
|
150
|
+
// Click the toggle to check it
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
152
|
+
await userEvent.click(toggleButton);
|
|
153
|
+
expect(statusText).toHaveTextContent('Checked: true');
|
|
154
|
+
|
|
155
|
+
// Click the toggle again to uncheck it
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
157
|
+
await userEvent.click(toggleButton);
|
|
158
|
+
expect(statusText).toHaveTextContent('Checked: false');
|
|
159
|
+
};
|
|
160
|
+
*/
|
|
161
|
+
|
|
162
|
+
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import { axe } from "vitest-axe";
|
|
5
|
+
import { Toggle } from './Toggle';
|
|
6
|
+
|
|
7
|
+
describe('Toggle Component', () => {
|
|
8
|
+
it('renders correctly with default props', () => {
|
|
9
|
+
render(<Toggle />);
|
|
10
|
+
const toggleButton = screen.getByRole('switch');
|
|
11
|
+
expect(toggleButton).toBeInTheDocument();
|
|
12
|
+
expect(toggleButton).toHaveAttribute('aria-checked', 'false');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('renders component with a label, toggles state on click', () => {
|
|
16
|
+
render(<Toggle label='Toggle' />);
|
|
17
|
+
const toggleButton = screen.getByRole('switch');
|
|
18
|
+
fireEvent.click(toggleButton);
|
|
19
|
+
expect(toggleButton).toHaveAttribute('aria-checked', 'true');
|
|
20
|
+
fireEvent.click(toggleButton);
|
|
21
|
+
expect(toggleButton).toHaveAttribute('aria-checked', 'false');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('calls onCheckedChange with the correct value', () => {
|
|
25
|
+
const onCheckedChangeMock = vi.fn();
|
|
26
|
+
render(<Toggle onCheckedChange={onCheckedChangeMock} />);
|
|
27
|
+
const toggleButton = screen.getByRole('switch');
|
|
28
|
+
|
|
29
|
+
fireEvent.click(toggleButton);
|
|
30
|
+
expect(onCheckedChangeMock).toHaveBeenCalledWith(true);
|
|
31
|
+
|
|
32
|
+
fireEvent.click(toggleButton);
|
|
33
|
+
expect(onCheckedChangeMock).toHaveBeenCalledWith(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('does not toggle when disabled', () => {
|
|
37
|
+
render(<Toggle disabled />);
|
|
38
|
+
const toggleButton = screen.getByRole('switch');
|
|
39
|
+
fireEvent.click(toggleButton);
|
|
40
|
+
expect(toggleButton).toHaveAttribute('aria-checked', 'false');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders with the default variant correctly', () => {
|
|
44
|
+
render(<Toggle variant="default" />);
|
|
45
|
+
const toggleButton = screen.getByRole('switch');
|
|
46
|
+
expect(toggleButton).toHaveClass('bg-[#d0cfd1]'); // Check default class
|
|
47
|
+
fireEvent.click(toggleButton);
|
|
48
|
+
expect(toggleButton).toHaveClass('bg-[#053ea6]'); // Check toggled class
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('renders with a custom variant correctly', () => {
|
|
52
|
+
render(<Toggle variant="default" className='bg-gray-300' />);
|
|
53
|
+
const toggleButton = screen.getByRole('switch');
|
|
54
|
+
expect(toggleButton).toHaveClass('bg-gray-300'); // Check MedCard class
|
|
55
|
+
// fireEvent.click(toggleButton);
|
|
56
|
+
// expect(toggleButton).toHaveClass('bg-[#0256AB]'); // Check toggled MedCard class
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('applies additional custom class names', () => {
|
|
60
|
+
render(
|
|
61
|
+
<Toggle className="custom-class" classNameButton="custom-button-class" />
|
|
62
|
+
);
|
|
63
|
+
const toggleButton = screen.getByRole('switch');
|
|
64
|
+
const toggleCircle = toggleButton.querySelector('div');
|
|
65
|
+
|
|
66
|
+
expect(toggleButton).toHaveClass('custom-class');
|
|
67
|
+
expect(toggleCircle).toHaveClass('custom-button-class');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('respects the defaultChecked prop', () => {
|
|
71
|
+
render(<Toggle defaultChecked />);
|
|
72
|
+
const toggleButton = screen.getByRole('switch');
|
|
73
|
+
expect(toggleButton).toHaveAttribute('aria-checked', 'true');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
describe('Toggle Component Accessibility', () => {
|
|
80
|
+
it('should have no accessibility violations when rendered with default props', async () => {
|
|
81
|
+
const { container } = render(<Toggle />);
|
|
82
|
+
const results = await axe(container);
|
|
83
|
+
expect(results).toHaveNoViolations();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should have no accessibility violations when toggled on', async () => {
|
|
87
|
+
const { container } = render(<Toggle defaultChecked />);
|
|
88
|
+
const results = await axe(container);
|
|
89
|
+
expect(results).toHaveNoViolations();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should have no accessibility violations when disabled', async () => {
|
|
93
|
+
const { container } = render(<Toggle disabled />);
|
|
94
|
+
const results = await axe(container);
|
|
95
|
+
expect(results).toHaveNoViolations();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should have no accessibility violations when rendered with a custom variant', async () => {
|
|
99
|
+
const { container } = render(<Toggle variant="MedCard" />);
|
|
100
|
+
const results = await axe(container);
|
|
101
|
+
expect(results).toHaveNoViolations();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should have accessible role and attributes', () => {
|
|
105
|
+
render(<Toggle />);
|
|
106
|
+
const toggleButton = screen.getByRole('switch');
|
|
107
|
+
expect(toggleButton).toHaveAttribute('aria-checked', 'false');
|
|
108
|
+
expect(toggleButton).toHaveAttribute('aria-disabled', 'false');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should have correct aria-checked attribute when toggled', () => {
|
|
112
|
+
render(<Toggle defaultChecked />);
|
|
113
|
+
const toggleButton = screen.getByRole('switch');
|
|
114
|
+
expect(toggleButton).toHaveAttribute('aria-checked', 'true');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should be inaccessible via pointer interactions when disabled', () => {
|
|
118
|
+
render(<Toggle disabled />);
|
|
119
|
+
const toggleButton = screen.getByRole('switch');
|
|
120
|
+
expect(toggleButton).toHaveAttribute('aria-disabled', 'true');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { on } from 'events';
|
|
2
|
+
import { ButtonHTMLAttributes, forwardRef, useEffect, useState } from 'react';
|
|
3
|
+
import { twMerge } from 'tailwind-merge';
|
|
4
|
+
|
|
5
|
+
interface VariantType {
|
|
6
|
+
variant: string;
|
|
7
|
+
classes: string;
|
|
8
|
+
buttonClasses: string;
|
|
9
|
+
toggledClasses: string;
|
|
10
|
+
toggledButtonClasses: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// w-16 = 64px, h-8 = 32px, w-6 = 24px h-6 = 24px
|
|
14
|
+
// ! required for height to be respected at breaks
|
|
15
|
+
// padding (p-1) used to space circle from end of container
|
|
16
|
+
const baseClasses = 'w-16 h-8 flex lg:w-[49.74px] lg:h-[24.87px]! lg:p-[3.1px] items-center p-1 rounded-2xl cursor-pointer transition-colors duration-300';
|
|
17
|
+
const buttonBaseClasses = 'bg-white w-6 h-6 lg:w-[18.65px] lg:h-[18.65px]! rounded-full shadow-[0px_4px_4.900000095367432px_0px_rgba(0,0,0,0.12)] transform transition-transform duration-300';
|
|
18
|
+
|
|
19
|
+
// w/h for std are working, on the lg: breakpoint the resulting w/h is 42x17 - 8px short on each dimension
|
|
20
|
+
// this 8px appears to be 4px of padding or margin ... p-1 === 4px
|
|
21
|
+
// changed padding to lg:p-[3.11px] and size is now 44x19
|
|
22
|
+
|
|
23
|
+
const variants: VariantType[] = [
|
|
24
|
+
{
|
|
25
|
+
variant: 'default',
|
|
26
|
+
classes: 'bg-[#d0cfd1] hover:bg-[#bbbabc]', // outer-button non-toggled
|
|
27
|
+
toggledClasses: 'bg-[#053ea6] hover:bg-[#0752dc]', // outer-button toggled
|
|
28
|
+
buttonClasses: 'translate-x-0', // inner div (circle)
|
|
29
|
+
toggledButtonClasses: 'translate-x-8 lg:translate-x-6' // inner div (circle) toggled
|
|
30
|
+
// lg:translate-x-[calc(100%-6px)] lg:translate-x-[28px]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
variant:'outlined',
|
|
34
|
+
classes: 'bg-slate-300 border border-black hover:bg-[#abb5c2]',
|
|
35
|
+
toggledClasses: 'bg-blue-500/50 hover:bg-blue-400 border border-black',
|
|
36
|
+
buttonClasses: 'translate-x-0',
|
|
37
|
+
toggledButtonClasses: 'translate-x-8 lg:translate-x-6'
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export interface ToggleProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
42
|
+
defaultChecked?: boolean;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
label?: string;
|
|
45
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
46
|
+
className?: string;
|
|
47
|
+
classNameButton?: string;
|
|
48
|
+
variant?: string; // Define allowed variants here, extend as necessary.
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const Toggle = forwardRef<HTMLButtonElement, ToggleProps>(
|
|
52
|
+
({ defaultChecked=false, disabled=false, onCheckedChange, label,
|
|
53
|
+
className, classNameButton, variant = "default", type = "button",
|
|
54
|
+
children, ...props }, ref) => {
|
|
55
|
+
|
|
56
|
+
const [isToggled, setIsToggled] = useState(defaultChecked);
|
|
57
|
+
|
|
58
|
+
// handle change function if present
|
|
59
|
+
const handleCheckedChange = () => {
|
|
60
|
+
const newValue = !isToggled;
|
|
61
|
+
setIsToggled(newValue);
|
|
62
|
+
if (onCheckedChange) { // if we have a handler function, call w/ new value
|
|
63
|
+
onCheckedChange(newValue);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<button
|
|
69
|
+
onClick={handleCheckedChange}
|
|
70
|
+
disabled={disabled}
|
|
71
|
+
role="switch"
|
|
72
|
+
aria-checked={isToggled}
|
|
73
|
+
aria-disabled={disabled}
|
|
74
|
+
{...props}
|
|
75
|
+
ref={ref}
|
|
76
|
+
aria-label={label ? label: `Toggle ${isToggled ? 'on': 'off'}`}
|
|
77
|
+
className={twMerge(`${baseClasses}
|
|
78
|
+
${isToggled ?
|
|
79
|
+
variants.find((v) => v.variant === variant)?.toggledClasses :
|
|
80
|
+
variants.find((v) => v.variant === variant)?.classes
|
|
81
|
+
}`,
|
|
82
|
+
className
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
<div
|
|
86
|
+
className={twMerge(`${buttonBaseClasses}
|
|
87
|
+
${ isToggled ?
|
|
88
|
+
variants.find((v) => v.variant === variant)?.toggledButtonClasses :
|
|
89
|
+
variants.find((v) => v.variant === variant)?.buttonClasses
|
|
90
|
+
}`,
|
|
91
|
+
classNameButton )}
|
|
92
|
+
></div>
|
|
93
|
+
</button>
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|