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

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

Merged
merged 1 commit into from Apr 29, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
91 changes: 54 additions & 37 deletions packages/next/src/client/components/layout-router.tsx
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -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>
}
>
<>
Expand Down
@@ -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>
))
}
</>
)
}
17 changes: 17 additions & 0 deletions test/e2e/app-dir/router-autoscroll/app/loading-scroll/page.tsx
@@ -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>
))
}
</>
)
}
13 changes: 10 additions & 3 deletions test/e2e/app-dir/router-autoscroll/app/page.tsx
Expand Up @@ -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>
</>
)
}
12 changes: 12 additions & 0 deletions test/e2e/app-dir/router-autoscroll/router-autoscroll.test.ts
Expand Up @@ -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)
})
})
}
)