react-datocms 5.0.3 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -13
- package/dist/cjs/Image/index.js +39 -138
- package/dist/cjs/Image/index.js.map +1 -1
- package/dist/cjs/Image/utils.js +52 -0
- package/dist/cjs/Image/utils.js.map +1 -0
- package/dist/cjs/SRCImage/index.js +44 -0
- package/dist/cjs/SRCImage/index.js.map +1 -0
- package/dist/cjs/SRCImage/utils.js +82 -0
- package/dist/cjs/SRCImage/utils.js.map +1 -0
- package/dist/cjs/Seo/nextUtils.js +15 -15
- package/dist/cjs/Seo/nextUtils.js.map +1 -1
- package/dist/cjs/Seo/renderMetaTags.js +1 -1
- package/dist/cjs/Seo/renderMetaTags.js.map +1 -1
- package/dist/cjs/StructuredText/index.js.map +1 -1
- package/dist/cjs/VideoPlayer/index.js +1 -1
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/useSiteSearch/index.js +9 -31
- package/dist/cjs/useSiteSearch/index.js.map +1 -1
- package/dist/cjs/useVideoPlayer/index.js.map +1 -1
- package/dist/esm/Image/index.js +31 -110
- package/dist/esm/Image/index.js.map +1 -1
- package/dist/esm/Image/utils.js +46 -0
- package/dist/esm/Image/utils.js.map +1 -0
- package/dist/esm/SRCImage/index.js +37 -0
- package/dist/esm/SRCImage/index.js.map +1 -0
- package/dist/esm/SRCImage/utils.js +52 -0
- package/dist/esm/SRCImage/utils.js.map +1 -0
- package/dist/esm/Seo/nextUtils.js +15 -15
- package/dist/esm/Seo/nextUtils.js.map +1 -1
- package/dist/esm/Seo/renderMetaTags.js +1 -1
- package/dist/esm/Seo/renderMetaTags.js.map +1 -1
- package/dist/esm/StructuredText/index.js.map +1 -1
- package/dist/esm/VideoPlayer/index.js +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/useSiteSearch/index.js +2 -1
- package/dist/esm/useSiteSearch/index.js.map +1 -1
- package/dist/esm/useVideoPlayer/index.js.map +1 -1
- package/dist/types/Image/index.d.ts +3 -4
- package/dist/types/Image/utils.d.ts +7 -0
- package/dist/types/SRCImage/index.d.ts +33 -0
- package/dist/types/SRCImage/utils.d.ts +6 -0
- package/dist/types/Seo/remixUtils.d.ts +1 -1
- package/dist/types/Seo/renderMetaTags.d.ts +1 -1
- package/dist/types/Seo/renderMetaTagsToString.d.ts +1 -1
- package/dist/types/StructuredText/index.d.ts +3 -3
- package/dist/types/VideoPlayer/index.d.ts +1 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/useQuerySubscription/index.d.ts +1 -1
- package/dist/types/useVideoPlayer/index.d.ts +2 -2
- package/package.json +3 -4
- package/src/Image/__tests__/__snapshots__/index.test.tsx.snap +387 -60
- package/src/Image/__tests__/index.test.tsx +55 -8
- package/src/Image/index.tsx +65 -177
- package/src/Image/utils.ts +58 -0
- package/src/SRCImage/__tests__/__snapshots__/index.test.tsx.snap +274 -0
- package/src/SRCImage/__tests__/index.test.tsx +91 -0
- package/src/SRCImage/index.tsx +99 -0
- package/src/SRCImage/utils.tsx +95 -0
- package/src/Seo/__tests__/index.test.tsx +1 -1
- package/src/Seo/nextUtils.ts +20 -20
- package/src/Seo/remixUtils.ts +1 -1
- package/src/Seo/renderMetaTags.tsx +2 -2
- package/src/Seo/renderMetaTagsToString.tsx +1 -1
- package/src/StructuredText/__tests__/index.test.tsx +2 -2
- package/src/StructuredText/index.tsx +10 -10
- package/src/VideoPlayer/index.tsx +2 -2
- package/src/index.ts +2 -1
- package/src/useQuerySubscription/index.ts +4 -4
- package/src/useSiteSearch/index.tsx +29 -28
- package/src/useVideoPlayer/index.ts +3 -6
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { mount } from 'enzyme';
|
|
2
2
|
import 'intersection-observer';
|
|
3
|
-
import
|
|
3
|
+
import React from 'react';
|
|
4
4
|
import { mockAllIsIntersecting } from 'react-intersection-observer/test-utils';
|
|
5
5
|
import { Image } from '../index.js';
|
|
6
6
|
|
|
7
7
|
const data = {
|
|
8
8
|
alt: 'DatoCMS swag',
|
|
9
9
|
aspectRatio: 1.7777777777777777,
|
|
10
|
-
base64:
|
|
11
|
-
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAoHBwgHBgoICAgLFQoLDhgQDg0NDh0eHREYIx8lJCIrHB0dLSs7GikyKSEuKjUlKDk1MjIyHyo4PTc+PDcxPjUBCgsLDg0OHBAQHDsoIig7Ozs7Ozs7OzsvOzs7Ozs7Ozs7Lzs7Ozs7Ozs7OzsvOzs7NTsvLy87NTU1Ly8vLzsvL//AABEIAA0AGAMBIgACEQEDEQH/xAAYAAACAwAAAAAAAAAAAAAAAAAGBwABBP/EACEQAAEEAAYDAAAAAAAAAAAAAAEAAgMEBQYHESEiFWFx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAwL/xAAZEQADAAMAAAAAAAAAAAAAAAAAAQIRITH/2gAMAwEAAhEDEQA/AFxLgDWTsAd1J5TGy7hEYqNAaNgECX7sjLMQAHJTEy1Zcarfia4lJMauAxqBhLY6ZlaOzDurWvUOd3jZPfCiEh4xs//Z',
|
|
10
|
+
base64: 'data:image/jpeg;base64,<IMAGE-DATA>',
|
|
12
11
|
height: 421,
|
|
13
12
|
sizes: '(max-width: 750px) 100vw, 750px',
|
|
14
13
|
src: 'https://www.datocms-assets.com/205/image.png?ar=16%3A9&fit=crop&w=750',
|
|
@@ -19,16 +18,14 @@ const data = {
|
|
|
19
18
|
};
|
|
20
19
|
|
|
21
20
|
const minimalData = {
|
|
22
|
-
base64:
|
|
23
|
-
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAoHBwgHBgoICAgLFQoLDhgQDg0NDh0eHREYIx8lJCIrHB0dLSs7GikyKSEuKjUlKDk1MjIyHyo4PTc+PDcxPjUBCgsLDg0OHBAQHDsoIig7Ozs7Ozs7OzsvOzs7Ozs7Ozs7Lzs7Ozs7Ozs7OzsvOzs7NTsvLy87NTU1Ly8vLzsvL//AABEIAA0AGAMBIgACEQEDEQH/xAAYAAACAwAAAAAAAAAAAAAAAAAGBwABBP/EACEQAAEEAAYDAAAAAAAAAAAAAAEAAgMEBQYHESEiFWFx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAwL/xAAZEQADAAMAAAAAAAAAAAAAAAAAAQIRITH/2gAMAwEAAhEDEQA/AFxLgDWTsAd1J5TGy7hEYqNAaNgECX7sjLMQAHJTEy1Zcarfia4lJMauAxqBhLY6ZlaOzDurWvUOd3jZPfCiEh4xs//Z',
|
|
21
|
+
base64: 'data:image/jpeg;base64,<IMAGE-DATA>',
|
|
24
22
|
height: 421,
|
|
25
23
|
src: 'https://www.datocms-assets.com/205/image.png?ar=16%3A9&fit=crop&w=750',
|
|
26
24
|
width: 750,
|
|
27
25
|
};
|
|
28
26
|
|
|
29
27
|
const minimalDataWithRelativeUrl = {
|
|
30
|
-
base64:
|
|
31
|
-
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAoHBwgHBgoICAgLFQoLDhgQDg0NDh0eHREYIx8lJCIrHB0dLSs7GikyKSEuKjUlKDk1MjIyHyo4PTc+PDcxPjUBCgsLDg0OHBAQHDsoIig7Ozs7Ozs7OzsvOzs7Ozs7Ozs7Lzs7Ozs7Ozs7OzsvOzs7NTsvLy87NTU1Ly8vLzsvL//AABEIAA0AGAMBIgACEQEDEQH/xAAYAAACAwAAAAAAAAAAAAAAAAAGBwABBP/EACEQAAEEAAYDAAAAAAAAAAAAAAEAAgMEBQYHESEiFWFx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAwL/xAAZEQADAAMAAAAAAAAAAAAAAAAAAQIRITH/2gAMAwEAAhEDEQA/AFxLgDWTsAd1J5TGy7hEYqNAaNgECX7sjLMQAHJTEy1Zcarfia4lJMauAxqBhLY6ZlaOzDurWvUOd3jZPfCiEh4xs//Z',
|
|
28
|
+
base64: 'data:image/jpeg;base64,<IMAGE-DATA>',
|
|
32
29
|
height: 421,
|
|
33
30
|
src: '/205/image.png?ar=16%3A9&fit=crop&w=750',
|
|
34
31
|
width: 750,
|
|
@@ -39,7 +36,7 @@ describe('Image', () => {
|
|
|
39
36
|
// we need the library to generate a different IntersectionObserver for each test
|
|
40
37
|
// otherwise the IntersectionObserver mocking won't work
|
|
41
38
|
|
|
42
|
-
(['intrinsic', 'fixed', 'responsive', 'fill'] as const)
|
|
39
|
+
for (const layout of ['intrinsic', 'fixed', 'responsive', 'fill'] as const) {
|
|
43
40
|
describe(`layout=${layout}`, () => {
|
|
44
41
|
describe('not visible', () => {
|
|
45
42
|
it('renders the blur-up thumb', () => {
|
|
@@ -100,5 +97,55 @@ describe('Image', () => {
|
|
|
100
97
|
});
|
|
101
98
|
});
|
|
102
99
|
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe('passing className and/or style', () => {
|
|
103
|
+
it('renders correctly', () => {
|
|
104
|
+
const wrapper = mount(
|
|
105
|
+
<Image
|
|
106
|
+
data={minimalData}
|
|
107
|
+
className="class-name"
|
|
108
|
+
style={{ border: '1px solid red' }}
|
|
109
|
+
pictureClassName="picture-class-name"
|
|
110
|
+
pictureStyle={{ border: '1px solid yellow ' }}
|
|
111
|
+
placeholderClassName="placeholder-class-name"
|
|
112
|
+
placeholderStyle={{ border: '1px solid green ' }}
|
|
113
|
+
/>,
|
|
114
|
+
);
|
|
115
|
+
mockAllIsIntersecting(true);
|
|
116
|
+
wrapper.update();
|
|
117
|
+
expect(wrapper).toMatchSnapshot();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('priority=true', () => {
|
|
122
|
+
it('renders correctly', () => {
|
|
123
|
+
const wrapper = mount(<Image data={minimalData} priority={true} />);
|
|
124
|
+
mockAllIsIntersecting(true);
|
|
125
|
+
wrapper.update();
|
|
126
|
+
expect(wrapper).toMatchSnapshot();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('usePlaceholder=false', () => {
|
|
131
|
+
it('renders correctly', () => {
|
|
132
|
+
const wrapper = mount(
|
|
133
|
+
<Image data={minimalData} usePlaceholder={false} />,
|
|
134
|
+
);
|
|
135
|
+
mockAllIsIntersecting(true);
|
|
136
|
+
wrapper.update();
|
|
137
|
+
expect(wrapper).toMatchSnapshot();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('explicit sizes', () => {
|
|
142
|
+
it('renders correctly', () => {
|
|
143
|
+
const wrapper = mount(
|
|
144
|
+
<Image data={minimalData} sizes="(max-width: 600px) 200px, 50vw" />,
|
|
145
|
+
);
|
|
146
|
+
mockAllIsIntersecting(true);
|
|
147
|
+
wrapper.update();
|
|
148
|
+
expect(wrapper).toMatchSnapshot();
|
|
149
|
+
});
|
|
103
150
|
});
|
|
104
151
|
});
|
package/src/Image/index.tsx
CHANGED
|
@@ -1,39 +1,22 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from 'react';
|
|
12
|
-
import { encode } from 'universal-base64';
|
|
3
|
+
// biome-ignore lint/style/useImportType: wrong warning
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { type CSSProperties, forwardRef, useRef } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
buildRegularSource,
|
|
8
|
+
buildWebpSource,
|
|
9
|
+
priorityProp,
|
|
10
|
+
} from '../SRCImage/utils.js';
|
|
13
11
|
import { useInView } from './useInView.js';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
fetchPriority?: string,
|
|
23
|
-
): Record<string, string | undefined> {
|
|
24
|
-
const [majorStr, minorStr] = version.split('.');
|
|
25
|
-
const major = parseInt(majorStr, 10);
|
|
26
|
-
const minor = parseInt(minorStr, 10);
|
|
27
|
-
if (major > 18 || (major === 18 && minor >= 3)) {
|
|
28
|
-
// In React 18.3.0 or newer, we must use camelCase
|
|
29
|
-
// prop to avoid "Warning: Invalid DOM property".
|
|
30
|
-
// See https://github.com/facebook/react/pull/25927
|
|
31
|
-
return { fetchPriority };
|
|
32
|
-
}
|
|
33
|
-
// In React 18.2.0 or older, we must use lowercase prop
|
|
34
|
-
// to avoid "Warning: Invalid DOM property".
|
|
35
|
-
return { fetchpriority: fetchPriority };
|
|
36
|
-
}
|
|
12
|
+
import {
|
|
13
|
+
absolutePositioning,
|
|
14
|
+
isIntersectionObserverAvailable,
|
|
15
|
+
isSsr,
|
|
16
|
+
universalBtoa,
|
|
17
|
+
useImageLoad,
|
|
18
|
+
useMergedRef,
|
|
19
|
+
} from './utils.js';
|
|
37
20
|
|
|
38
21
|
type Maybe<T> = T | null;
|
|
39
22
|
|
|
@@ -71,7 +54,7 @@ export type ImagePropTypes = {
|
|
|
71
54
|
pictureClassName?: string;
|
|
72
55
|
/** Additional CSS class for the placeholder image */
|
|
73
56
|
placeholderClassName?: string;
|
|
74
|
-
/** Duration (in ms) of the fade-in transition effect
|
|
57
|
+
/** Duration (in ms) of the fade-in transition effect upon image loading */
|
|
75
58
|
fadeInDuration?: number;
|
|
76
59
|
/** @deprecated Use the intersectionThreshold prop */
|
|
77
60
|
intersectionTreshold?: number;
|
|
@@ -79,8 +62,6 @@ export type ImagePropTypes = {
|
|
|
79
62
|
intersectionThreshold?: number;
|
|
80
63
|
/** 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 */
|
|
81
64
|
intersectionMargin?: string;
|
|
82
|
-
/** Whether enable lazy loading or not */
|
|
83
|
-
lazyLoad?: boolean;
|
|
84
65
|
/** Additional CSS rules to add to the root node */
|
|
85
66
|
style?: React.CSSProperties;
|
|
86
67
|
/** Additional CSS rules to add to the image inside the `<picture />` tag */
|
|
@@ -129,13 +110,13 @@ export type ImagePropTypes = {
|
|
|
129
110
|
};
|
|
130
111
|
|
|
131
112
|
type State = {
|
|
132
|
-
|
|
113
|
+
priority: boolean;
|
|
133
114
|
inView: boolean;
|
|
134
115
|
loaded: boolean;
|
|
135
116
|
};
|
|
136
117
|
|
|
137
|
-
const imageAddStrategy = ({
|
|
138
|
-
if (
|
|
118
|
+
const imageAddStrategy = ({ priority, inView, loaded }: State) => {
|
|
119
|
+
if (priority) {
|
|
139
120
|
return true;
|
|
140
121
|
}
|
|
141
122
|
|
|
@@ -150,8 +131,8 @@ const imageAddStrategy = ({ lazyLoad, inView, loaded }: State) => {
|
|
|
150
131
|
return true;
|
|
151
132
|
};
|
|
152
133
|
|
|
153
|
-
const imageShowStrategy = ({
|
|
154
|
-
if (
|
|
134
|
+
const imageShowStrategy = ({ priority, loaded }: State) => {
|
|
135
|
+
if (priority) {
|
|
155
136
|
return true;
|
|
156
137
|
}
|
|
157
138
|
|
|
@@ -166,51 +147,6 @@ const imageShowStrategy = ({ lazyLoad, loaded }: State) => {
|
|
|
166
147
|
return true;
|
|
167
148
|
};
|
|
168
149
|
|
|
169
|
-
const bogusBaseUrl = 'https://example.com/';
|
|
170
|
-
|
|
171
|
-
const buildSrcSet = (
|
|
172
|
-
src: string | null | undefined,
|
|
173
|
-
width: number | undefined,
|
|
174
|
-
candidateMultipliers: number[],
|
|
175
|
-
) => {
|
|
176
|
-
if (!(src && width)) {
|
|
177
|
-
return undefined;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return candidateMultipliers
|
|
181
|
-
.map((multiplier) => {
|
|
182
|
-
const url = new URL(src, bogusBaseUrl);
|
|
183
|
-
|
|
184
|
-
if (multiplier !== 1) {
|
|
185
|
-
url.searchParams.set('dpr', `${multiplier}`);
|
|
186
|
-
const maxH = url.searchParams.get('max-h');
|
|
187
|
-
const maxW = url.searchParams.get('max-w');
|
|
188
|
-
if (maxH) {
|
|
189
|
-
url.searchParams.set(
|
|
190
|
-
'max-h',
|
|
191
|
-
`${Math.floor(parseInt(maxH) * multiplier)}`,
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
if (maxW) {
|
|
195
|
-
url.searchParams.set(
|
|
196
|
-
'max-w',
|
|
197
|
-
`${Math.floor(parseInt(maxW) * multiplier)}`,
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const finalWidth = Math.floor(width * multiplier);
|
|
203
|
-
|
|
204
|
-
if (finalWidth < 50) {
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return `${url.toString().replace(bogusBaseUrl, '/')} ${finalWidth}w`;
|
|
209
|
-
})
|
|
210
|
-
.filter(Boolean)
|
|
211
|
-
.join(',');
|
|
212
|
-
};
|
|
213
|
-
|
|
214
150
|
export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
215
151
|
(
|
|
216
152
|
{
|
|
@@ -220,7 +156,6 @@ export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
|
220
156
|
intersectionThreshold,
|
|
221
157
|
intersectionMargin,
|
|
222
158
|
pictureClassName,
|
|
223
|
-
lazyLoad: rawLazyLoad = true,
|
|
224
159
|
style,
|
|
225
160
|
pictureStyle,
|
|
226
161
|
layout = 'intrinsic',
|
|
@@ -237,27 +172,9 @@ export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
|
237
172
|
},
|
|
238
173
|
ref,
|
|
239
174
|
) => {
|
|
240
|
-
const lazyLoad = priority ? false : rawLazyLoad;
|
|
241
|
-
|
|
242
|
-
const [loaded, setLoaded] = useState(false);
|
|
243
|
-
|
|
244
175
|
const imageRef = useRef<HTMLImageElement>(null);
|
|
245
176
|
|
|
246
|
-
const handleLoad = ()
|
|
247
|
-
onLoad?.();
|
|
248
|
-
setLoaded(true);
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
// https://stackoverflow.com/q/39777833/266535
|
|
252
|
-
useEffect(() => {
|
|
253
|
-
if (!imageRef.current) {
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (imageRef.current.complete && imageRef.current.naturalWidth) {
|
|
258
|
-
handleLoad();
|
|
259
|
-
}
|
|
260
|
-
}, []);
|
|
177
|
+
const [loaded, handleLoad] = useImageLoad(imageRef, onLoad);
|
|
261
178
|
|
|
262
179
|
const [viewRef, inView] = useInView({
|
|
263
180
|
threshold: intersectionThreshold || intersectionTreshold || 0,
|
|
@@ -266,82 +183,53 @@ export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
|
266
183
|
fallbackInView: true,
|
|
267
184
|
});
|
|
268
185
|
|
|
269
|
-
const
|
|
270
|
-
(_ref: HTMLDivElement) => {
|
|
271
|
-
viewRef(_ref);
|
|
272
|
-
if (ref) {
|
|
273
|
-
(ref as React.MutableRefObject<HTMLDivElement>).current = _ref;
|
|
274
|
-
}
|
|
275
|
-
},
|
|
276
|
-
[viewRef],
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
const absolutePositioning: React.CSSProperties = {
|
|
280
|
-
position: 'absolute',
|
|
281
|
-
left: 0,
|
|
282
|
-
top: 0,
|
|
283
|
-
width: '100%',
|
|
284
|
-
height: '100%',
|
|
285
|
-
maxWidth: 'none',
|
|
286
|
-
maxHeight: 'none',
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
const addImage = imageAddStrategy({
|
|
290
|
-
lazyLoad,
|
|
291
|
-
inView,
|
|
292
|
-
loaded,
|
|
293
|
-
});
|
|
294
|
-
const showImage = imageShowStrategy({
|
|
295
|
-
lazyLoad,
|
|
296
|
-
inView,
|
|
297
|
-
loaded,
|
|
298
|
-
});
|
|
186
|
+
const rootRef = useMergedRef(ref, viewRef);
|
|
299
187
|
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
srcSet={data.webpSrcSet}
|
|
303
|
-
sizes={sizes ?? data.sizes ?? undefined}
|
|
304
|
-
type="image/webp"
|
|
305
|
-
/>
|
|
306
|
-
);
|
|
188
|
+
const addImage = imageAddStrategy({ priority, inView, loaded });
|
|
189
|
+
const showImage = imageShowStrategy({ priority, inView, loaded });
|
|
307
190
|
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
srcSet={
|
|
311
|
-
data.srcSet ?? buildSrcSet(data.src, data.width, srcSetCandidates)
|
|
312
|
-
}
|
|
313
|
-
sizes={sizes ?? data.sizes ?? undefined}
|
|
314
|
-
/>
|
|
315
|
-
);
|
|
191
|
+
const webpSource = buildWebpSource(data, sizes);
|
|
192
|
+
const regularSource = buildRegularSource(data, sizes, srcSetCandidates);
|
|
316
193
|
|
|
317
194
|
const transition =
|
|
318
195
|
fadeInDuration > 0 ? `opacity ${fadeInDuration}ms` : undefined;
|
|
319
196
|
|
|
197
|
+
const basePlaceholderStyle: React.CSSProperties = {
|
|
198
|
+
transition,
|
|
199
|
+
opacity: showImage ? 0 : 1,
|
|
200
|
+
// During the opacity transition of the placeholder to the definitive version,
|
|
201
|
+
// hardware acceleration is triggered. This results in the browser trying to render the
|
|
202
|
+
// placeholder with your GPU, causing blurred edges. Solution: style the placeholder
|
|
203
|
+
// so the edges overflow the container
|
|
204
|
+
position: 'absolute',
|
|
205
|
+
left: '-5%',
|
|
206
|
+
top: '-5%',
|
|
207
|
+
width: '110%',
|
|
208
|
+
height: '110%',
|
|
209
|
+
maxWidth: 'none',
|
|
210
|
+
maxHeight: 'none',
|
|
211
|
+
...placeholderStyle,
|
|
212
|
+
};
|
|
213
|
+
|
|
320
214
|
const placeholder =
|
|
321
|
-
usePlaceholder &&
|
|
215
|
+
usePlaceholder && data.base64 ? (
|
|
322
216
|
<img
|
|
323
217
|
aria-hidden="true"
|
|
324
218
|
alt=""
|
|
325
|
-
src={data.base64
|
|
219
|
+
src={data.base64}
|
|
326
220
|
className={placeholderClassName}
|
|
327
221
|
style={{
|
|
328
|
-
backgroundColor: data.bgColor ?? undefined,
|
|
329
222
|
objectFit,
|
|
330
223
|
objectPosition,
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
width: '110%',
|
|
341
|
-
height: '110%',
|
|
342
|
-
maxWidth: 'none',
|
|
343
|
-
maxHeight: 'none',
|
|
344
|
-
...placeholderStyle,
|
|
224
|
+
...basePlaceholderStyle,
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
227
|
+
) : usePlaceholder && data.bgColor ? (
|
|
228
|
+
<div
|
|
229
|
+
className={placeholderClassName}
|
|
230
|
+
style={{
|
|
231
|
+
backgroundColor: data.bgColor,
|
|
232
|
+
...basePlaceholderStyle,
|
|
345
233
|
}}
|
|
346
234
|
/>
|
|
347
235
|
) : null;
|
|
@@ -360,7 +248,7 @@ export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
|
360
248
|
width: '100%',
|
|
361
249
|
...pictureStyle,
|
|
362
250
|
}}
|
|
363
|
-
src={`data:image/svg+xml;base64,${
|
|
251
|
+
src={`data:image/svg+xml;base64,${universalBtoa(svg)}`}
|
|
364
252
|
aria-hidden="true"
|
|
365
253
|
alt=""
|
|
366
254
|
/>
|
|
@@ -368,17 +256,17 @@ export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
|
368
256
|
|
|
369
257
|
return (
|
|
370
258
|
<div
|
|
371
|
-
ref={
|
|
259
|
+
ref={rootRef}
|
|
372
260
|
className={className}
|
|
373
261
|
style={{
|
|
374
262
|
overflow: 'hidden',
|
|
375
263
|
...(layout === 'fill'
|
|
376
264
|
? absolutePositioning
|
|
377
265
|
: layout === 'intrinsic'
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
266
|
+
? { position: 'relative', width: '100%', maxWidth: width }
|
|
267
|
+
: layout === 'fixed'
|
|
268
|
+
? { position: 'relative', width }
|
|
269
|
+
: { position: 'relative', width: '100%' }),
|
|
382
270
|
...style,
|
|
383
271
|
}}
|
|
384
272
|
>
|
|
@@ -396,7 +284,7 @@ export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
|
396
284
|
alt={data.alt ?? ''}
|
|
397
285
|
title={data.title ?? undefined}
|
|
398
286
|
onLoad={handleLoad}
|
|
399
|
-
{...
|
|
287
|
+
{...priorityProp(priority ? 'high' : undefined)}
|
|
400
288
|
className={pictureClassName}
|
|
401
289
|
style={{
|
|
402
290
|
opacity: showImage ? 1 : 0,
|
|
@@ -427,8 +315,8 @@ export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
|
427
315
|
objectPosition,
|
|
428
316
|
...pictureStyle,
|
|
429
317
|
}}
|
|
430
|
-
loading={
|
|
431
|
-
{...
|
|
318
|
+
loading={priority ? undefined : 'lazy'}
|
|
319
|
+
{...priorityProp(priority ? 'high' : undefined)}
|
|
432
320
|
/>
|
|
433
321
|
)}
|
|
434
322
|
</picture>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export const isSsr = typeof window === 'undefined';
|
|
4
|
+
|
|
5
|
+
export const isIntersectionObserverAvailable = isSsr
|
|
6
|
+
? false
|
|
7
|
+
: !!window.IntersectionObserver;
|
|
8
|
+
|
|
9
|
+
export const universalBtoa = (str: string): string =>
|
|
10
|
+
isSsr
|
|
11
|
+
? Buffer.from(str.toString(), 'binary').toString('base64')
|
|
12
|
+
: window.btoa(str);
|
|
13
|
+
|
|
14
|
+
export function useImageLoad(
|
|
15
|
+
ref: React.RefObject<HTMLImageElement>,
|
|
16
|
+
callback: (() => void) | undefined,
|
|
17
|
+
) {
|
|
18
|
+
const [loaded, setLoaded] = useState(false);
|
|
19
|
+
|
|
20
|
+
function handleLoad() {
|
|
21
|
+
setLoaded(true);
|
|
22
|
+
callback?.();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// https://stackoverflow.com/q/39777833/266535
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!ref.current) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (ref.current.complete && ref.current.naturalWidth) {
|
|
32
|
+
handleLoad();
|
|
33
|
+
}
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
return [loaded, handleLoad] as const;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useMergedRef<T>(...refs: React.Ref<T>[]): React.RefCallback<T> {
|
|
40
|
+
return useCallback((element: T) => {
|
|
41
|
+
for (let i = 0; i < refs.length; i++) {
|
|
42
|
+
const ref = refs[i];
|
|
43
|
+
if (typeof ref === 'function') ref(element);
|
|
44
|
+
else if (ref && typeof ref === 'object')
|
|
45
|
+
(ref as React.MutableRefObject<T>).current = element;
|
|
46
|
+
}
|
|
47
|
+
}, refs);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const absolutePositioning: React.CSSProperties = {
|
|
51
|
+
position: 'absolute',
|
|
52
|
+
left: 0,
|
|
53
|
+
top: 0,
|
|
54
|
+
width: '100%',
|
|
55
|
+
height: '100%',
|
|
56
|
+
maxWidth: 'none',
|
|
57
|
+
maxHeight: 'none',
|
|
58
|
+
};
|