Skip to content

Commit 1ba3c4c

Browse files
Merge pull request #3 from RedHat-UX/NGUI-175
feat(NGUI-175): Image component
2 parents 74dcf60 + d431d13 commit 1ba3c4c

File tree

6 files changed

+311
-11
lines changed

6 files changed

+311
-11
lines changed

README.md

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ This npm package provides a collection of reusable Patternfly React components t
99

1010
## Provides:
1111

12-
* Patternfly React Components
12+
- Patternfly React Components
13+
- ImageComponent
1314
- OneCardWrapper
1415
- TableWrapper
15-
* Dynamic Component Renderer
16+
- Dynamic Component Renderer
1617
- DynamicComponents
1718

1819
## Installation
1920

2021
**Pre-requisites:**
22+
2123
- React 18+
2224
- TypeScript
2325

@@ -30,27 +32,28 @@ npm install @rhngui/patternfly-react-renderer
3032
### OneCard Component
3133

3234
```jsx
33-
import { OneCardWrapper } from '@rhngui/patternfly-react-renderer';
35+
import { OneCardWrapper } from "@rhngui/patternfly-react-renderer";
3436

3537
const mockData = {
3638
title: "Movie Details",
37-
image: "https://image.tmdb.org/t/p/w440_and_h660_face/uXDfjJbdP4ijW5hWSBrPrlKpxab.jpg",
39+
image:
40+
"https://image.tmdb.org/t/p/w440_and_h660_face/uXDfjJbdP4ijW5hWSBrPrlKpxab.jpg",
3841
fields: [
3942
{
4043
name: "Title",
4144
data_path: "movie.title",
42-
data: ["Toy Story"]
45+
data: ["Toy Story"],
4346
},
4447
{
4548
name: "Year",
4649
data_path: "movie.year",
47-
data: [1995]
50+
data: [1995],
4851
},
4952
{
5053
name: "Genres",
5154
data_path: "movie.genres",
52-
data: ["Animation", "Adventure"]
53-
}
55+
data: ["Animation", "Adventure"],
56+
},
5457
],
5558
imageSize: "md",
5659
id: "movie-card",
@@ -61,7 +64,26 @@ function App() {
6164
}
6265
```
6366

