diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index 17687eb93a201..830771ce34471 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 | | --------- | ------------------------------------------------------------------------------------------------- | +| `v12.0.9` | `lazyRoot` prop added | | `v12.0.0` | `formats` configuration added.
AVIF support added.
Wrapper `
` changed to ``. | | `v11.1.0` | `onLoadingComplete` and `lazyBoundary` props added. | | `v11.0.0` | `src` prop support for static import.
`placeholder` prop added.
`blurDataURL` prop added. | @@ -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, diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 04f2f9a175fdd..0980f8abd535b 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -100,6 +100,7 @@ export type ImageProps = Omit< quality?: number | string priority?: boolean loading?: LoadingValue + lazyRoot?: React.RefObject | null lazyBoundary?: string placeholder?: PlaceholderValue blurDataURL?: string @@ -319,6 +320,7 @@ export default function Image({ unoptimized = false, priority = false, loading, + lazyRoot = null, lazyBoundary = '200px', className, quality, @@ -510,6 +512,7 @@ export default function Image({ } const [setIntersection, isIntersected] = useIntersection({ + rootRef: lazyRoot, rootMargin: lazyBoundary, disabled: !isLazy, }) diff --git a/packages/next/client/use-intersection.tsx b/packages/next/client/use-intersection.tsx index 62d188f579a1d..94ee9d97f1d1e 100644 --- a/packages/next/client/use-intersection.tsx +++ b/packages/next/client/use-intersection.tsx @@ -4,8 +4,14 @@ import { cancelIdleCallback, } from './request-idle-callback' -type UseIntersectionObserverInit = Pick -type UseIntersection = { disabled?: boolean } & UseIntersectionObserverInit +type UseIntersectionObserverInit = Pick< + IntersectionObserverInit, + 'rootMargin' | 'root' +> + +type UseIntersection = { disabled?: boolean } & UseIntersectionObserverInit & { + rootRef?: React.RefObject | null + } type ObserveCallback = (isVisible: boolean) => void type Observer = { id: string @@ -16,6 +22,7 @@ type Observer = { const hasIntersectionObserver = typeof IntersectionObserver !== 'undefined' export function useIntersection({ + rootRef, rootMargin, disabled, }: UseIntersection): [(element: T | null) => void, boolean] { @@ -23,7 +30,7 @@ export function useIntersection({ const unobserve = useRef() const [visible, setVisible] = useState(false) - + const [root, setRoot] = useState(rootRef ? rootRef.current : null) const setRef = useCallback( (el: T | null) => { if (unobserve.current) { @@ -37,11 +44,11 @@ export function useIntersection({ unobserve.current = observe( el, (isVisible) => isVisible && setVisible(isVisible), - { rootMargin } + { root, rootMargin } ) } }, - [isDisabled, rootMargin, visible] + [isDisabled, root, rootMargin, visible] ) useEffect(() => { @@ -53,6 +60,9 @@ export function useIntersection({ } }, [visible]) + useEffect(() => { + if (rootRef) setRoot(rootRef.current) + }, [rootRef]) return [setRef, visible] } diff --git a/test/integration/image-component/default/pages/lazy-noref.js b/test/integration/image-component/default/pages/lazy-noref.js new file mode 100644 index 0000000000000..3edfe7df79028 --- /dev/null +++ b/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 ( + <> +
+
hello
+
+ +
+
+ + ) +} +export default Page diff --git a/test/integration/image-component/default/pages/lazy-withref.js b/test/integration/image-component/default/pages/lazy-withref.js new file mode 100644 index 0000000000000..d21a4c0a9dad4 --- /dev/null +++ b/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 ( + <> +
+
hello
+
+ +
+
+ + ) +} +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 d7d2c050f7c49..5dae3d31658c8 100644 --- a/test/integration/image-component/default/test/index.test.js +++ b/test/integration/image-component/default/test/index.test.js @@ -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', () => {