Skip to content

Commit

Permalink
OPHJOD-1320: Add LazyImage component
Browse files Browse the repository at this point in the history
  • Loading branch information
sauanto committed Feb 25, 2025
1 parent 03a986d commit 7491fc0
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 8 deletions.
26 changes: 26 additions & 0 deletions lib/components/LazyImage/LazyImage.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Meta, StoryObj } from '@storybook/react';
import { LazyImage } from './LazyImage';

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

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/LazyImage.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 { LazyImage } from './LazyImage';

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

it('sets the alt attribute correctly', () => {
render(<LazyImage 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(<LazyImage 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/LazyImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';

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

/**
* LazyImage is a lazy loading image component that preloads the image before displaying it.
*/
export const LazyImage: React.FC<LazyImageProps> = ({ 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 { LazyImage } from '../LazyImage/LazyImage';

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} />
<LazyImage 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
<LazyImage
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

0 comments on commit 7491fc0

Please sign in to comment.