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

feat: Add Image Slider component #1595

Merged
merged 73 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
3d93145
Scaffold pre-observers
dlnr Jul 16, 2024
f986c7c
Testing intersection observers
dlnr Jul 18, 2024
4644b89
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
dlnr Jul 18, 2024
05b3155
Working intersectionObserver
dlnr Jul 19, 2024
dd01ba1
Test fix
dlnr Jul 19, 2024
01d17c2
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
dlnr Jul 19, 2024
175af14
Scrollbar and snapstop options
dlnr Jul 22, 2024
c33e0f0
Previous and Next slide functions
dlnr Jul 22, 2024
7678973
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
dlnr Jul 23, 2024
15f4794
Interactions and thumbnails testing
dlnr Jul 26, 2024
cd3e6c3
Thumbnails
dlnr Jul 26, 2024
d156035
Cleaning
dlnr Jul 26, 2024
23b6910
Some testing
dlnr Jul 26, 2024
c26634e
Some documentation and stories
dlnr Jul 26, 2024
cf8b78f
REfresh not needed
dlnr Jul 26, 2024
8fb8b3b
Aria roles for thumbnails
dlnr Jul 26, 2024
a3e244e
Snapstop on as default
dlnr Jul 26, 2024
c1e07ef
Mixed sizes story for demo purposes
dlnr Jul 26, 2024
a0793bf
Typo
dlnr Jul 26, 2024
bcfd07a
Thumbnail aria testing
dlnr Jul 26, 2024
6844fdf
Alt tekst (testing screen reader)
dlnr Jul 26, 2024
c178452
Translatable labels
dlnr Jul 26, 2024
f66c42e
ImageSliderItem test
dlnr Jul 26, 2024
39f5503
ImageSliderScroller test
dlnr Jul 26, 2024
869ee47
Thumbnails test
dlnr Jul 26, 2024
75464dc
Merge branch 'develop' into feature/DES-869-image-slider
RubenSibon Aug 2, 2024
a6bc6c8
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
dlnr Aug 12, 2024
7a8ccb9
Merge branch 'feature/DES-869-image-slider' of https://github.com/Ams…
dlnr Aug 12, 2024
5c7290d
Contrast color prop change
dlnr Aug 12, 2024
9cd66b5
Ok now you need two props
dlnr Aug 12, 2024
be167b0
Thumbnail hover effect
dlnr Aug 12, 2024
1cab90a
Getting used to new button props
dlnr Aug 12, 2024
d063e4a
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
dlnr Sep 12, 2024
308a999
Removed various sizes story
dlnr Sep 12, 2024
f11504f
Screen decorator for max-width
dlnr Sep 13, 2024
831a7a5
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
dlnr Sep 20, 2024
d877ef1
Removed outline in favor of opacity effect
dlnr Sep 20, 2024
d2bd221
Refactored to use an object
dlnr Sep 20, 2024
5ab2cfd
Test corrections
dlnr Sep 20, 2024
12867ce
Thumbnails keyboard control
dlnr Sep 23, 2024
6c26a17
JSdoc prop description
dlnr Sep 23, 2024
1c336c8
Source set images story
dlnr Sep 23, 2024
c1e3a70
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
dlnr Sep 27, 2024
e1e272a
Always use thumbnails and never show scroller
dlnr Sep 27, 2024
4c8b2e1
Test fix
dlnr Sep 27, 2024
f271005
Some comment resolutions
dlnr Sep 30, 2024
744327e
Rename map
dlnr Sep 30, 2024
ce03bbb
Hide controls when primary input is not a mouse
dlnr Sep 30, 2024
4b9732a
Lower breakpoint, buttons are only too big for phones
dlnr Sep 30, 2024
a04208d
Comments
dlnr Sep 30, 2024
9aca20a
Error fix
dlnr Sep 30, 2024
7f57b76
Thumbnail functions moved
dlnr Oct 1, 2024
f50ef71
Removed comment
dlnr Oct 1, 2024
11f670b
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
dlnr Oct 1, 2024
ed42e6a
Test fix
dlnr Oct 1, 2024
c821453
Moved controls to dedicated file
dlnr Oct 1, 2024
7026b6c
Moved scroller to dedicated file
dlnr Oct 1, 2024
f13eafb
Revert "Moved scroller to dedicated file"
dlnr Oct 2, 2024
6f7a3f6
Revert "Moved controls to dedicated file"
dlnr Oct 2, 2024
84942f5
Move controls again
dlnr Oct 2, 2024
92836a7
Controls test
dlnr Oct 2, 2024
263052e
Status badge
dlnr Oct 2, 2024
b8ce020
Sort all the things
VincentSmedinga Oct 3, 2024
fd82102
Add documentation
VincentSmedinga Oct 3, 2024
8e28d2b
Use full name for aspect ratio prop
VincentSmedinga Oct 3, 2024
c9c9e18
Make variable names more precise
VincentSmedinga Oct 3, 2024
95f5721
Replace switch with lookup table
VincentSmedinga Oct 3, 2024
66ccd62
Make variable names more precise
VincentSmedinga Oct 3, 2024
1fa45b3
Improve rendering performance
VincentSmedinga Oct 3, 2024
3dd85db
Update docs
VincentSmedinga Oct 3, 2024
216ee9f
Update scroll position after resize or orientation change
VincentSmedinga Oct 3, 2024
62a9403
Prevent unnecessary slide after resize or orientation change
VincentSmedinga Oct 3, 2024
45a23f0
Merge branch 'develop' into feature/DES-869-image-slider-object
VincentSmedinga Oct 3, 2024
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
5 changes: 5 additions & 0 deletions packages/css/src/components/image-slider/image-slider.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/
@import "../../common/breakpoint";

