1import React, { useState, useRef } from 'react';
2
3import { ImageProps, ImageSource } from '../Image.types';
4
5function findBestSourceForSize(
6  sources: ImageSource[] | undefined,
7  size: DOMRect | null
8): ImageSource | null {
9  return (
10    [...(sources || [])]
11      // look for the smallest image that's still larger then a container
12      ?.map((source) => {
13        if (!size) {
14          return { source, penalty: 0, covers: false };
15        }
16        const { width, height } =
17          typeof source === 'object' ? source : { width: null, height: null };
18        if (width == null || height == null) {
19          return { source, penalty: 0, covers: false };
20        }
21        if (width < size.width || height < size.height) {
22          return {
23            source,
24            penalty: Math.max(size.width - width, size.height - height),
25            covers: false,
26          };
27        }
28        return { source, penalty: (width - size.width) * (height - size.height), covers: true };
29      })
30      .sort((a, b) => a.penalty - b.penalty)
31      .sort((a, b) => Number(b.covers) - Number(a.covers))[0]?.source ?? null
32  );
33}
34
35export default function useSourceSelection(
36  sources?: ImageSource[],
37  sizeCalculation: ImageProps['responsivePolicy'] = 'live',
38  measurementCallback?: (target: HTMLElement, size: DOMRect) => void
39) {
40  const hasMoreThanOneSource = (sources?.length ?? 0) > 1;
41  // null - not calculated yet, DOMRect - size available
42  const [size, setSize] = useState<null | DOMRect>(null);
43  const resizeObserver = useRef<ResizeObserver | null>(null);
44
45  React.useEffect(() => {
46    return () => {
47      resizeObserver.current?.disconnect();
48    };
49  }, []);
50
51  const containerRef = React.useCallback(
52    (element: HTMLDivElement) => {
53      if (!hasMoreThanOneSource && !measurementCallback) {
54        return;
55      }
56      const rect = element?.getBoundingClientRect();
57      measurementCallback?.(element, rect);
58      setSize(rect);
59
60      if (sizeCalculation === 'live') {
61        resizeObserver.current?.disconnect();
62        if (!element) {
63          return;
64        }
65        resizeObserver.current = new ResizeObserver((entries) => {
66          setSize(entries[0].contentRect);
67          measurementCallback?.(entries[0].target as any, entries[0].contentRect);
68        });
69        resizeObserver.current.observe(element);
70      }
71    },
72    [hasMoreThanOneSource, sizeCalculation]
73  );
74
75  const bestSourceForSize = size !== undefined ? findBestSourceForSize(sources, size) : null;
76  const source = (hasMoreThanOneSource ? bestSourceForSize : sources?.[0]) ?? null;
77
78  return React.useMemo(
79    () => ({
80      containerRef,
81      source,
82    }),
83    [source]
84  );
85}
86