react-datocms 5.0.2 → 6.0.0
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 +38 -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 +43 -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/remixUtils.js +1 -1
- package/dist/cjs/Seo/remixUtils.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.map +1 -1
- package/dist/cjs/useVideoPlayer/index.js.map +1 -1
- package/dist/esm/Image/index.js +30 -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 +36 -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/remixUtils.js +1 -1
- package/dist/esm/Seo/remixUtils.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.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/index.d.ts +2 -1
- 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 +64 -177
- package/src/Image/utils.ts +58 -0
- package/src/SRCImage/__tests__/__snapshots__/index.test.tsx.snap +268 -0
- package/src/SRCImage/__tests__/index.test.tsx +91 -0
- package/src/SRCImage/index.tsx +98 -0
- package/src/SRCImage/utils.tsx +95 -0
- package/src/Seo/remixUtils.ts +1 -1
- package/src/VideoPlayer/index.tsx +1 -1
- package/src/index.ts +2 -1
- package/src/useSiteSearch/index.tsx +27 -27
- package/src/useVideoPlayer/index.ts +1 -4
|
@@ -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,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
version,
|
|
11
|
-
} from 'react';
|
|
12
|
-
import { encode } from 'universal-base64';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { type CSSProperties, forwardRef, useRef } from 'react';
|
|
5
|
+
import {
|
|
6
|
+
buildRegularSource,
|
|
7
|
+
buildWebpSource,
|
|
8
|
+
priorityProp,
|
|
9
|
+
} from '../SRCImage/utils.js';
|
|
13
10
|
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
|
-
}
|
|
11
|
+
import {
|
|
12
|
+
absolutePositioning,
|
|
13
|
+
isIntersectionObserverAvailable,
|
|
14
|
+
isSsr,
|
|
15
|
+
universalBtoa,
|
|
16
|
+
useImageLoad,
|
|
17
|
+
useMergedRef,
|
|
18
|
+
} from './utils.js';
|
|
37
19
|
|
|
38
20
|
type Maybe<T> = T | null;
|
|
39
21
|
|
|
@@ -71,7 +53,7 @@ export type ImagePropTypes = {
|
|
|
71
53
|
pictureClassName?: string;
|
|
72
54
|
/** Additional CSS class for the placeholder image */
|
|
73
55
|
placeholderClassName?: string;
|
|
74
|
-
/** Duration (in ms) of the fade-in transition effect
|
|
56
|
+
/** Duration (in ms) of the fade-in transition effect upon image loading */
|
|
75
57
|
fadeInDuration?: number;
|
|
76
58
|
/** @deprecated Use the intersectionThreshold prop */
|
|
77
59
|
intersectionTreshold?: number;
|
|
@@ -79,8 +61,6 @@ export type ImagePropTypes = {
|
|
|
79
61
|
intersectionThreshold?: number;
|
|
80
62
|
/** 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
63
|
intersectionMargin?: string;
|
|
82
|
-
/** Whether enable lazy loading or not */
|
|
83
|
-
lazyLoad?: boolean;
|
|
84
64
|
/** Additional CSS rules to add to the root node */
|
|
85
65
|
style?: React.CSSProperties;
|
|
86
66
|
/** Additional CSS rules to add to the image inside the `<picture />` tag */
|
|
@@ -129,13 +109,13 @@ export type ImagePropTypes = {
|
|
|
129
109
|
};
|
|
130
110
|
|
|
131
111
|
type State = {
|
|
132
|
-
|
|
112
|
+
priority: boolean;
|
|
133
113
|
inView: boolean;
|
|
134
114
|
loaded: boolean;
|
|
135
115
|
};
|
|
136
116
|
|
|
137
|
-
const imageAddStrategy = ({
|
|
138
|
-
if (
|
|
117
|
+
const imageAddStrategy = ({ priority, inView, loaded }: State) => {
|
|
118
|
+
if (priority) {
|
|
139
119
|
return true;
|
|
140
120
|
}
|
|
141
121
|
|
|
@@ -150,8 +130,8 @@ const imageAddStrategy = ({ lazyLoad, inView, loaded }: State) => {
|
|
|
150
130
|
return true;
|
|
151
131
|
};
|
|
152
132
|
|
|
153
|
-
const imageShowStrategy = ({
|
|
154
|
-
if (
|
|
133
|
+
const imageShowStrategy = ({ priority, loaded }: State) => {
|
|
134
|
+
if (priority) {
|
|
155
135
|
return true;
|
|
156
136
|
}
|
|
157
137
|
|
|
@@ -166,51 +146,6 @@ const imageShowStrategy = ({ lazyLoad, loaded }: State) => {
|
|
|
166
146
|
return true;
|
|
167
147
|
};
|
|
168
148
|
|
|
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
149
|
export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
215
150
|
(
|
|
216
151
|
{
|
|
@@ -220,7 +155,6 @@ export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
|
220
155
|
intersectionThreshold,
|
|
221
156
|
intersectionMargin,
|
|
222
157
|
pictureClassName,
|
|
223
|
-
lazyLoad: rawLazyLoad = true,
|
|
224
158
|
style,
|
|
225
159
|
pictureStyle,
|
|
226
160
|
layout = 'intrinsic',
|
|
@@ -237,27 +171,9 @@ export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
|
237
171
|
},
|
|
238
172
|
ref,
|
|
239
173
|
) => {
|
|
240
|
-
const lazyLoad = priority ? false : rawLazyLoad;
|
|
241
|
-
|
|
242
|
-
const [loaded, setLoaded] = useState(false);
|
|
243
|
-
|
|
244
174
|
const imageRef = useRef<HTMLImageElement>(null);
|
|
245
175
|
|
|
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
|
-
}, []);
|
|
176
|
+
const [loaded, handleLoad] = useImageLoad(imageRef, onLoad);
|
|
261
177
|
|
|
262
178
|
const [viewRef, inView] = useInView({
|
|
263
179
|
threshold: intersectionThreshold || intersectionTreshold || 0,
|
|
@@ -266,82 +182,53 @@ export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
|
266
182
|
fallbackInView: true,
|
|
267
183
|
});
|
|
268
184
|
|
|
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
|
-
});
|
|
185
|
+
const rootRef = useMergedRef(ref, viewRef);
|
|
299
186
|
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
srcSet={data.webpSrcSet}
|
|
303
|
-
sizes={sizes ?? data.sizes ?? undefined}
|
|
304
|
-
type="image/webp"
|
|
305
|
-
/>
|
|
306
|
-
);
|
|
187
|
+
const addImage = imageAddStrategy({ priority, inView, loaded });
|
|
188
|
+
const showImage = imageShowStrategy({ priority, inView, loaded });
|
|
307
189
|
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
srcSet={
|
|
311
|
-
data.srcSet ?? buildSrcSet(data.src, data.width, srcSetCandidates)
|
|
312
|
-
}
|
|
313
|
-
sizes={sizes ?? data.sizes ?? undefined}
|
|
314
|
-
/>
|
|
315
|
-
);
|
|
190
|
+
const webpSource = buildWebpSource(data, sizes);
|
|
191
|
+
const regularSource = buildRegularSource(data, sizes, srcSetCandidates);
|
|
316
192
|
|
|
317
193
|
const transition =
|
|
318
194
|
fadeInDuration > 0 ? `opacity ${fadeInDuration}ms` : undefined;
|
|
319
195
|
|
|
196
|
+
const basePlaceholderStyle: React.CSSProperties = {
|
|
197
|
+
transition,
|
|
198
|
+
opacity: showImage ? 0 : 1,
|
|
199
|
+
// During the opacity transition of the placeholder to the definitive version,
|
|
200
|
+
// hardware acceleration is triggered. This results in the browser trying to render the
|
|
201
|
+
// placeholder with your GPU, causing blurred edges. Solution: style the placeholder
|
|
202
|
+
// so the edges overflow the container
|
|
203
|
+
position: 'absolute',
|
|
204
|
+
left: '-5%',
|
|
205
|
+
top: '-5%',
|
|
206
|
+
width: '110%',
|
|
207
|
+
height: '110%',
|
|
208
|
+
maxWidth: 'none',
|
|
209
|
+
maxHeight: 'none',
|
|
210
|
+
...placeholderStyle,
|
|
211
|
+
};
|
|
212
|
+
|
|
320
213
|
const placeholder =
|
|
321
|
-
usePlaceholder &&
|
|
214
|
+
usePlaceholder && data.base64 ? (
|
|
322
215
|
<img
|
|
323
216
|
aria-hidden="true"
|
|
324
217
|
alt=""
|
|
325
|
-
src={data.base64
|
|
218
|
+
src={data.base64}
|
|
326
219
|
className={placeholderClassName}
|
|
327
220
|
style={{
|
|
328
|
-
backgroundColor: data.bgColor ?? undefined,
|
|
329
221
|
objectFit,
|
|
330
222
|
objectPosition,
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
width: '110%',
|
|
341
|
-
height: '110%',
|
|
342
|
-
maxWidth: 'none',
|
|
343
|
-
maxHeight: 'none',
|
|
344
|
-
...placeholderStyle,
|
|
223
|
+
...basePlaceholderStyle,
|
|
224
|
+
}}
|
|
225
|
+
/>
|
|
226
|
+
) : usePlaceholder && data.bgColor ? (
|
|
227
|
+
<div
|
|
228
|
+
className={placeholderClassName}
|
|
229
|
+
style={{
|
|
230
|
+
backgroundColor: data.bgColor,
|
|
231
|
+
...basePlaceholderStyle,
|
|
345
232
|
}}
|
|
346
233
|
/>
|
|
347
234
|
) : null;
|
|
@@ -360,7 +247,7 @@ export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
|
360
247
|
width: '100%',
|
|
361
248
|
...pictureStyle,
|
|
362
249
|
}}
|
|
363
|
-
src={`data:image/svg+xml;base64,${
|
|
250
|
+
src={`data:image/svg+xml;base64,${universalBtoa(svg)}`}
|
|
364
251
|
aria-hidden="true"
|
|
365
252
|
alt=""
|
|
366
253
|
/>
|
|
@@ -368,17 +255,17 @@ export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
|
368
255
|
|
|
369
256
|
return (
|
|
370
257
|
<div
|
|
371
|
-
ref={
|
|
258
|
+
ref={rootRef}
|
|
372
259
|
className={className}
|
|
373
260
|
style={{
|
|
374
261
|
overflow: 'hidden',
|
|
375
262
|
...(layout === 'fill'
|
|
376
263
|
? absolutePositioning
|
|
377
264
|
: layout === 'intrinsic'
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
265
|
+
? { position: 'relative', width: '100%', maxWidth: width }
|
|
266
|
+
: layout === 'fixed'
|
|
267
|
+
? { position: 'relative', width }
|
|
268
|
+
: { position: 'relative', width: '100%' }),
|
|
382
269
|
...style,
|
|
383
270
|
}}
|
|
384
271
|
>
|
|
@@ -396,7 +283,7 @@ export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
|
396
283
|
alt={data.alt ?? ''}
|
|
397
284
|
title={data.title ?? undefined}
|
|
398
285
|
onLoad={handleLoad}
|
|
399
|
-
{...
|
|
286
|
+
{...priorityProp(priority ? 'high' : undefined)}
|
|
400
287
|
className={pictureClassName}
|
|
401
288
|
style={{
|
|
402
289
|
opacity: showImage ? 1 : 0,
|
|
@@ -427,8 +314,8 @@ export const Image = forwardRef<HTMLDivElement, ImagePropTypes>(
|
|
|
427
314
|
objectPosition,
|
|
428
315
|
...pictureStyle,
|
|
429
316
|
}}
|
|
430
|
-
loading={
|
|
431
|
-
{...
|
|
317
|
+
loading={priority ? undefined : 'lazy'}
|
|
318
|
+
{...priorityProp(priority ? 'high' : undefined)}
|
|
432
319
|
/>
|
|
433
320
|
)}
|
|
434
321
|
</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
|
+
};
|