1import React, { useState, useRef } from 'react'; 2 3import { ImageProps, ImageSource } from '../Image.types'; 4import { isBlurhashString, isThumbhashString } from '../utils/resolveSources'; 5 6function findBestSourceForSize( 7 sources: ImageSource[] | undefined, 8 size: DOMRect | null 9): ImageSource | null { 10 if (sources?.length === 1) { 11 return sources[0]; 12 } 13 return ( 14 [...(sources || [])] 15 // look for the smallest image that's still larger then a container 16 ?.map((source) => { 17 if (!size) { 18 return { source, penalty: 0, covers: false }; 19 } 20 const { width, height } = 21 typeof source === 'object' ? source : { width: null, height: null }; 22 if (width == null || height == null) { 23 return { source, penalty: 0, covers: false }; 24 } 25 if (width < size.width || height < size.height) { 26 return { 27 source, 28 penalty: Math.max(size.width - width, size.height - height), 29 covers: false, 30 }; 31 } 32 return { source, penalty: (width - size.width) * (height - size.height), covers: true }; 33 }) 34 .sort((a, b) => a.penalty - b.penalty) 35 .sort((a, b) => Number(b.covers) - Number(a.covers))[0]?.source ?? null 36 ); 37} 38 39export interface SrcSetSource extends ImageSource { 40 srcset: string; 41 sizes: string; 42 // used as key and a fallback in case srcset is not supported 43 uri: string; 44 type: 'srcset'; 45} 46 47function getCSSMediaQueryForSource(source: ImageSource) { 48 return `(max-width: ${source.webMaxViewportWidth ?? source.width}px) ${source.width}px`; 49} 50 51function selectSource( 52 sources: ImageSource[] | undefined, 53 size: DOMRect | null, 54 responsivePolicy: ImageProps['responsivePolicy'] 55): ImageSource | SrcSetSource | null { 56 if (sources == null || sources.length === 0) { 57 return null; 58 } 59 60 if (responsivePolicy !== 'static') { 61 return findBestSourceForSize(sources, size); 62 } 63 const staticSupportedSources = sources 64 .filter( 65 (s) => s.uri && s.width != null && !isBlurhashString(s.uri) && !isThumbhashString(s.uri) 66 ) 67 .sort( 68 (a, b) => (a.webMaxViewportWidth ?? a.width ?? 0) - (b.webMaxViewportWidth ?? b.width ?? 0) 69 ); 70 71 if (staticSupportedSources.length === 0) { 72 console.warn( 73 "You've set the `static` responsivePolicy but none of the sources have the `width` properties set. Make sure you set both `width` and `webMaxViewportWidth` for best results when using static responsiveness. Falling back to the `initial` policy." 74 ); 75 return findBestSourceForSize(sources, size); 76 } 77 78 const srcset = staticSupportedSources 79 ?.map((source) => `${source.uri} ${source.width}w`) 80 .join(', '); 81 const sizes = `${staticSupportedSources 82 ?.map(getCSSMediaQueryForSource) 83 .join(', ')}, ${staticSupportedSources[staticSupportedSources.length - 1]?.width}px`; 84 return { 85 srcset, 86 sizes, 87 uri: staticSupportedSources[staticSupportedSources.length - 1]?.uri ?? '', 88 type: 'srcset', 89 }; 90} 91 92type UseSourceSelectionReturn = { 93 containerRef: (element: HTMLDivElement) => void; 94 source: ImageSource | SrcSetSource | null; 95}; 96 97export default function useSourceSelection( 98 sources?: ImageSource[], 99 responsivePolicy: ImageProps['responsivePolicy'] = 'static', 100 measurementCallback?: (target: HTMLElement, size: DOMRect) => void 101): UseSourceSelectionReturn { 102 const hasMoreThanOneSource = (sources?.length ?? 0) > 1; 103 // null - not calculated yet, DOMRect - size available 104 const [size, setSize] = useState<null | DOMRect>(null); 105 const resizeObserver = useRef<ResizeObserver | null>(null); 106 107 React.useEffect(() => { 108 return () => { 109 resizeObserver.current?.disconnect(); 110 }; 111 }, []); 112 113 const containerRef = React.useCallback( 114 (element: HTMLDivElement) => { 115 // we can't short circuit here since we need to read the size for better animated transitions 116 if (!hasMoreThanOneSource && !measurementCallback) { 117 return; 118 } 119 const rect = element?.getBoundingClientRect(); 120 measurementCallback?.(element, rect); 121 setSize(rect); 122 123 if (responsivePolicy === 'live') { 124 resizeObserver.current?.disconnect(); 125 if (!element) { 126 return; 127 } 128 resizeObserver.current = new ResizeObserver((entries) => { 129 setSize(entries[0].contentRect); 130 measurementCallback?.(entries[0].target as any, entries[0].contentRect); 131 }); 132 resizeObserver.current.observe(element); 133 } 134 }, 135 [hasMoreThanOneSource, responsivePolicy] 136 ); 137 138 const source = selectSource(sources, size, responsivePolicy); 139 140 return React.useMemo( 141 () => ({ 142 containerRef, 143 source, 144 }), 145 [source] 146 ); 147} 148