67+
### Image Component
68+
69+
```jsx
70+
import { DynamicComponent } from "@rhngui/patternfly-react-renderer";
71+
72+
const imageConfig = {
73+
component: "image",
74+
title: "Movie Poster",
75+
image:
76+
"https://image.tmdb.org/t/p/w440_and_h660_face/uXDfjJbdP4ijW5hWSBrPrlKpxab.jpg",
77+
id: "movie-poster-image",
78+
};
79+
80+
function App() {
81+
return <DynamicComponent config={imageConfig} />;
82+
}
83+
```
84+
6485
## Links
65-
* [Documentation](https://redhat-ux.github.io/next-gen-ui-agent/guide/renderer/patternfly_npm/)
66-
* [Source Code](https://github.com/RedHat-UX/next-gen-ui-agent/tree/main/libs_js/next_gen_ui_react)
67-
* [Contributing](https://redhat-ux.github.io/next-gen-ui-agent/development/contributing/)
86+
87+
- [Documentation](https://redhat-ux.github.io/next-gen-ui-agent/guide/renderer/patternfly_npm/)
88+
- [Source Code](https://github.com/RedHat-UX/next-gen-ui-agent/tree/main/libs_js/next_gen_ui_react)
89+
- [Contributing](https://redhat-ux.github.io/next-gen-ui-agent/development/contributing/)

src/components/DynamicComponents.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import "@patternfly/react-core/dist/styles/base.css";
22
import "@patternfly/chatbot/dist/css/main.css";
3+
import "../global.css";
4+
35
import isArray from "lodash/isArray";
46
import isEmpty from "lodash/isEmpty";
57
import map from "lodash/map";

src/components/ImageComponent.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core";
2+
import React, { useState } from "react";
3+
4+
interface ImageComponentProps {
5+
component: "image";
6+
id: string;
7+
image?: string | null;
8+
title: string;
9+
className?: string;
10+
}
11+
12+
const ImageComponent: React.FC<ImageComponentProps> = ({
13+
id,
14+
image,
15+
title,
16+
className,
17+
}) => {
18+
const [imageError, setImageError] = useState(false);
19+
20+
return (
21+
<Card id={id} className={className}>
22+
<CardHeader>
23+
<CardTitle>{title}</CardTitle>
24+
</CardHeader>
25+
<CardBody>
26+
{image && !imageError ? (
27+
<img
28+
src={image}
29+
alt={title}
30+
className="image-component-img"
31+
onError={() => setImageError(true)}
32+
/>
33+
) : (
34+
<div className="image-component-placeholder">
35+
{imageError ? "Image failed to load" : "No image provided"}
36+
</div>
37+
)}
38+
</CardBody>
39+
</Card>
40+
);
41+
};
42+
43+
export default ImageComponent;

src/constants/componentsMap.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import ImageComponent from "../components/ImageComponent";
12
import OneCardWrapper from "../components/OneCardWrapper";
23
import TableWrapper from "../components/TableWrapper";
34

45
export const componentsMap = {
56
"one-card": OneCardWrapper,
67
table: TableWrapper,
8+
image: ImageComponent,
79
};

src/global.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/* Image Component Styles */
2+
.image-component-img {
3+
width: 100%;
4+
height: auto;
5+
border-radius: var(--pf-global--BorderRadius--sm);
6+
object-fit: cover;
7+
}
8+
9+
.image-component-placeholder {
10+
width: 100%;
11+
height: 200px;
12+
background-color: var(--pf-global--Color--200);
13+
border-radius: var(--pf-global--BorderRadius--sm);
14+
display: flex;
15+
align-items: center;
16+
justify-content: center;
17+
color: var(--pf-global--Color--300);
18+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { render, screen, fireEvent } from "@testing-library/react";
2+
import "@testing-library/jest-dom";
3+
4+
import ImageComponent from "../../components/ImageComponent";
5+
6+
const mockImageData = {
7+
component: "image" as const,
8+
id: "test-id",
9+
image:
10+
"https://image.tmdb.org/t/p/w440_and_h660_face/uXDfjJbdP4ijW5hWSBrPrlKpxab.jpg",
11+
title: "Toy Story Poster",
12+
};
13+
14+
describe("ImageComponent", () => {
15+
const defaultProps = {
16+
component: "image" as const,
17+
id: mockImageData.id,
18+
image: mockImageData.image,
19+
title: mockImageData.title,
20+
};
21+
22+
it("renders with required props", () => {
23+
render(<ImageComponent {...defaultProps} />);
24+
25+
expect(screen.getByText("Toy Story Poster")).toBeInTheDocument();
26+
expect(
27+
screen.getByRole("img", { name: "Toy Story Poster" })
28+
).toBeInTheDocument();
29+
expect(screen.getByRole("img")).toHaveAttribute("src", mockImageData.image);
30+
});
31+
32+
it("renders image with correct attributes", () => {
33+
render(<ImageComponent {...defaultProps} />);
34+
35+
const image = screen.getByRole("img");
36+
expect(image).toHaveAttribute("src", mockImageData.image);
37+
expect(image).toHaveAttribute("alt", "Toy Story Poster");
38+
expect(image).toHaveStyle("width: 100%");
39+
expect(image).toHaveStyle("height: auto");
40+
expect(image).toHaveStyle("object-fit: cover");
41+
expect(image).toHaveStyle(
42+
"border-radius: var(--pf-global--BorderRadius--sm)"
43+
);
44+
});
45+
46+
it("applies correct card styling", () => {
47+
render(<ImageComponent {...defaultProps} />);
48+
49+
const card = screen.getByRole("img").closest('[data-testid="card"]') ||
50+
screen.getByRole("img").closest('div');
51+
expect(card).toBeInTheDocument();
52+
});
53+
54+
it("applies custom id and className", () => {
55+
const customId = "custom-test-id";
56+
const customClassName = "custom-class";
57+
58+
render(
59+
<ImageComponent
60+
{...defaultProps}
61+
id={customId}
62+
className={customClassName}
63+
/>
64+
);
65+
66+
const card = screen.getByRole("img").closest('[id="custom-test-id"]');
67+
expect(card).toBeInTheDocument();
68+
expect(card).toHaveClass(customClassName);
69+
});
70+
71+
it("renders placeholder when image is null", () => {
72+
render(<ImageComponent {...defaultProps} image={null} />);
73+
74+
expect(screen.queryByRole("img")).not.toBeInTheDocument();
75+
expect(screen.getByText("No image provided")).toBeInTheDocument();
76+
77+
const placeholder = screen.getByText("No image provided");
78+
expect(placeholder).toHaveStyle("width: 100%");
79+
expect(placeholder).toHaveStyle("height: 200px");
80+
expect(placeholder).toHaveStyle(
81+
"background-color: var(--pf-global--Color--200)"
82+
);
83+
expect(placeholder).toHaveStyle(
84+
"border-radius: var(--pf-global--BorderRadius--sm)"
85+
);
86+
expect(placeholder).toHaveStyle("display: flex");
87+
expect(placeholder).toHaveStyle("align-items: center");
88+
expect(placeholder).toHaveStyle("justify-content: center");
89+
expect(placeholder).toHaveStyle("color: var(--pf-global--Color--300)");
90+
});
91+
92+
it("renders placeholder when image is undefined", () => {
93+
const { image: _image, ...propsWithoutImage } = defaultProps;
94+
void _image; // Acknowledge unused variable
95+
96+
render(<ImageComponent {...propsWithoutImage} />);
97+
98+
expect(screen.queryByRole("img")).not.toBeInTheDocument();
99+
expect(screen.getByText("No image provided")).toBeInTheDocument();
100+
});
101+
102+
it("renders placeholder when image is empty string", () => {
103+
render(<ImageComponent {...defaultProps} image="" />);
104+
105+
expect(screen.queryByRole("img")).not.toBeInTheDocument();
106+
expect(screen.getByText("No image provided")).toBeInTheDocument();
107+
});
108+
109+
it("handles image load error", () => {
110+
render(<ImageComponent {...defaultProps} />);
111+
112+
const image = screen.getByRole("img");
113+
expect(image).toBeInTheDocument();
114+
115+
// Simulate image load error
116+
fireEvent.error(image);
117+
118+
// After error, image should be hidden and error message should appear
119+
expect(screen.queryByRole("img")).not.toBeInTheDocument();
120+
121+
// Check that error message is displayed
122+
const errorMessage = screen.getByText("Image failed to load");
123+
expect(errorMessage).toBeInTheDocument();
124+
expect(errorMessage).toHaveStyle("width: 100%");
125+
expect(errorMessage).toHaveStyle("height: 200px");
126+
expect(errorMessage).toHaveStyle(
127+
"background-color: var(--pf-global--Color--200)"
128+
);
129+
expect(errorMessage).toHaveStyle(
130+
"border-radius: var(--pf-global--BorderRadius--sm)"
131+
);
132+
expect(errorMessage).toHaveStyle("display: flex");
133+
expect(errorMessage).toHaveStyle("align-items: center");
134+
expect(errorMessage).toHaveStyle("justify-content: center");
135+
expect(errorMessage).toHaveStyle("color: var(--pf-global--Color--300)");
136+
});
137+
138+
it("renders with different image URLs", () => {
139+
const testImageUrl = "https://example.com/test-image.jpg";
140+
141+
render(<ImageComponent {...defaultProps} image={testImageUrl} />);
142+
143+
const image = screen.getByRole("img");
144+
expect(image).toHaveAttribute("src", testImageUrl);
145+
expect(image).toHaveAttribute("alt", "Toy Story Poster");
146+
});
147+
148+
it("renders with different titles", () => {
149+
const testTitle = "Different Movie Title";
150+
151+
render(<ImageComponent {...defaultProps} title={testTitle} />);
152+
153+
expect(screen.getByText(testTitle)).toBeInTheDocument();
154+
155+
const image = screen.getByRole("img");
156+
expect(image).toHaveAttribute("alt", testTitle);
157+
});
158+
159+
it("renders with different IDs", () => {
160+
const testId = "different-test-id";
161+
162+
render(<ImageComponent {...defaultProps} id={testId} />);
163+
164+
const card = screen.getByRole("img").closest('[id="different-test-id"]');
165+
expect(card).toBeInTheDocument();
166+
});
167+
168+
it("handles very long titles", () => {
169+
const longTitle =
170+
"This is a very long title that might wrap to multiple lines and should still be displayed correctly";
171+
172+
render(<ImageComponent {...defaultProps} title={longTitle} />);
173+
174+
expect(screen.getByText(longTitle)).toBeInTheDocument();
175+
});
176+
177+
it("handles special characters in title", () => {
178+
const specialTitle = "Movie Title with Special Characters: @#$%^&*()";
179+
180+
render(<ImageComponent {...defaultProps} title={specialTitle} />);
181+
182+
expect(screen.getByText(specialTitle)).toBeInTheDocument();
183+
184+
const image = screen.getByRole("img");
185+
expect(image).toHaveAttribute("alt", specialTitle);
186+
});
187+
188+
it("applies consistent styling across different scenarios", () => {
189+
const { rerender } = render(<ImageComponent {...defaultProps} />);
190+
191+
// Test with image
192+
const image = screen.getByRole("img");
193+
expect(image).toHaveStyle("width: 100%");
194+
expect(image).toHaveStyle("height: auto");
195+
expect(image).toHaveStyle("object-fit: cover");
196+
197+
// Test without image
198+
rerender(<ImageComponent {...defaultProps} image={null} />);
199+
const placeholder = screen.getByText("No image provided");
200+
expect(placeholder).toHaveStyle("width: 100%");
201+
expect(placeholder).toHaveStyle("height: 200px");
202+
});
203+
204+
it("maintains accessibility with proper alt text", () => {
205+
render(<ImageComponent {...defaultProps} />);
206+
207+
const image = screen.getByRole("img");
208+
expect(image).toHaveAttribute("alt", mockImageData.title);
209+
210+
// Alt text should match the title
211+
expect(image.getAttribute("alt")).toBe("Toy Story Poster");
212+
});
213+
});

0 commit comments

Comments
 (0)