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 7 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
5 changes: 3 additions & 2 deletions packages/next/src/client/components/navigation.ts
Original file line number Diff line number Diff line change
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,9 @@ export function useSelectedLayoutSegment(
return null
}

return selectedLayoutSegments[0]
return parallelRouteKey === 'children'
? selectedLayoutSegments[0]
: selectedLayoutSegments[selectedLayoutSegments.length - 1] ?? null
williamli marked this conversation as resolved.
Show resolved Hide resolved
}

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,33 @@
@tailwind base;
williamli marked this conversation as resolved.
Show resolved Hide resolved
@tailwind components;
@tailwind utilities;

:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}

body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}

@layer utilities {
.text-balance {
text-wrap: balance;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'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()

const authSegmentOutput = `${authSegment} (${typeof authSegment})`
const navSegmentOutput = `${navSegment} (${typeof navSegment})`
const routeSegmentOutput = `${routeSegment} (${typeof routeSegment})`
williamli marked this conversation as resolved.
Show resolved Hide resolved
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">{navSegmentOutput}</div>
</div>
<div>
authSegment (parallel route):{' '}
<div id="authSegment">{authSegmentOutput}</div>
</div>
<div>
routeSegment (app route):{' '}
<div id="routeSegment">{routeSegmentOutput}</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,165 @@
import { createNextDescribe } from 'e2e-utils'
import { check } from 'next-test-utils'

createNextDescribe(
'parallel-routes-use-selected-layout-segment.test.ts',
williamli marked this conversation as resolved.
Show resolved Hide resolved
{
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(),
/__DEFAULT__ \(string\)/
)
await check(
() => browser.elementById('authSegment').text(),
/__DEFAULT__ \(string\)/
)
await check(
() => browser.elementById('routeSegment').text(),
/null \(object\)/
)

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

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

// 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 \(string\)/
)
await check(
() => browser.elementById('authSegment').text(),
/login \(string\)/
)
await check(
() => browser.elementById('routeSegment').text(),
/null \(object\)/
)

// 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 \(string\)/
)
await check(
() => browser.elementById('authSegment').text(),
/reset \(string\)/
)
await check(
() => browser.elementById('routeSegment').text(),
/null \(object\)/
)

// 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 \(string\)/
)
await check(
() => browser.elementById('authSegment').text(),
/withEmail \(string\)/
)
await check(
() => browser.elementById('routeSegment').text(),
/null \(object\)/
)
})

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(),
/__DEFAULT__ \(string\)/
)
await check(
() => browser.elementById('authSegment').text(),
/__DEFAULT__ \(string\)/
)
await check(
() => browser.elementById('routeSegment').text(),
/null \(object\)/
)

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

// 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(),
/__DEFAULT__ \(string\)/
)
await check(
() => browser.elementById('authSegment').text(),
/reset \(string\)/
)
await check(
() => browser.elementById('routeSegment').text(),
/foo \(string\)/
)
})

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

// the /app/default.tsx is rendered since /reset/withMobile is only defined in @auth
await check(
() => browser.elementById('routeSegment').text(),
/__DEFAULT__ \(string\)/
)
})
}
)
10 changes: 10 additions & 0 deletions test/turbopack-tests-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4070,6 +4070,16 @@
"flakey": [],
"runtimeError": false
},
"test/e2e/app-dir/parallel-routes-use-selected-layout-segment/parallel-routes-use-selected-layout-segment.test.ts": {
williamli marked this conversation as resolved.
Show resolved Hide resolved
"passed": [
"parallel-routes-use-selected-layout-segment - useSelectedLayoutSegment should return the correct active layout segment for a soft nav parallel route",
"parallel-routes-use-selected-layout-segment - useSelectedLayoutSegment should return the correct active layout segment for a hard nav parallel route"
],
"failed": [],
"pending": [],
"flakey": [],
"runtimeError": false
},
"test/e2e/app-dir/params-hooks-compat/index.test.ts": {
"passed": [
"app-dir - params hooks compat should only access path params with useParams",
Expand Down