@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,241 @@
1
+ import React, { createRef } from "react";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import { Button, ButtonProps } from "./Button";
4
+ import { axe } from "vitest-axe";
5
+
6
+ describe("Button Component", () => {
7
+ it("renders the button with default styles", () => {
8
+ render(<Button>Default Button</Button>);
9
+ const button = screen.getByRole("button", { name: "Default Button" });
10
+
11
+ expect(button).toBeInTheDocument();
12
+ expect(button).toHaveClass("inline-flex items-center justify-center whitespace-nowrap");
13
+ });
14
+
15
+ it("applies custom classes and merges with default classes", () => {
16
+ render(<Button className="custom-class">Custom Class Button</Button>);
17
+ const button = screen.getByRole("button", { name: "Custom Class Button" });
18
+
19
+ expect(button).toHaveClass("custom-class");
20
+ expect(button).toHaveClass("inline-flex items-center justify-center whitespace-nowrap");
21
+ });
22
+
23
+ /*it("applies variant-specific styles", () => {
24
+ render(<Button variant="Outline">Outline Button</Button>);
25
+ const button = screen.getByRole("button", { name: "Outline Button" });
26
+
27
+ expect(button).toHaveClass("border-dha-mc-true-blue bg-white border-2 text-dha-mc-true-blue");
28
+ });*/
29
+
30
+ it("uses label prop if children are not provided", () => {
31
+ render(<Button label="Label Button" />);
32
+ const button = screen.getByRole("button", { name: "Label Button" });
33
+
34
+ expect(button).toBeInTheDocument();
35
+ expect(button).toHaveTextContent("Label Button");
36
+ });
37
+
38
+ it("prioritizes children over label prop", () => {
39
+ render(
40
+ <Button label="Label Button">Children Button</Button>
41
+ );
42
+ const button = screen.getByRole("button", { name: "Children Button" });
43
+
44
+ expect(button).toHaveTextContent("Children Button");
45
+ expect(button).not.toHaveTextContent("Label Button");
46
+ });
47
+
48
+ it("handles the onClick event", () => {
49
+ const handleClick = vi.fn();
50
+ render(<Button onClick={handleClick}>Clickable Button</Button>);
51
+ const button = screen.getByRole("button", { name: "Clickable Button" });
52
+
53
+ fireEvent.click(button);
54
+
55
+ expect(handleClick).toHaveBeenCalledTimes(1);
56
+ });
57
+
58
+ it("sets the correct button type", () => {
59
+ render(<Button type="submit">Submit Button</Button>);
60
+ const button = screen.getByRole("button", { name: "Submit Button" });
61
+
62
+ expect(button).toHaveAttribute("type", "submit");
63
+ });
64
+
65
+ it("should forward the ref to the button element", () => {
66
+ const ref = createRef<HTMLButtonElement>();
67
+ render(<Button ref={ref}>Button with Ref</Button>);
68
+
69
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement);
70
+ expect(ref.current?.textContent).toBe("Button with Ref");
71
+ });
72
+
73
+ it("renders a disabled button with appropriate styles", () => {
74
+ render(
75
+ <Button variant="filled" disabled>
76
+ Disabled Button
77
+ </Button>
78
+ );
79
+ const button = screen.getByRole("button", { name: "Disabled Button" });
80
+
81
+ expect(button).toBeDisabled();
82
+ expect(button).toHaveClass("disabled:border-[#e4e4e5]");
83
+ });
84
+
85
+ /*
86
+ it("falls back to the default variant if an unknown variant is provided", () => {
87
+ render(
88
+ <Button variant="unknown-variant">
89
+ Fallback Variant Button
90
+ </Button>
91
+ );
92
+ const button = screen.getByRole("button", { name: "Fallback Variant Button" });
93
+
94
+ expect(button).toHaveClass("bg-gray-200 hover:bg-blue-400 text-black");
95
+ });*/
96
+ });
97
+
98
+ describe('Button Accessibility Tests', () => {
99
+ const renderComponent = (props: ButtonProps) =>
100
+ render(<Button {...props} />);
101
+
102
+ it('should have no accessibility violations with default variant', async () => {
103
+ const { container } = renderComponent({ label: 'Click Me' });
104
+ const results = await axe(container);
105
+ expect(results).toHaveNoViolations();
106
+ });
107
+
108
+ it('should have no accessibility violations with custom variant', async () => {
109
+ const { container } = renderComponent({ label: 'Submit', variant: 'filled' });
110
+ const results = await axe(container);
111
+ expect(results).toHaveNoViolations();
112
+ });
113
+
114
+ it('should support disabled state without accessibility violations', async () => {
115
+ const { container } = renderComponent({ label: 'Disabled', disabled: true });
116
+ const results = await axe(container);
117
+ expect(results).toHaveNoViolations();
118
+ });
119
+
120
+ it('should handle custom class merging correctly', async () => {
121
+ const { container } = renderComponent({
122
+ label: 'Custom Class',
123
+ className: 'custom-class bg-red-500 text-white',
124
+ });
125
+ const results = await axe(container);
126
+ expect(results).toHaveNoViolations();
127
+ });
128
+
129
+ it('should render a button with accessible name using label', () => {
130
+ const { getByRole } = renderComponent({ label: 'Accessible Button' });
131
+ const button = getByRole('button', { name: 'Accessible Button' });
132
+ expect(button).toBeInTheDocument();
133
+ });
134
+
135
+ it('should render children and fallback to label if children are not provided', () => {
136
+ const { getByText, rerender } = renderComponent({ label: 'Fallback Label' });
137
+ expect(getByText('Fallback Label')).toBeInTheDocument();
138
+
139
+ rerender(<Button>Child Content</Button>);
140
+ expect(getByText('Child Content')).toBeInTheDocument();
141
+ });
142
+
143
+ it('should handle ARIA attributes for assistive technologies', () => {
144
+ const { getByRole } = renderComponent({
145
+ label: 'Button with ARIA',
146
+ 'aria-label': 'Custom ARIA Label',
147
+ });
148
+ const button = getByRole('button', { name: 'Custom ARIA Label' });
149
+ expect(button).toBeInTheDocument();
150
+ });
151
+
152
+ it('should render a submit button with proper type', () => {
153
+ const { getByRole } = renderComponent({ type: 'submit', label: 'Submit Button' });
154
+ const button = getByRole('button', { name: 'Submit Button' });
155
+ expect(button).toHaveAttribute('type', 'submit');
156
+ });
157
+
158
+ it('should handle click events', () => {
159
+ const handleClick = vi.fn();
160
+ const { getByRole } = renderComponent({ label: 'Click Me', onClick: handleClick });
161
+ const button = getByRole('button', { name: 'Click Me' });
162
+ button.click();
163
+ expect(handleClick).toHaveBeenCalledTimes(1);
164
+ });
165
+
166
+ it('should render with custom styles for hover and active states', async () => {
167
+ const { container } = renderComponent({
168
+ label: 'Hover Button',
169
+ className: 'hover:bg-blue-500 active:bg-blue-700',
170
+ });
171
+ const results = await axe(container);
172
+ expect(results).toHaveNoViolations();
173
+ });
174
+ });
175
+
176
+ describe("Button Icon and Selected Prop Tests", () => {
177
+ it("applies default variant's selected style when selected is true and no classNameSelected is provided", () => {
178
+ render(<Button label="Selected Button" selected variant="default" />);
179
+ const button = screen.getByRole("button", { name: /Selected Button/ });
180
+ expect(button.className).toContain("bg-gray-500");
181
+ expect(button.className).toContain("text-white");
182
+ });
183
+
184
+ it("applies custom classNameSelected when selected is true", () => {
185
+ render(
186
+ <Button
187
+ label="Custom Selected Button"
188
+ selected
189
+ classNameSelected="selected-custom"
190
+ variant="default"
191
+ />
192
+ );
193
+ const button = screen.getByRole("button", { name: /Custom Selected Button/ });
194
+ expect(button.className).toContain("selected-custom");
195
+ });
196
+
197
+ it("renders a left icon when iconPosition is 'left'", () => {
198
+ render(
199
+ <Button
200
+ label="Left Icon Button"
201
+ icon={<span data-testid="left-icon">Icon</span>}
202
+ iconPosition="left"
203
+ />
204
+ );
205
+ const button = screen.getByRole("button", { name: /Left Icon Button/ });
206
+ const leftIcon = screen.getByTestId("left-icon");
207
+ expect(leftIcon).toBeInTheDocument();
208
+ // Check that the icon is wrapped in a span with the proper class.
209
+ expect(leftIcon.parentElement).toHaveClass("icon-left");
210
+ });
211
+
212
+ it("renders icon only when iconPosition is 'iconOnly'", () => {
213
+ render(
214
+ <Button
215
+ icon={<span data-testid="icon-only">IconOnly</span>}
216
+ iconPosition="iconOnly"
217
+ />
218
+ );
219
+ const button = screen.getByRole("button");
220
+ const iconOnlySpan = screen.getByTestId("icon-only");
221
+ expect(iconOnlySpan).toBeInTheDocument();
222
+ expect(iconOnlySpan.parentElement).toHaveClass("size-6");
223
+ expect(button.querySelector(".button-text")).toBeNull();
224
+ expect(button.className).toMatch(/px-\[12px\]/);
225
+ expect(button.className).toMatch(/py-\[12px\]/);
226
+ });
227
+
228
+ it("renders a right icon when iconPosition is 'right'", () => {
229
+ render(
230
+ <Button
231
+ label="Right Icon Button"
232
+ icon={<span data-testid="right-icon">Icon</span>}
233
+ iconPosition="right"
234
+ />
235
+ );
236
+ const button = screen.getByRole("button", { name: /Right Icon Button/ });
237
+ const rightIcon = screen.getByTestId("right-icon");
238
+ expect(rightIcon).toBeInTheDocument();
239
+ expect(rightIcon.parentElement).toHaveClass("icon-right");
240
+ });
241
+ });
@@ -0,0 +1,121 @@
1
+ import { ButtonHTMLAttributes, forwardRef, ReactNode, useEffect, useState } from "react";
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ interface VariantType {
5
+ [key: string]: {normal: string, selected: string};
6
+ }
7
+
8
+ const baseClasses = 'inline-flex items-center justify-center whitespace-nowrap rounded-md ' +
9
+ 'ring-offset-background transition-colors focus-visible:outline-hidden font-["Arial"] ' +
10
+ 'disabled:pointer-events-none text-sm md:text-base lg:text-lg ' +
11
+ 'disabled:opacity-50 px-6 py-[8px] md:py-[12px] lg:py-[16px] h-[40px] md:h-[48px] lg:h-[56px] ';
12
+
13
+ const variants: VariantType = {
14
+ default: {
15
+ normal: 'border-2 border-gray-300 rounded-md bg-gray-200 hover:bg-slate-400 text-black text-sm md:text-base lg:text-lg hover:text-black ' +
16
+ 'hover:border-slate-600 disabled:bg-dha-mc-bottom-nav-background disabled:text-dha-mc-checkbox-inactive ' +
17
+ 'focus:border-black ' +
18
+ 'disabled:border-dha-mc-bottom-nav-background disabled:border-2 py-0 md:py-0 lg:py-0 h-[48px] mt-1',
19
+ selected: 'bg-gray-500 text-white', // Only used if 'selected' is true to indicate state when 'selected'
20
+ } ,
21
+ filled: {
22
+ normal: 'rounded-md bg-[#092068] hover:bg-[#0c2c8e] text-white text-sm md:text-base lg:text-lg hover:text-white ' +
23
+ 'focus:shadow-[0px_0px_0px_3px_rgba(238,131,255,1.00)] active:bg-[#0F37B3] disabled:bg-[#e4e4e5] disabled:text-[#939194] ' +
24
+ 'disabled:border-[#e4e4e5] ',
25
+ selected: '', // Only used if 'selected' is true
26
+ },
27
+ outline: {
28
+ normal: 'rounded-md border-[#092068] bg-white border-2 text-[#092068] text-sm md:text-base lg:text-lg disabled:border-dha-mc-secondary-border ' +
29
+ 'disabled:text-[#939194] hover:bg-[#d1dbfb] active:bg-[#9fc5f0] ' +
30
+ 'focus:shadow-[0px_0px_0px_3px_rgba(238,131,255,1.00)] ',
31
+ selected: '', // Only used if 'selected' is true
32
+ },
33
+ transparent: {
34
+ normal: 'rounded-md text-sm md:text-base lg:text-lg text-[#092068] hover:bg-[#d1dbfb] active:bg-[#9fc5f0] focus:shadow-[0px_0px_0px_3px_rgba(251,137,241,1.00)] ' +
35
+ 'disabled:text-[#939194]',
36
+ selected: '', // Only used if 'selected' is true
37
+ }
38
+ };
39
+
40
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
41
+ children?: ReactNode;
42
+ label?: string;
43
+ onClick?: () => void;
44
+ className?: string;
45
+ classNameGroup?: string; // not documented on local component - used by ButtonGroup
46
+ icon?: ReactNode | undefined; // Accepts any valid React element (e.g., image, icon component)
47
+ iconPosition?: "left" | "right" | "iconOnly" | undefined;
48
+ variant?: string; // Define allowed variants here, extend as necessary.
49
+ type?: "button" | "submit" | "reset";
50
+ selected?: boolean;
51
+ classNameSelected?: string;
52
+ }
53
+
54
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
55
+ ({ label, onClick, className, icon, iconPosition, variant = "default",
56
+ type = "button", children, selected = false, classNameSelected,
57
+ classNameGroup = '', ...props }, ref) => {
58
+
59
+ const [mergedClasses, setMergedClasses] = useState('');
60
+ // console.log('iconPosition: ', iconPosition);
61
+
62
+ // useEffect(() => {
63
+ // const variantClasses = variants[variant] || variants.default;
64
+ // setMergedClasses(twMerge(baseClasses, variantClasses, className));
65
+ // }, [className, variant]);
66
+
67
+ useEffect(() => {
68
+ let variantClasses = variants[variant].normal || variants.default.normal;
69
+
70
+ if (selected && !classNameSelected) {
71
+ variantClasses = twMerge(variantClasses, variants[variant].selected);
72
+ } else if (selected && classNameSelected) {
73
+ variantClasses = twMerge(variantClasses, classNameSelected);
74
+ }
75
+
76
+ setMergedClasses(twMerge(baseClasses, variantClasses))
77
+
78
+ }, [selected, classNameSelected, variant]);
79
+
80
+ // Button contains default classes, classNameGroup, and className
81
+ // we assign classes from ButtonGroup to classNameGroup
82
+
83
+ return (
84
+ <button
85
+ type={type}
86
+ onClick={onClick}
87
+ className={`${iconPosition === "iconOnly" ?
88
+ (twMerge(
89
+ mergedClasses,
90
+ `px-[12px] md:px-[14px] lg:px-[16px] py-[8px] md:py-[12px] lg:py-[16px]`,
91
+ classNameGroup, // passed in from ButtonGroup if present
92
+ className // dev over-rides
93
+ ))
94
+ : twMerge(mergedClasses, classNameGroup, className)}`}
95
+ ref={ref}
96
+ {...props}
97
+ >
98
+ {/* Conditionally render icon on the left or right based on iconPosition */}
99
+ {/* ms/e-3 === 12px */}
100
+ {iconPosition === "left" && icon && (
101
+ <span className="icon-left mr-3 size-6">{icon}</span>
102
+ )}
103
+ {
104
+ iconPosition === "iconOnly" && icon ?
105
+ (
106
+ <span className="size-6">{icon}</span>
107
+ )
108
+ :
109
+ (
110
+ <span className=''>{children ? children : label}</span>
111
+ )
112
+ }
113
+ {iconPosition === "right" && icon && (
114
+ <span className="icon-right ml-3 size-6">{icon}</span>
115
+ )}
116
+ </button>
117
+ );
118
+ });
119
+
120
+ // Chris suggested for debug component name labeling
121
+ Button.displayName = 'SDK Button'
@@ -0,0 +1,129 @@
1
+ import { Meta, StoryContext, StoryFn } from '@storybook/react';
2
+ import { Button, ButtonProps } from './Button';
3
+ import { ButtonGroup, ButtonGroupProps } from './ButtonGroup';
4
+ import { useState } from 'react';
5
+ import { within, expect, waitFor } from 'storybook/test';
6
+
7
+ // Meta object - defines basic storybook options for this story
8
+ export default {
9
+ title: 'Components/ButtonGroup',
10
+ component: ButtonGroup,
11
+ argTypes: {
12
+ variant: {
13
+ control: 'select',
14
+ options: ['default', 'filled', 'outline', 'transparent']
15
+ },
16
+ size: {
17
+ control: 'select',
18
+ options: ['default', 'sm', 'lg', 'icon'],
19
+ },
20
+ },
21
+ args: {
22
+ disabled: false, // set default argument values
23
+ // label: 'Button', // set default argument values
24
+ },
25
+ parameters: {
26
+ layout: 'centered', // options are 'centered', 'fullscreen', and 'padded' (default value)
27
+ backgrounds: {
28
+ default: 'white',
29
+ values: [
30
+ { name: 'white', value: '#ffffff' },
31
+ { name: 'medium', value: '#b5bbb7' },
32
+ { name: 'dark', value: '#000' },
33
+ ],
34
+ },
35
+ },
36
+ } as Meta<ButtonProps>;
37
+
38
+
39
+
40
+ export const DefaultOne: StoryFn = () => {
41
+
42
+ return (
43
+ <ButtonGroup>
44
+ <Button variant='filled'>Primary</Button>
45
+ </ButtonGroup>
46
+ );
47
+ };
48
+
49
+
50
+ export const DefaultTwo: StoryFn = () => {
51
+
52
+ return (
53
+ <ButtonGroup>
54
+ <Button variant='outline'>Secondary</Button>
55
+ <Button variant='filled'>Primary</Button>
56
+ </ButtonGroup>
57
+ );
58
+ };
59
+
60
+ export const DefaultThree: StoryFn = () => {
61
+
62
+ return (
63
+ <ButtonGroup>
64
+ <Button variant='outline'>Secondary</Button>
65
+ <Button variant='filled'>Primary</Button>
66
+ <Button variant='transparent'>Tertiary</Button>
67
+ </ButtonGroup>
68
+ );
69
+ };
70
+
71
+
72
+ export const ColumnOne: StoryFn = () => {
73
+
74
+ return (
75
+ <ButtonGroup variant='column'>
76
+ <Button variant='filled'>Primary</Button>
77
+ </ButtonGroup>
78
+ );
79
+ };
80
+
81
+ export const ColumnTwo: StoryFn = () => {
82
+
83
+ return (
84
+ <ButtonGroup variant='column'>
85
+ <Button variant='outline'>Secondary</Button>
86
+ <Button variant='filled'>Primary</Button>
87
+ </ButtonGroup>
88
+ );
89
+ };
90
+
91
+ export const ColumnThree: StoryFn = () => {
92
+
93
+ return (
94
+ <ButtonGroup variant='column'>
95
+ <Button variant='outline'>Secondary</Button>
96
+ <Button variant='filled'>Primary</Button>
97
+ <Button variant='transparent'>Tertiary</Button>
98
+ </ButtonGroup>
99
+ );
100
+ };
101
+
102
+
103
+ export const CustomThree: StoryFn = () => {
104
+
105
+ return (
106
+ <div className="">
107
+ <ButtonGroup variant='custom'
108
+ className='flex flex-row gap-4'
109
+ classNameButtons='size-14 border-4 bg-blue-200 border-purple-500'
110
+ >
111
+
112
+ <Button className='border-red-500 border-4 bg-white hover:bg-slate-200 hover:text-black'>A</Button>
113
+ <Button>B</Button>
114
+ <Button>C</Button>
115
+ </ButtonGroup>
116
+ </div>
117
+ );
118
+ };
119
+
120
+
121
+
122
+ // // Define "Alternate Classes" story
123
+ // export const AlternateClasses = {
124
+ // args: {
125
+ // children: 'Custom Classes',
126
+ // onClick: () => console.log('Clicked!'),
127
+ // className: 'border-8 border-black text-white bg-orange-500',
128
+ // }
129
+ // };
@@ -0,0 +1,89 @@
1
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2
+ import { vi } from 'vitest';
3
+ import { axe } from "vitest-axe";
4
+ import { ButtonGroup, ButtonGroupProps } from './ButtonGroup';
5
+ import { Button, ButtonProps } from './Button';
6
+ import { cp } from 'fs';
7
+ import { createRef } from 'react';
8
+
9
+ // getByText is synchronous
10
+ // findByText is asynchronous
11
+
12
+ describe('ButtonGroup Component', () => {
13
+ // Helper function to render the component with children
14
+ const renderComponent = (props: Partial<ButtonGroupProps> = {}) =>
15
+ render(
16
+ <ButtonGroup {...props}>
17
+ <Button>A</Button>
18
+ <Button>B</Button>
19
+ <Button>C</Button>
20
+ </ButtonGroup>
21
+ );
22
+
23
+ test('renders all children buttons', () => {
24
+ renderComponent();
25
+ expect(screen.getByText('A')).toBeInTheDocument();
26
+ expect(screen.getByText('B')).toBeInTheDocument();
27
+ expect(screen.getByText('C')).toBeInTheDocument();
28
+ });
29
+
30
+
31
+ test('applies custom className to the container', () => {
32
+ const customContainerClass = 'custom-container';
33
+ const { container } = render(
34
+ <ButtonGroup className={customContainerClass}>
35
+ <Button>A</Button>
36
+ </ButtonGroup>
37
+ );
38
+ expect(container.firstChild).toHaveClass(customContainerClass);
39
+ });
40
+
41
+ test('merges classNameButtons with each Button child', () => {
42
+ const groupButtonClass = 'group-spacing';
43
+ const customButtonClass = 'btn-custom';
44
+ const { container } = render(
45
+ <ButtonGroup classNameButtons={groupButtonClass}>
46
+ <Button classNameGroup={customButtonClass}>A</Button>
47
+ <Button>B</Button>
48
+ </ButtonGroup>
49
+ );
50
+ // Query all button elements (assumes Button renders a <button>)
51
+ const buttons = container.querySelectorAll('button');
52
+ expect(buttons.length).toBe(2);
53
+
54
+ // For the first button, verify that both the custom button class and group class are merged
55
+ expect(buttons[0].className).toContain(customButtonClass);
56
+ expect(buttons[0].className).toContain(groupButtonClass);
57
+ // For the second button, the group class should be present
58
+ expect(buttons[1].className).toContain(groupButtonClass);
59
+ });
60
+
61
+ test('has no accessibility violations', async () => {
62
+ const { container } = renderComponent();
63
+ const results = await axe(container);
64
+ expect(results).toHaveNoViolations();
65
+ });
66
+
67
+ test('forwards ref to the root element', () => {
68
+ const ref = createRef<HTMLDivElement>();
69
+ const { container } = render(
70
+ <ButtonGroup ref={ref}>
71
+ <Button>A</Button>
72
+ </ButtonGroup>
73
+ );
74
+ // The ref should be attached to the container (i.e. the first DOM element rendered)
75
+ expect(ref.current).toBe(container.firstChild);
76
+ });
77
+
78
+ test('renders non-element children without modification', () => {
79
+ const plainText = 'Plain Text';
80
+ const { container } = render(
81
+ <ButtonGroup>
82
+ {plainText}
83
+ <Button>A</Button>
84
+ </ButtonGroup>
85
+ );
86
+ expect(container.textContent).toContain(plainText);
87
+ });
88
+
89
+ });
@@ -0,0 +1,107 @@
1
+
2
+ /*
3
+ * Default is horizontal ('default'), optional is 'column'.
4
+ *
5
+ * Possible to have 1, 2, or 3 buttons in the predefined layout
6
+ *
7
+ * If the user chooses a 'custom' variant, we will forego the hard formatting on
8
+ * the buttons, i.e. 1st is secondary, 2nd is primary, etc.
9
+ *
10
+ * Scenarios for each of the two initial layouts, single, two, and three button
11
+ * versions of those layouts.
12
+ *
13
+ * In the three-button layout, the 'Tertiary' transparent
14
+ * button always appears below the first two, regardless of layout variant.
15
+ *
16
+ * I think we inform the developer, and give them example usage, that the primary
17
+ * button is filled, the secondary is outline, and the tertiary is transparent.
18
+ *
19
+ * Default layout:
20
+ * flex-row (1st two buttons)
21
+ * flex-row (3rd button)
22
+ *
23
+ * Columm Layout:
24
+ * flex row ( flex column --> two buttons)
25
+ * flex row (3rd button)
26
+ *
27
+ */
28
+
29
+ import { ButtonHTMLAttributes, Children, cloneElement, forwardRef, isValidElement, ReactElement, ReactNode, useEffect, useState } from "react";
30
+ import { twMerge } from 'tailwind-merge';
31
+ import { ButtonProps } from './Button';
32
+
33
+ interface VariantType {
34
+ [key: string]: {container: string, button: string,};
35
+ }
36
+
37
+ // const baseClasses = ' ';
38
+
39
+ const variants: VariantType = {
40
+ default: {
41
+ container: 'flex gap-4 w-full md:gap-8 lg:gap-12',
42
+ button: 'w-[150px]',
43
+ } ,
44
+ column: {
45
+ container: 'flex flex-col gap-3 w-full md:gap-4 lg:gap-6',
46
+ button: 'w-[150px]',
47
+ },
48
+ custom: {
49
+ container: '',
50
+ button: '',
51
+ }
52
+ };
53
+
54
+ export interface ButtonGroupProps extends ButtonHTMLAttributes<HTMLButtonElement> {
55
+ children?: ReactNode; // children (buttons) that comprise the ButtonGroup
56
+ className?: string; // classname to alter div that contains the ButtonGroup
57
+ classNameButtons?: string; // classnames to alter boundary / spacing between buttons
58
+ variant?: string;
59
+ }
60
+
61
+ export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProps>(
62
+ ({ className, children, classNameButtons, variant = 'default', ...props }, ref) => {
63
+ let size = 0;
64
+
65
+ const buttons = Children.map(children, (child) => {
66
+ if (isValidElement(child)) {
67
+ ++size;
68
+ // Merge the existing className on the child with the classNameEdges prop
69
+ const element = child as ReactElement<ButtonProps>;
70
+ const childClassName = element.props.classNameGroup || "";
71
+ return cloneElement(element, {
72
+ classNameGroup: twMerge(childClassName, variants[variant].button, classNameButtons),
73
+ });
74
+ }
75
+ return child;
76
+ });
77
+
78
+ const tertiary:boolean = (size === 3) && (variant === 'default') ? true : false;
79
+ const one = buttons?.slice(0,2);
80
+ const two = buttons?.slice(2);
81
+
82
+ // three buttons & !custom, return tertiary on bottom row
83
+ if (tertiary)
84
+ return (
85
+ <div className='inline-flex flex-col gap-1'>
86
+ <div className={twMerge(variants[variant].container, className)}>
87
+ {one}
88
+ </div>
89
+ <div className={twMerge(variants[variant].container, 'justify-center', className)}>
90
+ {two}
91
+ </div>
92
+ </div>
93
+ );
94
+
95
+ else
96
+
97
+ return (
98
+ <div className={twMerge("", variants[variant].container, className)} ref={ref}>
99
+ {buttons}
100
+ </div>
101
+ );
102
+
103
+ }
104
+ );
105
+
106
+ // Chris suggested for debug component name labeling
107
+ ButtonGroup.displayName = 'SDK ButtonGroup'