From 61cdc72e4c80c568a0e11a8292ac367fd11439ad Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 1 Jul 2021 14:51:20 -0400 Subject: [PATCH] Add `onLoadingComplete()` prop to Image component (#26824) This adds a new prop, `onLoadingComplete()`, to handle the most common use case of `ref`. I also added docs and a warning when using `ref` so we recommend the new prop instead. - Fixes #18398 - Fixes #22482 --- docs/api-reference/next/image.md | 5 ++ packages/next/client/image.tsx | 54 ++++++++++++------- .../default/pages/on-loading-complete.js | 32 +++++++++++ .../default/test/index.test.js | 29 ++++++++++ 4 files changed, 100 insertions(+), 20 deletions(-) create mode 100644 test/integration/image-component/default/pages/on-loading-complete.js diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index 0efb0121a3a8f..4dd7b2e878b10 100644 --- a/docs/api-reference/next/image.md +++ b/docs/api-reference/next/image.md @@ -195,6 +195,10 @@ The image position when using `layout="fill"`. [Learn more](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) +### onLoadingComplete + +A callback function that is invoked once the image is completely loaded and the placeholder has been removed. + ### loading > **Attention**: This property is only meant for advanced usage. Switching an @@ -242,6 +246,7 @@ Other properties on the `` component will be passed to the underlying - `srcSet`. Use [Device Sizes](/docs/basic-features/image-optimization.md#device-sizes) instead. +- `ref`. Use [`onLoadingComplete`](#onloadingcomplete) instead. - `decoding`. It is always `"async"`. ## Related diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 099a4dc3f33bc..909ddfdaa1395 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -120,6 +120,7 @@ export type ImageProps = Omit< unoptimized?: boolean objectFit?: ImgElementStyle['objectFit'] objectPosition?: ImgElementStyle['objectPosition'] + onLoadingComplete?: () => void } & (StringImageProps | ObjectImageProps) const { @@ -261,30 +262,37 @@ function defaultImageLoader(loaderProps: ImageLoaderProps) { // See https://stackoverflow.com/q/39777833/266535 for why we use this ref // handler instead of the img's onLoad attribute. -function removePlaceholder( +function handleLoading( img: HTMLImageElement | null, - placeholder: PlaceholderValue + placeholder: PlaceholderValue, + onLoadingComplete?: () => void ) { - if (placeholder === 'blur' && img) { - const handleLoad = () => { - if (!img.src.startsWith('data:')) { - const p = 'decode' in img ? img.decode() : Promise.resolve() - p.catch(() => {}).then(() => { + if (!img) { + return + } + const handleLoad = () => { + if (!img.src.startsWith('data:')) { + const p = 'decode' in img ? img.decode() : Promise.resolve() + p.catch(() => {}).then(() => { + if (placeholder === 'blur') { img.style.filter = 'none' img.style.backgroundSize = 'none' img.style.backgroundImage = 'none' - }) - } - } - if (img.complete) { - // If the real image fails to load, this will still remove the placeholder. - // This is the desired behavior for now, and will be revisited when error - // handling is worked on for the image component itself. - handleLoad() - } else { - img.onload = handleLoad + } + if (onLoadingComplete) { + onLoadingComplete() + } + }) } } + if (img.complete) { + // If the real image fails to load, this will still remove the placeholder. + // This is the desired behavior for now, and will be revisited when error + // handling is worked on for the image component itself. + handleLoad() + } else { + img.onload = handleLoad + } } export default function Image({ @@ -299,6 +307,7 @@ export default function Image({ height, objectFit, objectPosition, + onLoadingComplete, loader = defaultImageLoader, placeholder = 'empty', blurDataURL, @@ -401,6 +410,11 @@ export default function Image({ ) } } + if ('ref' in rest) { + console.warn( + `Image with src "${src}" is using unsupported "ref" property. Consider using the "onLoadingComplete" property instead.` + ) + } } let isLazy = !priority && (loading === 'lazy' || typeof loading === 'undefined') @@ -589,9 +603,9 @@ export default function Image({ {...imgAttributes} decoding="async" className={className} - ref={(element) => { - setRef(element) - removePlaceholder(element, placeholder) + ref={(img) => { + setRef(img) + handleLoading(img, placeholder, onLoadingComplete) }} style={imgStyle} /> diff --git a/test/integration/image-component/default/pages/on-loading-complete.js b/test/integration/image-component/default/pages/on-loading-complete.js new file mode 100644 index 0000000000000..2e7bf33db34aa --- /dev/null +++ b/test/integration/image-component/default/pages/on-loading-complete.js @@ -0,0 +1,32 @@ +import { useState } from 'react' +import Image from 'next/image' + +const Page = () => ( +
+

On Loading Complete Test

+ + +
+) + +function ImageWithMessage({ id, src }) { + const [msg, setMsg] = useState('[LOADING]') + return ( + <> + setMsg(`loaded img${id}`)} + /> +

{msg}

+ + ) +} + +export default Page diff --git a/test/integration/image-component/default/test/index.test.js b/test/integration/image-component/default/test/index.test.js index b10ba3a0a7102..540383a260608 100644 --- a/test/integration/image-component/default/test/index.test.js +++ b/test/integration/image-component/default/test/index.test.js @@ -182,6 +182,35 @@ function runTests(mode) { } }) + it('should callback onLoadingComplete when image is fully loaded', async () => { + let browser + try { + browser = await webdriver(appPort, '/on-loading-complete') + + await check( + () => browser.eval(`document.getElementById("img1").src`), + /test(.*)jpg/ + ) + + await check( + () => browser.eval(`document.getElementById("img2").src`), + /test(.*).png/ + ) + await check( + () => browser.eval(`document.getElementById("msg1").textContent`), + 'loaded img1' + ) + await check( + () => browser.eval(`document.getElementById("msg2").textContent`), + 'loaded img2' + ) + } finally { + if (browser) { + await browser.close() + } + } + }) + it('should work when using flexbox', async () => { let browser try {