From eb93a044f3114cc880e6e85df2ef54a985f6eb21 Mon Sep 17 00:00:00 2001 From: Jan Kaifer Date: Mon, 21 Nov 2022 16:17:38 +0100 Subject: [PATCH 01/10] Add ref forwarding for next/image --- packages/next/client/image.tsx | 1027 ++++++++++++++++---------------- 1 file changed, 526 insertions(+), 501 deletions(-) diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 80c0df3bf63a..fcde56b2d4b3 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -7,6 +7,7 @@ import React, { useContext, useMemo, useState, + forwardRef, } from 'react' import Head from '../shared/lib/head' import { getImageBlurSvg } from '../shared/lib/image-blur-svg' @@ -362,332 +363,353 @@ function handleLoading( }) } -const ImageElement = ({ - imgAttributes, - heightInt, - widthInt, - qualityInt, - className, - imgStyle, - blurStyle, - isLazy, - fill, - placeholder, - loading, - srcString, - config, - unoptimized, - loader, - onLoadRef, - onLoadingCompleteRef, - setBlurComplete, - setShowAltText, - onLoad, - onError, - ...rest -}: ImageElementProps) => { - loading = isLazy ? 'lazy' : loading - return ( - <> - { - if (!img) { - return - } - if (onError) { - // If the image has an error before react hydrates, then the error is lost. - // The workaround is to wait until the image is mounted which is after hydration, - // then we set the src again to trigger the error handler (if there was an error). - // eslint-disable-next-line no-self-assign - img.src = img.src - } - if (process.env.NODE_ENV !== 'production') { - if (!srcString) { - console.error(`Image is missing required "src" property:`, img) +const ImageElement = forwardRef( + ( + { + imgAttributes, + heightInt, + widthInt, + qualityInt, + className, + imgStyle, + blurStyle, + isLazy, + fill, + placeholder, + loading, + srcString, + config, + unoptimized, + loader, + onLoadRef, + onLoadingCompleteRef, + setBlurComplete, + setShowAltText, + onLoad, + onError, + ...rest + }, + forwardedRef + ) => { + loading = isLazy ? 'lazy' : loading + return ( + <> + { + if (forwardedRef) { + if (typeof forwardedRef === 'function') forwardedRef(img) + else if (typeof forwardedRef === 'object') { + // @ts-ignore - .current is read only it's usually assigned by react internally + forwardedRef.current = img + } + } + if (!img) { + return + } + if (onError) { + // If the image has an error before react hydrates, then the error is lost. + // The workaround is to wait until the image is mounted which is after hydration, + // then we set the src again to trigger the error handler (if there was an error). + // eslint-disable-next-line no-self-assign + img.src = img.src } - if (img.getAttribute('alt') === null) { - console.error( - `Image is missing required "alt" property. Please add Alternative Text to describe the image for screen readers and search engines.` + if (process.env.NODE_ENV !== 'production') { + if (!srcString) { + console.error( + `Image is missing required "src" property:`, + img + ) + } + if (img.getAttribute('alt') === null) { + console.error( + `Image is missing required "alt" property. Please add Alternative Text to describe the image for screen readers and search engines.` + ) + } + } + if (img.complete) { + handleLoading( + img, + srcString, + placeholder, + onLoadRef, + onLoadingCompleteRef, + setBlurComplete, + unoptimized ) } + }, + [ + srcString, + placeholder, + onLoadRef, + onLoadingCompleteRef, + setBlurComplete, + onError, + unoptimized, + forwardedRef, + ] + )} + onLoad={(event) => { + const img = event.currentTarget as ImgElementWithDataProp + handleLoading( + img, + srcString, + placeholder, + onLoadRef, + onLoadingCompleteRef, + setBlurComplete, + unoptimized + ) + }} + onError={(event) => { + // if the real image fails to load, this will ensure "alt" is visible + setShowAltText(true) + if (placeholder === 'blur') { + // If the real image fails to load, this will still remove the placeholder. + setBlurComplete(true) } - if (img.complete) { - handleLoading( - img, - srcString, - placeholder, - onLoadRef, - onLoadingCompleteRef, - setBlurComplete, - unoptimized - ) + if (onError) { + onError(event) } - }, - [ - srcString, - placeholder, - onLoadRef, - onLoadingCompleteRef, - setBlurComplete, - onError, - unoptimized, - ] - )} - onLoad={(event) => { - const img = event.currentTarget as ImgElementWithDataProp - handleLoading( - img, - srcString, - placeholder, - onLoadRef, - onLoadingCompleteRef, - setBlurComplete, - unoptimized - ) - }} - onError={(event) => { - // if the real image fails to load, this will ensure "alt" is visible - setShowAltText(true) - if (placeholder === 'blur') { - // If the real image fails to load, this will still remove the placeholder. - setBlurComplete(true) - } - if (onError) { - onError(event) - } - }} - /> - - ) -} - -export default function Image({ - src, - sizes, - unoptimized = false, - priority = false, - loading, - className, - quality, - width, - height, - fill, - style, - onLoad, - onLoadingComplete, - placeholder = 'empty', - blurDataURL, - layout, - objectFit, - objectPosition, - lazyBoundary, - lazyRoot, - ...all -}: ImageProps) { - const configContext = useContext(ImageConfigContext) - const config: ImageConfig = useMemo(() => { - const c = configEnv || configContext || imageConfigDefault - const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b) - const deviceSizes = c.deviceSizes.sort((a, b) => a - b) - return { ...c, allSizes, deviceSizes } - }, [configContext]) - - let rest: Partial = all - let loader: ImageLoaderWithConfig = rest.loader || defaultLoader - // Remove property so it's not spread on element - delete rest.loader - - if ('__next_img_default' in loader) { - // This special value indicates that the user - // didn't define a "loader" prop or config. - if (config.loader === 'custom') { - throw new Error( - `Image with src "${src}" is missing "loader" prop.` + - `\nRead more: https://nextjs.org/docs/messages/next-image-missing-loader` - ) - } - } else { - // The user defined a "loader" prop or config. - // Since the config object is internal only, we - // must not pass it to the user-defined "loader". - const customImageLoader = loader as ImageLoader - loader = (obj) => { - const { config: _, ...opts } = obj - return customImageLoader(opts) - } - } - - if (layout) { - if (layout === 'fill') { - fill = true - } - const layoutToStyle: Record | undefined> = { - intrinsic: { maxWidth: '100%', height: 'auto' }, - responsive: { width: '100%', height: 'auto' }, - } - const layoutToSizes: Record = { - responsive: '100vw', - fill: '100vw', - } - const layoutStyle = layoutToStyle[layout] - if (layoutStyle) { - style = { ...style, ...layoutStyle } - } - const layoutSizes = layoutToSizes[layout] - if (layoutSizes && !sizes) { - sizes = layoutSizes - } + }} + /> + + ) } - - let staticSrc = '' - let widthInt = getInt(width) - let heightInt = getInt(height) - let blurWidth: number | undefined - let blurHeight: number | undefined - if (isStaticImport(src)) { - const staticImageData = isStaticRequire(src) ? src.default : src - - if (!staticImageData.src) { - throw new Error( - `An object should only be passed to the image component src parameter if it comes from a static image import. It must include src. Received ${JSON.stringify( - staticImageData - )}` - ) - } - if (!staticImageData.height || !staticImageData.width) { - throw new Error( - `An object should only be passed to the image component src parameter if it comes from a static image import. It must include height and width. Received ${JSON.stringify( - staticImageData - )}` - ) +) + +const Image = forwardRef( + ( + { + src, + sizes, + unoptimized = false, + priority = false, + loading, + className, + quality, + width, + height, + fill, + style, + onLoad, + onLoadingComplete, + placeholder = 'empty', + blurDataURL, + layout, + objectFit, + objectPosition, + lazyBoundary, + lazyRoot, + ...all + }, + forwardedRef + ) => { + const configContext = useContext(ImageConfigContext) + const config: ImageConfig = useMemo(() => { + const c = configEnv || configContext || imageConfigDefault + const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b) + const deviceSizes = c.deviceSizes.sort((a, b) => a - b) + return { ...c, allSizes, deviceSizes } + }, [configContext]) + + let rest: Partial = all + let loader: ImageLoaderWithConfig = rest.loader || defaultLoader + // Remove property so it's not spread on element + delete rest.loader + + if ('__next_img_default' in loader) { + // This special value indicates that the user + // didn't define a "loader" prop or config. + if (config.loader === 'custom') { + throw new Error( + `Image with src "${src}" is missing "loader" prop.` + + `\nRead more: https://nextjs.org/docs/messages/next-image-missing-loader` + ) + } + } else { + // The user defined a "loader" prop or config. + // Since the config object is internal only, we + // must not pass it to the user-defined "loader". + const customImageLoader = loader as ImageLoader + loader = (obj) => { + const { config: _, ...opts } = obj + return customImageLoader(opts) + } } - blurWidth = staticImageData.blurWidth - blurHeight = staticImageData.blurHeight - blurDataURL = blurDataURL || staticImageData.blurDataURL - staticSrc = staticImageData.src - - if (!fill) { - if (!widthInt && !heightInt) { - widthInt = staticImageData.width - heightInt = staticImageData.height - } else if (widthInt && !heightInt) { - const ratio = widthInt / staticImageData.width - heightInt = Math.round(staticImageData.height * ratio) - } else if (!widthInt && heightInt) { - const ratio = heightInt / staticImageData.height - widthInt = Math.round(staticImageData.width * ratio) + if (layout) { + if (layout === 'fill') { + fill = true + } + const layoutToStyle: Record | undefined> = + { + intrinsic: { maxWidth: '100%', height: 'auto' }, + responsive: { width: '100%', height: 'auto' }, + } + const layoutToSizes: Record = { + responsive: '100vw', + fill: '100vw', + } + const layoutStyle = layoutToStyle[layout] + if (layoutStyle) { + style = { ...style, ...layoutStyle } + } + const layoutSizes = layoutToSizes[layout] + if (layoutSizes && !sizes) { + sizes = layoutSizes } } - } - src = typeof src === 'string' ? src : staticSrc - - let isLazy = - !priority && (loading === 'lazy' || typeof loading === 'undefined') - if (src.startsWith('data:') || src.startsWith('blob:')) { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs - unoptimized = true - isLazy = false - } - if (config.unoptimized) { - unoptimized = true - } - const [blurComplete, setBlurComplete] = useState(false) - const [showAltText, setShowAltText] = useState(false) + let staticSrc = '' + let widthInt = getInt(width) + let heightInt = getInt(height) + let blurWidth: number | undefined + let blurHeight: number | undefined + if (isStaticImport(src)) { + const staticImageData = isStaticRequire(src) ? src.default : src - const qualityInt = getInt(quality) + if (!staticImageData.src) { + throw new Error( + `An object should only be passed to the image component src parameter if it comes from a static image import. It must include src. Received ${JSON.stringify( + staticImageData + )}` + ) + } + if (!staticImageData.height || !staticImageData.width) { + throw new Error( + `An object should only be passed to the image component src parameter if it comes from a static image import. It must include height and width. Received ${JSON.stringify( + staticImageData + )}` + ) + } - if (process.env.NODE_ENV !== 'production') { - if (!src) { - // React doesn't show the stack trace and there's - // no `src` to help identify which image, so we - // instead console.error(ref) during mount. - unoptimized = true - } else { - if (fill) { - if (width) { - throw new Error( - `Image with src "${src}" has both "width" and "fill" properties. Only one should be used.` - ) - } - if (height) { - throw new Error( - `Image with src "${src}" has both "height" and "fill" properties. Only one should be used.` - ) - } - if (style?.position && style.position !== 'absolute') { - throw new Error( - `Image with src "${src}" has both "fill" and "style.position" properties. Images with "fill" always use position absolute - it cannot be modified.` - ) - } - if (style?.width && style.width !== '100%') { - throw new Error( - `Image with src "${src}" has both "fill" and "style.width" properties. Images with "fill" always use width 100% - it cannot be modified.` - ) - } - if (style?.height && style.height !== '100%') { - throw new Error( - `Image with src "${src}" has both "fill" and "style.height" properties. Images with "fill" always use height 100% - it cannot be modified.` - ) - } - } else { - if (typeof widthInt === 'undefined') { - throw new Error( - `Image with src "${src}" is missing required "width" property.` - ) - } else if (isNaN(widthInt)) { - throw new Error( - `Image with src "${src}" has invalid "width" property. Expected a numeric value in pixels but received "${width}".` - ) - } - if (typeof heightInt === 'undefined') { - throw new Error( - `Image with src "${src}" is missing required "height" property.` - ) - } else if (isNaN(heightInt)) { - throw new Error( - `Image with src "${src}" has invalid "height" property. Expected a numeric value in pixels but received "${height}".` - ) + blurWidth = staticImageData.blurWidth + blurHeight = staticImageData.blurHeight + blurDataURL = blurDataURL || staticImageData.blurDataURL + staticSrc = staticImageData.src + + if (!fill) { + if (!widthInt && !heightInt) { + widthInt = staticImageData.width + heightInt = staticImageData.height + } else if (widthInt && !heightInt) { + const ratio = widthInt / staticImageData.width + heightInt = Math.round(staticImageData.height * ratio) + } else if (!widthInt && heightInt) { + const ratio = heightInt / staticImageData.height + widthInt = Math.round(staticImageData.width * ratio) } } } - if (!VALID_LOADING_VALUES.includes(loading)) { - throw new Error( - `Image with src "${src}" has invalid "loading" property. Provided "${loading}" should be one of ${VALID_LOADING_VALUES.map( - String - ).join(',')}.` - ) + src = typeof src === 'string' ? src : staticSrc + + let isLazy = + !priority && (loading === 'lazy' || typeof loading === 'undefined') + if (src.startsWith('data:') || src.startsWith('blob:')) { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs + unoptimized = true + isLazy = false } - if (priority && loading === 'lazy') { - throw new Error( - `Image with src "${src}" has both "priority" and "loading='lazy'" properties. Only one should be used.` - ) + if (config.unoptimized) { + unoptimized = true } - if (placeholder === 'blur') { - if (widthInt && heightInt && widthInt * heightInt < 1600) { - warnOnce( - `Image with src "${src}" is smaller than 40x40. Consider removing the "placeholder='blur'" property to improve performance.` + const [blurComplete, setBlurComplete] = useState(false) + const [showAltText, setShowAltText] = useState(false) + + const qualityInt = getInt(quality) + + if (process.env.NODE_ENV !== 'production') { + if (!src) { + // React doesn't show the stack trace and there's + // no `src` to help identify which image, so we + // instead console.error(ref) during mount. + unoptimized = true + } else { + if (fill) { + if (width) { + throw new Error( + `Image with src "${src}" has both "width" and "fill" properties. Only one should be used.` + ) + } + if (height) { + throw new Error( + `Image with src "${src}" has both "height" and "fill" properties. Only one should be used.` + ) + } + if (style?.position && style.position !== 'absolute') { + throw new Error( + `Image with src "${src}" has both "fill" and "style.position" properties. Images with "fill" always use position absolute - it cannot be modified.` + ) + } + if (style?.width && style.width !== '100%') { + throw new Error( + `Image with src "${src}" has both "fill" and "style.width" properties. Images with "fill" always use width 100% - it cannot be modified.` + ) + } + if (style?.height && style.height !== '100%') { + throw new Error( + `Image with src "${src}" has both "fill" and "style.height" properties. Images with "fill" always use height 100% - it cannot be modified.` + ) + } + } else { + if (typeof widthInt === 'undefined') { + throw new Error( + `Image with src "${src}" is missing required "width" property.` + ) + } else if (isNaN(widthInt)) { + throw new Error( + `Image with src "${src}" has invalid "width" property. Expected a numeric value in pixels but received "${width}".` + ) + } + if (typeof heightInt === 'undefined') { + throw new Error( + `Image with src "${src}" is missing required "height" property.` + ) + } else if (isNaN(heightInt)) { + throw new Error( + `Image with src "${src}" has invalid "height" property. Expected a numeric value in pixels but received "${height}".` + ) + } + } + } + if (!VALID_LOADING_VALUES.includes(loading)) { + throw new Error( + `Image with src "${src}" has invalid "loading" property. Provided "${loading}" should be one of ${VALID_LOADING_VALUES.map( + String + ).join(',')}.` + ) + } + if (priority && loading === 'lazy') { + throw new Error( + `Image with src "${src}" has both "priority" and "loading='lazy'" properties. Only one should be used.` ) } - if (!blurDataURL) { - const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next-image-loader + if (placeholder === 'blur') { + if (widthInt && heightInt && widthInt * heightInt < 1600) { + warnOnce( + `Image with src "${src}" is smaller than 40x40. Consider removing the "placeholder='blur'" property to improve performance.` + ) + } + + if (!blurDataURL) { + const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next-image-loader - throw new Error( - `Image with src "${src}" has "placeholder='blur'" property but is missing the "blurDataURL" property. + throw new Error( + `Image with src "${src}" has "placeholder='blur'" property but is missing the "blurDataURL" property. Possible solutions: - Add a "blurDataURL" property, the contents should be a small Data URL to represent the image - Change the "src" property to a static import with one of the supported file types: ${VALID_BLUR_EXT.join( @@ -695,222 +717,225 @@ export default function Image({ )} - Remove the "placeholder" property, effectively no blur effect Read more: https://nextjs.org/docs/messages/placeholder-blur-data-url` - ) + ) + } } - } - if ('ref' in rest) { - warnOnce( - `Image with src "${src}" is using unsupported "ref" property. Consider using the "onLoadingComplete" property instead.` - ) - } - - if (!unoptimized && loader !== defaultLoader) { - const urlStr = loader({ - config, - src, - width: widthInt || 400, - quality: qualityInt || 75, - }) - let url: URL | undefined - try { - url = new URL(urlStr) - } catch (err) {} - if (urlStr === src || (url && url.pathname === src && !url.search)) { + if ('ref' in rest) { warnOnce( - `Image with src "${src}" has a "loader" property that does not implement width. Please implement it or use the "unoptimized" property instead.` + - `\nRead more: https://nextjs.org/docs/messages/next-image-missing-loader-width` + `Image with src "${src}" is using unsupported "ref" property. Consider using the "onLoadingComplete" property instead.` ) } - } - for (const [legacyKey, legacyValue] of Object.entries({ - layout, - objectFit, - objectPosition, - lazyBoundary, - lazyRoot, - })) { - if (legacyValue) { - warnOnce( - `Image with src "${src}" has legacy prop "${legacyKey}". Did you forget to run the codemod?` + - `\nRead more: https://nextjs.org/docs/messages/next-image-upgrade-to-13` - ) + if (!unoptimized && loader !== defaultLoader) { + const urlStr = loader({ + config, + src, + width: widthInt || 400, + quality: qualityInt || 75, + }) + let url: URL | undefined + try { + url = new URL(urlStr) + } catch (err) {} + if (urlStr === src || (url && url.pathname === src && !url.search)) { + warnOnce( + `Image with src "${src}" has a "loader" property that does not implement width. Please implement it or use the "unoptimized" property instead.` + + `\nRead more: https://nextjs.org/docs/messages/next-image-missing-loader-width` + ) + } } - } - if ( - typeof window !== 'undefined' && - !perfObserver && - window.PerformanceObserver - ) { - perfObserver = new PerformanceObserver((entryList) => { - for (const entry of entryList.getEntries()) { - // @ts-ignore - missing "LargestContentfulPaint" class with "element" prop - const imgSrc = entry?.element?.src || '' - const lcpImage = allImgs.get(imgSrc) - if ( - lcpImage && - !lcpImage.priority && - lcpImage.placeholder !== 'blur' && - !lcpImage.src.startsWith('data:') && - !lcpImage.src.startsWith('blob:') - ) { - // https://web.dev/lcp/#measure-lcp-in-javascript - warnOnce( - `Image with src "${lcpImage.src}" was detected as the Largest Contentful Paint (LCP). Please add the "priority" property if this image is above the fold.` + - `\nRead more: https://nextjs.org/docs/api-reference/next/image#priority` - ) - } + for (const [legacyKey, legacyValue] of Object.entries({ + layout, + objectFit, + objectPosition, + lazyBoundary, + lazyRoot, + })) { + if (legacyValue) { + warnOnce( + `Image with src "${src}" has legacy prop "${legacyKey}". Did you forget to run the codemod?` + + `\nRead more: https://nextjs.org/docs/messages/next-image-upgrade-to-13` + ) } - }) - try { - perfObserver.observe({ - type: 'largest-contentful-paint', - buffered: true, - }) - } catch (err) { - // Log error but don't crash the app - console.error(err) } - } - } - const imgStyle = Object.assign( - fill - ? { - position: 'absolute', - height: '100%', - width: '100%', - left: 0, - top: 0, - right: 0, - bottom: 0, - objectFit, - objectPosition, - } - : {}, - showAltText ? {} : { color: 'transparent' }, - style - ) - const blurStyle = - placeholder === 'blur' && blurDataURL && !blurComplete - ? { - backgroundSize: imgStyle.objectFit || 'cover', - backgroundPosition: imgStyle.objectPosition || '50% 50%', - backgroundRepeat: 'no-repeat', - backgroundImage: `url("data:image/svg+xml;charset=utf-8,${getImageBlurSvg( - { - widthInt, - heightInt, - blurWidth, - blurHeight, - blurDataURL, + if ( + typeof window !== 'undefined' && + !perfObserver && + window.PerformanceObserver + ) { + perfObserver = new PerformanceObserver((entryList) => { + for (const entry of entryList.getEntries()) { + // @ts-ignore - missing "LargestContentfulPaint" class with "element" prop + const imgSrc = entry?.element?.src || '' + const lcpImage = allImgs.get(imgSrc) + if ( + lcpImage && + !lcpImage.priority && + lcpImage.placeholder !== 'blur' && + !lcpImage.src.startsWith('data:') && + !lcpImage.src.startsWith('blob:') + ) { + // https://web.dev/lcp/#measure-lcp-in-javascript + warnOnce( + `Image with src "${lcpImage.src}" was detected as the Largest Contentful Paint (LCP). Please add the "priority" property if this image is above the fold.` + + `\nRead more: https://nextjs.org/docs/api-reference/next/image#priority` + ) } - )}")`, + } + }) + try { + perfObserver.observe({ + type: 'largest-contentful-paint', + buffered: true, + }) + } catch (err) { + // Log error but don't crash the app + console.error(err) } - : {} - - if (process.env.NODE_ENV === 'development') { - if (blurStyle.backgroundImage && blurDataURL?.startsWith('/')) { - // During `next dev`, we don't want to generate blur placeholders with webpack - // because it can delay starting the dev server. Instead, `next-image-loader.js` - // will inline a special url to lazily generate the blur placeholder at request time. - blurStyle.backgroundImage = `url("${blurDataURL}")` + } + } + const imgStyle = Object.assign( + fill + ? { + position: 'absolute', + height: '100%', + width: '100%', + left: 0, + top: 0, + right: 0, + bottom: 0, + objectFit, + objectPosition, + } + : {}, + showAltText ? {} : { color: 'transparent' }, + style + ) + + const blurStyle = + placeholder === 'blur' && blurDataURL && !blurComplete + ? { + backgroundSize: imgStyle.objectFit || 'cover', + backgroundPosition: imgStyle.objectPosition || '50% 50%', + backgroundRepeat: 'no-repeat', + backgroundImage: `url("data:image/svg+xml;charset=utf-8,${getImageBlurSvg( + { + widthInt, + heightInt, + blurWidth, + blurHeight, + blurDataURL, + } + )}")`, + } + : {} + + if (process.env.NODE_ENV === 'development') { + if (blurStyle.backgroundImage && blurDataURL?.startsWith('/')) { + // During `next dev`, we don't want to generate blur placeholders with webpack + // because it can delay starting the dev server. Instead, `next-image-loader.js` + // will inline a special url to lazily generate the blur placeholder at request time. + blurStyle.backgroundImage = `url("${blurDataURL}")` + } } - } - const imgAttributes = generateImgAttrs({ - config, - src, - unoptimized, - width: widthInt, - quality: qualityInt, - sizes, - loader, - }) + const imgAttributes = generateImgAttrs({ + config, + src, + unoptimized, + width: widthInt, + quality: qualityInt, + sizes, + loader, + }) - let srcString: string = src + let srcString: string = src - if (process.env.NODE_ENV !== 'production') { - if (typeof window !== 'undefined') { - let fullUrl: URL - try { - fullUrl = new URL(imgAttributes.src) - } catch (e) { - fullUrl = new URL(imgAttributes.src, window.location.href) + if (process.env.NODE_ENV !== 'production') { + if (typeof window !== 'undefined') { + let fullUrl: URL + try { + fullUrl = new URL(imgAttributes.src) + } catch (e) { + fullUrl = new URL(imgAttributes.src, window.location.href) + } + allImgs.set(fullUrl.href, { src, priority, placeholder }) } - allImgs.set(fullUrl.href, { src, priority, placeholder }) } - } - const linkProps: React.DetailedHTMLProps< - React.LinkHTMLAttributes, - HTMLLinkElement - > = { - // @ts-expect-error upgrade react types to react 18 - imageSrcSet: imgAttributes.srcSet, - imageSizes: imgAttributes.sizes, - crossOrigin: rest.crossOrigin, - } + const linkProps: React.DetailedHTMLProps< + React.LinkHTMLAttributes, + HTMLLinkElement + > = { + // @ts-expect-error upgrade react types to react 18 + imageSrcSet: imgAttributes.srcSet, + imageSizes: imgAttributes.sizes, + crossOrigin: rest.crossOrigin, + } - const onLoadRef = useRef(onLoad) - - useEffect(() => { - onLoadRef.current = onLoad - }, [onLoad]) - - const onLoadingCompleteRef = useRef(onLoadingComplete) - - useEffect(() => { - onLoadingCompleteRef.current = onLoadingComplete - }, [onLoadingComplete]) - - const imgElementArgs: ImageElementProps = { - isLazy, - imgAttributes, - heightInt, - widthInt, - qualityInt, - className, - imgStyle, - blurStyle, - loading, - config, - fill, - unoptimized, - placeholder, - loader, - srcString, - onLoadRef, - onLoadingCompleteRef, - setBlurComplete, - setShowAltText, - ...rest, + const onLoadRef = useRef(onLoad) + + useEffect(() => { + onLoadRef.current = onLoad + }, [onLoad]) + + const onLoadingCompleteRef = useRef(onLoadingComplete) + + useEffect(() => { + onLoadingCompleteRef.current = onLoadingComplete + }, [onLoadingComplete]) + + const imgElementArgs: ImageElementProps = { + isLazy, + imgAttributes, + heightInt, + widthInt, + qualityInt, + className, + imgStyle, + blurStyle, + loading, + config, + fill, + unoptimized, + placeholder, + loader, + srcString, + onLoadRef, + onLoadingCompleteRef, + setBlurComplete, + setShowAltText, + ...rest, + } + return ( + <> + {} + {priority ? ( + // Note how we omit the `href` attribute, as it would only be relevant + // for browsers that do not support `imagesrcset`, and in those cases + // it would likely cause the incorrect image to be preloaded. + // + // https://html.spec.whatwg.org/multipage/semantics.html#attr-link-imagesrcset + + + + ) : null} + + ) } - return ( - <> - {} - {priority ? ( - // Note how we omit the `href` attribute, as it would only be relevant - // for browsers that do not support `imagesrcset`, and in those cases - // it would likely cause the incorrect image to be preloaded. - // - // https://html.spec.whatwg.org/multipage/semantics.html#attr-link-imagesrcset - - - - ) : null} - - ) -} +) + +export default Image From fd99a57becc0462575d53fa51a2eb72e76a10a8f Mon Sep 17 00:00:00 2001 From: Jan Kaifer Date: Tue, 22 Nov 2022 14:56:47 +0100 Subject: [PATCH 02/10] Add e2e test with framer-motion --- test/e2e/link-with-api-rewrite/index.test.ts | 1 - test/e2e/next-image/app/pages/index.js | 32 +++++++++++++++++ test/e2e/next-image/index.test.ts | 36 ++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 test/e2e/next-image/app/pages/index.js create mode 100644 test/e2e/next-image/index.test.ts diff --git a/test/e2e/link-with-api-rewrite/index.test.ts b/test/e2e/link-with-api-rewrite/index.test.ts index 31fccb1a42b9..320cb66889b4 100644 --- a/test/e2e/link-with-api-rewrite/index.test.ts +++ b/test/e2e/link-with-api-rewrite/index.test.ts @@ -20,7 +20,6 @@ describe('link-with-api-rewrite', () => { it('should perform hard navigation for rewritten urls', async () => { const browser = await webdriver(next.url, '/') - try { // Click the link on the page, we expect that there will be a hard // navigation later (we do this be checking that the window global is diff --git a/test/e2e/next-image/app/pages/index.js b/test/e2e/next-image/app/pages/index.js new file mode 100644 index 000000000000..b7492a27dfd6 --- /dev/null +++ b/test/e2e/next-image/app/pages/index.js @@ -0,0 +1,32 @@ +import React, { useState } from 'react' +import Image from 'next/image' +import { motion } from 'framer-motion' + +const CustomImage = React.forwardRef((props, ref) => ( + Phill Murray +)) + +const MotionImage = motion(CustomImage) + +export default function Page() { + const [clicked, setClicked] = useState(false) + return ( + <> +

Framer demo

+ setClicked(true)} + initial={{ opacity: 1 }} + animate={{ opacity: clicked ? 0 : 1 }} + transition={{ duration: 0.5 }} + /> + + ) +} diff --git a/test/e2e/next-image/index.test.ts b/test/e2e/next-image/index.test.ts new file mode 100644 index 000000000000..72fb46d217be --- /dev/null +++ b/test/e2e/next-image/index.test.ts @@ -0,0 +1,36 @@ +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { waitFor } from 'next-test-utils' +import path from 'path' +import webdriver from 'next-webdriver' + +describe('next-image', () => { + let next: NextInstance + + const appDir = path.join(__dirname, 'app') + + beforeAll(async () => { + next = await createNext({ + files: { + pages: new FileRef(path.join(appDir, 'pages')), + // 'next.config.js': new FileRef(path.join(appDir, 'next.config.js')), + }, + dependencies: { + 'framer-motion': '7.6.9', + }, + }) + }) + afterAll(() => next.destroy()) + + it('allows framer-motion to animate opacity', async () => { + const browser = await webdriver(next.url, '/') + expect( + Number(await browser.elementById('img').getComputedCss('opacity')) + ).toBeCloseTo(1) + browser.elementById('img').click() + await waitFor(2000) + expect( + Number(await browser.elementById('img').getComputedCss('opacity')) + ).toBeCloseTo(0) + }) +}) From 25dd717f875701295b864f481ee326251222307d Mon Sep 17 00:00:00 2001 From: Jan Kaifer Date: Tue, 22 Nov 2022 15:22:13 +0100 Subject: [PATCH 03/10] Upade docs --- docs/api-reference/next/image.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index f7320edbb97a..8631c5c633ed 100644 --- a/docs/api-reference/next/image.md +++ b/docs/api-reference/next/image.md @@ -16,6 +16,7 @@ description: Enable Image Optimization with the built-in Image component. | Version | Changes | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `v13.0.1` | `ref` prop added. | | `v13.0.0` | `` wrapper removed. `layout`, `objectFit`, `objectPosition`, `lazyBoundary`, `lazyRoot` props removed. `alt` is required. `onLoadingComplete` receives reference to `img` element. Built-in loader config removed. | | `v12.3.0` | `remotePatterns` and `unoptimized` configuration is stable. | | `v12.2.0` | Experimental `remotePatterns` and experimental `unoptimized` configuration added. `layout="raw"` removed. | @@ -284,7 +285,6 @@ Other properties on the `` component will be passed to the underlying `img` element with the exception of the following: - `srcSet`. Use [Device Sizes](#device-sizes) instead. -- `ref`. Use [`onLoadingComplete`](#onloadingcomplete) instead. - `decoding`. It is always `"async"`. ## Configuration Options From 7e9e8a8aaff4124ab0f3fd3fe49bf834dac58a39 Mon Sep 17 00:00:00 2001 From: Jan Kaifer Date: Tue, 22 Nov 2022 16:37:49 +0100 Subject: [PATCH 04/10] use local image in test --- test/e2e/next-image/app/images/test.png | Bin 0 -> 1545 bytes test/e2e/next-image/app/pages/index.js | 5 +++-- test/e2e/next-image/index.test.ts | 5 +---- 3 files changed, 4 insertions(+), 6 deletions(-) create mode 100644 test/e2e/next-image/app/images/test.png diff --git a/test/e2e/next-image/app/images/test.png b/test/e2e/next-image/app/images/test.png new file mode 100644 index 0000000000000000000000000000000000000000..e14fafc5cf3bc63b70914ad20467f40f7fecd572 GIT binary patch literal 1545 zcmbVM{Xf$Q9A6%~&O#i9S`$MamWNGTo{nv7c{rq_?QHVUW~E6OQi@{ZJcmZ|?7l@Q zzFgO>LgSLHn=t)6BZh%t7EPMfk1SL z1Y86JvZ4In(b7~asTB_EYLbzJ#fBxtCqp2a7u(A{gEak&&i%OE5K&=dA02(f0EgVb zDQO?EwAgXhbPx#1STW3~N_6+*i-&gO&5gIVD)qtd)=yh(VkE{hpxOq=E?Uo-)5z*x z!Au!iA$YiLAm+*0qggP>?VsKD-2i&HQxQ3+OqX*8S}wK5H8(1QM_f{Jya%lp;-fFQ z-RxdA9ea)1aI;`EXvn#9J~1_}n?bl%WsA3~x1yF~ZJY?F%5TY1f>Os{GDi>X>C?IS zC87Oo3ZX}KJ*U`mZ%63leZQDa&ij+|L2Ig&kv$8+G!kJ)!A>IpI0!SpvZ=R*dmxwE z_A02!zif^Xi?D&?&%f0Tzbc>bI(#PkQsao89{0s~R(I*hM>py`YIH=n8s(l<+!VhFb)fj#H;uE`npo7 zY;0_#QmGRY6Algzb}0{05Qr9vi1UjyHCq}CIyy~&Xo)lk4660;XBm=IbzH;Vwux!6 z@U`%Q<6`U_r^#vHXzMH%_g}z&^bvih;Naksl&3F)p7Kn#$+goa*xhsUD|t?H%CawT z>JQ8!^fPzDF6c8waZPU1$^P~{X*y_EN`KC=6nc}~iEX#>ud*u)-GT=qZK~K!#eMKri|K2@v zeX7|gqiZ-a27vkY(m>jlb*A45J^WhNqUd5svx=i!WlyGoDxyIkDCJw8 zl1RKs=y0j+xtSIh@AZ-SU-~z%d7|iJXK0I}nj!QZ_;_V0t%N>WpH)B+RT91Kkuhzx zSp{CL@O&X!puOb5enarY#IKV0$GfaZ<5QCF#q6Ih66Bl1Pk?cT!sCl5^YK4KUf8=r z`aO#WUfA<6@Z|tBgFYm!h8b-eKV4c&$3bTW&<9YGGZ&`xG#9~EHI4;**~o$2bOc^F z)xqxjhTZjF)wtZ04Ns<6mIBW?61;SKUp&Ix#QrYF;SY_@rCeH2X2*tJ$*pAIHb zh#ej+0ZbcVCs7JzV7TsL6Jyyhc?vBAKW|d~E=#`(Epz?bhZI(;xeQ`sbe2CXvFp-!)9gAPmnDWWTsf>26XSP@ zv&2i`WrNZNf%ZoawxTiv7?Jj|6+NW@o>r`=449DMidcqyfhe1CUhQqXbvCSyC1#>! z&TQ9Zpp%MX zY5qJSn%bSF+=@PAVhp9?wWsW-al19&OZPE literal 0 HcmV?d00001 diff --git a/test/e2e/next-image/app/pages/index.js b/test/e2e/next-image/app/pages/index.js index b7492a27dfd6..2606dff8faa1 100644 --- a/test/e2e/next-image/app/pages/index.js +++ b/test/e2e/next-image/app/pages/index.js @@ -1,15 +1,16 @@ import React, { useState } from 'react' import Image from 'next/image' import { motion } from 'framer-motion' +import testPng from '../images/test.png' const CustomImage = React.forwardRef((props, ref) => ( Phill Murray )) diff --git a/test/e2e/next-image/index.test.ts b/test/e2e/next-image/index.test.ts index 72fb46d217be..58acc76758ce 100644 --- a/test/e2e/next-image/index.test.ts +++ b/test/e2e/next-image/index.test.ts @@ -11,10 +11,7 @@ describe('next-image', () => { beforeAll(async () => { next = await createNext({ - files: { - pages: new FileRef(path.join(appDir, 'pages')), - // 'next.config.js': new FileRef(path.join(appDir, 'next.config.js')), - }, + files: new FileRef(appDir), dependencies: { 'framer-motion': '7.6.9', }, From 5def8b43ab172fb9d016734341470de712650aa2 Mon Sep 17 00:00:00 2001 From: Jan Kaifer Date: Tue, 22 Nov 2022 21:21:11 +0100 Subject: [PATCH 05/10] Update docs/api-reference/next/image.md Co-authored-by: Steven --- docs/api-reference/next/image.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index 8631c5c633ed..8663f23c1544 100644 --- a/docs/api-reference/next/image.md +++ b/docs/api-reference/next/image.md @@ -16,7 +16,7 @@ description: Enable Image Optimization with the built-in Image component. | Version | Changes | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `v13.0.1` | `ref` prop added. | +| `v13.0.5` | `ref` prop added. | | `v13.0.0` | `` wrapper removed. `layout`, `objectFit`, `objectPosition`, `lazyBoundary`, `lazyRoot` props removed. `alt` is required. `onLoadingComplete` receives reference to `img` element. Built-in loader config removed. | | `v12.3.0` | `remotePatterns` and `unoptimized` configuration is stable. | | `v12.2.0` | Experimental `remotePatterns` and experimental `unoptimized` configuration added. `layout="raw"` removed. | From df6e63558aa4e3bae1e43c740dab5c1a726eeb20 Mon Sep 17 00:00:00 2001 From: Jan Kaifer Date: Tue, 22 Nov 2022 21:26:04 +0100 Subject: [PATCH 06/10] Remove line added by mistake --- test/e2e/link-with-api-rewrite/index.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/link-with-api-rewrite/index.test.ts b/test/e2e/link-with-api-rewrite/index.test.ts index 320cb66889b4..31fccb1a42b9 100644 --- a/test/e2e/link-with-api-rewrite/index.test.ts +++ b/test/e2e/link-with-api-rewrite/index.test.ts @@ -20,6 +20,7 @@ describe('link-with-api-rewrite', () => { it('should perform hard navigation for rewritten urls', async () => { const browser = await webdriver(next.url, '/') + try { // Click the link on the page, we expect that there will be a hard // navigation later (we do this be checking that the window global is From 536b0d75de9f89ba6effdc2ce3be0ee8a69ab0dd Mon Sep 17 00:00:00 2001 From: Jan Kaifer Date: Tue, 22 Nov 2022 21:28:48 +0100 Subject: [PATCH 07/10] Cleaned up next-image e2e test for easier extension --- .../app/pages/{index.js => farmer-motion.js} | 15 ++++++--------- test/e2e/next-image/index.test.ts | 4 ++-- 2 files changed, 8 insertions(+), 11 deletions(-) rename test/e2e/next-image/app/pages/{index.js => farmer-motion.js} (67%) diff --git a/test/e2e/next-image/app/pages/index.js b/test/e2e/next-image/app/pages/farmer-motion.js similarity index 67% rename from test/e2e/next-image/app/pages/index.js rename to test/e2e/next-image/app/pages/farmer-motion.js index 2606dff8faa1..9edba39ae0b3 100644 --- a/test/e2e/next-image/app/pages/index.js +++ b/test/e2e/next-image/app/pages/farmer-motion.js @@ -20,14 +20,11 @@ const MotionImage = motion(CustomImage) export default function Page() { const [clicked, setClicked] = useState(false) return ( - <> -

Framer demo

- setClicked(true)} - initial={{ opacity: 1 }} - animate={{ opacity: clicked ? 0 : 1 }} - transition={{ duration: 0.5 }} - /> - + setClicked(true)} + initial={{ opacity: 1 }} + animate={{ opacity: clicked ? 0 : 1 }} + transition={{ duration: 0.5 }} + /> ) } diff --git a/test/e2e/next-image/index.test.ts b/test/e2e/next-image/index.test.ts index 58acc76758ce..a804562b2f8f 100644 --- a/test/e2e/next-image/index.test.ts +++ b/test/e2e/next-image/index.test.ts @@ -4,7 +4,7 @@ import { waitFor } from 'next-test-utils' import path from 'path' import webdriver from 'next-webdriver' -describe('next-image', () => { +describe('next-image-forward-ref', () => { let next: NextInstance const appDir = path.join(__dirname, 'app') @@ -20,7 +20,7 @@ describe('next-image', () => { afterAll(() => next.destroy()) it('allows framer-motion to animate opacity', async () => { - const browser = await webdriver(next.url, '/') + const browser = await webdriver(next.url, '/framer-motion') expect( Number(await browser.elementById('img').getComputedCss('opacity')) ).toBeCloseTo(1) From bd20af7f358e95bb1b39442cab90fe7ab812b254 Mon Sep 17 00:00:00 2001 From: Jan Kaifer Date: Wed, 23 Nov 2022 10:06:33 +0100 Subject: [PATCH 08/10] Fix names in test --- .../app/images/test.png | Bin .../app/pages/framer-motion.js} | 0 .../index.test.ts | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename test/e2e/{next-image => next-image-forward-ref}/app/images/test.png (100%) rename test/e2e/{next-image/app/pages/farmer-motion.js => next-image-forward-ref/app/pages/framer-motion.js} (100%) rename test/e2e/{next-image => next-image-forward-ref}/index.test.ts (100%) diff --git a/test/e2e/next-image/app/images/test.png b/test/e2e/next-image-forward-ref/app/images/test.png similarity index 100% rename from test/e2e/next-image/app/images/test.png rename to test/e2e/next-image-forward-ref/app/images/test.png diff --git a/test/e2e/next-image/app/pages/farmer-motion.js b/test/e2e/next-image-forward-ref/app/pages/framer-motion.js similarity index 100% rename from test/e2e/next-image/app/pages/farmer-motion.js rename to test/e2e/next-image-forward-ref/app/pages/framer-motion.js diff --git a/test/e2e/next-image/index.test.ts b/test/e2e/next-image-forward-ref/index.test.ts similarity index 100% rename from test/e2e/next-image/index.test.ts rename to test/e2e/next-image-forward-ref/index.test.ts From c43049e9c09a203eda957200a0c5d2f86edf9b70 Mon Sep 17 00:00:00 2001 From: Jan Kaifer Date: Thu, 24 Nov 2022 10:08:26 +0100 Subject: [PATCH 09/10] Update test/e2e/next-image-forward-ref/index.test.ts Co-authored-by: Steven --- test/e2e/next-image-forward-ref/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/next-image-forward-ref/index.test.ts b/test/e2e/next-image-forward-ref/index.test.ts index a804562b2f8f..9ac2a98e7291 100644 --- a/test/e2e/next-image-forward-ref/index.test.ts +++ b/test/e2e/next-image-forward-ref/index.test.ts @@ -25,7 +25,7 @@ describe('next-image-forward-ref', () => { Number(await browser.elementById('img').getComputedCss('opacity')) ).toBeCloseTo(1) browser.elementById('img').click() - await waitFor(2000) + await waitFor(1000) expect( Number(await browser.elementById('img').getComputedCss('opacity')) ).toBeCloseTo(0) From 7d1ba5cb59ade0f6aaac58632d2eb7ff5bc44624 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 25 Nov 2022 09:35:00 -0500 Subject: [PATCH 10/10] Update docs to 13.0.6 --- docs/api-reference/next/image.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index 8663f23c1544..b3cb2726751f 100644 --- a/docs/api-reference/next/image.md +++ b/docs/api-reference/next/image.md @@ -16,7 +16,7 @@ description: Enable Image Optimization with the built-in Image component. | Version | Changes | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `v13.0.5` | `ref` prop added. | +| `v13.0.6` | `ref` prop added. | | `v13.0.0` | `` wrapper removed. `layout`, `objectFit`, `objectPosition`, `lazyBoundary`, `lazyRoot` props removed. `alt` is required. `onLoadingComplete` receives reference to `img` element. Built-in loader config removed. | | `v12.3.0` | `remotePatterns` and `unoptimized` configuration is stable. | | `v12.2.0` | Experimental `remotePatterns` and experimental `unoptimized` configuration added. `layout="raw"` removed. |