UNPKG

@react-spectrum/s2

Version:
1 lines 8.11 kB
{"mappings":"ACiHsB;EAAA;;;;EAAA;;;;EAAA;;;;EAKJ;;;;EAAA;;;;EAAA;;;;EAAA;;;;EAAA;;;;EAAA;;;;EAAA;;;;EAAA;;;;EAAA;;;;EAAA;;;;EAAA","sources":["a1371b57e9afeb00","packages/@react-spectrum/s2/src/Image.tsx"],"sourcesContent":["@import \"be7e52eb05f9e81c\";\n@import \"b32b10d1bd9c4a50\";\n","import {ContextValue, SlotProps} from 'react-aria-components';\nimport {createContext, ForwardedRef, forwardRef, HTMLAttributeReferrerPolicy, 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 ImageProps extends UnsafeStyles, SlotProps {\n /** The URL of the image. */\n src?: string,\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 /** 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\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\nexport const Image = forwardRef(function Image(props: ImageProps, domRef: ForwardedRef<HTMLDivElement>) {\n [props, domRef] = useSpectrumContextProps(props, domRef, ImageContext);\n\n let {\n src = '',\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 } = props;\n let hidden = (props as ImageContextValue).hidden;\n \n let {revealAll, register, unregister, load} = useContext(group);\n let [{state, src: lastSrc, loadTime}, dispatch] = useReducer(reducer, src, createState);\n\n if (src !== lastSrc && !hidden) {\n dispatch({type: 'update', src});\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(src);\n return () => {\n unregister(src);\n };\n }, [hidden, register, unregister, src]);\n\n let onLoad = useCallback(() => {\n load(src);\n dispatch({type: 'loaded'});\n }, [load, src]);\n\n let onError = useCallback(() => {\n dispatch({type: 'error'});\n unregister(src);\n }, [unregister, src]);\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 // If the image is already loaded, update state immediately instead of waiting for onLoad.\n if (state === 'loading' && imgRef.current?.complete) {\n // Queue a microtask so we don't hit React's update limit.\n // TODO: is this necessary?\n queueMicrotask(onLoad);\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(() => 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 && (\n <img\n {...getFetchPriorityProp(fetchPriority)}\n src={src || undefined}\n alt={alt}\n crossOrigin={crossOrigin}\n decoding={decoding}\n loading={loading}\n referrerPolicy={referrerPolicy}\n ref={imgRef}\n onLoad={onLoad}\n onError={onError}\n className={imgStyles({isRevealed, isTransitioning})} />\n )}\n </div>\n ), [slot, hidden, domRef, UNSAFE_style, UNSAFE_className, styles, isAnimating, errorState, src, alt, crossOrigin, decoding, fetchPriority, loading, referrerPolicy, onLoad, onError, isRevealed, isTransitioning]);\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"}