@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,208 @@
1
+ import React from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { IconTypes } from "./Icon.types";
4
+ import {
5
+ getBorderClasses,
6
+ getColorClasses,
7
+ getHoverClasses,
8
+ getIndicatorSizeClasses,
9
+ getSizeClasses,
10
+ getIndicatorColorClasses,
11
+ getBackgroundColorClasses,
12
+ } from "./iconClassNames";
13
+
14
+ const containerSizeMap = {
15
+ xs: "size-8",
16
+ sm: "size-8",
17
+ md: "size-10",
18
+ lg: "size-10",
19
+ };
20
+
21
+ const Icon: React.FC<IconTypes> = ({
22
+ onClick,
23
+ text,
24
+ iconLabel,
25
+ iconColor = "black",
26
+ backgroundColor = "none",
27
+ size = "md",
28
+ icon,
29
+ iconBorder = "none",
30
+ additionalContainerClasses,
31
+ to,
32
+ href,
33
+ hoverBackground = "primary",
34
+ hoverColor = "blue",
35
+ hoverBorder,
36
+ indicatorSize = "md",
37
+ indicatorNumber,
38
+ limitIndicator = true,
39
+ limitCharacters = (text: string) => {
40
+ if (text.length > 12) {
41
+ window.confirm(
42
+ `Text length is ${text.length} characters, and should be less than or equal to 12.`
43
+ );
44
+ return text.slice(0, 12);
45
+ }
46
+ return text;
47
+ },
48
+ }) => {
49
+ let ariaLabel;
50
+ let ariaNotification = indicatorNumber;
51
+ let iconSizeClasses = getSizeClasses(size);
52
+ let iconColorClasses = getColorClasses(iconColor);
53
+ let containerSize = containerSizeMap[size as keyof typeof containerSizeMap];
54
+ let iconName = icon?.props.icon.iconName;
55
+ let iconClasses = ` flex flex-col justify-center
56
+ ${iconSizeClasses}
57
+ `;
58
+ let backgroundColorClasses = getBackgroundColorClasses(backgroundColor);
59
+ let hoverClasses = getHoverClasses(
60
+ hoverBackground,
61
+ hoverColor,
62
+ hoverBorder
63
+ );
64
+ let indicatorSizeClasses = getIndicatorSizeClasses(indicatorSize);
65
+ let indicatorColorClasses = getIndicatorColorClasses(iconColor);
66
+ let borderClasses = getBorderClasses(iconColor, iconBorder);
67
+
68
+ let isIndicatorRedCircle = indicatorSize === "sm";
69
+
70
+ if (indicatorNumber && parseInt(indicatorNumber) > 99 && limitIndicator) {
71
+ indicatorNumber = "99+";
72
+ }
73
+
74
+ if (typeof text === "string" && limitCharacters) {
75
+ text = limitCharacters(text);
76
+ }
77
+
78
+ // if there is no text for the icon and no notifications, screen reader needs label to be announced for link
79
+ if (ariaNotification == "" && !text) {
80
+ ariaLabel = `${iconLabel}`;
81
+
82
+ // if there is no text for the icon and the icon has notifications, screen reader needs label to be announced type of link and number of notifications
83
+ // OR
84
+ // if there is text for the icon and the icon has notifications, screen reader needs label to be announced type of link and number of notifications because either
85
+ // way the ariaLabel will override the text
86
+ } else if (
87
+ (ariaNotification !== "" && !text) ||
88
+ (ariaNotification !== "" && text)
89
+ ) {
90
+ ariaLabel = `${iconLabel} with ${ariaNotification} notifications`;
91
+ } else {
92
+ ariaLabel = "null";
93
+ }
94
+
95
+ if (to) {
96
+ return (
97
+ <div>
98
+ <Link
99
+ to={to}
100
+ onClick={onClick}
101
+ tabIndex={0}
102
+ className={`flex flex-col items-center justify-center`}
103
+ aria-label={ariaLabel}
104
+ >
105
+ <div
106
+ data-testid="icon"
107
+ className={`flex flex-col items-center justify-center ${containerSize} ${hoverClasses} ${borderClasses} ${iconColorClasses} ${backgroundColorClasses} ${additionalContainerClasses}`}
108
+ >
109
+ <div className="flex h-full">
110
+ <span
111
+ data-testid={iconName}
112
+ className={iconClasses}
113
+ >
114
+ {icon}
115
+ </span>
116
+ {indicatorNumber &&
117
+ parseInt(indicatorNumber) !== 0 &&
118
+ !isIndicatorRedCircle ? (
119
+ <div
120
+ className="indicator"
121
+ data-testid="indicator"
122
+ >
123
+ <span
124
+ className={` ${indicatorSizeClasses} ${indicatorColorClasses}`}
125
+ >
126
+ <p className="flex items-center justify-center h-full w-full">
127
+ {indicatorNumber}
128
+ </p>
129
+ </span>
130
+ </div>
131
+ ) : null}
132
+ {isIndicatorRedCircle &&
133
+ indicatorNumber &&
134
+ parseInt(indicatorNumber) !== 0 ? (
135
+ <div
136
+ className="indicator"
137
+ data-testid="indicator"
138
+ >
139
+ <span
140
+ className={`${indicatorSizeClasses}`}
141
+ />
142
+ </div>
143
+ ) : null}
144
+ </div>
145
+ </div>
146
+ {text && <div className="">{text}</div>}
147
+ </Link>
148
+ </div>
149
+ );
150
+ } else {
151
+ return (
152
+ <div>
153
+ <a
154
+ href={href}
155
+ onClick={onClick}
156
+ tabIndex={0}
157
+ className={`flex flex-col items-center justify-center p-2 relative min-w-14`}
158
+ aria-label={ariaLabel}
159
+ >
160
+ <div
161
+ data-testid="icon"
162
+ className={`flex flex-col items-center justify-center ${containerSize} ${hoverClasses} ${borderClasses} ${iconColorClasses} ${backgroundColorClasses} ${additionalContainerClasses}`}
163
+ >
164
+ <div className="flex h-full">
165
+ <span
166
+ data-testid={iconName}
167
+ className={iconClasses}
168
+ >
169
+ {icon}
170
+ </span>
171
+ {indicatorNumber &&
172
+ parseInt(indicatorNumber) !== 0 &&
173
+ !isIndicatorRedCircle ? (
174
+ <div
175
+ className="indicator"
176
+ data-testid="indicator"
177
+ >
178
+ <span
179
+ className={` ${indicatorSizeClasses} ${indicatorColorClasses}`}
180
+ >
181
+ <p className="flex items-center justify-center h-full w-full">
182
+ {indicatorNumber}
183
+ </p>
184
+ </span>
185
+ </div>
186
+ ) : null}
187
+ {isIndicatorRedCircle &&
188
+ indicatorNumber &&
189
+ parseInt(indicatorNumber) !== 0 ? (
190
+ <div
191
+ className="indicator"
192
+ data-testid="indicator"
193
+ >
194
+ <span
195
+ className={`${indicatorSizeClasses}`}
196
+ />
197
+ </div>
198
+ ) : null}
199
+ </div>
200
+ </div>
201
+ {text && <div className="absolute top-10 ">{text}</div>}
202
+ </a>
203
+ </div>
204
+ );
205
+ }
206
+ };
207
+
208
+ export default Icon;
@@ -0,0 +1,24 @@
1
+ import React, { ReactNode } from "react";
2
+
3
+ export interface IconTypes {
4
+ onClick?: (
5
+ e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>
6
+ ) => void;
7
+ text?: ReactNode;
8
+ iconColor?: string;
9
+ iconLabel: string;
10
+ backgroundColor?: string;
11
+ size?: "xs" | "sm" | "md" | "lg" | "xl" | string;
12
+ icon: JSX.Element | null;
13
+ iconBorder?: "border" | "none" | string;
14
+ to?: string;
15
+ href?: string;
16
+ additionalContainerClasses?: string;
17
+ hoverColor: "green" | "blue" | string;
18
+ hoverBackground: "blue" | "green" | "grey" | "none" | string;
19
+ hoverBorder?: "border" | "none" | string;
20
+ limitCharacters?: (text: string) => string;
21
+ indicatorSize?: "xs" | "sm" | "md" | "lg" | string;
22
+ indicatorNumber?: string;
23
+ limitIndicator?: boolean;
24
+ }
@@ -0,0 +1,79 @@
1
+ import classNames from "classNames";
2
+
3
+ export const getSizeClasses = (size: string) => {
4
+ return classNames({
5
+ "text-md": size === "sm",
6
+ "text-lg": size === "md",
7
+ "text-xl": size === "lg",
8
+ });
9
+ };
10
+
11
+ export const getColorClasses = (iconColor: string) => {
12
+ return classNames({
13
+ "text-blue-800": iconColor === "blue",
14
+ "text-teal-800": iconColor === "green",
15
+ "text-slate-950": iconColor === "black",
16
+ });
17
+ };
18
+
19
+ export const getBackgroundColorClasses = (backgroundColor: string) => {
20
+ return classNames({
21
+ "bg-blue-100": backgroundColor === "blue",
22
+ "bg-teal-100": backgroundColor === "green",
23
+ "bg-slate-100": backgroundColor === "grey",
24
+ "bg-transparent": backgroundColor === "none",
25
+ });
26
+ };
27
+
28
+ export const getHoverClasses = (
29
+ hoverBackground: string | undefined,
30
+ hoverColor: string | undefined,
31
+ hoverBorder: string | undefined
32
+ ) => {
33
+ return classNames(
34
+ "cursor-pointer",
35
+ {
36
+ "hover:bg-blue-100": hoverBackground === "blue",
37
+ "hover:bg-teal-50": hoverBackground === "green",
38
+ "hover:bg-gray-100": hoverBackground === "grey",
39
+ "hover:bg-transparent": hoverBackground === "none",
40
+ },
41
+ {
42
+ "hover:text-teal-800 hover:border-teal-600": hoverColor === "green",
43
+ "hover:text-blue-800 hover:border-blue-800": hoverColor === "blue",
44
+ "hover:text-slate-950 hover:border-slate-950": hoverColor === "black",
45
+ },
46
+ {
47
+ "hover:border-transparent": hoverBorder === "none",
48
+ }
49
+ );
50
+ };
51
+
52
+ export const getIndicatorSizeClasses = (indicatorSize: string | undefined) => {
53
+ return classNames({
54
+ "size-[.5rem] rounded-full bg-red-500 absolute top-0":
55
+ indicatorSize === "sm",
56
+ "text-[.5rem] items-center justify-center h-4 w-4 rounded-full absolute top-[-4px] left-[-1px]":
57
+ indicatorSize === "md",
58
+ "text-[.65rem] items-center justify-center rounded-full h-5 w-5 absolute top-[-5px] left-[-1px]":
59
+ indicatorSize === "lg",
60
+ });
61
+ };
62
+
63
+ export const getIndicatorColorClasses = (color: string | undefined) => {
64
+ return classNames({
65
+ "bg-blue-800 text-white border-none": color === "blue",
66
+ "bg-teal-600 text-white border-none": color === "green",
67
+ "bg-black text-white border-none": color === "black",
68
+ });
69
+ };
70
+
71
+ export const getBorderClasses = (
72
+ color: string | undefined,
73
+ iconBorder: "border" | "none" | undefined
74
+ ) => {
75
+ return classNames("cursor-pointer", "border-2", "rounded-full", {
76
+ [`border-${color}-500`]: iconBorder === "border",
77
+ "border-transparent": iconBorder === "none",
78
+ });
79
+ };
@@ -0,0 +1,2 @@
1
+ export { default } from "./Icon";
2
+ export * from "./Icon.types";
@@ -0,0 +1,79 @@
1
+ import { Meta, StoryFn } from "@storybook/react";
2
+ import Image, { ImageTypes } from ".";
3
+
4
+ interface ImageStoryProps extends ImageTypes {
5
+ src: string | null;
6
+ alt: string;
7
+ background?: boolean;
8
+ additionalClasses?: string;
9
+ }
10
+
11
+ export default {
12
+ title: "Components/Image",
13
+ argTypes: {
14
+ src: {
15
+ control: {
16
+ disable: true,
17
+ },
18
+ description:
19
+ "Image source path. If no image is found, a placeholder image will be displayed.",
20
+ },
21
+ alt: {
22
+ control: {
23
+ type: "text",
24
+ },
25
+ description:
26
+ "Image description provided to screen reader users or label for decorative images (indicated by empty alt text).",
27
+ },
28
+ background: {
29
+ control: {
30
+ disable: true,
31
+ },
32
+ description:
33
+ "Whether the image is displayed as a background image or not.",
34
+ },
35
+ additionalClasses: {
36
+ control: {
37
+ type: "text",
38
+ },
39
+ table: {
40
+ disable: true,
41
+ },
42
+ },
43
+ },
44
+ // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
45
+ tags: ["autodocs"],
46
+ } as Meta<ImageStoryProps>;
47
+
48
+ const Template: StoryFn<ImageStoryProps> = (args) => <Image {...args} />;
49
+
50
+ export const DefaultImage = Template.bind({});
51
+ DefaultImage.args = {
52
+ src: "../../../assets/green-laptop.png",
53
+ alt: "Image of grey laptop open with a green decorative image displayed on screen.",
54
+ background: false,
55
+ additionalClasses: "",
56
+ };
57
+
58
+ export const BackgroundDecorativeImage = Template.bind({});
59
+ BackgroundDecorativeImage.args = {
60
+ src: "../../../assets/heroImage.png",
61
+ alt: "",
62
+ background: true,
63
+ additionalClasses: "min-w-full",
64
+ };
65
+
66
+ export const BackgroundImage = Template.bind({});
67
+ BackgroundImage.args = {
68
+ src: "../../../assets/team.png",
69
+ alt: "Image of five people working in office setting.",
70
+ background: true,
71
+ additionalClasses: "min-w-full",
72
+ };
73
+
74
+ export const ImageNotFound = Template.bind({});
75
+ ImageNotFound.args = {
76
+ src: null,
77
+ alt: "",
78
+ background: false,
79
+ };
@@ -0,0 +1,87 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, beforeEach, test } from "vitest";
3
+ import Image from "./Image";
4
+
5
+ describe("<Image /> display image", () => {
6
+ beforeEach(() => {
7
+ render(
8
+ <Image
9
+ src="../../../assets/green-laptop.png"
10
+ alt="Image of grey laptop open with a green decorative image displayed on screen."
11
+ background={false}
12
+ additionalClasses=""
13
+ />
14
+ );
15
+ });
16
+
17
+ test("renders Image component", () => {
18
+ expect(screen.getByTestId("image")).toBeInTheDocument();
19
+ });
20
+
21
+ test("contain correct alt text for image", () => {
22
+ expect(screen.getByTestId("image")).toHaveAttribute(
23
+ "alt",
24
+ "Image of grey laptop open with a green decorative image displayed on screen."
25
+ );
26
+ });
27
+ });
28
+
29
+ describe("<Image /> display decorative background image", () => {
30
+ beforeEach(() => {
31
+ render(
32
+ <Image
33
+ src="../../../assets/heroImage.png"
34
+ alt=""
35
+ background={true}
36
+ additionalClasses=""
37
+ />
38
+ );
39
+ });
40
+
41
+ test("renders Image component", () => {
42
+ expect(screen.getByTestId("image-background-hero")).toBeInTheDocument();
43
+ });
44
+
45
+ test("renders correct background image", () => {
46
+ // This test might need to be adjusted based on how the background image is applied
47
+ const backgroundImageElement = screen.getByTestId(
48
+ "image-background-hero"
49
+ );
50
+ expect(backgroundImageElement).toHaveStyle(
51
+ `background-image: url(../../../assets/heroImage.png)`
52
+ );
53
+ });
54
+ // FIXME:
55
+ // test("contain correct aria label for decorative background image", () => {
56
+ // expect(screen.getByTestId("image-background-hero")).toHaveAttribute(
57
+ // "aria-label",
58
+ // ""
59
+ // );
60
+ // });
61
+ });
62
+
63
+ describe("<Image /> display background image", () => {
64
+ beforeEach(() => {
65
+ render(
66
+ <Image
67
+ src="../../../assets/team.png"
68
+ alt="Image of five people working in office setting."
69
+ background={true}
70
+ additionalClasses=""
71
+ />
72
+ );
73
+ });
74
+
75
+ test("renders Image component", () => {
76
+ expect(screen.getByTestId("image-background-hero")).toBeInTheDocument();
77
+ });
78
+
79
+ test("renders correct background image", () => {
80
+ const backgroundImageElement = screen.getByTestId(
81
+ "image-background-hero"
82
+ );
83
+ expect(backgroundImageElement).toHaveStyle(
84
+ `background-image: url(../../../assets/team.png)`
85
+ );
86
+ });
87
+ });
@@ -0,0 +1,49 @@
1
+ import React, { Children, ReactNode } from "react";
2
+ import heroImage from "../../../assets/heroImage.png";
3
+ import imageNotFound from "../../../assets/placeholder-no-image-available.png";
4
+ import { CSSProperties } from "react";
5
+ import { ImageTypes } from ".";
6
+
7
+ const Image: React.FC<ImageTypes> = ({
8
+ src = heroImage,
9
+ alt = "",
10
+ background = true,
11
+ additionalClasses,
12
+ children,
13
+ textAlignment,
14
+ }) => {
15
+ const effectiveSrc = typeof src === "string" ? src : imageNotFound;
16
+ const effectiveAlt =
17
+ effectiveSrc === imageNotFound ? "Image not found." : alt;
18
+
19
+ let heroImg: CSSProperties = {
20
+ backgroundImage: `url(${effectiveSrc})`,
21
+ };
22
+
23
+ const defaultBgClasses = `bg-no-repeat bg-cover w-full min-h-72 p-4 flex ${textAlignment} `;
24
+
25
+ if (background) {
26
+ return (
27
+ <div
28
+ role={effectiveAlt != "" ? "img" : undefined}
29
+ data-testid="image-background-hero"
30
+ aria-label={effectiveAlt || undefined}
31
+ className={`${defaultBgClasses} ${additionalClasses}`}
32
+ style={heroImg}
33
+ >
34
+ {Children.map(children, (child) => child)}
35
+ </div>
36
+ );
37
+ } else {
38
+ return (
39
+ <img
40
+ src={`${effectiveSrc}`}
41
+ alt={alt}
42
+ className={`${additionalClasses}`}
43
+ data-testid="image"
44
+ />
45
+ );
46
+ }
47
+ };
48
+
49
+ export default Image;
@@ -0,0 +1,11 @@
1
+ import React, { ReactNode } from "react";
2
+
3
+ export interface ImageTypes {
4
+ src: string | null;
5
+ alt: string;
6
+ background?: boolean;
7
+ additionalClasses?: string;
8
+ children?: ReactNode;
9
+ textAlignment?: "justify-start" | "justify-end" | "justify-center";
10
+ renderChildren?: (props: { textPosition: string }) => React.ReactNode;
11
+ }
@@ -0,0 +1,2 @@
1
+ export { default } from "./Image";
2
+ export * from "./Image.types";