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 useSelectedLayoutSegment's support for parallel routes #60912

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
14 changes: 11 additions & 3 deletions packages/next/src/client/components/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '../../shared/lib/hooks-client-context.shared-runtime'
import { clientHookInServerComponentError } from './client-hook-in-server-component-error'
import { getSegmentValue } from './router-reducer/reducers/get-segment-value'
import { PAGE_SEGMENT_KEY } from '../../shared/lib/segment'
import { PAGE_SEGMENT_KEY, DEFAULT_SEGMENT_KEY } from '../../shared/lib/segment'

const INTERNAL_URLSEARCHPARAMS_INSTANCE = Symbol(
'internal for urlsearchparams readonly'
Expand Down Expand Up @@ -180,7 +180,6 @@ export function useParams<T extends Params = Params>(): T {
}, [globalLayoutRouter?.tree, pathParams])
}

// TODO-APP: handle parallel routes
/**
* Get the canonical parameters from the current level to the leaf node.
*/
Expand Down Expand Up @@ -243,7 +242,16 @@ export function useSelectedLayoutSegment(
return null
}

return selectedLayoutSegments[0]
const selectedLayoutSegment =
parallelRouteKey === 'children'
? selectedLayoutSegments[0]
: selectedLayoutSegments[selectedLayoutSegments.length - 1]

// if the default slot is showing, we return null since it's not technically "selected" (it's a fallback)
// and returning an internal value like `__DEFAULT__` would be confusing.
return selectedLayoutSegment === DEFAULT_SEGMENT_KEY
? null
: selectedLayoutSegment
}

export { redirect, permanentRedirect, RedirectType } from './redirect'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>/app/@auth/default.tsx</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>/app/@auth/login/page.tsx</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>/app/@auth/reset/page.tsx</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>/app/@auth/reset/withEmail/page.tsx</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>/app/@auth/reset/withMobile/page.tsx</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>/app/@nav/default.tsx</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>/app/@nav/login/page.tsx</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>/app/default.tsx</div>
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>/app/foo/page.tsx</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client'

import { useSelectedLayoutSegment } from 'next/navigation'
import Link from 'next/link'

export default function RootLayout({
children,
auth,
nav,
}: Readonly<{
children: React.ReactNode
auth: React.ReactNode
nav: React.ReactNode
}>) {
const authSegment = useSelectedLayoutSegment('auth')
const navSegment = useSelectedLayoutSegment('nav')
const routeSegment = useSelectedLayoutSegment()

return (
<html lang="en">
<body>
<section>
<nav>
<Link href="/">Main</Link>
<Link href="/foo">Foo (regular page)</Link>
<Link href="/login">
Login (/app/@auth/login) and (/app/@nav/login)
</Link>
<Link href="/reset">Reset (/app/@auth/reset)</Link>
<Link href="/reset/withEmail">
Reset with Email (/app/@auth/reset/withEmail)
</Link>
<Link href="/reset/withMobile">
Reset with Mobile (/app/@auth/reset/withMobile)
</Link>
</nav>
<div>
navSegment (parallel route): <div id="navSegment">{navSegment}</div>
</div>
<div>
authSegment (parallel route):{' '}
<div id="authSegment">{authSegment}</div>
</div>
<div>
routeSegment (app route):{' '}
<div id="routeSegment">{routeSegment}</div>
</div>

<section id="navSlot">{nav}</section>
<section id="authSlot">{auth}</section>
<section id="children">{children}</section>
</section>
</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>/app/page.tsx</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}

module.exports = nextConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { createNextDescribe } from 'e2e-utils'
import { check } from 'next-test-utils'

createNextDescribe(
'parallel-routes-use-selected-layout-segment',
{
files: __dirname,
},
({ next }) => {
it('hard nav to router page and soft nav around other router pages', async () => {
const browser = await next.browser('/')
await check(() => browser.elementById('navSegment').text(), /^$/)
await check(() => browser.elementById('authSegment').text(), /^$/)
await check(() => browser.elementById('routeSegment').text(), /^$/)

await browser.elementByCss('[href="/foo"]').click()
await check(() => browser.elementById('navSegment').text(), /^$/)
await check(() => browser.elementById('authSegment').text(), /^$/)
await check(() => browser.elementById('routeSegment').text(), /foo/)
})

it('hard nav to router page and soft nav to parallel routes', async () => {
const browser = await next.browser('/')
await check(() => browser.elementById('navSegment').text(), /^$/)
await check(() => browser.elementById('authSegment').text(), /^$/)
await check(() => browser.elementById('routeSegment').text(), /^$/)

// soft nav to /login, since both @nav and @auth has /login defined, we expect both navSegment and authSegment to be 'login'
await browser.elementByCss('[href="/login"]').click()
await check(() => browser.elementById('navSegment').text(), /login/)
await check(() => browser.elementById('authSegment').text(), /login/)
await check(() => browser.elementById('routeSegment').text(), /^$/)

// when navigating to /reset, the @auth slot will render the /reset page ('reset') while maintaining the currently active page for the @nav slot ('login') since /reset is only defined in @auth
await browser.elementByCss('[href="/reset"]').click()
await check(() => browser.elementById('navSegment').text(), /login/)
await check(() => browser.elementById('authSegment').text(), /reset/)
await check(() => browser.elementById('routeSegment').text(), /^$/)

// when navigating to nested path /reset/withEmail, the @auth slot will render the nested /reset/withEmail page ('reset') while maintaining the currently active page for the @nav slot ('login') since /reset/withEmail is only defined in @auth
await browser.elementByCss('[href="/reset/withEmail"]').click()
await check(() => browser.elementById('navSegment').text(), /login/)
await check(() => browser.elementById('authSegment').text(), /withEmail/)
await check(() => browser.elementById('routeSegment').text(), /^$/)
})

it('hard nav to router page and soft nav to parallel route and soft nav back to another router page', async () => {
const browser = await next.browser('/')
await check(() => browser.elementById('navSegment').text(), /^$/)
await check(() => browser.elementById('authSegment').text(), /^$/)
await check(() => browser.elementById('routeSegment').text(), /^$/)

// when navigating to /reset, the @auth slot will render the /reset page ('reset') while maintaining the currently active page for the @nav slot ('null') since /reset is only defined in @auth
await browser.elementByCss('[href="/reset"]').click()
await check(() => browser.elementById('navSegment').text(), /^$/)
await check(() => browser.elementById('authSegment').text(), /reset/)
await check(() => browser.elementById('routeSegment').text(), /^$/)

// when soft navigate to /foo, the @auth and @nav slot will maintain their the currently active states since they do not have /foo defined
await browser.elementByCss('[href="/foo"]').click()
await check(() => browser.elementById('navSegment').text(), /^$/)
await check(() => browser.elementById('authSegment').text(), /reset/)
await check(() => browser.elementById('routeSegment').text(), /foo/)
})

it('hard nav to parallel route', async () => {
const browser = await next.browser('/reset/withMobile')
await check(() => browser.elementById('navSegment').text(), /^$/)
await check(() => browser.elementById('authSegment').text(), /withMobile/)

// the /app/default.tsx is rendered since /reset/withMobile is only defined in @auth
await check(() => browser.elementById('routeSegment').text(), /^$/)
})
}
)