Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add onLoadingComplete() prop to Image component #26824

Merged
merged 7 commits into from
Jul 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/api-reference/next/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -242,6 +246,7 @@ Other properties on the `<Image />` 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
Expand Down
54 changes: 34 additions & 20 deletions packages/next/client/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export type ImageProps = Omit<
unoptimized?: boolean
objectFit?: ImgElementStyle['objectFit']
objectPosition?: ImgElementStyle['objectPosition']
onLoadingComplete?: () => void
} & (StringImageProps | ObjectImageProps)

const {
Expand Down Expand Up @@ -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({
Expand All @@ -299,6 +307,7 @@ export default function Image({
height,
objectFit,
objectPosition,
onLoadingComplete,
loader = defaultImageLoader,
placeholder = 'empty',
blurDataURL,
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState } from 'react'
import Image from 'next/image'

const Page = () => (
<div>
<h1>On Loading Complete Test</h1>
<ImageWithMessage id="1" src="/test.jpg" />
<ImageWithMessage
id="2"
src={require('../public/test.png')}
placeholder="blur"
/>
</div>
)

function ImageWithMessage({ id, src }) {
const [msg, setMsg] = useState('[LOADING]')
return (
<>
<Image
id={`img${id}`}
src={src}
width="400"
height="400"
onLoadingComplete={() => setMsg(`loaded img${id}`)}
/>
<p id={`msg${id}`}>{msg}</p>
</>
)
}

export default Page
29 changes: 29 additions & 0 deletions test/integration/image-component/default/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down