1+ import { ColorSchemeContext } from './Provider' ;
12import { ContextValue , SlotProps } from 'react-aria-components' ;
23import { createContext , ForwardedRef , forwardRef , HTMLAttributeReferrerPolicy , JSX , ReactNode , useCallback , useContext , useMemo , useReducer , useRef , version } from 'react' ;
34import { DefaultImageGroup , ImageGroup } from './ImageCoordinator' ;
@@ -9,9 +10,46 @@ import {UnsafeStyles} from './style-utils';
910import { useLayoutEffect } from '@react-aria/utils' ;
1011import { useSpectrumContextProps } from './useSpectrumContextProps' ;
1112
13+ export interface ImageSource {
14+ /**
15+ * A comma-separated list of image URLs and descriptors.
16+ * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/source#srcset).
17+ */
18+ srcSet ?: string | undefined ,
19+ /**
20+ * The color scheme for this image source. Unlike `media`, this respects the `Provider` color scheme setting.
21+ */
22+ colorScheme ?: 'light' | 'dark' ,
23+ /**
24+ * A media query describing when the source should render.
25+ * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/source#media).
26+ */
27+ media ?: string | undefined ,
28+ /**
29+ * A list of source sizes that describe the final rendered width of the image.
30+ * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/source#sizes).
31+ */
32+ sizes ?: string | undefined ,
33+ /**
34+ * The mime type of the image.
35+ * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/source#type).
36+ */
37+ type ?: string | undefined ,
38+ /**
39+ * The intrinsic width of the image.
40+ * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/source#width).
41+ */
42+ width ?: number ,
43+ /**
44+ * The intrinsic height of the image.
45+ * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/source#height).
46+ */
47+ height ?: number
48+ }
49+
1250export interface ImageProps extends UnsafeStyles , SlotProps {
13- /** The URL of the image. */
14- src ?: string ,
51+ /** The URL of the image or a list of conditional sources . */
52+ src ?: string | ImageSource [ ] ,
1553 // TODO
1654 // srcSet?: string,
1755 // sizes?: string,
@@ -61,10 +99,6 @@ export interface ImageProps extends UnsafeStyles, SlotProps {
6199 * If not provided, the default image group is used.
62100 */
63101 group ?: ImageGroup ,
64- /**
65- * Child `<source>` elements defining alternate versions of an image for different display/device scenarios.
66- */
67- children ?: ReactNode ,
68102 /**
69103 * Associates the image with a microdata object.
70104 * See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/itemprop).
@@ -159,7 +193,7 @@ export const Image = forwardRef(function Image(props: ImageProps, domRef: Forwar
159193 [ props , domRef ] = useSpectrumContextProps ( props , domRef , ImageContext ) ;
160194
161195 let {
162- src = '' ,
196+ src : srcProp = '' ,
163197 styles,
164198 UNSAFE_className = '' ,
165199 UNSAFE_style,
@@ -177,16 +211,17 @@ export const Image = forwardRef(function Image(props: ImageProps, domRef: Forwar
177211 slot,
178212 width,
179213 height,
180- children,
181214 itemProp
182215 } = props ;
183216 let hidden = ( props as ImageContextValue ) . hidden ;
217+ let colorScheme = useContext ( ColorSchemeContext ) ;
218+ let cacheKey = useMemo ( ( ) => typeof srcProp === 'object' ? JSON . stringify ( srcProp ) : srcProp , [ srcProp ] ) ;
184219
185220 let { revealAll, register, unregister, load} = useContext ( group ) ;
186- let [ { state, src : lastSrc , loadTime} , dispatch ] = useReducer ( reducer , src , createState ) ;
221+ let [ { state, src : lastCacheKey , loadTime} , dispatch ] = useReducer ( reducer , cacheKey , createState ) ;
187222
188- if ( src !== lastSrc && ! hidden ) {
189- dispatch ( { type : 'update' , src} ) ;
223+ if ( cacheKey !== lastCacheKey && ! hidden ) {
224+ dispatch ( { type : 'update' , src : cacheKey } ) ;
190225 }
191226
192227 if ( state === 'loaded' && revealAll && ! hidden ) {
@@ -199,21 +234,21 @@ export const Image = forwardRef(function Image(props: ImageProps, domRef: Forwar
199234 return ;
200235 }
201236
202- register ( src ) ;
237+ register ( cacheKey ) ;
203238 return ( ) => {
204- unregister ( src ) ;
239+ unregister ( cacheKey ) ;
205240 } ;
206- } , [ hidden , register , unregister , src ] ) ;
241+ } , [ hidden , register , unregister , cacheKey ] ) ;
207242
208243 let onLoad = useCallback ( ( ) => {
209- load ( src ) ;
244+ load ( cacheKey ) ;
210245 dispatch ( { type : 'loaded' } ) ;
211- } , [ load , src ] ) ;
246+ } , [ load , cacheKey ] ) ;
212247
213248 let onError = useCallback ( ( ) => {
214249 dispatch ( { type : 'error' } ) ;
215- unregister ( src ) ;
216- } , [ unregister , src ] ) ;
250+ unregister ( cacheKey ) ;
251+ } , [ unregister , cacheKey ] ) ;
217252
218253 let isSkeleton = useIsSkeleton ( ) ;
219254 let isAnimating = isSkeleton || state === 'loading' || state === 'loaded' ;
@@ -223,15 +258,20 @@ export const Image = forwardRef(function Image(props: ImageProps, domRef: Forwar
223258 return ;
224259 }
225260
261+ // In React act environments, run immediately.
262+ // @ts -ignore
263+ let isTestEnv = typeof IS_REACT_ACT_ENVIRONMENT === 'boolean' ? IS_REACT_ACT_ENVIRONMENT : typeof jest !== 'undefined' ;
264+ let runTask = isTestEnv ? fn => fn ( ) : queueMicrotask ;
265+
226266 // If the image is already loaded, update state immediately instead of waiting for onLoad.
227267 let img = imgRef . current ;
228268 if ( state === 'loading' && img ?. complete ) {
229269 if ( img . naturalWidth === 0 && img . naturalHeight === 0 ) {
230270 // Queue a microtask so we don't hit React's update limit.
231271 // TODO: is this necessary?
232- queueMicrotask ( onError ) ;
272+ runTask ( onError ) ;
233273 } else {
234- queueMicrotask ( onLoad ) ;
274+ runTask ( onLoad ) ;
235275 }
236276 }
237277
@@ -253,7 +293,7 @@ export const Image = forwardRef(function Image(props: ImageProps, domRef: Forwar
253293 let img = (
254294 < img
255295 { ...getFetchPriorityProp ( fetchPriority ) }
256- src = { src || undefined }
296+ src = { typeof srcProp === 'string' && srcProp ? srcProp : undefined }
257297 alt = { alt }
258298 crossOrigin = { crossOrigin }
259299 decoding = { decoding }
@@ -268,10 +308,28 @@ export const Image = forwardRef(function Image(props: ImageProps, domRef: Forwar
268308 className = { imgStyles ( { isRevealed, isTransitioning} ) } />
269309 ) ;
270310
271- if ( children ) {
311+ if ( Array . isArray ( srcProp ) ) {
272312 img = (
273313 < picture >
274- { children }
314+ { srcProp . map ( ( source , i ) => {
315+ let { colorScheme : sourceColorScheme , ...sourceProps } = source ;
316+ if ( sourceColorScheme ) {
317+ if ( ! colorScheme || colorScheme === 'light dark' ) {
318+ return (
319+ < source
320+ key = { i }
321+ { ...sourceProps }
322+ media = { `${ source . media ? `${ source . media } and ` : '' } (prefers-color-scheme: ${ sourceColorScheme } )` } />
323+ ) ;
324+ }
325+
326+ return sourceColorScheme === colorScheme
327+ ? < source key = { i } { ...sourceProps } />
328+ : null ;
329+ } else {
330+ return < source key = { i } { ...sourceProps } /> ;
331+ }
332+ } ) }
275333 { img }
276334 </ picture >
277335 ) ;
@@ -287,7 +345,7 @@ export const Image = forwardRef(function Image(props: ImageProps, domRef: Forwar
287345 { ! errorState && img }
288346 </ div >
289347 ) ;
290- } , [ slot , hidden , domRef , UNSAFE_style , UNSAFE_className , styles , isAnimating , errorState , src , alt , crossOrigin , decoding , fetchPriority , loading , referrerPolicy , width , height , onLoad , onError , isRevealed , isTransitioning , children , itemProp ] ) ;
348+ } , [ slot , hidden , domRef , UNSAFE_style , UNSAFE_className , styles , isAnimating , errorState , alt , crossOrigin , decoding , fetchPriority , loading , referrerPolicy , width , height , onLoad , onError , isRevealed , isTransitioning , srcProp , itemProp , colorScheme ] ) ;
291349} ) ;
292350
293351function getFetchPriorityProp ( fetchPriority ?: 'high' | 'low' | 'auto' ) : Record < string , string | undefined > {
0 commit comments