diff --git a/README.md b/README.md index ab151f2..e3fb1a2 100644 --- a/README.md +++ b/README.md @@ -247,8 +247,10 @@ export default withQuery(query)(Page); | fadeInDuration | integer | :x: | Duration (in ms) of the fade-in transition effect upoad image loading | 500 | | intersectionThreshold | float | :x: | Indicate at what percentage of the placeholder visibility the loading of the image should be triggered. A value of 0 means that as soon as even one pixel is visible, the callback will be run. A value of 1.0 means that the threshold isn't considered passed until every pixel is visible. | 0 | | intersectionMargin | string | :x: | Margin around the placeholder. Can have values similar to the CSS margin property (top, right, bottom, left). The values can be percentages. This set of values serves to grow or shrink each side of the placeholder element's bounding box before computing intersections. | "0px 0px 0px 0px" | -| lazyLoad | Boolean | :x: | Wheter enable lazy loading or not | true | -| explicitWidth | Boolean | :x: | Wheter the image wrapper should explicitely declare the width of the image or keep it fluid | false | +| lazyLoad | Boolean | :x: | Whether enable lazy loading or not | true | +| explicitWidth | Boolean | :x: | Whether the image wrapper should explicitely declare the width of the image or keep it fluid | false | +| onLoad | () => void | :x: | Function triggered when the image has finished loading | undefined | +| usePlaceholder | Boolean | :x: | Whether the component should use a blurred image placeholder | true | ### The `ResponsiveImage` object diff --git a/src/Image/index.tsx b/src/Image/index.tsx index 4ae4886..712bb86 100644 --- a/src/Image/index.tsx +++ b/src/Image/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from "react"; +import React, { useState, forwardRef, useCallback } from "react"; import "intersection-observer"; import { useInView } from "react-intersection-observer"; @@ -51,14 +51,18 @@ type ImagePropTypes = { intersectionThreshold?: number; /** Margin around the placeholder. Can have values similar to the CSS margin property (top, right, bottom, left). The values can be percentages. This set of values serves to grow or shrink each side of the placeholder element's bounding box before computing intersections */ intersectionMargin?: string; - /** Wheter enable lazy loading or not */ + /** Whether enable lazy loading or not */ lazyLoad?: boolean; /** Additional CSS rules to add to the root node */ style?: React.CSSProperties; /** Additional CSS rules to add to the image inside the `` tag */ pictureStyle?: React.CSSProperties; - /** Wheter the image wrapper should explicitely declare the width of the image or keep it fluid */ + /** Whether the image wrapper should explicitely declare the width of the image or keep it fluid */ explicitWidth?: boolean; + /** Triggered when the image finishes loading */ + onLoad?(): void; + /** Whether the component should use a blurred image placeholder */ + usePlaceholder?: boolean; }; type State = { @@ -99,144 +103,158 @@ const imageShowStrategy = ({ lazyLoad, loaded }: State) => { return true; }; -export const Image: React.FC = function ({ - className, - fadeInDuration, - intersectionTreshold, - intersectionThreshold, - intersectionMargin, - pictureClassName, - lazyLoad = true, - style, - pictureStyle, - explicitWidth, - data, -}) { - const [loaded, setLoaded] = useState(false); - - const handleLoad = useCallback(() => { - setLoaded(true); - }, []); - - const { ref, inView } = useInView({ - threshold: intersectionThreshold || intersectionTreshold || 0, - rootMargin: intersectionMargin || "0px 0px 0px 0px", - triggerOnce: true, - }); - - const absolutePositioning: React.CSSProperties = { - position: "absolute", - left: 0, - top: 0, - width: "100%", - height: "100%", - }; - - const addImage = imageAddStrategy({ - lazyLoad, - inView, - loaded, - }); - const showImage = imageShowStrategy({ - lazyLoad, - inView, - loaded, - }); - - const webpSource = data.webpSrcSet && ( - - ); - - const regularSource = data.srcSet && ( - - ); - - const transition = - typeof fadeInDuration === "undefined" || fadeInDuration > 0 - ? `opacity ${fadeInDuration || 500}ms ${fadeInDuration || 500}ms` - : undefined; - - const placeholder = ( -
- ); - - const { width, aspectRatio } = data; - const height = data.height || width / aspectRatio; - - const svg = ``; - - const sizer = ( - - ); - - return ( -
- {sizer} - {placeholder} - {addImage && ( - - {webpSource} - {regularSource} - {data.src && ( - {data.alt - )} - - )} - -
- ); -}; +export const Image = forwardRef( + ( + { + className, + fadeInDuration = 500, + intersectionTreshold, + intersectionThreshold, + intersectionMargin, + pictureClassName, + lazyLoad = true, + style, + pictureStyle, + explicitWidth, + data, + onLoad, + usePlaceholder = true, + }, + ref + ) => { + const [loaded, setLoaded] = useState(false); + + const handleLoad = () => { + onLoad?.(); + setLoaded(true); + }; + + const [viewRef, inView] = useInView({ + threshold: intersectionThreshold || intersectionTreshold || 0, + rootMargin: intersectionMargin || "0px 0px 0px 0px", + triggerOnce: true, + }); + + const callbackRef = useCallback( + (_ref: HTMLDivElement) => { + viewRef(_ref); + if (ref) (ref as React.MutableRefObject).current = _ref; + }, + [viewRef] + ); + + const absolutePositioning: React.CSSProperties = { + position: "absolute", + left: 0, + top: 0, + width: "100%", + height: "100%", + }; + + const addImage = imageAddStrategy({ + lazyLoad, + inView, + loaded, + }); + const showImage = imageShowStrategy({ + lazyLoad, + inView, + loaded, + }); + + const webpSource = data.webpSrcSet && ( + + ); + + const regularSource = data.srcSet && ( + + ); + + const transition = + fadeInDuration > 0 ? `opacity ${fadeInDuration}ms` : undefined; + + const placeholder = usePlaceholder ? ( +
+ ) : null; + + const { width, aspectRatio } = data; + const height = data.height || width / aspectRatio; + + const svg = ``; + + const sizer = ( + + ); + + return ( +
+ {sizer} + {placeholder} + {addImage && ( + + {webpSource} + {regularSource} + {data.src && ( + {data.alt} + )} + + )} + +
+ ); + } +);