import React from 'react'; import { Box, BoxProps, useSafeLayoutEffect } from '@stacks/ui'; import { useSpring, animated, config } from 'react-spring'; import { useInView } from 'react-intersection-observer'; import { makeCancelable } from '@common/utils'; import { ForwardRefExoticComponentWithAs, forwardRefWithAs } from '@stacks/ui-core'; interface ImageProps { /** The source of the image to load */ src: string; /** The source set of the image to load */ srcSet?: string; /** The alt text description of the image you are loading */ alt?: string; /** Sizes descriptor */ sizes?: string; } const loadImage = ( { src, srcSet, alt, sizes }: ImageProps, experimentalDecode = false ): Promise => // eslint-disable-next-line @typescript-eslint/no-misused-promises new Promise((resolve, reject) => { if (typeof Image !== 'undefined') { const image = new Image(); if (srcSet) { image.srcset = srcSet; } if (alt) { image.alt = alt; } if (sizes) { image.sizes = sizes; } image.src = src; /** @see: https://www.chromestatus.com/feature/5637156160667648 */ if (experimentalDecode && 'decode' in image) { return ( image // NOTE: .decode() is not in the TS defs yet // TODO: consider writing the .decode() definition and sending a PR //@ts-ignore .decode() //@ts-ignore .then((image: HTMLImageElement) => resolve(image)) .catch((err: any) => reject(err)) ); } image.onload = resolve; image.onerror = reject; } }); export const LazyImage: ForwardRefExoticComponentWithAs = forwardRefWithAs< BoxProps, 'img' >(({ as = 'img', src, srcSet, style = {}, placeholder, ...props }, forwardedRef) => { const [ref, inView] = useInView({ triggerOnce: true, rootMargin: '200px 0px', }); const [loading, setLoading] = React.useState(false); const [source, setSrc] = React.useState({ src: undefined, srcSet: undefined, }); const loadingPromise = makeCancelable(loadImage({ src, srcSet }, true)); const onLoad = React.useCallback( () => requestAnimationFrame(() => { console.log('on-load'); setSrc({ src, srcSet }); }), [] ); useSafeLayoutEffect(() => { if (!source.src && !loading && inView) { setLoading(true); loadingPromise.promise .then(_res => { console.log('loaded'); onLoad(); }) .catch(e => { // If the Loading Promise was canceled, it means we have stopped // loading due to unmount, rather than an error. if (!e.isCanceled) { console.error('failed to load image'); } }); } }, [source, loading, inView]); const styleProps = useSpring({ opacity: source.src ? 1 : 0, config: config.gentle }); const placeholderProps = useSpring({ opacity: source.src ? 0 : 1, config: config.gentle }); return ( {source.src ? ( ) : null} ); });