@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,178 @@
1
+ // SideBarNav.test.tsx
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+ import { SideBarNav, SideBarNavProps } from "./SideBarNav";
4
+ import { MemoryRouter } from "react-router-dom";
5
+ import { axe } from "vitest-axe";
6
+
7
+ describe("SideBarNav Component", () => {
8
+ const defaultProps: SideBarNavProps = {
9
+ appName: "Test App",
10
+ version: "v1.0",
11
+ image: "test-image.png",
12
+ menuItems: [
13
+ { target: "/home", children: "Home" },
14
+ { target: "/about", children: "About" },
15
+ ],
16
+ };
17
+
18
+ it("renders the component with image, app name, version, and menu items", () => {
19
+ render(
20
+ <MemoryRouter>
21
+ <SideBarNav {...defaultProps} />
22
+ </MemoryRouter>
23
+ );
24
+
25
+ // Check the image element
26
+ const image = screen.getByAltText("application icon");
27
+ expect(image).toBeInTheDocument();
28
+ expect(image).toHaveAttribute("src", "test-image.png");
29
+
30
+ // Check the app name
31
+ expect(screen.getByText("Test App")).toBeInTheDocument();
32
+
33
+ // Check the menu items rendered as NavLinks
34
+ const homeLink = screen.getByRole("link", { name: "Home" });
35
+ expect(homeLink).toBeInTheDocument();
36
+ expect(homeLink).toHaveAttribute("href", "/home");
37
+
38
+ const aboutLink = screen.getByRole("link", { name: "About" });
39
+ expect(aboutLink).toBeInTheDocument();
40
+ expect(aboutLink).toHaveAttribute("href", "/about");
41
+
42
+ // Check the version text
43
+ expect(screen.getByText("Version v1.0")).toBeInTheDocument();
44
+ });
45
+
46
+ it("merges custom class names with default classes", () => {
47
+ const props: SideBarNavProps = {
48
+ appName: "Test App",
49
+ version: "v1.0",
50
+ menuItems: [],
51
+ className: "custom-sidebar-class",
52
+ image: "test-image.png",
53
+ };
54
+
55
+ const { container } = render(
56
+ <MemoryRouter>
57
+ <SideBarNav {...props} />
58
+ </MemoryRouter>
59
+ );
60
+
61
+ // The outer div should contain both the default and custom classes.
62
+ const containerDiv = container.firstElementChild;
63
+ expect(containerDiv).toHaveClass("custom-sidebar-class");
64
+ });
65
+
66
+ it("renders an empty menu list when no menu items are provided", () => {
67
+ const props: SideBarNavProps = {
68
+ appName: "Test App",
69
+ version: "v1.0",
70
+ menuItems: [],
71
+ image: "test-image.png",
72
+ };
73
+
74
+ render(
75
+ <MemoryRouter>
76
+ <SideBarNav {...props} />
77
+ </MemoryRouter>
78
+ );
79
+
80
+ // Since menuItems is empty, no NavLink should be rendered.
81
+ const links = screen.queryAllByRole("link");
82
+ expect(links.length).toBe(0);
83
+ });
84
+
85
+ it("does not render a NavLink if a menu item lacks a target", () => {
86
+ const props: SideBarNavProps = {
87
+ appName: "Test App",
88
+ version: "v1.0",
89
+ menuItems: [
90
+ // Intentionally leaving out the 'target' property
91
+ { children: "No Link" } as any,
92
+ ],
93
+ image: "test-image.png",
94
+ };
95
+
96
+ render(
97
+ <MemoryRouter>
98
+ <SideBarNav {...props} />
99
+ </MemoryRouter>
100
+ );
101
+
102
+ // There should be no link since the condition checks for item.target.
103
+ const link = screen.queryByRole("link");
104
+ expect(link).toBeNull();
105
+ // The text "No Link" should not be rendered since it’s inside the conditional.
106
+ expect(screen.queryByText("No Link")).not.toBeInTheDocument();
107
+ });
108
+
109
+ it("closes the menu when clicking outside if clickOutsideCloses is true", () => {
110
+ const props: SideBarNavProps = {
111
+ appName: "Test App",
112
+ version: "v1.0",
113
+ menu: true,
114
+ clickOutsideCloses: true,
115
+ menuItems: [{ target: "/home", children: "Home" }],
116
+ image: "test-image.png",
117
+ };
118
+
119
+ const { container } = render(
120
+ <MemoryRouter>
121
+ <SideBarNav {...props} />
122
+ </MemoryRouter>
123
+ );
124
+
125
+ // Open the menu by clicking the hamburger button
126
+ const button = screen.getByTestId("hamburgerMenu");
127
+ fireEvent.click(button);
128
+
129
+ // Simulate clicking outside the sidebar (on document body)
130
+ fireEvent.mouseDown(document.body);
131
+ });
132
+
133
+ it("toggles the sidebar when the hamburger button is clicked, displays sidebar on the right", () => {
134
+ const props: SideBarNavProps = {
135
+ appName: "Test App",
136
+ version: "v1.0",
137
+ menu: true,
138
+ menuItems: [{ target: "/home", children: "Home" }],
139
+ image: "test-image.png",
140
+ };
141
+
142
+ // rendering this with the 'right' flag satisfies full coverage (no specific lines specified as uncovered)
143
+ // however, % Funcs still scores a 75 -- something to look into
144
+ const { container } = render(
145
+ <MemoryRouter>
146
+ <SideBarNav right {...props} />
147
+ </MemoryRouter>
148
+ );
149
+
150
+ const button = screen.getByTestId("hamburgerMenu");
151
+
152
+ // Click the button to toggle the menu open
153
+ fireEvent.click(button);
154
+
155
+ // Click again to toggle it closed
156
+ fireEvent.click(button);
157
+ });
158
+ });
159
+
160
+ describe("SideBarNav Accessibility Tests", () => {
161
+ it("has no accessibility violations", async () => {
162
+ const props: SideBarNavProps = {
163
+ appName: "Accessible App",
164
+ version: "v2.0",
165
+ image: "accessible-image.png",
166
+ menuItems: [{ target: "/dashboard", children: "Dashboard" }],
167
+ };
168
+
169
+ const { container } = render(
170
+ <MemoryRouter>
171
+ <SideBarNav {...props} />
172
+ </MemoryRouter>
173
+ );
174
+
175
+ const results = await axe(container);
176
+ expect(results).toHaveNoViolations();
177
+ });
178
+ });
@@ -0,0 +1,135 @@
1
+ import * as React from 'react';
2
+ import { forwardRef, useState, useEffect, useRef } from 'react';
3
+ import { NavLink } from 'react-router-dom';
4
+ import { List, ListItemProps, ListItem } from './List';
5
+ import { twMerge } from 'tailwind-merge';
6
+ import backArrowRight from '../assets/img/backArrowRight.svg';
7
+
8
+ export interface SideBarNavProps extends React.HTMLAttributes<HTMLDivElement> {
9
+ className?: string;
10
+ image: string;
11
+ classNameImage?: string;
12
+ classNameImageContainer?: string;
13
+ clickOutsideCloses?: boolean
14
+ menu?: boolean; // true - use integrated hamburger menu, false - react to customer event
15
+ classNameMenu?: string;
16
+ classNameMenuContainer?: string;
17
+ classNameMenuItem?: string;
18
+ version: string;
19
+ appName: string;
20
+ menuItems: ListItemProps[];
21
+ right?: boolean;
22
+ }
23
+
24
+ const defaultItemClass = 'pb-4';
25
+
26
+ export const SideBarNav = forwardRef<HTMLDivElement, SideBarNavProps>(
27
+ ({ className, appName, image, classNameImage = '', clickOutsideCloses,
28
+ classNameImageContainer = '', menu = false, classNameMenu = '',
29
+ classNameMenuItem = '',
30
+ classNameMenuContainer = '', version, menuItems, right=false,
31
+ children, ...props }, ref) => {
32
+ const sidebarRef = useRef<HTMLDivElement>(null);
33
+
34
+ const [showMenu, setShowMenu] = useState<boolean>(false);
35
+ const [itemClass, setItemClass] = useState<string>(defaultItemClass);
36
+
37
+ // build css class list for individual menu items (list items)
38
+ useEffect(() => {
39
+ if (right)
40
+ setItemClass(twMerge(defaultItemClass + ' text-right pe-2', classNameMenuItem));
41
+ else
42
+ setItemClass(twMerge(defaultItemClass, classNameMenuItem));
43
+ }, [right, itemClass, classNameMenuItem]);
44
+
45
+ useEffect(() => {
46
+ if (clickOutsideCloses) {
47
+ const handleClickOutside = (event: MouseEvent) => {
48
+ if (sidebarRef.current && !sidebarRef.current.contains(event.target as Node)) {
49
+ setShowMenu(false);
50
+ }
51
+ }
52
+
53
+ // attach event listener
54
+ document.addEventListener('mousedown', handleClickOutside);
55
+
56
+ return () => {
57
+ // cleanup listener on unmount
58
+ document.removeEventListener('mousedown', handleClickOutside);
59
+ }
60
+ }
61
+
62
+ }, [setShowMenu]);
63
+
64
+ // console.log('menu: ', menu);
65
+ // console.log('showMenu: ', showMenu);
66
+
67
+ return (
68
+ // eslint-disable-next-line react/jsx-no-useless-fragment
69
+ <>
70
+ {/* Hamburger Menu - display? */}
71
+ { menu &&
72
+ <button aria-label='navigation' data-testid="hamburgerMenu" className={twMerge('size-8 mb-4', classNameMenuContainer)} onClick={() => setShowMenu(!showMenu)}>
73
+ <Menu classes={twMerge('fill-slate-500 hover:fill-black size-8', classNameMenu)} />
74
+ </button>
75
+ }
76
+
77
+ {/* SideBarNav - display? */}
78
+ <div ref={sidebarRef}
79
+ className={twMerge("relative w-56 md:w-[328px] lg:w-[432px] h-screen flex flex-col px-4 pb-4 pt-10 bg-white overflow-y-auto",
80
+ // menu is used but hidden off screen
81
+ menu && 'fixed z-20 right-0 top-0 translate-x-full transition-transform duration-300',
82
+ // menu used and displayed
83
+ menu && showMenu && 'translate-x-0',
84
+ className
85
+ )}>
86
+ <button className={twMerge('-mt-8 w-full flex justify-end')} onClick={() => setShowMenu(!showMenu)}>
87
+ <img className={twMerge('fill-slate-500 hover:fill-black')} src={backArrowRight} alt='back arrow' />
88
+ </button>
89
+ <p className='w-full flex justify-end pr-0.5 -mb-8'>Back</p>
90
+
91
+ {/* Image */}
92
+
93
+ {/* Logo */}
94
+ <div className={twMerge("w-16 h-16 mx-auto", classNameImageContainer)}>
95
+ <img src={image} className={twMerge('border border-[#bbbabc] w-16 h-16 bg-[#eeeeef] ring-1', classNameImage)} alt="application icon" />
96
+ </div>
97
+
98
+ <div className="text-center text-xl mt-2">{appName}</div>
99
+
100
+ {/* Separator */}
101
+ <div className="[204px] mt-3 h-[2px] border-[0.5px] border-[#D9D9D9] bg-[#D9D9D9]"></div>
102
+
103
+ {/* Menu List */}
104
+ {/* <div className="mt-6 border-2 border-red-500"> */}
105
+ <List className="pl-4 lg:pl-6 mt-6">
106
+ {menuItems.map((item, index) => (
107
+ <ListItem
108
+ key={index}
109
+ className={itemClass}
110
+ >
111
+ {item.target && (
112
+ <NavLink to={item.target}>{item.children}</NavLink>
113
+ )}
114
+ </ListItem>
115
+ ))}
116
+ </List>
117
+
118
+ {/* Version # - parent div must be 'relative' */}
119
+ <div className='align-bottom mt-auto text-right right-2 bottom-1'>Version {version}</div>
120
+
121
+ </div>
122
+
123
+ </>
124
+ );
125
+ }
126
+ );
127
+
128
+
129
+ export const Menu = ({ classes='fill-green-600'}, ) => (
130
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" className={classes} >
131
+ <path d="M0 0h24v24H0z" fill="none"/>
132
+ <path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
133
+ </svg>
134
+ );
135
+
@@ -0,0 +1,77 @@
1
+ import { Meta } from '@storybook/react';
2
+ import { Skeleton, SkeletonProps, SkelProps, SkelCircle, SkelLine, SkelRow, SkelSection } from './Skeleton';
3
+
4
+ // Meta object - defines basic storybook options for this story
5
+ export default {
6
+ title: 'Components/Skeleton',
7
+ component: Skeleton,
8
+ argTypes: {
9
+ variant: {
10
+ control: 'select',
11
+ options: ['default','larger']
12
+ },
13
+ },
14
+ args: {
15
+ disabled: false, // set default argument values
16
+ children:
17
+ <Skeleton>
18
+ <SkelCircle inline /><SkelLine inline />
19
+ <SkelLine />
20
+ <SkelSection />
21
+ <SkelCircle />
22
+ <SkelRow />
23
+ <SkelLine />
24
+ </Skeleton>
25
+ // label: 'Button', // set default argument values
26
+ },
27
+ parameters: {
28
+ layout: 'centered', // options are 'centered', 'fullscreen', and 'padded' (default value)
29
+ backgrounds: { default: 'dark' }, // options are light, medium, or dark
30
+ },
31
+ } as Meta<SkeletonProps>;
32
+
33
+ // Define "Default" story
34
+ export const Default = {
35
+ args: {
36
+
37
+ }
38
+ };
39
+
40
+ export const AlternateBaseColor = {
41
+ args: {
42
+ children:
43
+ <Skeleton>
44
+ <SkelCircle className='bg-blue-500/50' inline /><SkelLine className='bg-blue-500/50' inline />
45
+ <SkelLine className='bg-blue-500/50' />
46
+ <SkelSection className='bg-blue-500/50' />
47
+ </Skeleton>
48
+ }
49
+ };
50
+
51
+ /*
52
+ Skeleton, SkeletonProps, SkelProps, SkelCircle, SkelLine, SkelRow, SkelSection
53
+ */
54
+
55
+ // export const ImageRight = {
56
+ // args: {
57
+ // variant: 'imageRight',
58
+ // }
59
+ // };
60
+
61
+ // Define "DHA Med Card" story that extends "Default"
62
+ // export const DHAMedCard = {
63
+ // args: {
64
+ // children: 'DHA Med Card',
65
+ // variant: 'MedCard',
66
+ // }
67
+ // };
68
+
69
+ // Define "DHA Disabled" story
70
+ // export const DHADisabled = {
71
+ // args: {
72
+ // // children: 'Disabled Button',
73
+ // disabled: true,
74
+ // label: 'Disabled Button',
75
+ // }
76
+ // };
77
+
@@ -0,0 +1,16 @@
1
+ .skelContainer {
2
+ border: 3px solid green;
3
+ background-color: blue;
4
+ width: 100%;
5
+ color: red;
6
+ /* background: linear-gradient(-45deg, #eee 40%, #fafafa 50%, #eee 60%);
7
+ background-size: 300%;
8
+ background-position-x: 100%;
9
+ animation: shimmer 1s infinite linear; */
10
+ }
11
+
12
+ @keyframes shimmer {
13
+ to {
14
+ background-position: 0%;
15
+ }
16
+ }
@@ -0,0 +1,83 @@
1
+ import { render, screen, fireEvent } from "@testing-library/react";
2
+ import { describe, it, expect } from "vitest";
3
+ import { axe } from "vitest-axe";
4
+ import { Skeleton, SkelCircle, SkelLine, SkelSection, SkelRow } from "./Skeleton";
5
+
6
+ describe("Skeleton Component", () => {
7
+ it("renders the Skeleton component with default props", () => {
8
+ render(<Skeleton />);
9
+ const skeleton = screen.getByLabelText("Skeleton Component");
10
+ expect(skeleton).toBeInTheDocument();
11
+ expect(skeleton).toHaveClass("flex flex-row flex-wrap animate-pulse h-48 w-56 pt-2");
12
+ });
13
+
14
+ it("renders the Skeleton component with custom className", () => {
15
+ render(<Skeleton className="custom-class" />);
16
+ const skeleton = screen.getByLabelText("Skeleton Component");
17
+ expect(skeleton).toHaveClass("custom-class");
18
+ });
19
+
20
+ it("renders the Skeleton component with a custom variant", () => {
21
+ const classNames = "h-32 w-32";
22
+ render(<Skeleton className={classNames} />);
23
+ const skeleton = screen.getByLabelText("Skeleton Component");
24
+ expect(skeleton).toHaveClass("h-32 w-32");
25
+ });
26
+
27
+ it("passes accessibility checks", async () => {
28
+ const { container } = render(<Skeleton />);
29
+ const results = await axe(container);
30
+ expect(results).toHaveNoViolations();
31
+ });
32
+ });
33
+
34
+ describe("Skeleton Subcomponents", () => {
35
+ it("renders SkelCircle in inline mode", () => {
36
+ render(<SkelCircle inline />);
37
+ const skelCircle = screen.getByRole("presentation", { hidden: true });
38
+ expect(skelCircle).toHaveClass("rounded-full bg-slate-300 h-6 w-6 inline-block mx-2 me-1 my-1");
39
+ });
40
+
41
+ it("renders SkelCircle in non-inline mode", () => {
42
+ render(<SkelCircle />);
43
+ const skelCircle = screen.getByRole("presentation", { hidden: true });
44
+ expect(skelCircle).toHaveClass("rounded-full bg-slate-300 h-6 w-6 mx-2 my-1 me-0");
45
+ });
46
+
47
+ it("renders SkelLine in inline mode", () => {
48
+ render(<SkelLine inline />);
49
+ const skelLine = screen.getByRole("presentation", { hidden: true });
50
+ expect(skelLine).toHaveClass("rounded-lg bg-slate-300 h-6 inline-block mx-2 my-1 grow");
51
+ });
52
+
53
+ it("renders SkelLine in non-inline mode", () => {
54
+ render(<SkelLine />);
55
+ const skelLine = screen.getByRole("presentation", { hidden: true });
56
+ expect(skelLine).toHaveClass("rounded-lg bg-slate-300 h-6 mx-2 my-1 grow");
57
+ });
58
+
59
+ it("renders SkelSection with default props", () => {
60
+ render(<SkelSection />);
61
+ const skelSection = screen.getByRole("presentation", { hidden: true });
62
+ expect(skelSection).toHaveClass("rounded-lg bg-slate-300 grow h-24 my-1 mx-2");
63
+ });
64
+
65
+ it("renders SkelRow with default props", () => {
66
+ render(<SkelRow />);
67
+ const skelRow = screen.getByRole("presentation", { hidden: true });
68
+ expect(skelRow).toHaveClass("basis-full h-0");
69
+ });
70
+
71
+ it("passes accessibility checks for all subcomponents", async () => {
72
+ const { container } = render(
73
+ <div>
74
+ <SkelCircle />
75
+ <SkelLine />
76
+ <SkelSection />
77
+ <SkelRow />
78
+ </div>
79
+ );
80
+ const results = await axe(container);
81
+ expect(results).toHaveNoViolations();
82
+ });
83
+ });
@@ -0,0 +1,103 @@
1
+ import * as React from 'react';
2
+ import { FC } from 'react';
3
+ import { twMerge } from 'tailwind-merge';
4
+
5
+ interface VariantType {
6
+ [key: string]: string;
7
+ }
8
+
9
+ // variant styling definition, both size and layout (portrait vs landscape)
10
+ const variants: VariantType = {
11
+ default: ' h-48 w-56 pt-2',
12
+ custom: ' h-48 w-56 pt-2',
13
+ }
14
+
15
+ export interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
16
+ variant?: string; //
17
+ className?: string; // container classes
18
+ }
19
+
20
+ export const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
21
+ ({ className, variant = 'default', children, ...props }, ref) => {
22
+
23
+ return (
24
+ <div className={
25
+ twMerge('flex flex-row flex-wrap animate-pulse',
26
+ variants[variant], className)}
27
+ aria-label='Skeleton Component'
28
+ role='status'
29
+ ref={ref}
30
+ {...props}
31
+ >
32
+ {children}
33
+ </div>
34
+ );
35
+ }
36
+ );
37
+ Skeleton.displayName = 'Card';
38
+
39
+ export interface SkelProps {
40
+ className?: string;
41
+ inline?: boolean; // inline (i.e. span) or not (i.e. div?)
42
+ }
43
+
44
+ // SkelCircle
45
+ export const SkelCircle: FC<SkelProps> = ({className = '', inline = false}) => {
46
+ return inline ? (
47
+ <div
48
+ role="presentation"
49
+ className={twMerge(
50
+ "rounded-full bg-slate-300 h-6 w-6 inline-block mx-2 me-1 my-1", className
51
+ )}
52
+ />
53
+ ) : (
54
+ <>
55
+ <div className="basis-full h-0"></div>
56
+ <div
57
+ role="presentation"
58
+ className={twMerge(
59
+ "rounded-full bg-slate-300 h-6 w-6 mx-2 my-1 me-0", className
60
+ )}
61
+ />
62
+ <div className="basis-full h-0"></div>
63
+ </>
64
+ );
65
+ };
66
+
67
+ // SkeletonLine
68
+ export const SkelLine: FC<SkelProps> = ({className='', inline = false}) => {
69
+ return inline ? (
70
+ <div
71
+ role="presentation"
72
+ className={twMerge(
73
+ "rounded-lg bg-slate-300 h-6 inline-block mx-2 my-1 grow", className
74
+ )}
75
+ ></div>
76
+ ) : (
77
+ <>
78
+ <div className="basis-full h-0"></div>
79
+ <div
80
+ role="presentation"
81
+ className={twMerge(
82
+ "rounded-lg bg-slate-300 h-6 mx-2 my-1 grow", className
83
+ )}
84
+ ></div>
85
+ <div className="basis-full h-0"></div>
86
+ </>
87
+ );
88
+ };
89
+
90
+ // SkelSection
91
+ export const SkelSection: FC<SkelProps> = ({className=''}) => {
92
+ return (
93
+ <div role="presentation" className={twMerge("rounded-lg bg-slate-300 grow h-24 my-1 mx-2", className)} />
94
+ );
95
+ };
96
+
97
+
98
+ // SkelRow
99
+ export const SkelRow: FC<SkelProps> = ({className = ''}) => {
100
+ return (
101
+ <div role="presentation" className="basis-full h-0"></div>
102
+ );
103
+ };
@@ -0,0 +1,76 @@
1
+ import React from "react";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import { describe, it, expect, vi } from "vitest";
4
+ import { axe } from "vitest-axe";
5
+ import { SkipLink } from "./SkipLink";
6
+
7
+ describe("SkipLink Component", () => {
8
+ it("renders the child element and applies the default className", () => {
9
+ const child = <a href="#main">Skip Link</a>;
10
+ render(<SkipLink>{child}</SkipLink>);
11
+ const linkElement = screen.getByRole("link", { name: /skip link/i });
12
+ expect(linkElement).toBeInTheDocument();
13
+ // Default className should be applied to the child element
14
+ expect(linkElement).toHaveClass("skip-link");
15
+ });
16
+
17
+ it("handles click events by focusing the target container and cleaning up tabindex", () => {
18
+ // Create a container element that matches the skipTo query
19
+ const container = document.createElement("main");
20
+ container.id = "main";
21
+ document.body.appendChild(container);
22
+ // Spy on the container's focus method
23
+ const focusSpy = vi.spyOn(container, "focus");
24
+
25
+ // Use fake timers to test the timeout behavior
26
+ vi.useFakeTimers();
27
+ const child = <a href="#main">Skip to main</a>;
28
+ // Pass a skipTo prop that matches our container
29
+ render(<SkipLink skipTo="#main">{child}</SkipLink>);
30
+ const linkElement = screen.getByRole("link", { name: /skip to main/i });
31
+
32
+ // Simulate click on the skip link
33
+ fireEvent.click(linkElement);
34
+
35
+ // After clicking, the container should have tabindex set to -1 and focus called
36
+ expect(container.tabIndex).toBe(-1);
37
+ expect(focusSpy).toHaveBeenCalled();
38
+
39
+ // Fast-forward time to trigger removal of tabindex (after 1000ms)
40
+ vi.advanceTimersByTime(1000);
41
+ expect(container.getAttribute("tabindex")).toBeNull();
42
+
43
+ // Clean up
44
+ vi.useRealTimers();
45
+ document.body.removeChild(container);
46
+ });
47
+
48
+ // What is the purpose of the timer in SkipLink?
49
+ // it("cleans up the timer on unmount", () => {
50
+ // // Spy on clearTimeout to ensure it is called during cleanup
51
+ // const clearTimeoutSpy = vi.spyOn(global, "clearTimeout");
52
+ // vi.useFakeTimers();
53
+
54
+ // const child = <a href="#main">Skip Link</a>;
55
+ // const { unmount } = render(<SkipLink skipTo="#main">{child}</SkipLink>);
56
+ // const linkElement = screen.getByRole("link", { name: /skip link/i });
57
+
58
+ // // Fire the click to create a timer
59
+ // fireEvent.click(linkElement);
60
+
61
+ // // Unmount the component which should trigger the cleanup useEffect
62
+ // unmount();
63
+
64
+ // expect(clearTimeoutSpy).toHaveBeenCalled();
65
+
66
+ // vi.useRealTimers();
67
+ // clearTimeoutSpy.mockRestore();
68
+ // });
69
+
70
+ it("has no accessibility violations", async () => {
71
+ const child = <a href="#main">Skip Link</a>;
72
+ const { container } = render(<SkipLink skipTo="#main">{child}</SkipLink>);
73
+ const results = await axe(container);
74
+ expect(results).toHaveNoViolations();
75
+ });
76
+ });