ImageModal
A ready-made Modal showing one image at full size with a loading placeholder; lazily loaded, so it needs a Suspense boundary.
import { ImageModal, ImageModalProps } from '@salla.sa/twilight-theme-engine/components/modal';In plain words
Click a small picture, see it big: ImageModal is a Modal already filled with one image, an optional title and a close button. While the image downloads, a grey pulsing box holds its place.
Its code is downloaded separately, the first time the component renders. So put it inside React's <Suspense>: a component that shows a fallback (or nothing) while something inside it is still loading.
Signature
const ImageModal: React.LazyExoticComponent<(props: ImageModalProps) => ReactNode>
interface ImageModalProps {
isOpen: boolean;
onClose: () => void;
src: string; // nothing renders while it is empty
alt?: string; // default ''
title?: string; // shown in the header
imgProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, 'src' | 'alt'>;
}Try it live
Loading products…
import { Suspense, useCallback, useState } from 'react';
import { ImageModal } from '@salla.sa/twilight-theme-engine/components/modal';
export function ZoomableImage({ src, alt }: { src: string; alt: string }) {
const [isOpen, setIsOpen] = useState(false);
const close = useCallback(() => setIsOpen(false), []);
return (
<>
<button type="button" onClick={() => setIsOpen(true)}>
<img src={src} alt={alt} width={96} height={96} />
</button>
<Suspense fallback={null}>
<ImageModal isOpen={isOpen} onClose={close} src={src} alt={alt} title={alt} />
</Suspense>
</>
);
}
Example
import { Suspense, useCallback, useState } from 'react';
import { ImageModal } from '@salla.sa/twilight-theme-engine/components/modal';
export function ZoomableImage({ src, alt }: { src: string; alt: string }) {
const [isOpen, setIsOpen] = useState(false);
const close = useCallback(() => setIsOpen(false), []);
return (
<>
<button type="button" onClick={() => setIsOpen(true)}>
<img src={src} alt={alt} width={120} height={120} />
</button>
<Suspense fallback={null}>
<ImageModal isOpen={isOpen} onClose={close} src={src} alt={alt} title={alt} />
</Suspense>
</>
);
}
How it behaves
The export is
React.lazy(() => import('./ImageModal')). The eager component is not exported under any name.It renders
<Modal size="lg" position="center">with<Modal.Header onClose showCloseButton>{title}</Modal.Header>and a centered body. The image ismax-w-full max-h-[70vh] object-contain.The image is keyed by
src, so opening a different picture shows the placeholder again until it loads. Returnsnullwhilesrcis empty, even withisOpen.The engine uses it for the product gallery zoom (
ProductGallery) and the tax certificate link in the footer.Everything on the Modal page applies, including the stable
onClose.
Gotchas
Because it is lazy, rendering it at all, even with
isOpen={false}, starts the download and suspends. Without your own<Suspense fallback={null}>around it, the nearest boundary above (possibly the whole page) shows its fallback while the chunk loads.imgPropsis spread after the component's ownonLoadandclassName(ImageModal.tsx). PassingimgProps.onLoadreplaces the handler that reveals the image, so it stays hidden behind the placeholder; passingimgProps.classNamereplaces the sizing and show/hide classes. Pass other attributes only, such asloadingordecoding.There is no error handling: an image that fails to load leaves the grey placeholder pulsing, with no message.