Skip to content

Commit

Permalink
Merge pull request #41 from dbismut/master
Browse files Browse the repository at this point in the history
Improvements to the Image component
  • Loading branch information
stefanoverna authored Jan 3, 2022
2 parents 976e85e + 5a15d3f commit e4a2119
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 146 deletions.
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>
);
}
);

0 comments on commit e4a2119

Please sign in to comment.