Skip to content

Commit

Permalink
feat: Add loading state to ImageGallery (#3462)
Browse files Browse the repository at this point in the history
  • Loading branch information
johannes-ross committed Jan 16, 2025
1 parent 5305ff1 commit 6c0c3e1
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 44 deletions.
79 changes: 37 additions & 42 deletions packages/components/src/ImageGallery/ImageGallery.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useEffect, useRef, useState } from 'react'
import styled from '@emotion/styled'
import PhotoSwipeLightbox from 'photoswipe/lightbox'
import { Box, Flex, Image as ThemeImage } from 'theme-ui'
import { Flex, Image as ThemeImage } from 'theme-ui'

import { Icon } from '../Icon/Icon'
import { ImageGalleryThumbnail } from '../ImageGalleryThumbnail/ImageGalleryThumbnail'
import { Loader } from '../Loader/Loader'

import type { PhotoSwipeOptions } from 'photoswipe/lightbox'
import type { CardProps } from 'theme-ui'

import 'photoswipe/style.css'

Expand Down Expand Up @@ -35,18 +36,9 @@ interface IState {
activeImageIndex: number
showLightbox: boolean
images: Array<IImageGalleryItem>
showActiveImgLoading: boolean
}

const ThumbCard = styled<CardProps & React.ComponentProps<any>>(Box)`
cursor: pointer;
padding: 5px;
overflow: hidden;
transition: 0.2s ease-in-out;
&:hover {
transform: translateY(-5px);
}
`

