Files
web/packages/design-system/lib/components/Lightbox/FullView/index.tsx
T
Erik Tiekstra b437f8b806 feat(SW-3152): Respecting image aspect ratio inside image gallery/lightbox
* feat(SW-3152): Respecting image aspect ratio inside image gallery/lightbox
* feat(BOOK-144): Make image clickable instead of a button to avoid being able to click outside of the image area

Approved-by: Bianca Widstam
Approved-by: Chuma Mcphoy (We Ahead)
2025-09-15 09:02:48 +00:00

157 lines
3.9 KiB
TypeScript

'use client'
import { AnimatePresence, motion } from 'motion/react'
import { useEffect, useState } from 'react'
import { useIntl } from 'react-intl'
import Image from '../../Image'
import { IconButton } from '../../IconButton'
import { MaterialIcon } from '../../Icons/MaterialIcon'
import { Typography } from '../../Typography'
import { LightboxImage } from '../index'
import styles from './fullView.module.css'
type FullViewProps = {
image: LightboxImage
onClose: () => void
onNext: () => void
onPrev: () => void
currentIndex: number
totalImages: number
hideLabel?: boolean
}
export default function FullView({
image,
onClose,
onNext,
onPrev,
currentIndex,
totalImages,
hideLabel,
}: FullViewProps) {
const intl = useIntl()
const [animateLeft, setAnimateLeft] = useState(true)
function handleSwipe(offset: number) {
if (offset > 30) {
setAnimateLeft(false)
onPrev()
}
if (offset < -30) {
setAnimateLeft(true)
onNext()
}
}
function handleNext() {
setAnimateLeft(true)
onNext()
}
function handlePrev() {
setAnimateLeft(false)
onPrev()
}
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'ArrowLeft') {
handlePrev()
} else if (e.key === 'ArrowRight') {
handleNext()
}
}
useEffect(() => {
window.addEventListener('keydown', handleKeyDown)
return () => {
window.removeEventListener('keydown', handleKeyDown)
}
})
const variants = {
initial: (animateLeft: boolean) => ({
opacity: 0,
x: animateLeft ? 300 : -300,
}),
animate: { opacity: 1, x: 0 },
exit: (animateLeft: boolean) => ({
opacity: 0,
x: animateLeft ? -300 : 300,
}),
}
return (
<div className={styles.fullView}>
<IconButton
theme="Inverted"
style="Muted"
className={styles.closeButton}
onPress={onClose}
aria-label={intl.formatMessage({
defaultMessage: 'Close',
})}
>
<MaterialIcon icon="close" color="CurrentColor" size={24} />
</IconButton>
<div className={styles.header}>
<Typography variant="Tag/sm">
<span className={styles.imageCount}>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{`${currentIndex + 1} / ${totalImages}`}
</span>
</Typography>
</div>
<div className={styles.content}>
<AnimatePresence initial={false} custom={animateLeft}>
<motion.div
key={image.src}
custom={animateLeft}
variants={variants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.3 }}
className={styles.motionContainer}
drag="x"
onDragEnd={(_e, info) => handleSwipe(info.offset.x)}
>
<div className={styles.imageWrapper}>
<Image
alt={image.alt}
fill
sizes="90vw"
src={image.src}
className={styles.image}
/>
{image.caption && !hideLabel ? (
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.caption}>{image.caption}</p>
</Typography>
) : null}
</div>
</motion.div>
</AnimatePresence>
</div>
<IconButton
theme="Inverted"
className={`${styles.navigationButton} ${styles.prev}`}
onClick={handlePrev}
>
<MaterialIcon icon="arrow_back" color="CurrentColor" />
</IconButton>
<IconButton
theme="Inverted"
className={`${styles.navigationButton} ${styles.next}`}
onClick={handleNext}
>
<MaterialIcon icon="arrow_forward" color="CurrentColor" />
</IconButton>
</div>
)
}