Skip to content

Commit

Permalink
Add lazyRoot optional property to next/image component (#33290)
Browse files Browse the repository at this point in the history
* Added 'rootEl' oprional property to next/Image component resembling 'root' option of the Intersection Observer API

* changed 'rootEl' to 'lazyBoundary' and its type as well

* added test, fixed initial root detection

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

Co-authored-by: Steven <steven@ceriously.com>

* prop names changed

* added 'lazyroot' prop to the documentation

* removed unused import

* Apply suggestions from code review

* Update docs with lazyRoot added in 12.0.9

Co-authored-by: Steven <steven@ceriously.com>
  • Loading branch information
11koukou and styfle committed Jan 25, 2022
1 parent 9dd0399 commit 7452c0b
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 5 deletions.
7 changes: 7 additions & 0 deletions docs/api-reference/next/image.md
Expand Up @@ -16,6 +16,7 @@ description: Enable Image Optimization with the built-in Image component.

| Version | Changes |
| --------- | ------------------------------------------------------------------------------------------------- |
| `v12.0.9` | `lazyRoot` prop added |
| `v12.0.0` | `formats` configuration added.<br/>AVIF support added.<br/>Wrapper `<div>` changed to `<span>`. |
| `v11.1.0` | `onLoadingComplete` and `lazyBoundary` props added. |
| `v11.0.0` | `src` prop support for static import.<br/>`placeholder` prop added.<br/>`blurDataURL` prop added. |
Expand Down Expand Up @@ -221,6 +222,12 @@ A string (with similar syntax to the margin property) that acts as the bounding

[Learn more](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin)

### lazyRoot

A React [Ref](https://reactjs.org/docs/refs-and-the-dom.html) pointing to the Element which the [lazyBoundary](#lazyBoundary) calculates for the Intersection detection. Defaults to `null`, referring to the document viewport.

[Learn more](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root)

### unoptimized

When true, the source image will be served as-is instead of changing quality,
Expand Down
3 changes: 3 additions & 0 deletions packages/next/client/image.tsx
Expand Up @@ -100,6 +100,7 @@ export type ImageProps = Omit<
quality?: number | string
priority?: boolean
loading?: LoadingValue
lazyRoot?: React.RefObject<HTMLElement> | null
lazyBoundary?: string
placeholder?: PlaceholderValue
blurDataURL?: string
Expand Down Expand Up @@ -319,6 +320,7 @@ export default function Image({
unoptimized = false,
priority = false,
loading,
lazyRoot = null,
lazyBoundary = '200px',
className,
quality,
Expand Down Expand Up @@ -510,6 +512,7 @@ export default function Image({
}

const [setIntersection, isIntersected] = useIntersection<HTMLImageElement>({
rootRef: lazyRoot,
rootMargin: lazyBoundary,
disabled: !isLazy,
})
Expand Down
20 changes: 15 additions & 5 deletions packages/next/client/use-intersection.tsx
Expand Up @@ -4,8 +4,14 @@ import {
cancelIdleCallback,
} from './request-idle-callback'

type UseIntersectionObserverInit = Pick<IntersectionObserverInit, 'rootMargin'>
type UseIntersection = { disabled?: boolean } & UseIntersectionObserverInit
type UseIntersectionObserverInit = Pick<
IntersectionObserverInit,
'rootMargin' | 'root'
>

type UseIntersection = { disabled?: boolean } & UseIntersectionObserverInit & {
rootRef?: React.RefObject<HTMLElement> | null
}
type ObserveCallback = (isVisible: boolean) => void
type Observer = {
id: string
Expand All @@ -16,14 +22,15 @@ type Observer = {
const hasIntersectionObserver = typeof IntersectionObserver !== 'undefined'

export function useIntersection<T extends Element>({
rootRef,
rootMargin,
disabled,
}: UseIntersection): [(element: T | null) => void, boolean] {
const isDisabled: boolean = disabled || !hasIntersectionObserver

const unobserve = useRef<Function>()
const [visible, setVisible] = useState(false)

const [root, setRoot] = useState(rootRef ? rootRef.current : null)
const setRef = useCallback(
(el: T | null) => {
if (unobserve.current) {
Expand All @@ -37,11 +44,11 @@ export function useIntersection<T extends Element>({
unobserve.current = observe(
el,
(isVisible) => isVisible && setVisible(isVisible),
{ rootMargin }
{ root, rootMargin }
)
}
},
[isDisabled, rootMargin, visible]
[isDisabled, root, rootMargin, visible]
)

useEffect(() => {
Expand All @@ -53,6 +60,9 @@ export function useIntersection<T extends Element>({
}
}, [visible])

useEffect(() => {
if (rootRef) setRoot(rootRef.current)
}, [rootRef])
return [setRef, visible]
}

Expand Down
32 changes: 32 additions & 0 deletions test/integration/image-component/default/pages/lazy-noref.js
@@ -0,0 +1,32 @@
import React, { useRef } from 'react'
import Image from 'next/image'

const Page = () => {
const myRef = useRef(null)

return (
<>
<div
ref={myRef}
style={{
width: '100%',
height: '400px',
position: 'relative',
overflowY: 'scroll',
}}
>
<div style={{ width: '400px', height: '600px' }}>hello</div>
<div style={{ width: '400px', position: 'relative', height: '600px' }}>
<Image
id="myImage"
src="/test.jpg"
width="400"
height="400"
lazyBoundary="1800px"
/>
</div>
</div>
</>
)
}
export default Page
33 changes: 33 additions & 0 deletions test/integration/image-component/default/pages/lazy-withref.js
@@ -0,0 +1,33 @@
import React, { useRef } from 'react'
import Image from 'next/image'

const Page = () => {
const myRef = useRef(null)

return (
<>
<div
ref={myRef}
style={{
width: '100%',
height: '400px',
position: 'relative',
overflowY: 'scroll',
}}
>
<div style={{ width: '400px', height: '600px' }}>hello</div>
<div style={{ width: '400px', position: 'relative', height: '600px' }}>
<Image
lazyRoot={myRef}
id="myImage"
src="/test.jpg"
width="400"
height="400"
lazyBoundary="1800px"
/>
</div>
</div>
</>
)
}
export default Page
31 changes: 31 additions & 0 deletions test/integration/image-component/default/test/index.test.js
Expand Up @@ -1008,6 +1008,37 @@ function runTests(mode) {
}
}
})

it('should load the image when the lazyRoot prop is used', async () => {
let browser
try {
//trying on '/lazy-noref' it fails
browser = await webdriver(appPort, '/lazy-withref')

await check(async () => {
const result = await browser.eval(
`document.getElementById('myImage').naturalWidth`
)

if (result < 400) {
throw new Error('Incorrectly loaded image')
}

return 'result-correct'
}, /result-correct/)

expect(
await hasImageMatchingUrl(
browser,
`http://localhost:${appPort}/_next/image?url=%2Ftest.jpg&w=828&q=75`
)
).toBe(true)
} finally {
if (browser) {
await browser.close()
}
}
})
}

describe('Image Component Tests', () => {
Expand Down

0 comments on commit 7452c0b

Please sign in to comment.