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

Improvements to the Image component #41

Merged
merged 10 commits into from
Jan 3, 2022
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
306 changes: 162 additions & 144 deletions src/Image/index.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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 `<picture />` 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 = {
Expand Down Expand Up @@ -99,144 +103,158 @@ const imageShowStrategy = ({ lazyLoad, loaded }: State) => {
return true;
};

export const Image: React.FC<ImagePropTypes> = function ({
className,
fadeInDuration,
intersectionTreshold,
intersectionThreshold,
intersectionMargin,
pictureClassName,
lazyLoad = true,
style,
pictureStyle,
explicitWidth,
data,
}) {
const [loaded, setLoaded] = useState<boolean>(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 && (
<source srcSet={data.webpSrcSet} sizes={data.sizes} type="image/webp" />
);

const regularSource = data.srcSet && (
<source srcSet={data.srcSet} sizes={data.sizes} />
);

const transition =
typeof fadeInDuration === "undefined" || fadeInDuration > 0
? `opacity ${fadeInDuration || 500}ms ${fadeInDuration || 500}ms`
: undefined;

const placeholder = (
<div
style={{
backgroundImage: data.base64 ? `url(${data.base64})` : undefined,
backgroundColor: data.bgColor,
backgroundSize: "cover",
opacity: showImage ? 0 : 1,
transition,
...absolutePositioning,
}}
/>
);

const { width, aspectRatio } = data;
const height = data.height || width / aspectRatio;

const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"></svg>`;

const sizer = (
<img
className={pictureClassName}
style={{
display: "block",
width: explicitWidth ? `${width}px` : "100%",
...pictureStyle,
}}
src={`data:image/svg+xml;base64,${universalBtoa(svg)}`}
role="presentation"
/>
);

return (
<div
ref={ref}
className={className}
style={{
display: explicitWidth ? "inline-block" : "block",
overflow: "hidden",
...style,
position: "relative",
}}
>
{sizer}
{placeholder}
{addImage && (
<picture>
{webpSource}
{regularSource}
{data.src && (
<img
src={data.src}
alt={data.alt ?? ''}
title={data.title}
onLoad={handleLoad}
className={pictureClassName}
style={{
...absolutePositioning,
...pictureStyle,
opacity: showImage ? 1 : 0,
transition,
}}
/>
)}
</picture>
)}
<noscript>
<picture>
{webpSource}
{regularSource}
{data.src && (
<img
src={data.src}
alt={data.alt ?? ''}
title={data.title}
className={pictureClassName}
style={{ ...absolutePositioning, ...pictureStyle }}
loading="lazy"
/>
)}
</picture>
</noscript>
</div>
);
};
export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
(
{
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<HTMLDivElement>).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 && (
<source srcSet={data.webpSrcSet} sizes={data.sizes} type="image/webp" />
);

const regularSource = data.srcSet && (
<source srcSet={data.srcSet} sizes={data.sizes} />
);

const transition =
fadeInDuration > 0 ? `opacity ${fadeInDuration}ms` : undefined;

const placeholder = usePlaceholder ? (
<div
style={{
backgroundImage: data.base64 ? `url(${data.base64})` : undefined,
backgroundColor: data.bgColor,
backgroundSize: "cover",
opacity: showImage ? 0 : 1,
transition,
...absolutePositioning,
}}
/>
) : null;

const { width, aspectRatio } = data;
const height = data.height || width / aspectRatio;

const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"></svg>`;

const sizer = (
<img
className={pictureClassName}
style={{
display: "block",
width: explicitWidth ? `${width}px` : "100%",
...pictureStyle,
}}
src={`data:image/svg+xml;base64,${universalBtoa(svg)}`}
role="presentation"
/>
);

return (
<div
ref={callbackRef}
className={className}
style={{
display: explicitWidth ? "inline-block" : "block",
overflow: "hidden",
position: "relative",
...style,
}}
>
{sizer}
{placeholder}
{addImage && (
<picture>
{webpSource}
{regularSource}
{data.src && (
<img
src={data.src}
alt={data.alt}
title={data.title}
onLoad={handleLoad}
className={pictureClassName}
style={{
...absolutePositioning,
...pictureStyle,
opacity: showImage ? 1 : 0,
transition,
}}
/>
)}
</picture>
)}
<noscript>
<picture>
{webpSource}
{regularSource}
{data.src && (
<img
src={data.src}
alt={data.alt ?? ""}
title={data.title}
className={pictureClassName}
style={{ ...absolutePositioning, ...pictureStyle }}
loading="lazy"
/>
)}
</picture>
</noscript>
</div>
);
}
);