@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,218 @@
1
+ import { render, screen, act } from "@testing-library/react";
2
+ import { describe, it, expect } from "vitest";
3
+ import { axe } from "vitest-axe";
4
+ import { MemoryRouter } from "react-router-dom";
5
+ import { Breadcrumbs } from "./Breadcrumbs";
6
+ //import vector from '../assets/img/vector.svg';
7
+
8
+ // Mock the ResizeObserver
9
+ const ResizeObserverMock = vi.fn(() => ({
10
+ observe: vi.fn(),
11
+ unobserve: vi.fn(),
12
+ disconnect: vi.fn(),
13
+ }));
14
+
15
+ // Stub the global ResizeObserver
16
+ vi.stubGlobal('ResizeObserver', ResizeObserverMock);
17
+
18
+ describe("Breadcrumbs Component", () => {
19
+ it("renders the breadcrumbs with the correct route structure", () => {
20
+ render(
21
+ <MemoryRouter initialEntries={["/home/about/contact"]}>
22
+ <Breadcrumbs />
23
+ </MemoryRouter>
24
+ );
25
+
26
+ expect(screen.getByText("Home")).toBeInTheDocument();
27
+ expect(screen.getByText("About")).toBeInTheDocument();
28
+ expect(screen.getByText("Contact")).toBeInTheDocument();
29
+ });
30
+
31
+ it("renders the last breadcrumb as plain text", () => {
32
+ render(
33
+ <MemoryRouter initialEntries={["/home/about/contact"]}>
34
+ <Breadcrumbs />
35
+ </MemoryRouter>
36
+ );
37
+
38
+ const contact = screen.getByText("Contact");
39
+ expect(contact).not.toHaveAttribute("href");
40
+ expect(contact).toHaveAttribute("aria-current", "page");
41
+ });
42
+
43
+ it("renders intermediate breadcrumbs as links", () => {
44
+ render(
45
+ <MemoryRouter initialEntries={["/home/about/contact"]}>
46
+ <Breadcrumbs />
47
+ </MemoryRouter>
48
+ );
49
+
50
+ const home = screen.getByText("Home");
51
+ const about = screen.getByText("About");
52
+
53
+ expect(home).toHaveAttribute("href", "/home");
54
+ expect(about).toHaveAttribute("href", "/home/about");
55
+ });
56
+
57
+ // if coverage suffers, fix this test
58
+ // it("uses the provided separator", () => {
59
+ // render(
60
+ // <MemoryRouter initialEntries={["/home/about/contact"]}>
61
+ // <Breadcrumbs separator={vector} />
62
+ // </MemoryRouter>
63
+ // );
64
+
65
+ // expect(screen.getAllByAltText("vector").length).toBe(2); // Ensure separator is rendered between breadcrumbs
66
+ // });
67
+
68
+ it("applies custom classes to the container and links", () => {
69
+ render(
70
+ <MemoryRouter initialEntries={["/home/about/contact"]}>
71
+ <Breadcrumbs
72
+ classNameContainer="custom-container"
73
+ className="custom-link"
74
+ />
75
+ </MemoryRouter>
76
+ );
77
+
78
+ expect(screen.getByLabelText("Breadcrumbs")).toHaveClass("custom-container");
79
+ const links = screen.getAllByRole("link");
80
+ links.forEach((link) => expect(link).toHaveClass("custom-link"));
81
+ });
82
+
83
+ it("has no accessibility violations", async () => {
84
+ const { container } = render(
85
+ <MemoryRouter initialEntries={["/home/about/contact"]}>
86
+ <Breadcrumbs />
87
+ </MemoryRouter>
88
+ );
89
+
90
+ const results = await axe(container);
91
+ expect(results).toHaveNoViolations();
92
+ });
93
+
94
+ it("handles routes with hyphens and decodes them", () => {
95
+ render(
96
+ <MemoryRouter initialEntries={["/home/about-page/contact-us"]}>
97
+ <Breadcrumbs />
98
+ </MemoryRouter>
99
+ );
100
+
101
+ expect(screen.getByText("About Page")).toBeInTheDocument();
102
+ expect(screen.getByText("Contact Us")).toBeInTheDocument();
103
+ });
104
+
105
+ // it("renders no breadcrumbs if the path is root ('/')", () => {
106
+ // render(
107
+ // <MemoryRouter initialEntries={["/"]}>
108
+ // <Breadcrumbs />
109
+ // </MemoryRouter>
110
+ // );
111
+
112
+ // expect(screen.queryByRole("listitem")).not.toBeInTheDocument();
113
+ // });
114
+
115
+ it("verifies manually specified routes exist", () => {
116
+ render(
117
+ <MemoryRouter >
118
+ <Breadcrumbs routes={[{ name: 'Main', route: '/' }, { name: 'Tools', route: '/tools' }]}/>
119
+ </MemoryRouter>
120
+ );
121
+
122
+ expect(screen.getByText("Main")).toBeInTheDocument();
123
+ expect(screen.getByText("Tools")).toBeInTheDocument();
124
+ });
125
+ });
126
+
127
+ describe("Breadcrumbs — overflow → ellipsis", () => {
128
+ beforeEach(() => {
129
+ // Mock measurements so that scrollWidth > clientWidth → triggers ellipsis
130
+ Object.defineProperty(HTMLElement.prototype, "clientWidth", {
131
+ configurable: true,
132
+ get() {
133
+ return 100;
134
+ },
135
+ });
136
+ Object.defineProperty(HTMLElement.prototype, "scrollWidth", {
137
+ configurable: true,
138
+ get() {
139
+ return 200;
140
+ },
141
+ });
142
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
143
+ configurable: true,
144
+ get() {
145
+ return 100;
146
+ },
147
+ });
148
+
149
+ // Simple ResizeObserver stub that immediately invokes the callback
150
+ class MockRO {
151
+ callback: ResizeObserverCallback;
152
+ constructor(cb: ResizeObserverCallback) {
153
+ this.callback = cb;
154
+ }
155
+ observe() {
156
+ // call synchronously so ellipsis state flips immediately
157
+ this.callback(
158
+ [
159
+ {
160
+ target: document.createElement("div"),
161
+ contentRect: {} as DOMRectReadOnly,
162
+ } as unknown as ResizeObserverEntry,
163
+ ],
164
+ this
165
+ );
166
+ }
167
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
168
+ unobserve() {}
169
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
170
+ disconnect() {}
171
+ }
172
+ // @ts-expect-error
173
+ global.ResizeObserver = MockRO;
174
+ });
175
+
176
+ afterEach(() => {
177
+ // clean up our globals
178
+ delete (HTMLElement.prototype as any).clientWidth;
179
+ delete (HTMLElement.prototype as any).scrollWidth;
180
+ delete (HTMLElement.prototype as any).offsetWidth;
181
+ // @ts-ignore
182
+ delete global.ResizeObserver;
183
+ });
184
+
185
+ it("renders only the final crumb with “...” when overflowed", async () => {
186
+ // wrap in act so that useEffect and ResizeObserver fire
187
+ await act(async () => {
188
+ render(
189
+ <MemoryRouter initialEntries={["/first/second"]}>
190
+ <Breadcrumbs />
191
+ </MemoryRouter>
192
+ );
193
+ });
194
+
195
+ // “...” placeholder should be present
196
+ expect(screen.getByText("...")).toBeInTheDocument();
197
+
198
+ // final segment (“Second”) should render as plain text with aria-current
199
+ const lastCrumb = screen.getByText("Second");
200
+ expect(lastCrumb).toBeInTheDocument();
201
+ expect(lastCrumb).toHaveAttribute("aria-current", "page");
202
+
203
+ });
204
+
205
+ it("has no detectable accessibility violations", async () => {
206
+ let container: HTMLElement;
207
+ await act(async () => {
208
+ const rendered = render(
209
+ <MemoryRouter initialEntries={["/a/b/c"]}>
210
+ <Breadcrumbs />
211
+ </MemoryRouter>
212
+ );
213
+ container = rendered.container;
214
+ });
215
+ const results = await axe(container!);
216
+ expect(results).toHaveNoViolations();
217
+ });
218
+ });
@@ -0,0 +1,219 @@
1
+ import * as React from 'react';
2
+ import { twMerge } from 'tailwind-merge';
3
+ import { useLocation, Link } from 'react-router-dom';
4
+ import separatorSvg from '../assets/img/separator.svg';
5
+ import home from '../assets/img/home-gray.svg';
6
+ import { useEffect, useRef, useState, useMemo } from 'react';
7
+
8
+ function prettify(name: string) {
9
+ return name
10
+ .replace(/-/g, ' ')
11
+ .replace(/\b\w/g, c => c.toUpperCase());
12
+ }
13
+ interface VariantType {
14
+ [key: string]: string;
15
+ }
16
+
17
+ // variant styling definition, both size and layout (portrait vs landscape)
18
+ const variants: VariantType = {
19
+ default: 'overflow-x-auto w-full text-base md:text-lg', // image top
20
+ bold: 'overflow-x-auto font-bold w-full text-base md:text-lg',
21
+ }
22
+
23
+ export interface route {
24
+ name: string;
25
+ route: string;
26
+ }
27
+
28
+ export interface BreadcrumbsProps extends React.HTMLAttributes<HTMLDivElement> {
29
+ className?: string;
30
+ classNameContainer?: string;
31
+ variant?: string;
32
+ separator?: string; // i.e. >>, or -->, etc.
33
+ routes?: route[];
34
+ }
35
+
36
+ export const Breadcrumbs = React.forwardRef<HTMLDivElement, BreadcrumbsProps>(
37
+ ({ classNameContainer, className, routes, variant = 'default',
38
+ separator = separatorSvg, ...props }, ref) => {
39
+ const containerRef = useRef<HTMLDivElement>(null);
40
+ const iconRef = useRef<HTMLImageElement>(null);
41
+ const [ellipsis, setEllipsis] = useState(false);
42
+ const [currentTime, setCurrentTime] = useState<number>();
43
+ const [reqContentWidth, setReqContentWidth] = useState<number>(0); // content width required to display non-ellipsis breadcrumbs
44
+
45
+ // Define Routes
46
+ const { pathname } = useLocation();
47
+
48
+ const finalRoutes: route[] = useMemo(() => {
49
+ // if manualRoutes was passed in, use it
50
+ if (routes && routes.length > 0) {
51
+ return routes;
52
+ }
53
+
54
+ const segments = pathname
55
+ .split('/')
56
+ .filter(Boolean);
57
+
58
+ return segments.map((segment, idx, arr) => ({
59
+ name: prettify(decodeURIComponent(segment)),
60
+ route: '/' + arr.slice(0, idx + 1).join('/'),
61
+ }));
62
+ }, [routes, pathname]);
63
+
64
+
65
+ // measures containerWidth at page load
66
+ // should re-measure at home-icon swap
67
+ useEffect(() => {
68
+ const fullContainer = containerRef.current;
69
+ if (fullContainer) {
70
+ setReqContentWidth(fullContainer.offsetWidth);
71
+ }
72
+
73
+ // build auto-routes if manualRoutes not present
74
+ }, []);
75
+
76
+ const checkOverflow = () => {
77
+ const container = containerRef.current;
78
+
79
+ if ((container && !currentTime) || (container && currentTime && Date.now() > (currentTime + 200))) {
80
+
81
+ if (ellipsis) {
82
+ if (container.clientWidth > reqContentWidth) {
83
+ // console.log('setting ellipsis false');
84
+ setEllipsis(false);
85
+ }
86
+ } else {
87
+ if (container.scrollWidth > container.clientWidth) {
88
+ // console.log('setting ellipsis true');
89
+ setEllipsis(true);
90
+ }
91
+ }
92
+
93
+ setCurrentTime(Date.now());
94
+ }
95
+ };
96
+
97
+ useEffect(() => {
98
+ // clientWidth = 0 for inline or w/o css, otherwise width of inner element in pixels
99
+ // scrollWidth = measurement of width of elements content
100
+ // console.log('breadcrumbs useEffect');
101
+
102
+ const container = containerRef.current;
103
+ if (!container) {
104
+ // console.log('no container - returning');
105
+ return;
106
+ }
107
+
108
+ // run once to set initial state
109
+ checkOverflow();
110
+
111
+ // whenever the container itself changes size, re-check
112
+ const ro = new ResizeObserver(checkOverflow);
113
+ ro.observe(container);
114
+ return () => { ro.disconnect(); };
115
+ // eslint-disable-next-line react-hooks/exhaustive-deps
116
+ }, [currentTime, finalRoutes]); // re‑run when the crumbs change
117
+
118
+ return (
119
+ <nav
120
+ className={twMerge(variants[variant], classNameContainer)}
121
+ {...props}
122
+ ref={ref}
123
+ aria-label="Breadcrumbs"
124
+ >
125
+ <div
126
+ ref={containerRef}
127
+ className="flex list-none flex-nowrap text-clip overflow-hidden text-nowrap w-full"
128
+ >
129
+ {/* First item / home page link */}
130
+ <span className="breadcrumb-item inline-flex items-center shrink-0">
131
+ <Link
132
+ to="/"
133
+ className={twMerge(
134
+ "text-[#747476] hover:text-[#0000ff] md:hover:underline capitalize me-2 hidden md:inline-flex",
135
+ className
136
+ )}>Homepage
137
+ </Link>
138
+ <Link
139
+ to="/"
140
+ className={twMerge(
141
+ "capitalize me-2 inline-flex md:hidden",
142
+ className
143
+ )}><img src={home} ref={iconRef} alt='home icon'></img>
144
+ </Link>
145
+ {finalRoutes.length > 0 ? <span className={twMerge("me-2", className)}><img src={separator} alt='vector'></img></span> : ''}
146
+ </span>
147
+
148
+ <span
149
+ className=''
150
+ // ref={ellipsisRef}
151
+ // ellipsisRef is used to measure size of container w/ content - may have to move to a different span
152
+ >
153
+ {finalRoutes.map((part, index) => {
154
+ // Reconstruct the path up to the current segment
155
+ // const path = '/' + routes.slice(0, index + 1).join('/');
156
+
157
+ if (ellipsis) {
158
+ return (
159
+ <span key={index} className='inline-flex items-center shrink-0 text-nowrap'>
160
+ {index === finalRoutes.length - 1 ? (
161
+ <>
162
+ <span className='text-nowrap me-3'>...</span>
163
+ <span className={twMerge("me-3", className)}><img src={separator} alt='vector'></img></span>
164
+ <span
165
+ className="text-black font-bold capitalize text-nowrap"
166
+ aria-current="page"
167
+ >
168
+ {decodeURIComponent(part.name).replace(/-/g, ' ')}
169
+ </span>
170
+ </>
171
+ ) : (
172
+ ''
173
+ )}
174
+ </span>
175
+ );
176
+ } else {
177
+
178
+ return (
179
+ <span
180
+ key={index}
181
+ className="breadcrumb-item inline-flex items-center shrink-0"
182
+ >
183
+
184
+ {index === finalRoutes.length - 1 ? (
185
+ // Render the current page as plain text (last item in list)
186
+ <span
187
+ className="text-black font-bold capitalize"
188
+ aria-current="page"
189
+ >
190
+ {decodeURIComponent(part.name).replace(/-/g, ' ')}
191
+ </span>
192
+ ) : (
193
+ // Render intermediate items as links
194
+ <Link
195
+ to={part.route}
196
+ className={twMerge(
197
+ "text-[#747476] hover:text-[#0000ff] md:hover:underline capitalize me-3",
198
+ className
199
+ )}
200
+ >
201
+ {decodeURIComponent(part.name).replace(/-/g, ' ')}
202
+ </Link>
203
+ )}
204
+ {/* Separator for intermediate items */}
205
+ {index < finalRoutes.length - 1 && (
206
+ <span className={twMerge("me-3", className)}><img src={separator} alt='vector'></img></span>
207
+ )}
208
+ </span>
209
+ );
210
+
211
+ }
212
+
213
+ })}
214
+ </span>
215
+ </div>
216
+ </nav>
217
+ );
218
+ }
219
+ );
@@ -0,0 +1,220 @@
1
+ import { Meta, StoryContext } from '@storybook/react';
2
+ import { Button, ButtonProps } from './Button';
3
+ import { useState } from 'react';
4
+ import { within, expect, waitFor } from 'storybook/test';
5
+
6
+ // Meta object - defines basic storybook options for this story
7
+ export default {
8
+ title: 'Components/Button',
9
+ component: Button,
10
+ argTypes: {
11
+ variant: {
12
+ control: 'select',
13
+ options: ['default', 'filled', 'outline', 'transparent']
14
+ },
15
+ },
16
+ args: {
17
+ disabled: false, // set default argument values
18
+ // label: 'Button', // set default argument values
19
+ },
20
+ parameters: {
21
+ layout: 'centered', // options are 'centered', 'fullscreen', and 'padded' (default value)
22
+ backgrounds: { default: 'dark' }, // options are light, medium, or dark
23
+ },
24
+ } as Meta<ButtonProps>;
25
+
26
+ // Define "Default" story
27
+ export const Default = {
28
+ args: {
29
+ children: 'Button',
30
+ variant: 'default',
31
+ size: 'default',
32
+ }
33
+ };
34
+
35
+ export const LeftIcon = {
36
+ args: {
37
+ label: 'Button with left icon',
38
+ // Pass an icon element with a test ID so we can find it.
39
+ icon: <svg data-testid="left-icon" />,
40
+ iconPosition: 'left',
41
+ },
42
+ play: async ({ canvasElement }: StoryContext) => {
43
+ const canvas = within(canvasElement);
44
+ const button = canvas.getByRole('button', { name: /button with left icon/i });
45
+
46
+ // Line 87–89: There should be a span with class "icon-left"
47
+ const leftIconSpan = button.querySelector('.icon-left');
48
+ expect(leftIconSpan).toBeInTheDocument();
49
+
50
+ // Verify the icon element is rendered inside that span.
51
+ const iconElement = leftIconSpan?.querySelector('[data-testid="left-icon"]');
52
+ expect(iconElement).toBeInTheDocument();
53
+
54
+ // Also, because iconPosition is "left", the text is rendered in a span with class "button-text".
55
+ const textSpan = button.querySelector('.button-text');
56
+ expect(textSpan).toHaveTextContent('Button with left icon');
57
+ },
58
+ };
59
+
60
+ export const IconOnly = {
61
+ args: {
62
+ label: 'Icon Only', // This label will be ignored in rendering because an icon is provided.
63
+ icon: <svg data-testid="icon-only" />,
64
+ iconPosition: 'iconOnly',
65
+ },
66
+ play: async ({ canvasElement }: StoryContext) => {
67
+ const canvas = within(canvasElement);
68
+ const button = canvas.getByRole('button');
69
+
70
+ // The conditional (lines 91–98) should render only the icon.
71
+ // There should be no span with class "button-text"
72
+ const textSpan = button.querySelector('.button-text');
73
+ expect(textSpan).toBeNull();
74
+
75
+ // Instead, the icon is wrapped in a span with class "size-6".
76
+ const iconSpan = button.querySelector('.size-6');
77
+ expect(iconSpan).toBeInTheDocument();
78
+ const iconElement = iconSpan?.querySelector('[data-testid="icon-only"]');
79
+ expect(iconElement).toBeInTheDocument();
80
+ },
81
+ };
82
+
83
+ export const RightIcon = {
84
+ args: {
85
+ label: 'Button with right icon',
86
+ icon: <svg data-testid="right-icon" />,
87
+ iconPosition: 'right',
88
+ },
89
+ play: async ({ canvasElement }: StoryContext) => {
90
+ const canvas = within(canvasElement);
91
+ const button = canvas.getByRole('button', { name: /button with right icon/i });
92
+
93
+ // The text should be rendered (via the else branch of the conditional)...
94
+ const textSpan = button.querySelector('.button-text');
95
+ expect(textSpan).toHaveTextContent('Button with right icon');
96
+
97
+ // ...and line 101 should cause a span with class "icon-right" to render.
98
+ const rightIconSpan = button.querySelector('.icon-right');
99
+ expect(rightIconSpan).toBeInTheDocument();
100
+
101
+ // Confirm that the icon element is inside the right icon span.
102
+ const iconElement = rightIconSpan?.querySelector('[data-testid="right-icon"]');
103
+ expect(iconElement).toBeInTheDocument();
104
+ },
105
+ };
106
+
107
+ // Define "Blank" story
108
+ export const Filled = {
109
+ args: {
110
+ children: 'Button',
111
+ variant: 'filled',
112
+ size: 'default',
113
+ }
114
+ };
115
+
116
+ // Define "Blank" story
117
+ export const Outline = {
118
+ args: {
119
+ children: 'Button',
120
+ variant: 'outline',
121
+ size: 'default',
122
+ }
123
+ };
124
+
125
+ // Define "Blank" story
126
+ export const Transparent = {
127
+ args: {
128
+ children: 'Button',
129
+ variant: 'transparent',
130
+ size: 'default',
131
+ }
132
+ };
133
+
134
+ export const SelectedDefault = {
135
+ args: {
136
+ label: 'Selected Default',
137
+ selected: true,
138
+ variant: 'default',
139
+ // Do not pass classNameSelected so that line 69 executes
140
+ },
141
+ play: async ({ canvasElement }: StoryContext) => {
142
+ const canvas = within(canvasElement);
143
+ // Get the button by its role and accessible name.
144
+ const button = canvas.getByRole('button', { name: /selected default/i });
145
+
146
+ // For the "default" variant, the selected state (when no custom classNameSelected are provided)
147
+ // appends "bg-gray-500 text-white" (see variants.default.selected).
148
+ await expect(button.className).toContain('bg-gray-500');
149
+ await expect(button.className).toContain('text-white');
150
+ },
151
+ };
152
+
153
+ export const SelectedCustom = {
154
+ args: {
155
+ label: 'Selected Custom',
156
+ selected: true,
157
+ variant: 'default',
158
+ classNameSelected: 'bg-custom text-custom', // This custom value should be merged on line 71
159
+ },
160
+ play: async ({ canvasElement }: StoryContext) => {
161
+ const canvas = within(canvasElement);
162
+ const button = canvas.getByRole('button', { name: /selected custom/i });
163
+
164
+ // Check that the custom selected classes are present in the className.
165
+ await expect(button.className).toContain('bg-custom');
166
+ await expect(button.className).toContain('text-custom');
167
+ },
168
+ };
169
+
170
+ // Define "Blank" story
171
+ export const OutlineWithCustomClass = {
172
+ args: {
173
+ children: 'Button',
174
+ variant: 'outline',
175
+ size: 'default',
176
+ className: 'text-purple-600 hover:text-purple-700',
177
+ }
178
+ };
179
+
180
+ // Define "DHA Disabled" story
181
+ export const DHADisabled = {
182
+ args: {
183
+ disabled: true,
184
+ label: 'Disabled Button',
185
+ }
186
+ };
187
+
188
+
189
+ // Define "Alternate Classes" story
190
+ export const AlternateClasses = {
191
+ args: {
192
+ children: 'Custom Classes',
193
+ onClick: () => console.log('Clicked!'),
194
+ className: 'border-8 border-black text-white bg-orange-500',
195
+ }
196
+ };
197
+
198
+ // Define a story using an alternate method that contains state values
199
+ export const ButtonWithClickHandler = ({ disabled, ...rest }: ButtonProps) => {
200
+ // const [clicked, setClicked] = useState(false);
201
+ // const handleClick = () => setClicked(!clicked);
202
+ const [variant, setVariant] = useState<string>('filled');
203
+
204
+ const handleClick = (() => {
205
+ if (variant === 'filled')
206
+ setVariant('outline');
207
+ else
208
+ setVariant('filled');
209
+ })
210
+
211
+ return (
212
+ <Button
213
+ onClick={handleClick}
214
+ variant={variant}
215
+ disabled={disabled}
216
+ >
217
+ Click Me! I am {variant}.
218
+ </Button>
219
+ );
220
+ }