@agilant/toga-blox 1.0.5

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 (138) hide show
  1. package/Dockerfile +9 -0
  2. package/README.md +69 -0
  3. package/assets/Logo.png +0 -0
  4. package/assets/compass-card-image-2.png +0 -0
  5. package/assets/compass-card-image-3.png +0 -0
  6. package/assets/compass-card-image-4.png +0 -0
  7. package/assets/compass-card-image.png +0 -0
  8. package/assets/compass-logo.png +0 -0
  9. package/assets/compass-tech-hero-bg.png +0 -0
  10. package/assets/contact-image.png +0 -0
  11. package/assets/green-laptop.png +0 -0
  12. package/assets/heroImage.png +0 -0
  13. package/assets/placeholder-no-image-available.png +0 -0
  14. package/assets/team.png +0 -0
  15. package/declarations.d.ts +4 -0
  16. package/docker-compose.yml +22 -0
  17. package/global.css +4 -0
  18. package/index.js +4 -0
  19. package/nodemon.json +5 -0
  20. package/package.json +70 -0
  21. package/postcss.config.js +6 -0
  22. package/src/components/Badge/Badge.stories.tsx +284 -0
  23. package/src/components/Badge/Badge.test.tsx +185 -0
  24. package/src/components/Badge/Badge.tsx +137 -0
  25. package/src/components/Badge/Badge.types.tsx +28 -0
  26. package/src/components/Badge/badgeClassNames.tsx +152 -0
  27. package/src/components/Badge/index.ts +2 -0
  28. package/src/components/Card/Card.stories.tsx +91 -0
  29. package/src/components/Card/Card.test.tsx +53 -0
  30. package/src/components/Card/Card.tsx +30 -0
  31. package/src/components/Card/Card.types.ts +11 -0
  32. package/src/components/Card/DUMMYPRODUCTDATA.json +670 -0
  33. package/src/components/Card/cardClassNames.ts +49 -0
  34. package/src/components/Card/index.ts +3 -0
  35. package/src/components/Card/templates/CompassCardTemplate.tsx +58 -0
  36. package/src/components/Card/templates/HorizontalCardTemplate.tsx +184 -0
  37. package/src/components/Card/templates/VerticalCardTemplate.tsx +154 -0
  38. package/src/components/Footer/ContactInfoItem.tsx +20 -0
  39. package/src/components/Footer/DUMMYFOOTERDATA.json +132 -0
  40. package/src/components/Footer/Footer.stories.tsx +292 -0
  41. package/src/components/Footer/Footer.test.tsx +90 -0
  42. package/src/components/Footer/Footer.tsx +159 -0
  43. package/src/components/Footer/Footer.types.tsx +61 -0
  44. package/src/components/Footer/footerClassNames.tsx +57 -0
  45. package/src/components/FormButton/FormButton.stories.tsx +199 -0
  46. package/src/components/FormButton/FormButton.test.tsx +73 -0
  47. package/src/components/FormButton/FormButton.tsx +116 -0
  48. package/src/components/FormButton/FormButton.types.ts +32 -0
  49. package/src/components/FormButton/formButtonClassNames.tsx +153 -0
  50. package/src/components/FormButton/index.ts +2 -0
  51. package/src/components/GenericList/DUMMYLISTDATA.json +560 -0
  52. package/src/components/GenericList/GenericList.stories.tsx +104 -0
  53. package/src/components/GenericList/GenericList.test.tsx +29 -0
  54. package/src/components/GenericList/GenericList.tsx +146 -0
  55. package/src/components/GenericList/genericListClassNames.tsx +8 -0
  56. package/src/components/GenericList/templates/DummyDataList.tsx +23 -0
  57. package/src/components/GenericList/templates/DynamicIconList.tsx +74 -0
  58. package/src/components/HamburgerButton/HamburgerButton.tsx +68 -0
  59. package/src/components/HamburgerButton/HamburgerButton.types.tsx +6 -0
  60. package/src/components/HamburgerButton/index.ts +2 -0
  61. package/src/components/Header/DUMMYICONDATA.json +136 -0
  62. package/src/components/Header/Header.stories.tsx +521 -0
  63. package/src/components/Header/Header.test.tsx +323 -0
  64. package/src/components/Header/Header.tsx +289 -0
  65. package/src/components/Header/Header.types.ts +52 -0
  66. package/src/components/Header/headerClassNames.tsx +50 -0
  67. package/src/components/Header/headerContext.tsx +125 -0
  68. package/src/components/Header/index.ts +2 -0
  69. package/src/components/Hero/Hero.stories.tsx +69 -0
  70. package/src/components/Hero/Hero.test.tsx +109 -0
  71. package/src/components/Hero/Hero.tsx +58 -0
  72. package/src/components/Hero/Hero.types.ts +9 -0
  73. package/src/components/Hero/index.ts +2 -0
  74. package/src/components/Icon/Icon.stories.tsx +227 -0
  75. package/src/components/Icon/Icon.test.tsx +53 -0
  76. package/src/components/Icon/Icon.tsx +208 -0
  77. package/src/components/Icon/Icon.types.ts +24 -0
  78. package/src/components/Icon/iconClassNames.ts +79 -0
  79. package/src/components/Icon/index.ts +2 -0
  80. package/src/components/Image/Image.stories.tsx +79 -0
  81. package/src/components/Image/Image.test.tsx +87 -0
  82. package/src/components/Image/Image.tsx +49 -0
  83. package/src/components/Image/Image.types.ts +11 -0
  84. package/src/components/Image/index.ts +2 -0
  85. package/src/components/Input/Input.stories.tsx +651 -0
  86. package/src/components/Input/Input.test.tsx +90 -0
  87. package/src/components/Input/Input.tsx +226 -0
  88. package/src/components/Input/Input.types.ts +52 -0
  89. package/src/components/Input/InputMemoTypes.tsx +32 -0
  90. package/src/components/Input/index.ts +2 -0
  91. package/src/components/Input/inputClassNames.tsx +169 -0
  92. package/src/components/MobileMenu/MobileMenu.tsx +41 -0
  93. package/src/components/MobileMenu/MobileMenu.types.tsx +30 -0
  94. package/src/components/MobileMenu/index.ts +2 -0
  95. package/src/components/Nav/DUMMYNAVDATA.json +234 -0
  96. package/src/components/Nav/Nav.stories.tsx +181 -0
  97. package/src/components/Nav/Nav.test.tsx +89 -0
  98. package/src/components/Nav/Nav.tsx +242 -0
  99. package/src/components/Nav/Nav.types.tsx +35 -0
  100. package/src/components/Nav/index.ts +2 -0
  101. package/src/components/Nav/navClassNames.tsx +192 -0
  102. package/src/components/Page/TableDataDummy.tsx +216 -0
  103. package/src/components/Page/ViewPageTemplate.stories.tsx +546 -0
  104. package/src/components/Page/ViewPageTemplate.test.tsx +361 -0
  105. package/src/components/Page/ViewPageTemplate.tsx +10 -0
  106. package/src/components/Page/ViewPageTemplate.types.ts +6 -0
  107. package/src/components/Page/index.ts +2 -0
  108. package/src/components/PageSection/PageSection.stories.tsx +114 -0
  109. package/src/components/PageSection/PageSection.tsx +12 -0
  110. package/src/components/PageSection/PageSection.types.ts +6 -0
  111. package/src/components/PageSection/PageSections.test.tsx +88 -0
  112. package/src/components/PageSection/index.ts +2 -0
  113. package/src/components/Text/Text.stories.tsx +60 -0
  114. package/src/components/Text/Text.test.tsx +52 -0
  115. package/src/components/Text/Text.tsx +80 -0
  116. package/src/components/Text/Text.types.ts +12 -0
  117. package/src/components/Text/index.ts +2 -0
  118. package/src/components/Toaster/Toaster.stories.tsx +122 -0
  119. package/src/components/Toaster/Toaster.test.tsx +61 -0
  120. package/src/components/Toaster/Toaster.tsx +80 -0
  121. package/src/components/Toaster/Toaster.types.ts +12 -0
  122. package/src/components/Toaster/index.ts +2 -0
  123. package/src/hoc/index.ts +2 -0
  124. package/src/hoc/styling/withStoryBook.tsx +19 -0
  125. package/src/main.css +3 -0
  126. package/src/setupTests.ts +1 -0
  127. package/src/userHoc/index.ts +1 -0
  128. package/src/userHoc/withMemo.tsx +20 -0
  129. package/src/utils/assertTagName.tsx +7 -0
  130. package/src/utils/generateAccordionItem.tsx +102 -0
  131. package/src/utils/generateFooterContacts.tsx +75 -0
  132. package/src/utils/generateNavMenu.tsx +54 -0
  133. package/src/utils/generateSocialList.tsx +34 -0
  134. package/src/utils/getFontAwesomeIcon.tsx +25 -0
  135. package/src/utils/inputValidation.tsx +26 -0
  136. package/tailwind.config.js +32 -0
  137. package/tsconfig.json +25 -0
  138. package/vite.config.ts +33 -0