.ams-image-slider {
display: grid;
Expand Down Expand Up @@ -47,6 +48,10 @@
grid-column: 1/-1;
grid-row: 1;
justify-content: space-between;

@media (pointer: coarse) and (max-width: $ams-breakpoint-medium) {
display: none;
}
RubenSibon marked this conversation as resolved.
Show resolved Hide resolved
}

.ams-image-slider__control {
Expand Down
23 changes: 11 additions & 12 deletions packages/react/src/ImageSlider/ImageSlider.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { render } from '@testing-library/react'
import { createRef } from 'react'
import { ImageSlider, SlideProps } from './ImageSlider'
import { ImageSlider, ImageSliderImageProps } from './ImageSlider'
import '@testing-library/jest-dom'

const observe = jest.fn()
Expand All @@ -20,7 +20,7 @@ window.IntersectionObserver = jest.fn(() => ({
}))

describe('Image slider', () => {
const slides: SlideProps[] = [
const images: ImageSliderImageProps[] = [
{
src: 'https://picsum.photos/id/122/320/180',
alt: 'Bridge',
Expand All @@ -39,33 +39,32 @@ describe('Image slider', () => {
]

it('renders', () => {
const { container } = render(<ImageSlider slides={slides} />)
const { container } = render(<ImageSlider images={images} />)

const component = container.querySelector(':only-child')

expect(component).toBeInTheDocument()
expect(component).toBeVisible()
})

it('renders slides', () => {
const { container } = render(<ImageSlider slides={slides} />)
it('renders images', () => {
const { container } = render(<ImageSlider images={images} />)

const slideElements = container.querySelectorAll('.ams-image-slider__item')
const slideArray = Array.from(slideElements)
const slides = Array.from(container.querySelectorAll('.ams-image-slider__item'))

expect(slideArray).toHaveLength(3)
expect(slides).toHaveLength(3)
})

it('renders a design system BEM class name', () => {
const { container } = render(<ImageSlider slides={slides} />)
const { container } = render(<ImageSlider images={images} />)

const component = container.querySelector(':only-child')

expect(component).toHaveClass('ams-image-slider')
})

it('renders an additional class name', () => {
const { container } = render(<ImageSlider slides={slides} className="extra" />)
const { container } = render(<ImageSlider images={images} className="extra" />)

const component = container.querySelector(':only-child')

Expand All @@ -75,15 +74,15 @@ describe('Image slider', () => {
it('supports ForwardRef in React', () => {
const ref = createRef<HTMLDivElement>()

const { container } = render(<ImageSlider slides={slides} ref={ref} />)
const { container } = render(<ImageSlider images={images} ref={ref} />)

const component = container.querySelector(':only-child')

expect(ref.current).toBe(component)
})

it('renders thumbnails', () => {
const { container } = render(<ImageSlider slides={slides}></ImageSlider>)
const { container } = render(<ImageSlider images={images}></ImageSlider>)

expect(container.querySelector('.ams-image-slider__thumbnail')).toBeInTheDocument()
})
Expand Down
102 changes: 22 additions & 80 deletions packages/react/src/ImageSlider/ImageSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,20 @@
* Copyright Gemeente Amsterdam
*/

import { ChevronLeftIcon, ChevronRightIcon } from '@amsterdam/design-system-react-icons'
import clsx from 'clsx'
import { forwardRef, KeyboardEvent as ReactKeyboardEvent, useEffect, useRef, useState } from 'react'
import { forwardRef, useEffect, useRef, useState } from 'react'
import type { ForwardedRef, HTMLAttributes } from 'react'
import { ImageSliderContext } from './ImageSliderContext'
import { ImageSliderControls } from './ImageSliderControls'
import { ImageSliderItem } from './ImageSliderItem'
import { ImageSliderScroller } from './ImageSliderScroller'
import { ImageSliderThumbnails } from './ImageSliderThumbnails'
import { Ratio } from '../AspectRatio'
import { IconButton } from '../IconButton'
import { Image } from '../Image/Image'
import { Image, ImageProps } from '../Image/Image'

export type SlideProps = {
src: string
/** Prove a URL to the image */
export type ImageSliderImageProps = ImageProps & {
/** Define an aspect ratio to use on the image */
ratio: Ratio
/** Define an aspect ratio to use on all images */
alt: string
/** Describe to image */
srcSet?: string
/** Provide a src-set array to use responsive images */
sizes?: string
/** Provide a sizes attribute to each image */
}

export type ImageSliderProps = {
Expand All @@ -38,7 +29,7 @@ export type ImageSliderProps = {
/** Label for the previous button */
previousLabel?: string
/** An array of images to display */
slides: SlideProps[]
images: ImageSliderImageProps[]
} & HTMLAttributes<HTMLDivElement>

export const ImageSliderRoot = forwardRef(
Expand All @@ -49,7 +40,7 @@ export const ImageSliderRoot = forwardRef(
imageLabel = 'Afbeelding',
nextLabel = 'Volgende',
previousLabel = 'Vorige',
slides,
images,
...restProps
}: ImageSliderProps,
ref: ForwardedRef<HTMLDivElement>,
Expand All @@ -61,13 +52,12 @@ export const ImageSliderRoot = forwardRef(
const hasIntersected = new Set<IntersectionObserverEntry>()

const inView = (observations: IntersectionObserverEntry[]) => {
const slides = targetRef.current?.children || []
const slidesArray = Array.from(slides)
const images = Array.from(targetRef.current?.children || [])

for (let observation of observations) {
hasIntersected.add(observation)
if (observation.isIntersecting) {
setCurrentSlideId(slidesArray.indexOf(observation.target as HTMLElement))
setCurrentSlideId(images.indexOf(observation.target as HTMLElement))
}
}
}
Expand All @@ -92,8 +82,8 @@ export const ImageSliderRoot = forwardRef(
}

if (targetRef.current) {
const slides = Array.from(targetRef.current.children)
for (let slide of slides) observer.observe(slide)
const images = Array.from(targetRef.current.children)
for (let slide of images) observer.observe(slide)

targetRef.current.addEventListener('scrollend', synchronise)
targetRef.current.addEventListener('keydown', handleKeyDown)
Expand All @@ -111,32 +101,6 @@ export const ImageSliderRoot = forwardRef(
updateControls()
}

const handleThumbsKeyDown = (event: ReactKeyboardEvent<HTMLElement>) => {
const target = event.target as HTMLElement
const element = target.parentElement?.children[currentSlideId]

if (event.key === 'ArrowRight') {
const next = element?.nextElementSibling as HTMLElement | null

if (next === element) return

if (next) {
next.focus()
goToNextSlide()
}
}
if (event.key === 'ArrowLeft') {
const previous = element?.previousElementSibling as HTMLElement | null

if (previous === element) return

if (previous) {
previous.focus()
goToPreviousSlide()
}
}
}

const goToSlide = (element: HTMLElement) => {
const sliderScroller = targetRef.current

Expand Down Expand Up @@ -182,53 +146,31 @@ export const ImageSliderRoot = forwardRef(
}

return (
<ImageSliderContext.Provider value={{ currentSlide: currentSlideId, goToSlideId }}>
<ImageSliderContext.Provider
value={{ atStart, atEnd, currentSlideId, goToNextSlide, goToPreviousSlide, goToSlideId }}
>
<div
{...restProps}
ref={ref}
aria-roledescription="carousel"
tabIndex={-1}
className={clsx('ams-image-slider', controls && 'ams-image-slider--controls', className)}
>
{controls && (
<div className="ams-image-slider__controls">
<IconButton
svg={ChevronLeftIcon}
label={previousLabel}
inverseColor={true}
className="ams-image-slider__control ams-image-slider__control--previous"
onClick={() => goToPreviousSlide()}
disabled={atStart}
/>
<IconButton
svg={ChevronRightIcon}
label={nextLabel}
inverseColor={true}
className="ams-image-slider__control ams-image-slider__control--next"
onClick={() => goToNextSlide()}
disabled={atEnd}
/>
</div>
)}
{controls && <ImageSliderControls previousLabel={previousLabel} nextLabel={nextLabel} />}
<ImageSliderScroller tabIndex={0} ref={targetRef} aria-live="polite" role="group">
{slides.map((slide, index) => (
{images.map((image, index) => (
<ImageSliderItem key={index} slideId={index}>
<Image
src={slide.src}
srcSet={slide.srcSet}
sizes={slide.sizes}
alt={slide.alt}
className={`ams-aspect-ratio--${slide.ratio}`}
src={image.src}
srcSet={image.srcSet}
sizes={image.sizes}
alt={image.alt}
className={`ams-aspect-ratio--${image.ratio}`}
/>
</ImageSliderItem>
))}
</ImageSliderScroller>
<ImageSliderThumbnails
thumbnails={slides}
imageLabel={imageLabel}
currentSlide={currentSlideId}
onKeyDown={handleThumbsKeyDown}
/>
<ImageSliderThumbnails thumbnails={images} imageLabel={imageLabel} />
</div>
</ImageSliderContext.Provider>
)
Expand Down
12 changes: 10 additions & 2 deletions packages/react/src/ImageSlider/ImageSliderContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@
import { createContext } from 'react'

export type ImageSliderContextValue = {
currentSlide: number
atStart: boolean
atEnd: boolean
currentSlideId: number
goToNextSlide: () => void
goToPreviousSlide: () => void
// eslint-disable-next-line no-unused-vars
goToSlideId: (id: number) => void
}

const defaultValues: ImageSliderContextValue = {
currentSlide: 0,
atStart: true,
atEnd: false,
currentSlideId: 0,
goToNextSlide: () => {},
goToPreviousSlide: () => {},
goToSlideId: () => {},
}

Expand Down
26 changes: 26 additions & 0 deletions packages/react/src/ImageSlider/ImageSliderControls.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { render } from '@testing-library/react'
import { createRef } from 'react'
import { ImageSliderControls } from './ImageSliderControls'
import '@testing-library/jest-dom'

describe('Image slider controls', () => {
const nextLabel = 'Volgende'
const previousLabel = 'Vorige'
it('renders', () => {
const { container } = render(<ImageSliderControls nextLabel={nextLabel} previousLabel={previousLabel} />)

const component = container.querySelector(':only-child')

expect(component).toBeInTheDocument()
expect(component).toBeVisible()
})
it('supports ForwardRef in React', () => {
const ref = createRef<HTMLDivElement>()

const { container } = render(<ImageSliderControls ref={ref} nextLabel={nextLabel} previousLabel={previousLabel} />)

const component = container.querySelector(':only-child')

expect(ref.current).toBe(component)
})
})
47 changes: 47 additions & 0 deletions packages/react/src/ImageSlider/ImageSliderControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import { ChevronLeftIcon, ChevronRightIcon } from '@amsterdam/design-system-react-icons'
import clsx from 'clsx'
import { forwardRef, useContext } from 'react'
import type { ForwardedRef, HTMLAttributes } from 'react'
import { ImageSliderContext } from './ImageSliderContext'
import { IconButton } from '../IconButton'

export type ImageSliderControlsProps = {
previousLabel: string
nextLabel: string
} & HTMLAttributes<HTMLDivElement>

export const ImageSliderControls = forwardRef(
(
{ previousLabel, nextLabel, className, ...restProps }: ImageSliderControlsProps,
ref: ForwardedRef<HTMLDivElement>,
) => {
const { atStart, atEnd, goToPreviousSlide, goToNextSlide } = useContext(ImageSliderContext)
return (
<div {...restProps} ref={ref} className={clsx('ams-image-slider__controls', className)}>
<IconButton
svg={ChevronLeftIcon}
label={previousLabel}
inverseColor={true}
className="ams-image-slider__control ams-image-slider__control--previous"
onClick={() => goToPreviousSlide()}
disabled={atStart}
/>
<IconButton
svg={ChevronRightIcon}
label={nextLabel}
inverseColor={true}
className="ams-image-slider__control ams-image-slider__control--next"
onClick={() => goToNextSlide()}
disabled={atEnd}
/>
</div>
)
},
)

ImageSliderControls.displayName = 'ImageSliderControls'
4 changes: 2 additions & 2 deletions packages/react/src/ImageSlider/ImageSliderItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export type ImageSliderItemProps = {

export const ImageSliderItem = forwardRef(
({ children, slideId, className, ...restProps }: ImageSliderItemProps, ref: ForwardedRef<HTMLDivElement>) => {
const { currentSlide } = useContext(ImageSliderContext)
const isInView = currentSlide === slideId
const { currentSlideId } = useContext(ImageSliderContext)
const isInView = currentSlideId === slideId

return (
<div
Expand Down
Loading