react-datocms 5.0.3 → 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.
Files changed (45) hide show
  1. package/README.md +4 -13
  2. package/dist/cjs/Image/index.js +38 -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 +43 -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/VideoPlayer/index.js +1 -1
  11. package/dist/cjs/index.js +2 -1
  12. package/dist/cjs/index.js.map +1 -1
  13. package/dist/cjs/useSiteSearch/index.js.map +1 -1
  14. package/dist/cjs/useVideoPlayer/index.js.map +1 -1
  15. package/dist/esm/Image/index.js +30 -110
  16. package/dist/esm/Image/index.js.map +1 -1
  17. package/dist/esm/Image/utils.js +46 -0
  18. package/dist/esm/Image/utils.js.map +1 -0
  19. package/dist/esm/SRCImage/index.js +36 -0
  20. package/dist/esm/SRCImage/index.js.map +1 -0
  21. package/dist/esm/SRCImage/utils.js +52 -0
  22. package/dist/esm/SRCImage/utils.js.map +1 -0
  23. package/dist/esm/VideoPlayer/index.js +1 -1
  24. package/dist/esm/index.js +2 -1
  25. package/dist/esm/index.js.map +1 -1
  26. package/dist/esm/useSiteSearch/index.js.map +1 -1
  27. package/dist/esm/useVideoPlayer/index.js.map +1 -1
  28. package/dist/types/Image/index.d.ts +3 -4
  29. package/dist/types/Image/utils.d.ts +7 -0
  30. package/dist/types/SRCImage/index.d.ts +33 -0
  31. package/dist/types/SRCImage/utils.d.ts +6 -0
  32. package/dist/types/index.d.ts +2 -1
  33. package/package.json +3 -4
  34. package/src/Image/__tests__/__snapshots__/index.test.tsx.snap +387 -60
  35. package/src/Image/__tests__/index.test.tsx +55 -8
  36. package/src/Image/index.tsx +64 -177
  37. package/src/Image/utils.ts +58 -0
  38. package/src/SRCImage/__tests__/__snapshots__/index.test.tsx.snap +268 -0
  39. package/src/SRCImage/__tests__/index.test.tsx +91 -0
  40. package/src/SRCImage/index.tsx +98 -0
  41. package/src/SRCImage/utils.tsx +95 -0
  42. package/src/VideoPlayer/index.tsx +1 -1
  43. package/src/index.ts +2 -1
  44. package/src/useSiteSearch/index.tsx +27 -27
  45. package/src/useVideoPlayer/index.ts +1 -4
@@ -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,21 @@
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
+ 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
- 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
- }
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 upoad image loading */
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
- lazyLoad: boolean;
112
+ priority: boolean;
133
113
  inView: boolean;
134
114
  loaded: boolean;
135
115
  };
136
116
 
137
- const imageAddStrategy = ({ lazyLoad, inView, loaded }: State) => {
138
- if (!lazyLoad) {
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 = ({ lazyLoad, loaded }: State) => {
154
- if (!lazyLoad) {
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 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
- });
185
+ const rootRef = useMergedRef(ref, viewRef);
299
186
 
300
- const webpSource = data.webpSrcSet && (
301
- <source
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 regularSource = (
309
- <source
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 && (data.bgColor || data.base64) ? (
214
+ usePlaceholder && data.base64 ? (
322
215
  <img
323
216
  aria-hidden="true"
324
217
  alt=""
325
- src={data.base64 ?? undefined}
218
+ src={data.base64}
326
219
  className={placeholderClassName}
327
220
  style={{
328
- backgroundColor: data.bgColor ?? undefined,
329
221
  objectFit,
330
222
  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,
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,${encode(svg)}`}
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={callbackRef}
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
- ? { position: 'relative', width: '100%', maxWidth: width }
379
- : layout === 'fixed'
380
- ? { position: 'relative', width }
381
- : { position: 'relative', width: '100%' }),
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
- {...fetchPriorityProp(priority ? 'high' : undefined)}
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={lazyLoad ? 'lazy' : undefined}
431
- {...fetchPriorityProp(priority ? 'high' : undefined)}
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
+ };