@dhasdk/simple-ui 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc +12 -0
- package/.storybook/main.ts +35 -0
- package/.storybook/preview.ts +4 -0
- package/BAKpostcss.config.jsBAK +15 -0
- package/BAKtailwind.config.mjsBAK +99 -0
- package/README.md +464 -16
- package/coverage/storybook/coverage-storybook.json +32411 -0
- package/coverage/storybook/lcov-report/Accordion.tsx.html +805 -0
- package/coverage/storybook/lcov-report/Badge.tsx.html +346 -0
- package/coverage/storybook/lcov-report/Breadcrumbs.tsx.html +742 -0
- package/coverage/storybook/lcov-report/Button.tsx.html +448 -0
- package/coverage/storybook/lcov-report/ButtonGroup.tsx.html +403 -0
- package/coverage/storybook/lcov-report/Card.tsx.html +292 -0
- package/coverage/storybook/lcov-report/CharacterCounter.tsx.html +253 -0
- package/coverage/storybook/lcov-report/CheckBox.tsx.html +1555 -0
- package/coverage/storybook/lcov-report/DatePicker.tsx.html +826 -0
- package/coverage/storybook/lcov-report/Input.tsx.html +1012 -0
- package/coverage/storybook/lcov-report/List.tsx.html +364 -0
- package/coverage/storybook/lcov-report/Modal.tsx.html +745 -0
- package/coverage/storybook/lcov-report/Pill.tsx.html +358 -0
- package/coverage/storybook/lcov-report/Search.tsx.html +997 -0
- package/coverage/storybook/lcov-report/SearchContent.tsx.html +235 -0
- package/coverage/storybook/lcov-report/SectionHeader.tsx.html +358 -0
- package/coverage/storybook/lcov-report/Select.tsx.html +1012 -0
- package/coverage/storybook/lcov-report/Shield.tsx.html +802 -0
- package/coverage/storybook/lcov-report/SideBarNav.tsx.html +490 -0
- package/coverage/storybook/lcov-report/Skeleton.tsx.html +394 -0
- package/coverage/storybook/lcov-report/Slider.tsx.html +385 -0
- package/coverage/storybook/lcov-report/Status.tsx.html +322 -0
- package/coverage/storybook/lcov-report/Tabs.tsx.html +610 -0
- package/coverage/storybook/lcov-report/Toggle.tsx.html +373 -0
- package/coverage/storybook/lcov-report/Tooltip.tsx.html +496 -0
- package/coverage/storybook/lcov-report/base.css +224 -0
- package/coverage/storybook/lcov-report/block-navigation.js +87 -0
- package/coverage/storybook/lcov-report/favicon.png +0 -0
- package/coverage/storybook/lcov-report/index.html +476 -0
- package/coverage/storybook/lcov-report/prettify.css +1 -0
- package/coverage/storybook/lcov-report/prettify.js +2 -0
- package/coverage/storybook/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/storybook/lcov-report/sorter.js +196 -0
- package/coverage/storybook/lcov.info +2312 -0
- package/dist/README.md +1815 -0
- package/eslint.config.mjs +13 -0
- package/package.json +6 -7
- package/project.json +11 -0
- package/src/assets/img/Frame.svg +5 -0
- package/src/assets/img/backArrowRight.svg +10 -0
- package/src/assets/img/bc-separator.png +0 -0
- package/src/assets/img/calendar.png +0 -0
- package/src/assets/img/calendar.svg +4 -0
- package/src/assets/img/check.svg +5 -0
- package/src/assets/img/check_box.svg +10 -0
- package/src/assets/img/check_box_empty.svg +10 -0
- package/src/assets/img/check_box_fill.svg +10 -0
- package/src/assets/img/check_box_fill_empty.svg +10 -0
- package/src/assets/img/chevron-down-white.svg +2 -0
- package/src/assets/img/chevron-down.svg +2 -0
- package/src/assets/img/chevron-left.svg +1 -0
- package/src/assets/img/chevron-right-light.svg +4 -0
- package/src/assets/img/chevron-right.svg +3 -0
- package/src/assets/img/chevron-up-white.svg +1 -0
- package/src/assets/img/chevron-up.svg +1 -0
- package/src/assets/img/clock.svg +6 -0
- package/src/assets/img/close.svg +1 -0
- package/src/assets/img/close2.svg +6 -0
- package/src/assets/img/closeModal.svg +10 -0
- package/src/assets/img/close_icon_dark.svg +10 -0
- package/src/assets/img/close_small.svg +3 -0
- package/src/assets/img/emergency_home.svg +10 -0
- package/src/assets/img/first-aid-kit.svg +7 -0
- package/src/assets/img/heartbeat.svg +4 -0
- package/src/assets/img/home-gray.svg +3 -0
- package/src/assets/img/home.svg +3 -0
- package/src/assets/img/hospital.jpg +0 -0
- package/src/assets/img/indeterminate_check_box.svg +10 -0
- package/src/assets/img/indeterminate_check_box_fill.svg +10 -0
- package/src/assets/img/info_24_ 1d4ed8.svg +3 -0
- package/src/assets/img/info_24_ 2c6441.svg +3 -0
- package/src/assets/img/marker_check_by_default.svg +10 -0
- package/src/assets/img/marker_check_by_default_fill.svg +10 -0
- package/src/assets/img/minus-accordion.svg +5 -0
- package/src/assets/img/minus.svg +3 -0
- package/src/assets/img/open.svg +1 -0
- package/src/assets/img/pill-white.svg +7 -0
- package/src/assets/img/pill.svg +5 -0
- package/src/assets/img/plus-accordion.svg +5 -0
- package/src/assets/img/plus.svg +4 -0
- package/src/assets/img/prescription.svg +6 -0
- package/src/assets/img/search.svg +10 -0
- package/src/assets/img/search_icon_light.svg +10 -0
- package/src/assets/img/separator.svg +3 -0
- package/src/assets/img/stethoscope-white.svg +8 -0
- package/src/assets/img/stethoscope.svg +8 -0
- package/src/assets/img/thumb_up.svg +10 -0
- package/src/assets/img/vector.svg +3 -0
- package/src/assets/img/warning-badge-disabled.svg +11 -0
- package/src/assets/img/warning-badge-green.svg +11 -0
- package/src/assets/img/warning-badge-red.svg +11 -0
- package/src/assets/img/warning-badge-yellow.svg +11 -0
- package/src/assets/img/warning.svg +10 -0
- package/src/global.d.ts +13 -0
- package/{index.d.ts → src/index.ts} +13 -5
- package/src/lib/Accordian--Accordian.stories.tsx +312 -0
- package/src/lib/Accordion.spec.tsx +384 -0
- package/src/lib/Accordion.tsx +240 -0
- package/src/lib/AppointmentPicker.spec.tsx +138 -0
- package/src/lib/AppointmentPicker.tsx +97 -0
- package/src/lib/Badge--Badge.stories.tsx +60 -0
- package/src/lib/Badge.spec.tsx +70 -0
- package/src/lib/Badge.tsx +87 -0
- package/src/lib/Breadcrumbs-Breadcrumbs.stories.tsx +114 -0
- package/src/lib/Breadcrumbs.spec.tsx +218 -0
- package/src/lib/Breadcrumbs.tsx +219 -0
- package/src/lib/Button--Button.stories.tsx +220 -0
- package/src/lib/Button.spec.tsx +241 -0
- package/src/lib/Button.tsx +121 -0
- package/src/lib/ButtonGroup--ButtonGroup.stories.tsx +129 -0
- package/src/lib/ButtonGroup.spec.tsx +89 -0
- package/src/lib/ButtonGroup.tsx +107 -0
- package/src/lib/Card--Card.stories.tsx +113 -0
- package/src/lib/Card.spec.tsx +112 -0
- package/src/lib/Card.tsx +69 -0
- package/src/lib/CharacterCounter--CharacterCounter.stories.tsx +169 -0
- package/src/lib/CharacterCounter.spec.tsx +123 -0
- package/src/lib/CharacterCounter.tsx +56 -0
- package/src/lib/CheckBox--CheckBox.stories.tsx +107 -0
- package/src/lib/CheckBox.spec.tsx +412 -0
- package/src/lib/CheckBox.tsx +491 -0
- package/src/lib/DatePicker--DatePicker.stories.tsx +228 -0
- package/src/lib/DatePicker.spec.tsx +424 -0
- package/src/lib/DatePicker.tsx +247 -0
- package/src/lib/Input--Input.stories.tsx +449 -0
- package/src/lib/Input.spec.tsx +281 -0
- package/src/lib/Input.tsx +309 -0
- package/src/lib/List--List.stories.tsx +157 -0
- package/src/lib/List.spec.tsx +211 -0
- package/src/lib/List.tsx +93 -0
- package/src/lib/Modal--Modal.stories.tsx +454 -0
- package/src/lib/Modal.spec.tsx +202 -0
- package/src/lib/Modal.tsx +220 -0
- package/src/lib/Pill--Pill.stories.tsx +98 -0
- package/src/lib/Pill.spec.tsx +103 -0
- package/src/lib/Pill.tsx +91 -0
- package/src/lib/ProgressBar.spec.tsx +106 -0
- package/src/lib/ProgressBar.tsx +112 -0
- package/src/lib/RadioGroup.spec.tsx +84 -0
- package/src/lib/RadioGroup.tsx +74 -0
- package/src/lib/RadioIcon.tsx +13 -0
- package/src/lib/Search--Search.stories.tsx +67 -0
- package/src/lib/Search.spec.tsx +182 -0
- package/src/lib/Search.tsx +304 -0
- package/src/lib/SearchContent.tsx +51 -0
- package/src/lib/SectionHeader--SectionHeader.stories.tsx +98 -0
- package/src/lib/SectionHeader.spec.tsx +60 -0
- package/src/lib/SectionHeader.tsx +91 -0
- package/src/lib/Select--Select.stories.tsx +387 -0
- package/src/lib/Select.spec.tsx +493 -0
- package/src/lib/Select.tsx +311 -0
- package/src/lib/Shield--Shield.stories.tsx +196 -0
- package/src/lib/Shield.spec.tsx +275 -0
- package/src/lib/Shield.tsx +239 -0
- package/src/lib/SideBarNav--SideBarNav.stories.tsx +136 -0
- package/src/lib/SideBarNav.spec.tsx +178 -0
- package/src/lib/SideBarNav.tsx +135 -0
- package/src/lib/Skeleton--Skeleton.stories.tsx +77 -0
- package/src/lib/Skeleton.module.css +16 -0
- package/src/lib/Skeleton.spec.tsx +83 -0
- package/src/lib/Skeleton.tsx +103 -0
- package/src/lib/SkipLink.spec.tsx +76 -0
- package/src/lib/SkipLink.tsx +48 -0
- package/src/lib/Slider--Slider.stories.tsx +108 -0
- package/src/lib/Slider.module.css +109 -0
- package/src/lib/Slider.spec.tsx +67 -0
- package/src/lib/Slider.tsx +101 -0
- package/src/lib/Status--Status.stories.tsx +93 -0
- package/src/lib/Status.spec.tsx +118 -0
- package/src/lib/Status.tsx +79 -0
- package/src/lib/Tabs--Tabs.stories.tsx +294 -0
- package/src/lib/Tabs.spec.tsx +249 -0
- package/src/lib/Tabs.tsx +188 -0
- package/src/lib/Tester.spec.tsx +17 -0
- package/src/lib/Toggle--Toggle.stories.tsx +162 -0
- package/src/lib/Toggle.spec.tsx +122 -0
- package/src/lib/Toggle.tsx +96 -0
- package/src/lib/Tooltip--Tooltip.stories.tsx +315 -0
- package/src/lib/Tooltip.spec.tsx +307 -0
- package/src/lib/Tooltip.tsx +137 -0
- package/src/lib/bak-simple-ui.stories.tsx-bak +24 -0
- package/src/styles.css +190 -0
- package/tsconfig.json +25 -0
- package/tsconfig.lib.json +42 -0
- package/tsconfig.spec.json +29 -0
- package/tsconfig.storybook.json +36 -0
- package/vite.config.mts +87 -0
- package/vitest.setup.ts +12 -0
- package/index.css +0 -1
- package/index.js +0 -35
- package/index.mjs +0 -4981
- package/lib/Accordion.d.ts +0 -36
- package/lib/AppointmentPicker.d.ts +0 -21
- package/lib/Badge.d.ts +0 -11
- package/lib/Breadcrumbs.d.ts +0 -13
- package/lib/Button.d.ts +0 -15
- package/lib/ButtonGroup.d.ts +0 -8
- package/lib/Card.d.ts +0 -11
- package/lib/CharacterCounter.d.ts +0 -11
- package/lib/CheckBox.d.ts +0 -30
- package/lib/DatePicker.d.ts +0 -7
- package/lib/Input.d.ts +0 -16
- package/lib/List.d.ts +0 -22
- package/lib/Modal.d.ts +0 -18
- package/lib/Pill.d.ts +0 -13
- package/lib/ProgressBar.d.ts +0 -19
- package/lib/RadioGroup.d.ts +0 -15
- package/lib/Search.d.ts +0 -26
- package/lib/SearchContent.d.ts +0 -6
- package/lib/SectionHeader.d.ts +0 -18
- package/lib/Select.d.ts +0 -19
- package/lib/Shield.d.ts +0 -12
- package/lib/SideBarNav.d.ts +0 -21
- package/lib/Skeleton.d.ts +0 -15
- package/lib/SkipLink.d.ts +0 -22
- package/lib/Slider.d.ts +0 -14
- package/lib/Status.d.ts +0 -10
- package/lib/Tabs.d.ts +0 -23
- package/lib/Toggle.d.ts +0 -11
- package/lib/Tooltip.d.ts +0 -14
|
@@ -0,0 +1,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
|
+
}
|