Fix scroll when loading.js/ts is used (#48986)

### What?

Whenever you navigated and a page suspended through `loading` or an error happened caught by `error` in the first level of segments (e.g. `/dashboard` but not `/dashboard/settings`) scroll would not be applied. This happened because the focus and scroll handling component is rendered as part of `InnerLayoutRouter` and the Suspense / Error boundary was rendered **around** `InnerLayoutRouter`. This behavior is incorrect as we still want to immediately scroll to the place where the loading is rendered.

This PR fixes the behavior by allowing the scroll to apply to loading / error too.

### How?

Moved the scrolling component around the loading/error/innerlayout boundary and added tests.
This commit is contained in:
Tim Neutkens 2023-04-29 19:43:55 +02:00 committed by GitHub
parent 986039db6e
commit 905b62e599
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 106 additions and 40 deletions

View file

@ -157,7 +157,7 @@ interface ScrollAndFocusHandlerProps {
children: React.ReactNode
segmentPath: FlightSegmentPath
}
class ScrollAndFocusHandler extends React.Component<ScrollAndFocusHandlerProps> {
class InnerScrollAndFocusHandler extends React.Component<ScrollAndFocusHandlerProps> {
handlePotentialScroll = () => {
// Handle scroll and focus, it's only applied once in the first useEffect that triggers that changed.
const { focusAndScrollRef, segmentPath } = this.props
@ -268,6 +268,28 @@ class ScrollAndFocusHandler extends React.Component<ScrollAndFocusHandlerProps>
}
}
function ScrollAndFocusHandler({
segmentPath,
children,
}: {
segmentPath: FlightSegmentPath
children: React.ReactNode
}) {
const context = useContext(GlobalLayoutRouterContext)
if (!context) {
throw new Error('invariant global layout router not mounted')
}
return (
<InnerScrollAndFocusHandler
segmentPath={segmentPath}
focusAndScrollRef={context.focusAndScrollRef}
>
{children}
</InnerScrollAndFocusHandler>
)
}
/**
* InnerLayoutRouter handles rendering the provided segment based on the cache.
*/
@ -296,7 +318,7 @@ function InnerLayoutRouter({
throw new Error('invariant global layout router not mounted')
}
const { changeByServerResponse, tree: fullTree, focusAndScrollRef } = context
const { changeByServerResponse, tree: fullTree } = context
// Read segment path from the parallel router cache node.
let childNode = childNodes.get(cacheKey)
@ -418,14 +440,7 @@ function InnerLayoutRouter({
</LayoutRouterContext.Provider>
)
// Ensure root layout is not wrapped in a div as the root layout renders `<html>`
return (
<ScrollAndFocusHandler
focusAndScrollRef={focusAndScrollRef}
segmentPath={segmentPath}
>
{subtree}
</ScrollAndFocusHandler>
)
return subtree
}
/**
@ -551,34 +566,36 @@ export default function OuterLayoutRouter({
<TemplateContext.Provider
key={cacheKey}
value={
<ErrorBoundary errorComponent={error} errorStyles={errorStyles}>
<LoadingBoundary
hasLoading={hasLoading}
loading={loading}
loadingStyles={loadingStyles}
>
<NotFoundBoundary
notFound={notFound}
notFoundStyles={notFoundStyles}
asNotFound={asNotFound}
<ScrollAndFocusHandler segmentPath={segmentPath}>
<ErrorBoundary errorComponent={error} errorStyles={errorStyles}>
<LoadingBoundary
hasLoading={hasLoading}
loading={loading}
loadingStyles={loadingStyles}
>
<RedirectBoundary>
<InnerLayoutRouter
parallelRouterKey={parallelRouterKey}
url={url}
tree={tree}
childNodes={childNodesForParallelRouter!}
childProp={isChildPropSegment ? childProp : null}
segmentPath={segmentPath}
cacheKey={cacheKey}
isActive={
currentChildSegmentValue === preservedSegmentValue
}
/>
</RedirectBoundary>
</NotFoundBoundary>
</LoadingBoundary>
</ErrorBoundary>
<NotFoundBoundary
notFound={notFound}
notFoundStyles={notFoundStyles}
asNotFound={asNotFound}
>
<RedirectBoundary>
<InnerLayoutRouter
parallelRouterKey={parallelRouterKey}
url={url}
tree={tree}
childNodes={childNodesForParallelRouter!}
childProp={isChildPropSegment ? childProp : null}
segmentPath={segmentPath}
cacheKey={cacheKey}
isActive={
currentChildSegmentValue === preservedSegmentValue
}
/>
</RedirectBoundary>
</NotFoundBoundary>
</LoadingBoundary>
</ErrorBoundary>
</ScrollAndFocusHandler>
}
>
<>

View file

@ -0,0 +1,13 @@
export default function Loading() {
return (
<>
<div id="loading-component">Loading component</div>
{
// Repeat 500 elements
Array.from({ length: 500 }, (_, i) => (
<div key={i}>Loading {i}...</div>
))
}
</>
)
}

View file

@ -0,0 +1,17 @@
export const dynamic = 'force-dynamic'
export default async function Page() {
await new Promise((resolve) => setTimeout(resolve, 5000))
return (
<>
<div style={{ display: 'none' }}>Content that is hidden.</div>
<div id="content-that-is-visible">Content which is not hidden.</div>
{
// Repeat 500 elements
Array.from({ length: 500 }, (_, i) => (
<div key={i}>{i}</div>
))
}
</>
)
}

View file

@ -9,9 +9,16 @@ export default function Page() {
<div key={i}>{i}</div>
))
}
<Link href="/invisible-first-element" id="to-invisible-first-element">
To invisible first element
</Link>
<div>
<Link href="/loading-scroll" id="to-loading-scroll">
To loading scroll
</Link>
</div>
<div>
<Link href="/invisible-first-element" id="to-invisible-first-element">
To invisible first element
</Link>
</div>
</>
)
}

View file

@ -170,6 +170,18 @@ createNextDescribe(
.waitForElementByCss('#content-that-is-visible')
await check(() => browser.eval('window.scrollY'), 0)
})
it('Should apply scroll when loading.js is used', async () => {
const browser = await webdriver(next.url, '/')
await browser.eval('window.scrollTo(0, 500)')
await browser
.elementByCss('#to-loading-scroll')
.click()
.waitForElementByCss('#loading-component')
await check(() => browser.eval('window.scrollY'), 0)
await browser.waitForElementByCss('#content-that-is-visible')
await check(() => browser.eval('window.scrollY'), 0)
})
})
}
)