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.
Files changed (72) hide show
  1. package/README.md +4 -13
  2. package/dist/cjs/Image/index.js +39 -138
  3. package/dist/cjs/Image/index.js.map +1 -1
  4. package/dist/cjs/Image/utils.js +52 -0
  5. package/dist/cjs/Image/utils.js.map +1 -0
  6. package/dist/cjs/SRCImage/index.js +44 -0
  7. package/dist/cjs/SRCImage/index.js.map +1 -0
  8. package/dist/cjs/SRCImage/utils.js +82 -0
  9. package/dist/cjs/SRCImage/utils.js.map +1 -0
  10. package/dist/cjs/Seo/nextUtils.js +15 -15
  11. package/dist/cjs/Seo/nextUtils.js.map +1 -1
  12. package/dist/cjs/Seo/renderMetaTags.js +1 -1
  13. package/dist/cjs/Seo/renderMetaTags.js.map +1 -1
  14. package/dist/cjs/StructuredText/index.js.map +1 -1
  15. package/dist/cjs/VideoPlayer/index.js +1 -1
  16. package/dist/cjs/index.js +2 -1
  17. package/dist/cjs/index.js.map +1 -1
  18. package/dist/cjs/useSiteSearch/index.js +9 -31
  19. package/dist/cjs/useSiteSearch/index.js.map +1 -1
  20. package/dist/cjs/useVideoPlayer/index.js.map +1 -1
  21. package/dist/esm/Image/index.js +31 -110
  22. package/dist/esm/Image/index.js.map +1 -1
  23. package/dist/esm/Image/utils.js +46 -0
  24. package/dist/esm/Image/utils.js.map +1 -0
  25. package/dist/esm/SRCImage/index.js +37 -0
  26. package/dist/esm/SRCImage/index.js.map +1 -0
  27. package/dist/esm/SRCImage/utils.js +52 -0
  28. package/dist/esm/SRCImage/utils.js.map +1 -0
  29. package/dist/esm/Seo/nextUtils.js +15 -15
  30. package/dist/esm/Seo/nextUtils.js.map +1 -1
  31. package/dist/esm/Seo/renderMetaTags.js +1 -1
  32. package/dist/esm/Seo/renderMetaTags.js.map +1 -1
  33. package/dist/esm/StructuredText/index.js.map +1 -1
  34. package/dist/esm/VideoPlayer/index.js +1 -1
  35. package/dist/esm/index.js +2 -1
  36. package/dist/esm/index.js.map +1 -1
  37. package/dist/esm/useSiteSearch/index.js +2 -1
  38. package/dist/esm/useSiteSearch/index.js.map +1 -1
  39. package/dist/esm/useVideoPlayer/index.js.map +1 -1
  40. package/dist/types/Image/index.d.ts +3 -4
  41. package/dist/types/Image/utils.d.ts +7 -0
  42. package/dist/types/SRCImage/index.d.ts +33 -0
  43. package/dist/types/SRCImage/utils.d.ts +6 -0
  44. package/dist/types/Seo/remixUtils.d.ts +1 -1
  45. package/dist/types/Seo/renderMetaTags.d.ts +1 -1
  46. package/dist/types/Seo/renderMetaTagsToString.d.ts +1 -1
  47. package/dist/types/StructuredText/index.d.ts +3 -3
  48. package/dist/types/VideoPlayer/index.d.ts +1 -1
  49. package/dist/types/index.d.ts +2 -1
  50. package/dist/types/useQuerySubscription/index.d.ts +1 -1
  51. package/dist/types/useVideoPlayer/index.d.ts +2 -2
  52. package/package.json +3 -4
  53. package/src/Image/__tests__/__snapshots__/index.test.tsx.snap +387 -60
  54. package/src/Image/__tests__/index.test.tsx +55 -8
  55. package/src/Image/index.tsx +65 -177
  56. package/src/Image/utils.ts +58 -0
  57. package/src/SRCImage/__tests__/__snapshots__/index.test.tsx.snap +274 -0
  58. package/src/SRCImage/__tests__/index.test.tsx +91 -0
  59. package/src/SRCImage/index.tsx +99 -0
  60. package/src/SRCImage/utils.tsx +95 -0
  61. package/src/Seo/__tests__/index.test.tsx +1 -1
  62. package/src/Seo/nextUtils.ts +20 -20
  63. package/src/Seo/remixUtils.ts +1 -1
  64. package/src/Seo/renderMetaTags.tsx +2 -2
  65. package/src/Seo/renderMetaTagsToString.tsx +1 -1
  66. package/src/StructuredText/__tests__/index.test.tsx +2 -2
  67. package/src/StructuredText/index.tsx +10 -10
  68. package/src/VideoPlayer/index.tsx +2 -2
  69. package/src/index.ts +2 -1
  70. package/src/useQuerySubscription/index.ts +4 -4
  71. package/src/useSiteSearch/index.tsx +29 -28
  72. package/src/useVideoPlayer/index.ts +3 -6
@@ -1,14 +1,13 @@
1
1
  import { mount } from 'enzyme';
2
2
  import 'intersection-observer';
3
- import * as React from 'react';
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).forEach((layout) => {
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
  });
@@ -1,39 +1,22 @@
1
1
  'use client';
2
2
 
3
- import React, {
4
- CSSProperties,
5
- forwardRef,
6
- useCallback,
7
- useEffect,
8
- useRef,
9
- useState,
10
- version,
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
- const isSsr = typeof window === 'undefined';
16
-
17
- const isIntersectionObserverAvailable = isSsr
18
- ? false
19
- : !!window.IntersectionObserver;
20
-
21
- function fetchPriorityProp(
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 upoad image loading */
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
- lazyLoad: boolean;
113
+ priority: boolean;
133
114
  inView: boolean;
134
115
  loaded: boolean;
135
116
  };
136
117
 
137
- const imageAddStrategy = ({ lazyLoad, inView, loaded }: State) => {
138
- if (!lazyLoad) {
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 = ({ lazyLoad, loaded }: State) => {
154
- if (!lazyLoad) {
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 callbackRef = useCallback(
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 webpSource = data.webpSrcSet && (
301
- <source
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 regularSource = (
309
- <source
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 && (data.bgColor || data.base64) ? (
215
+ usePlaceholder && data.base64 ? (
322
216
  <img
323
217
  aria-hidden="true"
324
218
  alt=""
325
- src={data.base64 ?? undefined}
219
+ src={data.base64}
326
220
  className={placeholderClassName}
327
221
  style={{
328
- backgroundColor: data.bgColor ?? undefined,
329
222
  objectFit,
330
223
  objectPosition,
331
- transition,
332
- opacity: showImage ? 0 : 1,
333
- // During the opacity transition of the placeholder to the definitive version,
334
- // hardware acceleration is triggered. This results in the browser trying to render the
335
- // placeholder with your GPU, causing blurred edges. Solution: style the placeholder
336
- // so the edges overflow the container
337
- position: 'absolute',
338
- left: '-5%',
339
- top: '-5%',
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,${encode(svg)}`}
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={callbackRef}
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
- ? { position: 'relative', width: '100%', maxWidth: width }
379
- : layout === 'fixed'
380
- ? { position: 'relative', width }
381
- : { position: 'relative', width: '100%' }),
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
- {...fetchPriorityProp(priority ? 'high' : undefined)}
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={lazyLoad ? 'lazy' : undefined}
431
- {...fetchPriorityProp(priority ? 'high' : undefined)}
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
+ };