const NavButton = styled('button')`
background: transparent;
border: 0;
Expand All @@ -62,6 +54,7 @@ export const ImageGallery = (props: ImageGalleryProps) => {
activeImageIndex: 0,
showLightbox: false,
images: [],
showActiveImgLoading: true,
})
const lightbox = useRef<PhotoSwipeLightbox>()

Expand Down Expand Up @@ -113,6 +106,14 @@ export const ImageGallery = (props: ImageGalleryProps) => {
setState({
...state,
activeImageIndex: imageIndex,
showActiveImgLoading: imageIndex !== activeImageIndex,
})
}

const setActiveImgLoaded = () => {
setState({
...state,
showActiveImgLoading: false,
})
}

Expand All @@ -132,7 +133,16 @@ export const ImageGallery = (props: ImageGalleryProps) => {

return activeImage ? (
<Flex sx={{ flexDirection: 'column' }}>
<Flex sx={{ width: '100%', position: 'relative' }}>
{state.showActiveImgLoading && (
<Loader sx={{ position: 'absolute', alignSelf: 'center' }} />
)}
<Flex
sx={{
width: '100%',
position: 'relative',
opacity: `${state.showActiveImgLoading ? 0 : 1}`,
}}
>
<ThemeImage
loading="lazy"
data-cy="active-image"
Expand All @@ -144,9 +154,8 @@ export const ImageGallery = (props: ImageGalleryProps) => {
height: [300, 450],
}}
src={activeImage.downloadUrl}
onClick={() => {
triggerLightbox()
}}
onClick={triggerLightbox}
onLoad={setActiveImgLoaded}
alt={activeImage.alt ?? activeImage.name}
crossOrigin=""
/>
Expand Down Expand Up @@ -193,36 +202,22 @@ export const ImageGallery = (props: ImageGalleryProps) => {
</>
) : null}
</Flex>
{showThumbnails ? (
{showThumbnails && (
<Flex sx={{ width: '100%', flexWrap: 'wrap' }} mx={[2, 2, '-5px']}>
{images.map((image, index: number) => (
<ThumbCard
data-cy="thumbnail"
data-testid="thumbnail"
mb={3}
mt={4}
opacity={image === activeImage ? 1.0 : 0.5}
onClick={() => setActive(index)}
key={index}
>
<ThemeImage
loading="lazy"
src={image.thumbnailUrl}
key={index}
alt={image.alt ?? image.name}
sx={{
width: 100,
height: 67,
objectFit: props.allowPortrait ? 'contain' : 'cover',
borderRadius: 1,
border: '1px solid offWhite',
}}
crossOrigin=""
/>
</ThumbCard>
<ImageGalleryThumbnail
activeImageIndex={activeImageIndex}
allowPortrait={props.allowPortrait ?? false}
setActiveIndex={setActive}
key={image.thumbnailUrl}
alt={image.alt}
index={index}
name={image.name}
thumbnailUrl={image.thumbnailUrl}
/>
))}
</Flex>
) : null}
)}
</Flex>
) : null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { ImageGalleryThumbnail } from './ImageGalleryThumbnail'

import type { Meta, StoryFn } from '@storybook/react'
import type { ImageGalleryThumbnailProps } from './ImageGalleryThumbnail'

export default {
title: 'Layout/ImageGallery/ImageGalleryThumbnail',
component: ImageGalleryThumbnail,
} as Meta<typeof ImageGalleryThumbnail>

export const Default: StoryFn<typeof ImageGalleryThumbnail> = (
props: ImageGalleryThumbnailProps,
) => {
return (
<ImageGalleryThumbnail
{...props}
activeImageIndex={0}
allowPortrait={false}
alt="alt"
name="name"
index={0}
setActiveIndex={() => {}}
thumbnailUrl="https://picsum.photos/id/29/150/150"
/>
)
}

export const AllowPortrait: StoryFn<typeof ImageGalleryThumbnail> = (
props: ImageGalleryThumbnailProps,
) => {
return (
<ImageGalleryThumbnail
{...props}
activeImageIndex={0}
allowPortrait={true}
alt="alt"
name="name"
index={0}
setActiveIndex={() => {}}
thumbnailUrl="https://picsum.photos/id/29/150/150"
/>
)
}

export const DisallowPortrait: StoryFn<typeof ImageGalleryThumbnail> = (
props: ImageGalleryThumbnailProps,
) => {
return (
<ImageGalleryThumbnail
{...props}
activeImageIndex={0}
allowPortrait={false}
alt="alt"
name="name"
index={0}
setActiveIndex={() => {}}
thumbnailUrl="https://picsum.photos/id/29/150/150"
/>
)
}

export const ImageIsActive: StoryFn<typeof ImageGalleryThumbnail> = (
props: ImageGalleryThumbnailProps,
) => {
return (
<ImageGalleryThumbnail
{...props}
activeImageIndex={0}
allowPortrait={false}
alt="alt"
name="name"
index={0}
setActiveIndex={() => {}}
thumbnailUrl="https://picsum.photos/id/29/150/150"
/>
)
}

export const ImageIsNotActive: StoryFn<typeof ImageGalleryThumbnail> = (
props: ImageGalleryThumbnailProps,
) => {
return (
<ImageGalleryThumbnail
{...props}
activeImageIndex={1}
allowPortrait={false}
alt="alt"
name="name"
index={0}
setActiveIndex={() => {}}
thumbnailUrl="https://picsum.photos/id/29/150/150"
/>
)
}

export const ThumbnailUrlInvalidAltText: StoryFn<
typeof ImageGalleryThumbnail
> = (props: ImageGalleryThumbnailProps) => {
return (
<ImageGalleryThumbnail
{...props}
activeImageIndex={1}
allowPortrait={false}
alt="alt"
name="name"
index={0}
setActiveIndex={() => {}}
thumbnailUrl="https://fastly.picsum.photos/404"
/>
)
}

export const ThumbnailUrlInvalidNameText: StoryFn<
typeof ImageGalleryThumbnail
> = (props: ImageGalleryThumbnailProps) => {
return (
<ImageGalleryThumbnail
{...props}
activeImageIndex={1}
allowPortrait={false}
name="name"
index={0}
setActiveIndex={() => {}}
thumbnailUrl="https://fastly.picsum.photos/404"
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import '@testing-library/jest-dom/vitest'

import { describe, expect, it, vi } from 'vitest'

import { render } from '../test/utils'
import { ImageGalleryThumbnail } from './ImageGalleryThumbnail'

describe('ImageGalleryThumbnail', () => {
it('calls Callback, when image clicked', () => {
const mockFn = vi.fn()
const { getByTestId } = render(
<ImageGalleryThumbnail
activeImageIndex={0}
allowPortrait={false}
alt="alt"
name="name"
index={0}
setActiveIndex={mockFn}
thumbnailUrl="https://picsum.photos/id/29/150/150"
/>,
)
getByTestId('thumbnail').click()
expect(mockFn).toHaveBeenCalledTimes(1)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { useState } from 'react'
import styled from '@emotion/styled'
import { Box, Image as ThemeImage } from 'theme-ui'

import { Loader } from '../Loader/Loader'

import type { CardProps } from 'theme-ui'

import 'photoswipe/style.css'

export interface ImageGalleryThumbnailProps {
setActiveIndex: (index: number) => void
allowPortrait: boolean
activeImageIndex: number
thumbnailUrl: string
index: number
alt?: string
name?: string
}

export const ThumbCard = styled<CardProps & React.ComponentProps<any>>(Box)`
cursor: pointer;
padding: 5px;
overflow: hidden;
transition: 0.2s ease-in-out;
&:hover {
transform: translateY(-5px);
}
`

export const ImageGalleryThumbnail = (props: ImageGalleryThumbnailProps) => {
const [thumbnailLoaded, setThumbnailLoaded] = useState<boolean>(false)

return (
<>
{!thumbnailLoaded && (
<Loader sx={{ mb: 3, mt: 4, width: 100, height: 67 }} />
)}
<ThumbCard
data-cy="thumbnail"
data-testid="thumbnail"
mb={3}
mt={4}
opacity={props.index === props.activeImageIndex ? 1.0 : 0.5}
onClick={() => props.setActiveIndex(props.index)}
>
<ThemeImage
loading="lazy"
src={props.thumbnailUrl}
alt={props.alt ?? props.name}
onLoad={() => setThumbnailLoaded(true)}
onError={() => setThumbnailLoaded(true)}
sx={{
width: thumbnailLoaded ? 100 : 0,
height: 67,
objectFit: props.allowPortrait ? 'contain' : 'cover',
borderRadius: 1,
border: '1px solid offWhite',
}}
crossOrigin=""
/>
</ThumbCard>
</>
)
}
4 changes: 3 additions & 1 deletion packages/components/src/MapFilterList/MapFilterList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export const MapFilterList = (props: IProps) => {
const isActive = (checkingFilter: string) =>
!!activeFilters.find((filter) => filter.label === checkingFilter)

const buttonLabel = `${pinCount} result${pinCount === 1 ? '' : 's'} in current view`
const buttonLabel = `${pinCount} result${
pinCount === 1 ? '' : 's'
} in current view`

return (
<Flex
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export const MemberTypeVerticalList = (props: IProps) => {
)
return (
<CardButton
data-cy={`MemberTypeVerticalList-Item${isSelected ? '-active' : ''}`}
data-cy={`MemberTypeVerticalList-Item${
isSelected ? '-active' : ''
}`}
data-testid="MemberTypeVerticalList-Item"
title={item._id}
key={index}
Expand Down

0 comments on commit 6c0c3e1

Please sign in to comment.