@@ -0,0 +1,52 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, beforeEach, test } from "vitest";
3
+ import Text from "./Text";
4
+
5
+ describe("<Text />", () => {
6
+ beforeEach(() => {
7
+ render(
8
+ <Text
9
+ text="Testing"
10
+ size="md"
11
+ color="red"
12
+ tag="h1"
13
+ fontFamily="sans"
14
+ />
15
+ );
16
+ });
17
+
18
+ test("renders Text component", () => {
19
+ expect(screen.getByTestId("text")).toBeInTheDocument();
20
+ });
21
+
22
+ test("contain correct text", () => {
23
+ expect(screen.getByTestId("text")).toHaveTextContent("Testing");
24
+ });
25
+
26
+ // Style-based tests might need adjustments
27
+
28
+ // test("renders correct font color", () => {
29
+ // // Adjust the test based on how the color is applied
30
+ // // For example, if using Tailwind, check for the relevant class instead
31
+ // const textElement = screen.getByTestId("text");
32
+ // expect(textElement).toHaveClass("text-red-500");
33
+ // });
34
+
35
+ FIXME: test("renders correct font size", () => {
36
+ // Check for class or style attribute
37
+ const textElement = screen.getByTestId("text");
38
+ expect(textElement).toHaveClass("text-md"); // Or the specific class for medium size
39
+ });
40
+
41
+ test("renders correct font family", () => {
42
+ const textElement = screen.getByTestId("text");
43
+ // Check for the presence of the Tailwind CSS class for 'sans' font family
44
+ expect(textElement).toHaveClass("font-sans");
45
+ });
46
+
47
+ test("renders correct element type", () => {
48
+ // Assuming 'as' prop is set to 'h1' in your test setup
49
+ const textElement = screen.getByTestId("text");
50
+ expect(textElement.tagName).toBe("H1"); // Check that the element is an 'h1'
51
+ });
52
+ });
@@ -0,0 +1,80 @@
1
+ import React, { ReactNode } from "react";
2
+ import { TextTypes } from "./Text.types";
3
+
4
+ const sizeClassesMap = {
5
+ xs: "text-xs",
6
+ sm: "text-sm",
7
+ md: "text-md",
8
+ lg: "text-lg",
9
+ xl: "text-xl",
10
+ xxl: "text-2xl",
11
+ xxxl: "text-3xl",
12
+ };
13
+
14
+ const colorClassesMap: { [key: string]: string } = {
15
+ slate: "text-slate-950",
16
+ red: "text-red-800",
17
+ blue: "text-blue-800",
18
+ green: "text-teal-800",
19
+ black: "text-slate-950",
20
+ white: "text-slate-100",
21
+ };
22
+
23
+ const fontFamilyMap = {
24
+ sans: "font-sans",
25
+ serif: "font-serif",
26
+ mono: "font-mono",
27
+ };
28
+
29
+ const Text: React.FC<TextTypes> = ({
30
+ size,
31
+ color = "slate",
32
+ tag = "span",
33
+ text,
34
+ fontFamily = "sans",
35
+ additionalClasses,
36
+ ...props
37
+ }) => {
38
+ const textSizeClasses = sizeClassesMap[size as keyof typeof sizeClassesMap];
39
+ const textColorClasses = colorClassesMap[color];
40
+ const fontFamilyClasses =
41
+ fontFamilyMap[fontFamily as keyof typeof fontFamilyMap];
42
+
43
+ let textClasses = `
44
+ ${additionalClasses}
45
+ ${textSizeClasses}
46
+ ${textColorClasses}
47
+ ${fontFamilyClasses}
48
+
49
+ `;
50
+
51
+ // Fixing the TS error on TagName w/ adding type string triggered this error on ElementTag:
52
+ // The error message is indicating that a string value is not assignable to
53
+ // the type React.ElementType. This is because React.ElementType is expecting
54
+ // a React component, not a string. However, in React, a string can be used
55
+ // as a component if it represents a valid HTML tag.
56
+
57
+ // The issue here is that TypeScript is not correctly inferring the type of tag.
58
+ // You can solve this by explicitly telling TypeScript that tag can be a string
59
+ // representing a valid HTML tag or a React component. Here's how you can do it:
60
+ // This tells TypeScript that tag can be a string representing a valid HTML tag
61
+ // (like "span", "div", etc.) or a React component. This should solve your issue.
62
+ // If it doesn't, please provide more context or check the rest of
63
+ // your code for potential type mismatches.
64
+
65
+ // Solution:
66
+ const ElementTag: React.ElementType = tag as
67
+ | keyof JSX.IntrinsicElements
68
+ | React.ComponentType;
69
+
70
+ // Was previously:
71
+ // const ElementTag = tag ?? "span";
72
+
73
+ return (
74
+ <ElementTag data-testid="text" className={`${textClasses}`} {...props}>
75
+ {text}
76
+ </ElementTag>
77
+ );
78
+ };
79
+
80
+ export default Text;
@@ -0,0 +1,12 @@
1
+ import { ReactNode } from "react";
2
+
3
+ export type TagName = "span" | "p" | "h1" | "h2" | "h3" | "h4" | "h5" | string;
4
+
5
+ export interface TextTypes {
6
+ size: string;
7
+ color: "red" | "blue" | "green" | "black" | "slate" | string;
8
+ tag?: TagName;
9
+ text: ReactNode;
10
+ fontFamily: "sans" | "mono" | "serif" | string;
11
+ additionalClasses?: string | undefined;
12
+ }
@@ -0,0 +1,2 @@
1
+ export { default } from "./Text";
2
+ export * from "./Text.types";
@@ -0,0 +1,122 @@
1
+ import React from "react";
2
+
3
+ import Toaster, { ToasterTypes } from ".";
4
+ import { Meta, StoryFn } from "@storybook/react";
5
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6
+ import { faCheck, faExclamation, faX } from "@fortawesome/free-solid-svg-icons";
7
+
8
+ export default {
9
+ title: "Components/Toaster",
10
+ component: Toaster,
11
+ argTypes: {
12
+ color: {
13
+ control: "select",
14
+ description: "The main color displayed on the toaster",
15
+ options: ["green-800", "red-800", "blue-800", "orange-800"],
16
+ },
17
+
18
+ icon: {
19
+ table: {
20
+ disable: true,
21
+ },
22
+ },
23
+ title: {
24
+ table: {
25
+ disable: true,
26
+ },
27
+ },
28
+ message: {
29
+ control: "text",
30
+ description: "The main text displayed on the toaster",
31
+ },
32
+ additionalClasses: {
33
+ table: {
34
+ disable: true,
35
+ },
36
+ },
37
+ clearText: {
38
+ control: "text",
39
+ description:
40
+ "The text displayed on the clear message button if set to true",
41
+ },
42
+ hasClearText: {
43
+ control: "boolean",
44
+ description: "must be set to true to display the clear text button",
45
+ },
46
+ fullColor: {
47
+ control: "boolean",
48
+ description: "toggle the background color of toaster",
49
+ },
50
+ hasBumper: {
51
+ control: "boolean",
52
+ description: "toggle the left bumper toaster",
53
+ },
54
+ clearMessage: {
55
+ table: {
56
+ disable: true,
57
+ },
58
+ },
59
+ },
60
+
61
+ tags: ["autodocs"],
62
+ parameters: {
63
+ // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
64
+ layout: "centered",
65
+ },
66
+ } as Meta;
67
+
68
+ const minWidth = "min-w-[200px]";
69
+
70
+ const Template: StoryFn<ToasterTypes> = (args) => (
71
+ <div className="">
72
+ <Toaster {...args} />
73
+ </div>
74
+ );
75
+
76
+ export const Default = Template.bind({});
77
+ Default.args = {
78
+ message: "Your action was successful!",
79
+ additionalClasses: `border-blue-500 ${minWidth}`,
80
+ };
81
+
82
+ export const WithIconFail = Template.bind({});
83
+ WithIconFail.args = {
84
+ ...Default.args,
85
+ message: "Your action failed!",
86
+ color: "red-800",
87
+ icon: <FontAwesomeIcon icon={faX} />,
88
+ additionalClasses: `border-red-500 ${minWidth}`,
89
+ };
90
+
91
+ export const WithIconSuccess = Template.bind({});
92
+ WithIconSuccess.args = {
93
+ ...Default.args,
94
+ color: "green-800",
95
+ icon: <FontAwesomeIcon icon={faCheck} />,
96
+ additionalClasses: `border-green-500 ${minWidth}`,
97
+ };
98
+
99
+ export const WithIconAlert = Template.bind({});
100
+ WithIconAlert.args = {
101
+ ...Default.args,
102
+ message: "Alert, are you sure!",
103
+ color: "orange-800",
104
+ icon: <FontAwesomeIcon icon={faExclamation} />,
105
+ additionalClasses: `border-orange-500 ${minWidth}`,
106
+ };
107
+
108
+ export const HasClearMessage = Template.bind({});
109
+ HasClearMessage.args = {
110
+ ...Default.args,
111
+ clearText: "Clear Message",
112
+ color: "blue-800",
113
+ hasClearText: true,
114
+ clearMessage: () => alert("Message Cleared!"),
115
+ };
116
+
117
+ export const FullColor = Template.bind({});
118
+ FullColor.args = {
119
+ ...Default.args,
120
+ fullColor: true,
121
+ color: "blue-800",
122
+ };
@@ -0,0 +1,61 @@
1
+ import { render, screen, fireEvent } from "@testing-library/react";
2
+ import { describe, expect, beforeEach, test, vi } from "vitest";
3
+ import Toaster from "./Toaster";
4
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5
+ import { faCoffee, faTimes } from "@fortawesome/free-solid-svg-icons";
6
+
7
+ describe("Toaster Component Tests", () => {
8
+ test("renders correctly with minimal props", () => {
9
+ render(<Toaster message="Test Message" color="green-500" />);
10
+ expect(screen.getByTestId("toaster")).toHaveTextContent("Test Message");
11
+ expect(screen.getByTestId("toaster")).toHaveClass("border-green-500");
12
+ });
13
+
14
+ test("renders with an icon when provided", () => {
15
+ render(
16
+ <Toaster
17
+ message="Test Message"
18
+ color="blue-500"
19
+ icon={<FontAwesomeIcon icon={faCoffee} />}
20
+ />
21
+ );
22
+ expect(screen.getByTestId("toaster")).toBeInTheDocument();
23
+ });
24
+
25
+ test("renders the title when provided", () => {
26
+ render(
27
+ <Toaster
28
+ message="Test Message"
29
+ color="red-500"
30
+ title="Test Title"
31
+ />
32
+ );
33
+ expect(screen.getByTestId("toaster")).toHaveTextContent("Test Title");
34
+ });
35
+
36
+ test("renders with full color background", () => {
37
+ render(
38
+ <Toaster
39
+ message="Test Message"
40
+ color="blue-500"
41
+ title="Test Title"
42
+ fullColor
43
+ />
44
+ );
45
+ expect(screen.getByTestId("toaster")).toHaveClass("bg-blue-500");
46
+ });
47
+
48
+ test("clears the message on clear icon click", async () => {
49
+ const clearMessage = vi.fn();
50
+ render(
51
+ <Toaster
52
+ message="Test Message"
53
+ color="orange-500"
54
+ clearMessage={clearMessage}
55
+ />
56
+ );
57
+ const clearButton = screen.getByTestId("clearMessage");
58
+ fireEvent.click(clearButton);
59
+ expect(clearMessage).toHaveBeenCalled();
60
+ });
61
+ });
@@ -0,0 +1,80 @@
1
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
2
+ import React from "react";
3
+ import Button from "../FormButton/FormButton";
4
+ import { faTimes } from "@fortawesome/free-solid-svg-icons";
5
+ import { ToasterTypes } from ".";
6
+
7
+ const Toaster: React.FC<ToasterTypes> = ({
8
+ message,
9
+ additionalClasses = "",
10
+ clearMessage,
11
+ color,
12
+ icon,
13
+ fullColor = false,
14
+ hasBumper = true,
15
+ title = "",
16
+ hasClearText,
17
+ clearText,
18
+ }) => {
19
+ const handleClearMessage = () => {
20
+ if (clearMessage) {
21
+ clearMessage(true);
22
+ }
23
+ };
24
+
25
+ // Simplified Color Handling
26
+ const iconColorMap: { [key: string]: string } = {
27
+ "green-800": "text-green-800",
28
+ "red-800": "text-red-800",
29
+ "orange-800": "text-orange-800",
30
+ "blue-800": "text-blue-800",
31
+ };
32
+ const iconClass = iconColorMap[color] || "";
33
+ // Improved CSS Class Composition
34
+ const borderClass = `border ${
35
+ hasBumper ? `border-l-4 border-${color}` : "border-l-1"
36
+ } `;
37
+ const backgroundColorClass = fullColor ? `bg-${color}` : "bg-white";
38
+
39
+ const textColorClass = fullColor ? "text-white" : "text-black";
40
+
41
+ return (
42
+ <div
43
+ data-testid="toaster"
44
+ className={`${borderClass} border-${color} p-4 shadow-lg shadow-boxShadow text-sm ${backgroundColorClass} rounded ${additionalClasses}`}
45
+ >
46
+ {title ? (
47
+ <h3 className="ml-8 font-bold text-xl text-left">{title}</h3>
48
+ ) : (
49
+ <></>
50
+ )}
51
+ <div className="flex items-center justify-between">
52
+ <span className={`mr-2 ${iconClass}`}>{icon}</span>
53
+ <span className={textColorClass}>{message}</span>
54
+ {clearMessage && !hasClearText && (
55
+ <span className="cursor-pointer hover:bg-gray-200 py-1 px-2 rounded-md">
56
+ <FontAwesomeIcon
57
+ data-testid="clearMessage"
58
+ onClick={handleClearMessage}
59
+ className={`cursor-pointer ${textColorClass}`}
60
+ icon={faTimes}
61
+ />
62
+ </span>
63
+ )}
64
+ {hasClearText && clearText && (
65
+ <Button
66
+ onClick={handleClearMessage}
67
+ text={clearText}
68
+ color="primary"
69
+ shape="outline"
70
+ type={"button"}
71
+ isDisabled={false}
72
+ additionalClasses="ml-2 px-4"
73
+ />
74
+ )}
75
+ </div>
76
+ </div>
77
+ );
78
+ };
79
+
80
+ export default Toaster;
@@ -0,0 +1,12 @@
1
+ export interface ToasterTypes {
2
+ message: string;
3
+ additionalClasses?: string;
4
+ clearMessage?: React.Dispatch<React.SetStateAction<boolean>>;
5
+ color: "green-800" | "red-800" | "yellow-800" | "blue-800" | "orange-800";
6
+ icon?: JSX.Element;
7
+ fullColor?: boolean;
8
+ hasBumper?: boolean;
9
+ title?: string;
10
+ hasClearText?: boolean;
11
+ clearText?: string;
12
+ }
@@ -0,0 +1,2 @@
1
+ export { default } from "./Toaster";
2
+ export * from "./Toaster.types";
@@ -0,0 +1,2 @@
1
+ // src/hoc/index.ts
2
+ export { default as withStoryBook } from "./styling/withStoryBook";
@@ -0,0 +1,19 @@
1
+ import React, { CSSProperties } from "react";
2
+
3
+ export interface WithStoryBookProps {
4
+ storybookStyle?: CSSProperties;
5
+ }
6
+
7
+ const withStoryBook = <P extends object>(
8
+ Component: React.ComponentType<P>
9
+ ): React.FC<P & WithStoryBookProps> => {
10
+ return ({ storybookStyle, ...props }: WithStoryBookProps) => {
11
+ return (
12
+ <div style={storybookStyle}>
13
+ <Component {...(props as P)} />
14
+ </div>
15
+ );
16
+ };
17
+ };
18
+
19
+ export default withStoryBook;
package/src/main.css ADDED
@@ -0,0 +1,3 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
@@ -0,0 +1 @@
1
+ import "@testing-library/jest-dom";
@@ -0,0 +1 @@
1
+ export { default as withMemo } from "./withMemo";
@@ -0,0 +1,20 @@
1
+ // withMemo.jsx
2
+ import React, { memo } from "react";
3
+
4
+ // Define a generic type that extends React component props
5
+ type ComponentType<P = {}> = React.ComponentType<P>;
6
+
7
+ // This is the type for the props comparison function
8
+ type ArePropsEqual<P> = (
9
+ prevProps: Readonly<P>,
10
+ nextProps: Readonly<P>
11
+ ) => boolean;
12
+
13
+ const withMemo = <P extends {}>(
14
+ Component: ComponentType<P>,
15
+ arePropsEqual?: ArePropsEqual<P>
16
+ ) => {
17
+ return memo(Component, arePropsEqual);
18
+ };
19
+
20
+ export default withMemo;
@@ -0,0 +1,7 @@
1
+ import { TagName } from "../components/Text/Text";
2
+ function assertTagName(tag: string): asserts tag is TagName {
3
+ if (!["span", "p", "h1", "h2", "h3", "h4", "h5"].includes(tag)) {
4
+ throw new Error(`Invalid tag name: ${tag}`);
5
+ }
6
+ }
7
+ export default assertTagName;
@@ -0,0 +1,102 @@
1
+ import React, { Fragment, useState } from "react";
2
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3
+ import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
4
+
5
+ type NavItem = {
6
+ title: string;
7
+ links: { link: string; menuItem: string }[];
8
+ };
9
+
10
+ type AccordionProps = {
11
+ navData: NavItem[];
12
+ accordionParentStyle: string;
13
+ accordionExpandedStyle: string;
14
+ };
15
+
16
+ export const Accordion: React.FC<AccordionProps> = ({
17
+ navData,
18
+ accordionParentStyle,
19
+ accordionExpandedStyle,
20
+ }) => {
21
+ // setting state to be an object, with each key being the index of the accordion item ->
22
+ // allows for multiple accordions to be open at once
23
+ // had to reduce in order for aria-expanded to work
24
+ const [accordionExpanded, setAccordionExpanded] = useState<
25
+ Record<number, boolean>
26
+ >(navData.reduce((acc, _, index) => ({ ...acc, [index]: false }), {}));
27
+
28
+ const toggleAccordion = (index: number) => {
29
+ setAccordionExpanded((prevState) => ({
30
+ ...prevState,
31
+ [index]: !prevState[index],
32
+ }));
33
+ };
34
+
35
+ // Expand the accordion when Enter key is pressed for tab users
36
+ const handleKeyDown = (
37
+ event: React.KeyboardEvent<HTMLDivElement>,
38
+ index: number
39
+ ) => {
40
+ if (event.key === "Enter") {
41
+ toggleAccordion(index);
42
+ }
43
+ };
44
+
45
+ return (
46
+ <>
47
+ {navData.map((item: NavItem, index: number) => (
48
+ <Fragment key={item.title}>
49
+ {/* SCREEN READER:
50
+ 1. should announce expanded status
51
+ 2. should announce that parent item is a button
52
+ 3. should announce title of accordion
53
+ 4. should be able to use enter key to open
54
+ 5. should announce that this is a navigation */}
55
+ <div
56
+ className={`cursor-pointer ${accordionParentStyle}`}
57
+ role="button"
58
+ onClick={() => toggleAccordion(index)}
59
+ aria-expanded={accordionExpanded[index]}
60
+ tabIndex={0}
61
+ onKeyDown={(event) => handleKeyDown(event, index)} // Attach handleKeyDown function here
62
+ >
63
+ <div className="px-6 text-left items-center h-10 select-none flex justify-between flex-row">
64
+ <div className="flex w-3/4">{item.title}</div>
65
+ <div className="flex w-1/2 justify-end">
66
+ <FontAwesomeIcon
67
+ className={`text-xs transition-transform duration-300 transform ${
68
+ accordionExpanded[index]
69
+ ? "rotate-180"
70
+ : ""
71
+ }`}
72
+ icon={faChevronDown}
73
+ />
74
+ </div>
75
+ </div>
76
+ </div>
77
+ <div
78
+ className={`px-6 w-full overflow-hidden transition-[max-height] duration-500 ease-in-out ${
79
+ accordionExpanded[index] ? "max-h-96" : "max-h-0"
80
+ }`}
81
+ aria-hidden={!accordionExpanded[index]}
82
+ tabIndex={accordionExpanded[index] ? 0 : -1} // Set tabIndex dynamically
83
+ >
84
+ <ul className={`${accordionExpandedStyle}`}>
85
+ {item.links.map((link) => (
86
+ <li key={link.menuItem} className="py-1">
87
+ <a
88
+ href={link.link}
89
+ className="hover:underline"
90
+ data-testid={link.menuItem}
91
+ >
92
+ {link.menuItem}
93
+ </a>
94
+ </li>
95
+ ))}
96
+ </ul>
97
+ </div>
98
+ </Fragment>
99
+ ))}
100
+ </>
101
+ );
102
+ };
@@ -0,0 +1,75 @@
1
+ import React from "react";
2
+ import Text from "../components/Text/Text";
3
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4
+ import {
5
+ faEnvelope,
6
+ faLocationDot,
7
+ faPhone,
8
+ } from "@fortawesome/free-solid-svg-icons";
9
+ import { ContactInfoItem } from "../components/Footer/ContactInfoItem";
10
+
11
+ interface ContactProps {
12
+ contactDataList: {
13
+ id: string;
14
+ address: string;
15
+ city: string;
16
+ state: string;
17
+ googleLink: string;
18
+ phone: string;
19
+ email: string;
20
+ }[];
21
+ }
22
+
23
+ export const FooterContacts: React.FC<ContactProps> = ({ contactDataList }) => {
24
+ return contactDataList.map((contact) => {
25
+ const contactDetails = [
26
+ {
27
+ icon: faPhone,
28
+ text: contact.phone,
29
+ },
30
+ {
31
+ icon: faEnvelope,
32
+ text: contact.email,
33
+ },
34
+ ];
35
+
36
+ return (
37
+ <div
38
+ key={contact.id}
39
+ data-testid="contact-info"
40
+ className="flex flex-col pt-2 w-60"
41
+ >
42
+ <ContactInfoItem icon={faLocationDot}>
43
+ <a
44
+ className="flex flex-col hover:underline"
45
+ href={contact.googleLink}
46
+ target="_blank"
47
+ >
48
+ {[contact.address, contact.city, contact.state].map(
49
+ (text, index) => (
50
+ <Text
51
+ key={index}
52
+ size={""}
53
+ color={""}
54
+ text={text}
55
+ fontFamily={""}
56
+ />
57
+ )
58
+ )}
59
+ </a>
60
+ </ContactInfoItem>
61
+ {contactDetails.map((detail, index) => (
62
+ <ContactInfoItem key={index} icon={detail.icon}>
63
+ <Text
64
+ size={""}
65
+ color={""}
66
+ text={detail.text}
67
+ fontFamily={""}
68
+ additionalClasses="my-1"
69
+ />
70
+ </ContactInfoItem>
71
+ ))}
72
+ </div>
73
+ );
74
+ });
75
+ };