@dhasdk/simple-ui 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/.babelrc +12 -0
  2. package/.storybook/main.ts +35 -0
  3. package/.storybook/preview.ts +4 -0
  4. package/BAKpostcss.config.jsBAK +15 -0
  5. package/BAKtailwind.config.mjsBAK +99 -0
  6. package/README.md +464 -16
  7. package/coverage/storybook/coverage-storybook.json +32411 -0
  8. package/coverage/storybook/lcov-report/Accordion.tsx.html +805 -0
  9. package/coverage/storybook/lcov-report/Badge.tsx.html +346 -0
  10. package/coverage/storybook/lcov-report/Breadcrumbs.tsx.html +742 -0
  11. package/coverage/storybook/lcov-report/Button.tsx.html +448 -0
  12. package/coverage/storybook/lcov-report/ButtonGroup.tsx.html +403 -0
  13. package/coverage/storybook/lcov-report/Card.tsx.html +292 -0
  14. package/coverage/storybook/lcov-report/CharacterCounter.tsx.html +253 -0
  15. package/coverage/storybook/lcov-report/CheckBox.tsx.html +1555 -0
  16. package/coverage/storybook/lcov-report/DatePicker.tsx.html +826 -0
  17. package/coverage/storybook/lcov-report/Input.tsx.html +1012 -0
  18. package/coverage/storybook/lcov-report/List.tsx.html +364 -0
  19. package/coverage/storybook/lcov-report/Modal.tsx.html +745 -0
  20. package/coverage/storybook/lcov-report/Pill.tsx.html +358 -0
  21. package/coverage/storybook/lcov-report/Search.tsx.html +997 -0
  22. package/coverage/storybook/lcov-report/SearchContent.tsx.html +235 -0
  23. package/coverage/storybook/lcov-report/SectionHeader.tsx.html +358 -0
  24. package/coverage/storybook/lcov-report/Select.tsx.html +1012 -0
  25. package/coverage/storybook/lcov-report/Shield.tsx.html +802 -0
  26. package/coverage/storybook/lcov-report/SideBarNav.tsx.html +490 -0
  27. package/coverage/storybook/lcov-report/Skeleton.tsx.html +394 -0
  28. package/coverage/storybook/lcov-report/Slider.tsx.html +385 -0
  29. package/coverage/storybook/lcov-report/Status.tsx.html +322 -0
  30. package/coverage/storybook/lcov-report/Tabs.tsx.html +610 -0
  31. package/coverage/storybook/lcov-report/Toggle.tsx.html +373 -0
  32. package/coverage/storybook/lcov-report/Tooltip.tsx.html +496 -0
  33. package/coverage/storybook/lcov-report/base.css +224 -0
  34. package/coverage/storybook/lcov-report/block-navigation.js +87 -0
  35. package/coverage/storybook/lcov-report/favicon.png +0 -0
  36. package/coverage/storybook/lcov-report/index.html +476 -0
  37. package/coverage/storybook/lcov-report/prettify.css +1 -0
  38. package/coverage/storybook/lcov-report/prettify.js +2 -0
  39. package/coverage/storybook/lcov-report/sort-arrow-sprite.png +0 -0
  40. package/coverage/storybook/lcov-report/sorter.js +196 -0
  41. package/coverage/storybook/lcov.info +2312 -0
  42. package/dist/README.md +1815 -0
  43. package/eslint.config.mjs +13 -0
  44. package/package.json +6 -7
  45. package/project.json +11 -0
  46. package/src/assets/img/Frame.svg +5 -0
  47. package/src/assets/img/backArrowRight.svg +10 -0
  48. package/src/assets/img/bc-separator.png +0 -0
  49. package/src/assets/img/calendar.png +0 -0
  50. package/src/assets/img/calendar.svg +4 -0
  51. package/src/assets/img/check.svg +5 -0
  52. package/src/assets/img/check_box.svg +10 -0
  53. package/src/assets/img/check_box_empty.svg +10 -0
  54. package/src/assets/img/check_box_fill.svg +10 -0
  55. package/src/assets/img/check_box_fill_empty.svg +10 -0
  56. package/src/assets/img/chevron-down-white.svg +2 -0
  57. package/src/assets/img/chevron-down.svg +2 -0
  58. package/src/assets/img/chevron-left.svg +1 -0
  59. package/src/assets/img/chevron-right-light.svg +4 -0
  60. package/src/assets/img/chevron-right.svg +3 -0
  61. package/src/assets/img/chevron-up-white.svg +1 -0
  62. package/src/assets/img/chevron-up.svg +1 -0
  63. package/src/assets/img/clock.svg +6 -0
  64. package/src/assets/img/close.svg +1 -0
  65. package/src/assets/img/close2.svg +6 -0
  66. package/src/assets/img/closeModal.svg +10 -0
  67. package/src/assets/img/close_icon_dark.svg +10 -0
  68. package/src/assets/img/close_small.svg +3 -0
  69. package/src/assets/img/emergency_home.svg +10 -0
  70. package/src/assets/img/first-aid-kit.svg +7 -0
  71. package/src/assets/img/heartbeat.svg +4 -0
  72. package/src/assets/img/home-gray.svg +3 -0
  73. package/src/assets/img/home.svg +3 -0
  74. package/src/assets/img/hospital.jpg +0 -0
  75. package/src/assets/img/indeterminate_check_box.svg +10 -0
  76. package/src/assets/img/indeterminate_check_box_fill.svg +10 -0
  77. package/src/assets/img/info_24_ 1d4ed8.svg +3 -0
  78. package/src/assets/img/info_24_ 2c6441.svg +3 -0
  79. package/src/assets/img/marker_check_by_default.svg +10 -0
  80. package/src/assets/img/marker_check_by_default_fill.svg +10 -0
  81. package/src/assets/img/minus-accordion.svg +5 -0
  82. package/src/assets/img/minus.svg +3 -0
  83. package/src/assets/img/open.svg +1 -0
  84. package/src/assets/img/pill-white.svg +7 -0
  85. package/src/assets/img/pill.svg +5 -0
  86. package/src/assets/img/plus-accordion.svg +5 -0
  87. package/src/assets/img/plus.svg +4 -0
  88. package/src/assets/img/prescription.svg +6 -0
  89. package/src/assets/img/search.svg +10 -0
  90. package/src/assets/img/search_icon_light.svg +10 -0
  91. package/src/assets/img/separator.svg +3 -0
  92. package/src/assets/img/stethoscope-white.svg +8 -0
  93. package/src/assets/img/stethoscope.svg +8 -0
  94. package/src/assets/img/thumb_up.svg +10 -0
  95. package/src/assets/img/vector.svg +3 -0
  96. package/src/assets/img/warning-badge-disabled.svg +11 -0
  97. package/src/assets/img/warning-badge-green.svg +11 -0
  98. package/src/assets/img/warning-badge-red.svg +11 -0
  99. package/src/assets/img/warning-badge-yellow.svg +11 -0
  100. package/src/assets/img/warning.svg +10 -0
  101. package/src/global.d.ts +13 -0
  102. package/{index.d.ts → src/index.ts} +13 -5
  103. package/src/lib/Accordian--Accordian.stories.tsx +312 -0
  104. package/src/lib/Accordion.spec.tsx +384 -0
  105. package/src/lib/Accordion.tsx +240 -0
  106. package/src/lib/AppointmentPicker.spec.tsx +138 -0
  107. package/src/lib/AppointmentPicker.tsx +97 -0
  108. package/src/lib/Badge--Badge.stories.tsx +60 -0
  109. package/src/lib/Badge.spec.tsx +70 -0
  110. package/src/lib/Badge.tsx +87 -0
  111. package/src/lib/Breadcrumbs-Breadcrumbs.stories.tsx +114 -0
  112. package/src/lib/Breadcrumbs.spec.tsx +218 -0
  113. package/src/lib/Breadcrumbs.tsx +219 -0
  114. package/src/lib/Button--Button.stories.tsx +220 -0
  115. package/src/lib/Button.spec.tsx +241 -0
  116. package/src/lib/Button.tsx +121 -0
  117. package/src/lib/ButtonGroup--ButtonGroup.stories.tsx +129 -0
  118. package/src/lib/ButtonGroup.spec.tsx +89 -0
  119. package/src/lib/ButtonGroup.tsx +107 -0
  120. package/src/lib/Card--Card.stories.tsx +113 -0
  121. package/src/lib/Card.spec.tsx +112 -0
  122. package/src/lib/Card.tsx +69 -0
  123. package/src/lib/CharacterCounter--CharacterCounter.stories.tsx +169 -0
  124. package/src/lib/CharacterCounter.spec.tsx +123 -0
  125. package/src/lib/CharacterCounter.tsx +56 -0
  126. package/src/lib/CheckBox--CheckBox.stories.tsx +107 -0
  127. package/src/lib/CheckBox.spec.tsx +412 -0
  128. package/src/lib/CheckBox.tsx +491 -0
  129. package/src/lib/DatePicker--DatePicker.stories.tsx +228 -0
  130. package/src/lib/DatePicker.spec.tsx +424 -0
  131. package/src/lib/DatePicker.tsx +247 -0
  132. package/src/lib/Input--Input.stories.tsx +449 -0
  133. package/src/lib/Input.spec.tsx +281 -0
  134. package/src/lib/Input.tsx +309 -0
  135. package/src/lib/List--List.stories.tsx +157 -0
  136. package/src/lib/List.spec.tsx +211 -0
  137. package/src/lib/List.tsx +93 -0
  138. package/src/lib/Modal--Modal.stories.tsx +454 -0
  139. package/src/lib/Modal.spec.tsx +202 -0
  140. package/src/lib/Modal.tsx +220 -0
  141. package/src/lib/Pill--Pill.stories.tsx +98 -0
  142. package/src/lib/Pill.spec.tsx +103 -0
  143. package/src/lib/Pill.tsx +91 -0
  144. package/src/lib/ProgressBar.spec.tsx +106 -0
  145. package/src/lib/ProgressBar.tsx +112 -0
  146. package/src/lib/RadioGroup.spec.tsx +84 -0
  147. package/src/lib/RadioGroup.tsx +74 -0
  148. package/src/lib/RadioIcon.tsx +13 -0
  149. package/src/lib/Search--Search.stories.tsx +67 -0
  150. package/src/lib/Search.spec.tsx +182 -0
  151. package/src/lib/Search.tsx +304 -0
  152. package/src/lib/SearchContent.tsx +51 -0
  153. package/src/lib/SectionHeader--SectionHeader.stories.tsx +98 -0
  154. package/src/lib/SectionHeader.spec.tsx +60 -0
  155. package/src/lib/SectionHeader.tsx +91 -0
  156. package/src/lib/Select--Select.stories.tsx +387 -0
  157. package/src/lib/Select.spec.tsx +493 -0
  158. package/src/lib/Select.tsx +311 -0
  159. package/src/lib/Shield--Shield.stories.tsx +196 -0
  160. package/src/lib/Shield.spec.tsx +275 -0
  161. package/src/lib/Shield.tsx +239 -0
  162. package/src/lib/SideBarNav--SideBarNav.stories.tsx +136 -0
  163. package/src/lib/SideBarNav.spec.tsx +178 -0
  164. package/src/lib/SideBarNav.tsx +135 -0
  165. package/src/lib/Skeleton--Skeleton.stories.tsx +77 -0
  166. package/src/lib/Skeleton.module.css +16 -0
  167. package/src/lib/Skeleton.spec.tsx +83 -0
  168. package/src/lib/Skeleton.tsx +103 -0
  169. package/src/lib/SkipLink.spec.tsx +76 -0
  170. package/src/lib/SkipLink.tsx +48 -0
  171. package/src/lib/Slider--Slider.stories.tsx +108 -0
  172. package/src/lib/Slider.module.css +109 -0
  173. package/src/lib/Slider.spec.tsx +67 -0
  174. package/src/lib/Slider.tsx +101 -0
  175. package/src/lib/Status--Status.stories.tsx +93 -0
  176. package/src/lib/Status.spec.tsx +118 -0
  177. package/src/lib/Status.tsx +79 -0
  178. package/src/lib/Tabs--Tabs.stories.tsx +294 -0
  179. package/src/lib/Tabs.spec.tsx +249 -0
  180. package/src/lib/Tabs.tsx +188 -0
  181. package/src/lib/Tester.spec.tsx +17 -0
  182. package/src/lib/Toggle--Toggle.stories.tsx +162 -0
  183. package/src/lib/Toggle.spec.tsx +122 -0
  184. package/src/lib/Toggle.tsx +96 -0
  185. package/src/lib/Tooltip--Tooltip.stories.tsx +315 -0
  186. package/src/lib/Tooltip.spec.tsx +307 -0
  187. package/src/lib/Tooltip.tsx +137 -0
  188. package/src/lib/bak-simple-ui.stories.tsx-bak +24 -0
  189. package/src/styles.css +190 -0
  190. package/tsconfig.json +25 -0
  191. package/tsconfig.lib.json +42 -0
  192. package/tsconfig.spec.json +29 -0
  193. package/tsconfig.storybook.json +36 -0
  194. package/vite.config.mts +87 -0
  195. package/vitest.setup.ts +12 -0
  196. package/index.css +0 -1
  197. package/index.js +0 -35
  198. package/index.mjs +0 -4981
  199. package/lib/Accordion.d.ts +0 -36
  200. package/lib/AppointmentPicker.d.ts +0 -21
  201. package/lib/Badge.d.ts +0 -11
  202. package/lib/Breadcrumbs.d.ts +0 -13
  203. package/lib/Button.d.ts +0 -15
  204. package/lib/ButtonGroup.d.ts +0 -8
  205. package/lib/Card.d.ts +0 -11
  206. package/lib/CharacterCounter.d.ts +0 -11
  207. package/lib/CheckBox.d.ts +0 -30
  208. package/lib/DatePicker.d.ts +0 -7
  209. package/lib/Input.d.ts +0 -16
  210. package/lib/List.d.ts +0 -22
  211. package/lib/Modal.d.ts +0 -18
  212. package/lib/Pill.d.ts +0 -13
  213. package/lib/ProgressBar.d.ts +0 -19
  214. package/lib/RadioGroup.d.ts +0 -15
  215. package/lib/Search.d.ts +0 -26
  216. package/lib/SearchContent.d.ts +0 -6
  217. package/lib/SectionHeader.d.ts +0 -18
  218. package/lib/Select.d.ts +0 -19
  219. package/lib/Shield.d.ts +0 -12
  220. package/lib/SideBarNav.d.ts +0 -21
  221. package/lib/Skeleton.d.ts +0 -15
  222. package/lib/SkipLink.d.ts +0 -22
  223. package/lib/Slider.d.ts +0 -14
  224. package/lib/Status.d.ts +0 -10
  225. package/lib/Tabs.d.ts +0 -23
  226. package/lib/Toggle.d.ts +0 -11
  227. package/lib/Tooltip.d.ts +0 -14
@@ -0,0 +1,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
+