Skip to content

Commit d57a03c

Browse files
committed
Merge remote-tracking branch 'upstream/canary' into vary-accept
2 parents b62ee67 + 93f6254 commit d57a03c

File tree

7 files changed

+121
-41
lines changed

7 files changed

+121
-41
lines changed

docs/api-reference/next/image.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ The image position when using `layout="fill"`.
195195

196196
[Learn more](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position)
197197

198+
### onLoadingComplete
199+
200+
A callback function that is invoked once the image is completely loaded and the placeholder has been removed.
201+
198202
### loading
199203

200204
> **Attention**: This property is only meant for advanced usage. Switching an
@@ -242,6 +246,7 @@ Other properties on the `<Image />` component will be passed to the underlying
242246
- `srcSet`. Use
243247
[Device Sizes](/docs/basic-features/image-optimization.md#device-sizes)
244248
instead.
249+
- `ref`. Use [`onLoadingComplete`](#onloadingcomplete) instead.
245250
- `decoding`. It is always `"async"`.
246251

247252
## Related

packages/next/client/image.tsx

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export type ImageProps = Omit<
120120
unoptimized?: boolean
121121
objectFit?: ImgElementStyle['objectFit']
122122
objectPosition?: ImgElementStyle['objectPosition']
123+
onLoadingComplete?: () => void
123124
} & (StringImageProps | ObjectImageProps)
124125

125126
const {
@@ -261,30 +262,37 @@ function defaultImageLoader(loaderProps: ImageLoaderProps) {
261262

262263
// See https://stackoverflow.com/q/39777833/266535 for why we use this ref
263264
// handler instead of the img's onLoad attribute.
264-
function removePlaceholder(
265+
function handleLoading(
265266
img: HTMLImageElement | null,
266-
placeholder: PlaceholderValue
267+
placeholder: PlaceholderValue,
268+
onLoadingComplete?: () => void
267269
) {
268-
if (placeholder === 'blur' && img) {
269-
const handleLoad = () => {
270-
if (!img.src.startsWith('data:')) {
271-
const p = 'decode' in img ? img.decode() : Promise.resolve()
272-
p.catch(() => {}).then(() => {
270+
if (!img) {
271+
return
272+
}
273+
const handleLoad = () => {
274+
if (!img.src.startsWith('data:')) {
275+
const p = 'decode' in img ? img.decode() : Promise.resolve()
276+
p.catch(() => {}).then(() => {
277+
if (placeholder === 'blur') {
273278
img.style.filter = 'none'
274279
img.style.backgroundSize = 'none'
275280
img.style.backgroundImage = 'none'
276-
})
277-
}
278-
}
279-
if (img.complete) {
280-
// If the real image fails to load, this will still remove the placeholder.
281-
// This is the desired behavior for now, and will be revisited when error
282-
// handling is worked on for the image component itself.
283-
handleLoad()
284-
} else {
285-
img.onload = handleLoad
281+
}
282+
if (onLoadingComplete) {
283+
onLoadingComplete()
284+
}
285+
})
286286
}
287287
}
288+
if (img.complete) {
289+
// If the real image fails to load, this will still remove the placeholder.
290+
// This is the desired behavior for now, and will be revisited when error
291+
// handling is worked on for the image component itself.
292+
handleLoad()
293+
} else {
294+
img.onload = handleLoad
295+
}
288296
}
289297

290298
export default function Image({
@@ -299,6 +307,7 @@ export default function Image({
299307
height,
300308
objectFit,
301309
objectPosition,
310+
onLoadingComplete,
302311
loader = defaultImageLoader,
303312
placeholder = 'empty',
304313
blurDataURL,
@@ -401,6 +410,11 @@ export default function Image({
401410
)
402411
}
403412
}
413+
if ('ref' in rest) {
414+
console.warn(
415+
`Image with src "${src}" is using unsupported "ref" property. Consider using the "onLoadingComplete" property instead.`
416+
)
417+
}
404418
}
405419
let isLazy =
406420
!priority && (loading === 'lazy' || typeof loading === 'undefined')
@@ -589,9 +603,9 @@ export default function Image({
589603
{...imgAttributes}
590604
decoding="async"
591605
className={className}
592-
ref={(element) => {
593-
setRef(element)
594-
removePlaceholder(element, placeholder)
606+
ref={(img) => {
607+
setRef(img)
608+
handleLoading(img, placeholder, onLoadingComplete)
595609
}}
596610
style={imgStyle}
597611
/>

test/integration/custom-error-page-exception/test/index.test.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,29 @@
22

33
import { join } from 'path'
44
import webdriver from 'next-webdriver'
5-
import { nextBuild, nextStart, findPort, killApp } from 'next-test-utils'
5+
import { nextBuild, nextStart, findPort, killApp, check } from 'next-test-utils'
66

77
jest.setTimeout(1000 * 60 * 1)
88

99
const appDir = join(__dirname, '..')
10-
const navSel = '#nav'
11-
const errorMessage = 'Application error: a client-side exception has occurred'
10+
let appPort
11+
let app
1212

1313
describe('Custom error page exception', () => {
14+
beforeAll(async () => {
15+
await nextBuild(appDir)
16+
appPort = await findPort()
17+
app = await nextStart(appDir, appPort)
18+
})
19+
afterAll(() => killApp(app))
1420
it('should handle errors from _error render', async () => {
15-
const { code } = await nextBuild(appDir)
16-
const appPort = await findPort()
17-
const app = await nextStart(appDir, appPort)
21+
const navSel = '#nav'
1822
const browser = await webdriver(appPort, '/')
1923
await browser.waitForElementByCss(navSel).elementByCss(navSel).click()
20-
const text = await (await browser.elementByCss('#__next')).text()
21-
killApp(app)
2224

23-
expect(code).toBe(0)
24-
expect(text).toMatch(errorMessage)
25+
await check(
26+
() => browser.eval('document.documentElement.innerHTML'),
27+
/Application error: a client-side exception has occurred/
28+
)
2529
})
2630
})

test/integration/fallback-modules/test/index.test.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@ describe('Build Output', () => {
4444
const indexSize = parsePageSize('/')
4545
const indexFirstLoad = parsePageFirstLoad('/')
4646

47-
expect(parseFloat(indexSize)).toBeLessThanOrEqual(3.1)
48-
expect(parseFloat(indexSize)).toBeGreaterThanOrEqual(2)
47+
// expect(parseFloat(indexSize)).toBeLessThanOrEqual(3.1)
48+
// expect(parseFloat(indexSize)).toBeGreaterThanOrEqual(2)
4949
expect(indexSize.endsWith('kB')).toBe(true)
5050

51-
expect(parseFloat(indexFirstLoad)).toBeLessThanOrEqual(
52-
process.env.NEXT_PRIVATE_TEST_WEBPACK4_MODE ? 68.1 : 67.9
53-
)
54-
expect(parseFloat(indexFirstLoad)).toBeGreaterThanOrEqual(60)
51+
// expect(parseFloat(indexFirstLoad)).toBeLessThanOrEqual(
52+
// process.env.NEXT_PRIVATE_TEST_WEBPACK4_MODE ? 68.1 : 67.9
53+
// )
54+
// expect(parseFloat(indexFirstLoad)).toBeGreaterThanOrEqual(60)
5555
expect(indexFirstLoad.endsWith('kB')).toBe(true)
5656
})
5757
})

test/integration/font-optimization/fixtures/with-google/manifest-snapshot.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
[
22
{
33
"url": "https://fonts.googleapis.com/css?family=Voces",
4-
"content": "@font-face{font-family:'Voces';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/voces/v12/-F6_fjJyLyU8d7PGDmk.woff) format('woff')}@font-face{font-family:'Voces';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/voces/v12/-F6_fjJyLyU8d7PIDm_6pClI_ik.woff2) format('woff2');unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Voces';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/voces/v12/-F6_fjJyLyU8d7PGDm_6pClI.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}"
5-
},
6-
{
7-
"url": "https://fonts.googleapis.com/css2?family=Modak",
8-
"content": "@font-face{font-family:'Modak';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/modak/v8/EJRYQgs1XtIEsnME.woff) format('woff')}@font-face{font-family:'Modak';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/modak/v8/EJRYQgs1XtIEskMB-hR77LKVTy8.woff2) format('woff2');unicode-range:U+0900-097F,U+1CD0-1CF6,U+1CF8-1CF9,U+200C-200D,U+20A8,U+20B9,U+25CC,U+A830-A839,U+A8E0-A8FB}@font-face{font-family:'Modak';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/modak/v8/EJRYQgs1XtIEskMO-hR77LKVTy8.woff2) format('woff2');unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Modak';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/modak/v8/EJRYQgs1XtIEskMA-hR77LKV.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}"
4+
"content": "@font-face{font-family:'Voces';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/voces/v15/-F6_fjJyLyU8d7PGDmk.woff) format('woff')}@font-face{font-family:'Voces';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/voces/v15/-F6_fjJyLyU8d7PIDm_6pClI_ik.woff2) format('woff2');unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Voces';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/voces/v15/-F6_fjJyLyU8d7PGDm_6pClI.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}"
95
},
106
{
117
"url": "https://fonts.googleapis.com/css2?family=Modak",
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useState } from 'react'
2+
import Image from 'next/image'
3+
4+
const Page = () => (
5+
<div>
6+
<h1>On Loading Complete Test</h1>
7+
<ImageWithMessage id="1" src="/test.jpg" />
8+
<ImageWithMessage
9+
id="2"
10+
src={require('../public/test.png')}
11+
placeholder="blur"
12+
/>
13+
</div>
14+
)
15+
16+
function ImageWithMessage({ id, src }) {
17+
const [msg, setMsg] = useState('[LOADING]')
18+
return (
19+
<>
20+
<Image
21+
id={`img${id}`}
22+
src={src}
23+
width="400"
24+
height="400"
25+
onLoadingComplete={() => setMsg(`loaded img${id}`)}
26+
/>
27+
<p id={`msg${id}`}>{msg}</p>
28+
</>
29+
)
30+
}
31+
32+
export default Page

test/integration/image-component/default/test/index.test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,35 @@ function runTests(mode) {
182182
}
183183
})
184184

185+
it('should callback onLoadingComplete when image is fully loaded', async () => {
186+
let browser
187+
try {
188+
browser = await webdriver(appPort, '/on-loading-complete')
189+
190+
await check(
191+
() => browser.eval(`document.getElementById("img1").src`),
192+
/test(.*)jpg/
193+
)
194+
195+
await check(
196+
() => browser.eval(`document.getElementById("img2").src`),
197+
/test(.*).png/
198+
)
199+
await check(
200+
() => browser.eval(`document.getElementById("msg1").textContent`),
201+
'loaded img1'
202+
)
203+
await check(
204+
() => browser.eval(`document.getElementById("msg2").textContent`),
205+
'loaded img2'
206+
)
207+
} finally {
208+
if (browser) {
209+
await browser.close()
210+
}
211+
}
212+
})
213+
185214
it('should work when using flexbox', async () => {
186215
let browser
187216
try {

0 commit comments

Comments
 (0)