-
Notifications
You must be signed in to change notification settings - Fork 296
test: regression tests for inherited intercepting route slot deduplication #721
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
Changes from all commits
3fa9df7
60b3796
3b05f17
97882cb
e988757
90dcd9e
9479f3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import db from '#/lib/db'; | ||
| import { Boundary } from '#/ui/boundary'; | ||
| import { XMarkIcon } from '@heroicons/react/24/solid'; | ||
| import Image from 'next/image'; | ||
| import Link from 'next/link'; | ||
| import { notFound } from 'next/navigation'; | ||
|
|
||
| export default async function Page({ | ||
| params, | ||
| }: { | ||
| params: Promise<{ id: string }>; | ||
| }) { | ||
| const { id } = await params; | ||
| const product = db.product.find({ where: { id } }); | ||
| if (!product) { | ||
| notFound(); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="pointer-events-none fixed inset-0 z-20 flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm"> | ||
| <Boundary | ||
| label="@modal/(.)photo/[id]/page.tsx" | ||
| color="cyan" | ||
| kind="solid" | ||
| animateRerendering={false} | ||
| className="pointer-events-auto flex w-full max-w-3xl flex-col gap-6 rounded-2xl bg-gray-950" | ||
| > | ||
| <div className="flex items-start justify-between gap-4"> | ||
| <div className="flex flex-col gap-2"> | ||
| <div className="text-sm uppercase tracking-[0.2em] text-cyan-300"> | ||
| Intercepted in modal | ||
| </div> | ||
| <h2 className="text-2xl font-semibold text-white">{product.name}</h2> | ||
| </div> | ||
|
|
||
| <Link | ||
| href="/intercepting-routes" | ||
| className="rounded-full border border-gray-800 p-2 text-gray-400 hover:border-gray-700 hover:text-white" | ||
| aria-label="Close modal" | ||
| > | ||
| <XMarkIcon className="size-5" /> | ||
| </Link> | ||
| </div> | ||
|
|
||
| <div className="grid gap-6 lg:grid-cols-[minmax(0,16rem)_1fr]"> | ||
| <div className="overflow-hidden rounded-2xl bg-gray-900/60 p-6"> | ||
| <Image | ||
| src={`/shop/${product.image}`} | ||
| alt={product.name} | ||
| width={320} | ||
| height={320} | ||
| className="mx-auto brightness-150" | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="flex flex-col gap-4"> | ||
| <p className="text-sm leading-6 text-gray-400"> | ||
| The browser URL is already pointing at the product detail page, but | ||
| the source gallery stays mounted underneath because the navigation | ||
| was intercepted by the parallel slot. | ||
| </p> | ||
| <div className="font-mono text-sm text-cyan-300"> | ||
| {`$${product.price.toFixed(2)}`} | ||
| </div> | ||
| <div className="text-xs text-gray-500"> | ||
| Refresh this URL to see the standalone detail page instead. | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </Boundary> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export default function Default() { | ||
| return null; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import db from '#/lib/db'; | ||
| import { Mdx } from '#/ui/codehike'; | ||
| import { Boundary } from '#/ui/boundary'; | ||
| import { type Metadata } from 'next'; | ||
| import React from 'react'; | ||
| import readme from './readme.mdx'; | ||
|
|
||
| export async function generateMetadata(): Promise<Metadata> { | ||
| const demo = db.demo.find({ where: { slug: 'intercepting-routes' } }); | ||
|
|
||
| return { | ||
| title: demo.name, | ||
| openGraph: { title: demo.name, images: [`/api/og?title=${demo.name}`] }, | ||
| }; | ||
| } | ||
|
|
||
| export default function Layout({ | ||
| children, | ||
| modal, | ||
| }: { | ||
| children: React.ReactNode; | ||
| modal: React.ReactNode; | ||
| }) { | ||
| return ( | ||
| <> | ||
| <Boundary label="Demo" kind="solid" animateRerendering={false}> | ||
| <Mdx source={readme} collapsed={true} /> | ||
| </Boundary> | ||
|
|
||
| <div className="relative flex flex-col gap-6"> | ||
| <Boundary | ||
| label="layout.tsx" | ||
| kind="solid" | ||
| animateRerendering={false} | ||
| className="flex flex-col gap-6" | ||
| > | ||
| {children} | ||
| </Boundary> | ||
|
|
||
| {modal} | ||
| </div> | ||
| </> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import db from '#/lib/db'; | ||
| import { Boundary } from '#/ui/boundary'; | ||
| import { ProductCard, ProductList } from '#/ui/product-card'; | ||
| import Link from 'next/link'; | ||
|
|
||
| export default function Page() { | ||
| const products = db.product.findMany({ limit: 6 }); | ||
|
|
||
| return ( | ||
| <Boundary label="page.tsx" size="small" className="flex flex-col gap-5"> | ||
| <div className="flex flex-col gap-2"> | ||
| <h1 className="text-xl font-semibold text-gray-200"> | ||
| Product gallery with modal interception | ||
| </h1> | ||
| <p className="max-w-2xl text-sm text-gray-400"> | ||
| This route stays visible while the target URL updates to a nested detail | ||
| page. A direct load of the same URL renders the standalone detail page. | ||
| </p> | ||
| </div> | ||
|
|
||
| <ProductList title="Products" count={products.length}> | ||
| {products.map((product) => ( | ||
| <Link key={product.id} href={`/intercepting-routes/photo/${product.id}`}> | ||
| <ProductCard | ||
| product={product} | ||
| className="rounded-xl border border-transparent p-2 transition hover:border-gray-800" | ||
| /> | ||
| </Link> | ||
| ))} | ||
| </ProductList> | ||
| </Boundary> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import db from '#/lib/db'; | ||
| import { Boundary } from '#/ui/boundary'; | ||
| import { ChevronLeftIcon } from '@heroicons/react/24/solid'; | ||
| import Image from 'next/image'; | ||
| import Link from 'next/link'; | ||
| import { notFound } from 'next/navigation'; | ||
|
|
||
| export default async function Page({ | ||
| params, | ||
| }: { | ||
| params: Promise<{ id: string }>; | ||
| }) { | ||
| const { id } = await params; | ||
| const product = db.product.find({ where: { id } }); | ||
| if (!product) { | ||
| notFound(); | ||
| } | ||
|
|
||
| return ( | ||
| <Boundary label="photo/[id]/page.tsx" className="flex flex-col gap-6"> | ||
| <Link | ||
| href="/intercepting-routes" | ||
| className="inline-flex items-center gap-2 text-sm font-medium text-gray-400 hover:text-white" | ||
| > | ||
| <ChevronLeftIcon className="size-4" /> | ||
| Back to gallery | ||
| </Link> | ||
|
|
||
| <div className="grid gap-6 lg:grid-cols-[minmax(0,20rem)_1fr]"> | ||
| <div className="overflow-hidden rounded-2xl bg-gray-900/60 p-8"> | ||
| <Image | ||
| src={`/shop/${product.image}`} | ||
| alt={product.name} | ||
| width={400} | ||
| height={400} | ||
| className="mx-auto brightness-150" | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="flex flex-col gap-4"> | ||
| <div className="text-sm uppercase tracking-[0.2em] text-gray-500"> | ||
| Direct visit | ||
| </div> | ||
| <h1 className="text-3xl font-semibold text-white">{product.name}</h1> | ||
| <p className="max-w-xl text-sm leading-6 text-gray-400"> | ||
| Loading this URL directly should render the standalone page. Navigating | ||
| from the gallery should keep the gallery visible and render this content | ||
| in the parallel modal slot instead. | ||
| </p> | ||
| <div className="font-mono text-sm text-cyan-300"> | ||
| {`$${product.price.toFixed(2)}`} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </Boundary> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| Intercepting Routes let you mask a target URL with the current layout during navigation. | ||
|
|
||
| - Click a product card below to navigate to its detail URL. | ||
| - During in-app navigation, `@modal/(.)photo/[id]` should render inside the modal slot. | ||
| - If you load the detail URL directly, `photo/[id]/page.tsx` renders as the full page instead. | ||
|
|
||
| - [Docs](https://nextjs.org/docs/app/api-reference/file-conventions/intercepting-routes) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -258,7 +258,11 @@ function discoverSlotSubRoutes( | |
| // that useSelectedLayoutSegments() sees the correct segment list at runtime. | ||
| rawSegments: string[]; | ||
| // Pre-computed URL parts, params, isDynamic from convertSegmentsToRouteParts. | ||
| converted: { urlSegments: string[]; params: string[]; isDynamic: boolean }; | ||
| converted: { | ||
| urlSegments: string[]; | ||
|
Comment on lines
+261
to
+262
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a formatting-only change (expanding the inline type literal to multi-line). It's fine stylistically but unrelated to the regression tests. Since the maintainer already noted the Set dedupe is merging in another PR, this formatting change may cause a merge conflict with that work. Consider dropping it to keep this PR focused on tests only. |
||
| params: string[]; | ||
| isDynamic: boolean; | ||
| }; | ||
| slotPages: Map<string, string>; | ||
| } | ||
| >(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same nit as the modal page — this JSX
$works but reads like a broken template literal: