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

app router: fix scrolling behaviour for parallel routes #48346

Merged
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
1 change: 1 addition & 0 deletions packages/next/src/client/app-index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ export function hydrate() {
focusAndScrollRef: {
apply: false,
hashFragment: null,
segmentPaths: [],
},
nextUrl: null,
}}
Expand Down
22 changes: 21 additions & 1 deletion packages/next/src/client/components/layout-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,30 @@ function getHashFragmentDomNode(hashFragment: string) {
interface ScrollAndFocusHandlerProps {
focusAndScrollRef: FocusAndScrollRef
children: React.ReactNode
segmentPath: FlightSegmentPath
}
class ScrollAndFocusHandler extends React.Component<ScrollAndFocusHandlerProps> {
handlePotentialScroll = () => {
// Handle scroll and focus, it's only applied once in the first useEffect that triggers that changed.
const { focusAndScrollRef } = this.props

if (focusAndScrollRef.apply) {
// segmentPaths is an array of segment paths that should be scrolled to
// if the current segment path is not in the array, the scroll is not applied
// unless the array is empty, in which case the scroll is always applied
if (
focusAndScrollRef.segmentPaths.length !== 0 &&
!focusAndScrollRef.segmentPaths.some(
(segmentPath) =>
segmentPath.length === this.props.segmentPath.length &&
segmentPath.every((segment, index) =>
matchSegment(segment, this.props.segmentPath[index])
)
)
) {
return
}

let domNode:
| ReturnType<typeof getHashFragmentDomNode>
| ReturnType<typeof findDOMNode> = null
Expand Down Expand Up @@ -376,7 +393,10 @@ function InnerLayoutRouter({
)
// Ensure root layout is not wrapped in a div as the root layout renders `<html>`
return (
<ScrollAndFocusHandler focusAndScrollRef={focusAndScrollRef}>
<ScrollAndFocusHandler
focusAndScrollRef={focusAndScrollRef}
segmentPath={segmentPath}
>
{subtree}
</ScrollAndFocusHandler>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe('createInitialRouterState', () => {
canonicalUrl: initialCanonicalUrl,
prefetchCache: new Map(),
pushRef: { pendingPush: false, mpaNavigation: false },
focusAndScrollRef: { apply: false, hashFragment: null },
focusAndScrollRef: { apply: false, hashFragment: null, segmentPaths: [] },
cache: expectedCache,
nextUrl: '/linking',
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function createInitialRouterState({
cache,
prefetchCache: new Map(),
pushRef: { pendingPush: false, mpaNavigation: false },
focusAndScrollRef: { apply: false, hashFragment: null },
focusAndScrollRef: { apply: false, hashFragment: null, segmentPaths: [] },
canonicalUrl:
// location.href is read as the initial value for canonicalUrl in the browser
// This is safe to do as canonicalUrl can't be rendered, it's only used to control the history updates in the useEffect further down in this file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export function handleMutable(
// All navigation requires scroll and focus management to trigger.
focusAndScrollRef: {
apply:
typeof mutable.applyFocusAndScroll !== 'undefined'
? mutable.applyFocusAndScroll
mutable?.scrollableSegments !== undefined
? true
: state.focusAndScrollRef.apply,
hashFragment:
// Empty hash should trigger default behavior of scrolling layout into view.
Expand All @@ -40,6 +40,8 @@ export function handleMutable(
? // Remove leading # and decode hash to make non-latin hashes work.
decodeURIComponent(mutable.hashFragment.slice(1))
: null,
segmentPaths:
mutable?.scrollableSegments ?? state.focusAndScrollRef.segmentPaths,
},
// Apply cache.
cache: mutable.cache ? mutable.cache : state.cache,
Expand Down