Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OPHJOD-1320: Add PreloadImage component #227

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions lib/components/LazyImage/PreloadImage.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Meta, StoryObj } from '@storybook/react';
import { PreloadImage } from './PreloadImage';

const meta = {
title: 'PreloadImage',
component: PreloadImage,
tags: ['autodocs'],
} satisfies Meta<typeof PreloadImage>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
parameters: {
docs: {
description: {
story: 'This is a lazy loading image component that preloads the image before displaying it.',
},
},
},
args: {
src: 'https://images.unsplash.com/photo-1523464862212-d6631d073194?q=80&w=260',
alt: 'Woman standing in front of a colourful wall',
},
};
23 changes: 23 additions & 0 deletions lib/components/LazyImage/PreloadImage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { PreloadImage } from './PreloadImage';

describe('PreloadImage', () => {
it('renders without crashing', () => {
render(<PreloadImage src="test.jpg" alt="test image" />);
const imgElement = screen.getByAltText('test image');
expect(imgElement).toBeInTheDocument();
});

it('sets the alt attribute correctly', () => {
render(<PreloadImage src="test.jpg" alt="test image" />);
const imgElement = screen.getByAltText('test image');
expect(imgElement).toHaveAttribute('alt', 'test image');
});

it('applies the correct styles when the image is not loaded', () => {
render(<PreloadImage src="test.jpg" alt="test image" />);
const imgElement = screen.getByAltText('test image');
expect(imgElement).toHaveStyle('opacity: 0');
});
});
39 changes: 39 additions & 0 deletions lib/components/LazyImage/PreloadImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';

interface PreloadImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
/** The source of the image to be preloaded */
src: string;
/** The alt text for the image */
alt: string;
}

/**
* PreloadImage is a lazy loading image component that preloads the image before displaying it.
*/
export const PreloadImage: React.FC<PreloadImageProps> = ({ src, alt, ...props }) => {
const [loaded, setLoaded] = React.useState(false);
const [isCached, setIsCached] = React.useState(false);

React.useEffect(() => {
const img = new Image();
img.src = src;
if (img.complete) {
setIsCached(true);
setLoaded(true);
} else {
img.onload = () => setLoaded(true);
}
img.onload = () => setLoaded(true);
}, [src]);

return (
<div className={`ds:flex ds:bg-secondary-5 ${props.className}`.trim()}>
<img
src={loaded ? src : undefined}
alt={alt}
style={{ opacity: loaded ? 1 : 0, transition: isCached ? 'none' : 'opacity 0.3s' }}
className="ds:w-full ds:h-full ds:object-cover"
/>
</div>
);
};
7 changes: 4 additions & 3 deletions lib/components/MediaCard/MediaCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { MdFavorite, MdFavoriteBorder } from 'react-icons/md';
import { useMediaQueries } from '../../hooks/useMediaQueries';
import { PreloadImage } from '../LazyImage/PreloadImage';

type LinkComponent =
| {
Expand Down Expand Up @@ -134,7 +135,7 @@ const MediaCardVertical = ({
to,
children,
}: MediaCardImplProps) => {
const variantImageClassNames = 'ds:object-cover ds:h-[147px]';
const variantImageClassNames = 'ds:object-cover ds:h-[147px] ds:min-h-[147px]';
const labelRef = React.useRef<HTMLDivElement>(null);
const SINGLE_LINE_LABEL_HEIGHT = 27;
const [lineClampClassNames, setLineClampClassNames] = React.useState('ds:line-clamp-3');
Expand All @@ -151,7 +152,7 @@ const MediaCardVertical = ({
return (
<LinkOrDiv to={to} linkComponent={Link} className="ds:relative ds:flex ds:flex-col ds:w-[261px] ds:min-h-[299px]">
{imageSrc ? (
<img className={`${variantImageClassNames}`} src={imageSrc} alt={imageAlt} />
<PreloadImage className={`${variantImageClassNames}`} src={imageSrc} alt={imageAlt} />
) : (
<span className={`ds:w-full ds:h-full ds:bg-secondary-5 ds:max-w-[265px] ${variantImageClassNames}`}></span>
)}
Expand Down Expand Up @@ -184,7 +185,7 @@ const MediaCardHorizontal = ({
<div className="ds:shrink-0">
{imageSrc ? (
sm && (
<img
<PreloadImage
className="ds:sm:w-[193px] ds:lg:w-[255px] ds:sm:min-w-full ds:sm:min-h-full ds:sm:h-0 ds:object-cover"
src={imageSrc}
alt={imageAlt}
Expand Down
14 changes: 9 additions & 5 deletions lib/components/MediaCard/__snapshots__/MediaCard.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ exports[`MediaCard > renders the MediaCard component with default vertical varia
<div
class="ds:overflow-clip ds:rounded ds:shadow-border ds:bg-white ds:relative ds:flex ds:flex-col ds:w-[261px] ds:min-h-[299px]"
>
<img
alt="Image for default"
class="ds:object-cover ds:h-[147px]"
src="default.jpg"
/>
<div
class="ds:flex ds:bg-secondary-5 ds:object-cover ds:h-[147px] ds:min-h-[147px]"
>
<img
alt="Image for default"
class="ds:w-full ds:h-full ds:object-cover"
style="opacity: 0; transition: opacity 0.3s;"
/>
</div>
<div
class="ds:px-5 ds:pt-4 ds:pb-5 ds:text-black ds:flex ds:flex-col ds:justify-between ds:h-full ds:flex-nowrap"
>
Expand Down
Loading