@react-spectrum/s2
Version:
Spectrum 2 UI components in React
1 lines • 12.1 kB
Source Map (JSON)
{"mappings":"ACsKsB;EAAA;;;;EAAA;;;;EAAA;;;;EAKJ;;;;EAAA;;;;EAAA;;;;EAAA;;;;EAAA;;;;EAAA;;;;EAAA;;;;EAAA;;;;EAAA;;;;EA6IU;;;;EAAA","sources":["a1371b57e9afeb00","packages/@react-spectrum/s2/src/Image.tsx"],"sourcesContent":["@import \"be7e52eb05f9e81c\";\n@import \"b32b10d1bd9c4a50\";\n@import \"0e039ca03c56f78b\";\n","import {ColorSchemeContext} from './Provider';\nimport {ContextValue, SlotProps} from 'react-aria-components';\nimport {createContext, ForwardedRef, forwardRef, HTMLAttributeReferrerPolicy, JSX, ReactNode, useCallback, useContext, useMemo, useReducer, useRef, version} from 'react';\nimport {DefaultImageGroup, ImageGroup} from './ImageCoordinator';\nimport {loadingStyle, useIsSkeleton, useLoadingAnimation} from './Skeleton';\nimport {mergeStyles} from '../style/runtime';\nimport {style} from '../style' with {type: 'macro'};\nimport {StyleString} from '../style/types';\nimport {UnsafeStyles} from './style-utils';\nimport {useLayoutEffect} from '@react-aria/utils';\nimport {useSpectrumContextProps} from './useSpectrumContextProps';\n\nexport interface ImageSource {\n /**\n * A comma-separated list of image URLs and descriptors.\n * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/source#srcset).\n */\n srcSet?: string | undefined,\n /**\n * The color scheme for this image source. Unlike `media`, this respects the `Provider` color scheme setting.\n */\n colorScheme?: 'light' | 'dark',\n /**\n * A media query describing when the source should render.\n * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/source#media).\n */\n media?: string | undefined,\n /**\n * A list of source sizes that describe the final rendered width of the image.\n * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/source#sizes).\n */\n sizes?: string | undefined,\n /**\n * The mime type of the image.\n * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/source#type).\n */\n type?: string | undefined,\n /**\n * The intrinsic width of the image.\n * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/source#width).\n */\n width?: number,\n /**\n * The intrinsic height of the image.\n * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/source#height).\n */\n height?: number\n}\n\nexport interface ImageProps extends UnsafeStyles, SlotProps {\n /** The URL of the image or a list of conditional sources. */\n src?: string | ImageSource[],\n // TODO\n // srcSet?: string,\n // sizes?: string,\n /** Accessible alt text for the image. */\n alt?: string,\n /**\n * Indicates if the fetching of the image must be done using a CORS request.\n * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin).\n */\n crossOrigin?: 'anonymous' | 'use-credentials',\n /**\n * Whether the browser should decode images synchronously or asynchronously.\n * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#decoding).\n */\n decoding?: 'async' | 'auto' | 'sync',\n /**\n * Provides a hint of the relative priority to use when fetching the image.\n * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#fetchpriority).\n */\n fetchPriority?: 'high' | 'low' | 'auto',\n /**\n * Whether the image should be loaded immediately or lazily when scrolled into view.\n * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#loading).\n */\n loading?: 'eager' | 'lazy',\n /**\n * A string indicating which referrer to use when fetching the resource.\n * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#referrerpolicy).\n */\n referrerPolicy?: HTMLAttributeReferrerPolicy,\n /**\n * The intrinsic width of the image.\n * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#width).\n */\n width?: number,\n /**\n * The intrinsic height of the image.\n * [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#height).\n */\n height?: number,\n /** Spectrum-defined styles, returned by the `style()` macro. */\n styles?: StyleString,\n /** A function that is called to render a fallback when the image fails to load. */\n renderError?: () => ReactNode,\n /**\n * A group of images to coordinate between, matching the group passed to the `<ImageCoordinator>` component.\n * If not provided, the default image group is used.\n */\n group?: ImageGroup,\n /**\n * Associates the image with a microdata object.\n * See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/itemprop).\n */\n itemProp?: string\n}\n\ninterface ImageContextValue extends ImageProps {\n hidden?: boolean\n}\n\nexport const ImageContext = createContext<ContextValue<Partial<ImageContextValue>, HTMLDivElement>>(null);\n\ntype ImageState = 'loading' | 'loaded' | 'revealed' | 'error';\ninterface State {\n state: ImageState,\n src: string,\n startTime: number,\n loadTime: number\n}\n\ntype Action = \n | {type: 'update', src: string}\n | {type: 'loaded'}\n | {type: 'revealed'}\n | {type: 'error'};\n\nfunction createState(src: string): State {\n return {\n state: 'loading',\n src,\n startTime: Date.now(),\n loadTime: 0\n };\n}\n\nfunction reducer(state: State, action: Action): State {\n switch (action.type) {\n case 'update': {\n return {\n state: 'loading',\n src: action.src,\n startTime: Date.now(),\n loadTime: 0\n };\n }\n case 'loaded':\n case 'error': {\n return {\n ...state,\n state: action.type\n };\n }\n case 'revealed': {\n return {\n ...state,\n state: 'revealed',\n loadTime: Date.now() - state.startTime\n };\n }\n default:\n return state;\n }\n}\n\nconst wrapperStyles = style({\n backgroundColor: 'gray-100',\n overflow: 'hidden'\n});\n\nconst imgStyles = style({\n display: 'block',\n width: 'full',\n height: 'full',\n objectFit: 'inherit',\n objectPosition: 'inherit',\n opacity: {\n default: 0,\n isRevealed: 1\n },\n transition: {\n default: 'none',\n isTransitioning: 'opacity'\n },\n transitionDuration: 500\n});\n\n/**\n * An image with support for skeleton loading and custom error states.\n */\nexport const Image = forwardRef(function Image(props: ImageProps, domRef: ForwardedRef<HTMLDivElement>): JSX.Element | null {\n [props, domRef] = useSpectrumContextProps(props, domRef, ImageContext);\n\n let {\n src: srcProp = '',\n styles,\n UNSAFE_className = '',\n UNSAFE_style,\n renderError,\n group = DefaultImageGroup,\n // TODO\n // srcSet,\n // sizes,\n alt,\n crossOrigin,\n decoding,\n fetchPriority,\n loading,\n referrerPolicy,\n slot,\n width,\n height,\n itemProp\n } = props;\n let hidden = (props as ImageContextValue).hidden;\n let colorScheme = useContext(ColorSchemeContext);\n let cacheKey = useMemo(() => typeof srcProp === 'object' ? JSON.stringify(srcProp) : srcProp, [srcProp]);\n \n let {revealAll, register, unregister, load} = useContext(group);\n let [{state, src: lastCacheKey, loadTime}, dispatch] = useReducer(reducer, cacheKey, createState);\n\n if (cacheKey !== lastCacheKey && !hidden) {\n dispatch({type: 'update', src: cacheKey});\n }\n\n if (state === 'loaded' && revealAll && !hidden) {\n dispatch({type: 'revealed'});\n }\n\n let imgRef = useRef<HTMLImageElement | null>(null);\n useLayoutEffect(() => {\n if (hidden) {\n return;\n }\n\n register(cacheKey);\n return () => {\n unregister(cacheKey);\n };\n }, [hidden, register, unregister, cacheKey]);\n\n let onLoad = useCallback(() => {\n load(cacheKey);\n dispatch({type: 'loaded'});\n }, [load, cacheKey]);\n\n let onError = useCallback(() => {\n dispatch({type: 'error'});\n unregister(cacheKey);\n }, [unregister, cacheKey]);\n\n let isSkeleton = useIsSkeleton();\n let isAnimating = isSkeleton || state === 'loading' || state === 'loaded';\n let animation = useLoadingAnimation(isAnimating);\n useLayoutEffect(() => {\n if (hidden) {\n return;\n }\n\n // In React act environments, run immediately.\n // @ts-ignore\n let isTestEnv = typeof IS_REACT_ACT_ENVIRONMENT === 'boolean' ? IS_REACT_ACT_ENVIRONMENT : typeof jest !== 'undefined';\n let runTask = isTestEnv ? fn => fn() : queueMicrotask;\n\n // If the image is already loaded, update state immediately instead of waiting for onLoad.\n let img = imgRef.current;\n if (state === 'loading' && img?.complete) {\n if (img.naturalWidth === 0 && img.naturalHeight === 0) {\n // Queue a microtask so we don't hit React's update limit.\n // TODO: is this necessary?\n runTask(onError);\n } else {\n runTask(onLoad);\n }\n }\n\n animation(domRef.current);\n });\n\n if (props.alt == null && process.env.NODE_ENV !== 'production') {\n console.warn(\n 'The `alt` prop was not provided to an image. ' +\n 'Add `alt` text for screen readers, or set `alt=\"\"` prop to indicate that the image ' +\n 'is decorative or redundant with displayed text and should not be announced by screen readers.'\n );\n }\n\n let errorState = !isSkeleton && state === 'error' && renderError?.();\n let isRevealed = state === 'revealed' && !isSkeleton;\n let isTransitioning = isRevealed && loadTime > 200;\n return useMemo(() => {\n let img = (\n <img\n {...getFetchPriorityProp(fetchPriority)}\n src={typeof srcProp === 'string' && srcProp ? srcProp : undefined}\n alt={alt}\n crossOrigin={crossOrigin}\n decoding={decoding}\n loading={loading}\n referrerPolicy={referrerPolicy}\n width={width}\n height={height}\n ref={imgRef}\n itemProp={itemProp}\n onLoad={onLoad}\n onError={onError}\n className={imgStyles({isRevealed, isTransitioning})} />\n );\n\n if (Array.isArray(srcProp)) {\n img = (\n <picture className={style({objectFit: 'inherit', objectPosition: 'inherit'})}>\n {srcProp.map((source, i) => {\n let {colorScheme: sourceColorScheme, ...sourceProps} = source;\n if (sourceColorScheme) {\n if (!colorScheme || colorScheme === 'light dark') {\n return (\n <source\n key={i}\n {...sourceProps}\n media={`${source.media ? `${source.media} and ` : ''}(prefers-color-scheme: ${sourceColorScheme})`} />\n );\n }\n\n return sourceColorScheme === colorScheme\n ? <source key={i} {...sourceProps} />\n : null;\n } else {\n return <source key={i} {...sourceProps} />;\n }\n })}\n {img}\n </picture>\n );\n }\n\n return hidden ? null : (\n <div\n ref={domRef}\n slot={slot || undefined}\n style={UNSAFE_style}\n className={UNSAFE_className + mergeStyles(wrapperStyles, styles) + ' ' + (isAnimating ? loadingStyle : '')}>\n {errorState}\n {!errorState && img}\n </div>\n );\n }, [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]);\n});\n\nfunction getFetchPriorityProp(fetchPriority?: 'high' | 'low' | 'auto'): Record<string, string | undefined> {\n const pieces = version.split('.');\n const major = parseInt(pieces[0], 10);\n if (major >= 19) {\n return {fetchPriority};\n }\n return {fetchpriority: fetchPriority};\n}\n"],"names":[],"version":3,"file":"Image.css.map"}