From 18c916a92f0255ccdf91289dd34f5f7a91bbc0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= Date: Thu, 20 Oct 2022 13:45:16 +0200 Subject: [PATCH] Full remaining path in selected layout segment (#41562) Make `useSelectedLayoutSegment` include the remaining segments from the current level to the leaf node. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- packages/next/client/components/navigation.ts | 42 ++++++++++++++++--- .../(group)/second/[...catchall]/page.js | 11 +++++ .../first/[dynamic]/(group)/second/page.js | 3 ++ .../first/[dynamic]/page.js | 3 ++ .../first/layout.js | 14 +++++++ .../use-selected-layout-segment/first/page.js | 3 ++ .../use-selected-layout-segment/layout.js | 14 +++++++ .../server/page.js | 1 - test/e2e/app-dir/app/middleware.js | 12 ++++++ test/e2e/app-dir/app/next.config.js | 5 +++ test/e2e/app-dir/index.test.ts | 31 ++++++++++++++ 11 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/[dynamic]/(group)/second/[...catchall]/page.js create mode 100644 test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/[dynamic]/(group)/second/page.js create mode 100644 test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/[dynamic]/page.js create mode 100644 test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/layout.js create mode 100644 test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/page.js create mode 100644 test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/layout.js diff --git a/packages/next/client/components/navigation.ts b/packages/next/client/components/navigation.ts index 7da7495b27da7..efca123183312 100644 --- a/packages/next/client/components/navigation.ts +++ b/packages/next/client/components/navigation.ts @@ -1,5 +1,6 @@ // useLayoutSegments() // Only the segments for the current place. ['children', 'dashboard', 'children', 'integrations'] -> /dashboard/integrations (/dashboard/layout.js would get ['children', 'dashboard', 'children', 'integrations']) +import type { FlightRouterState } from '../../server/app-render' import { useContext, useMemo } from 'react' import { SearchParamsContext, @@ -105,16 +106,45 @@ export function usePathname(): string { // return useContext(LayoutSegmentsContext) // } +// TODO-APP: handle parallel routes +function getSelectedLayoutSegmentPath( + tree: FlightRouterState, + parallelRouteKey: string, + first = true, + segmentPath: string[] = [] +): string[] { + let node: FlightRouterState + if (first) { + // Use the provided parallel route key on the first parallel route + node = tree[1][parallelRouteKey] + } else { + // After first parallel route prefer children, if there's no children pick the first parallel route. + const parallelRoutes = tree[1] + node = parallelRoutes.children ?? Object.values(parallelRoutes)[0] + } + + if (!node) return segmentPath + const segment = node[0] + const segmentValue = Array.isArray(segment) ? segment[1] : segment + if (!segmentValue) return segmentPath + + segmentPath.push(segmentValue) + + return getSelectedLayoutSegmentPath( + node, + parallelRouteKey, + false, + segmentPath + ) +} + // TODO-APP: Expand description when the docs are written for it. /** - * Get the current segment one level down from the layout. + * Get the canonical segment path from this level to the leaf node. */ export function useSelectedLayoutSegment( parallelRouteKey: string = 'children' -): string { +): string[] { const { tree } = useContext(LayoutRouterContext) - - const segment = tree[1][parallelRouteKey][0] - - return Array.isArray(segment) ? segment[1] : segment + return getSelectedLayoutSegmentPath(tree, parallelRouteKey) } diff --git a/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/[dynamic]/(group)/second/[...catchall]/page.js b/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/[dynamic]/(group)/second/[...catchall]/page.js new file mode 100644 index 0000000000000..7e15d00e0a26a --- /dev/null +++ b/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/[dynamic]/(group)/second/[...catchall]/page.js @@ -0,0 +1,11 @@ +'use client' + +import { useSelectedLayoutSegment } from 'next/navigation' + +export default function Page() { + const selectedLayoutSegment = useSelectedLayoutSegment() + + return ( +

{JSON.stringify(selectedLayoutSegment)}

+ ) +} diff --git a/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/[dynamic]/(group)/second/page.js b/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/[dynamic]/(group)/second/page.js new file mode 100644 index 0000000000000..c17431379f962 --- /dev/null +++ b/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/[dynamic]/(group)/second/page.js @@ -0,0 +1,3 @@ +export default function Page() { + return null +} diff --git a/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/[dynamic]/page.js b/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/[dynamic]/page.js new file mode 100644 index 0000000000000..c17431379f962 --- /dev/null +++ b/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/[dynamic]/page.js @@ -0,0 +1,3 @@ +export default function Page() { + return null +} diff --git a/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/layout.js b/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/layout.js new file mode 100644 index 0000000000000..10342ef050bf8 --- /dev/null +++ b/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/layout.js @@ -0,0 +1,14 @@ +'use client' + +import { useSelectedLayoutSegment } from 'next/navigation' + +export default function Layout({ children }) { + const selectedLayoutSegment = useSelectedLayoutSegment() + + return ( + <> +

{JSON.stringify(selectedLayoutSegment)}

+ {children} + + ) +} diff --git a/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/page.js b/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/page.js new file mode 100644 index 0000000000000..c17431379f962 --- /dev/null +++ b/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/first/page.js @@ -0,0 +1,3 @@ +export default function Page() { + return null +} diff --git a/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/layout.js b/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/layout.js new file mode 100644 index 0000000000000..90311bb47861f --- /dev/null +++ b/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/layout.js @@ -0,0 +1,14 @@ +'use client' + +import { useSelectedLayoutSegment } from 'next/navigation' + +export default function Layout({ children }) { + const selectedLayoutSegment = useSelectedLayoutSegment() + + return ( + <> +

{JSON.stringify(selectedLayoutSegment)}

+ {children} + + ) +} diff --git a/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/server/page.js b/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/server/page.js index 35c72bc285712..c38d4f5ab1023 100644 --- a/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/server/page.js +++ b/test/e2e/app-dir/app/app/hooks/use-selected-layout-segment/server/page.js @@ -1,4 +1,3 @@ -'use client' // TODO-APP: enable once test is not skipped. // import { useSelectedLayoutSegment } from 'next/navigation' diff --git a/test/e2e/app-dir/app/middleware.js b/test/e2e/app-dir/app/middleware.js index 1c0cb01aa16b3..b9163ca06adef 100644 --- a/test/e2e/app-dir/app/middleware.js +++ b/test/e2e/app-dir/app/middleware.js @@ -14,6 +14,18 @@ export function middleware(request) { return NextResponse.rewrite(new URL('/dashboard', request.url)) } + if ( + request.nextUrl.pathname === + '/hooks/use-selected-layout-segment/rewritten-middleware' + ) { + return NextResponse.rewrite( + new URL( + '/hooks/use-selected-layout-segment/first/slug3/second/catch/all', + request.url + ) + ) + } + if (request.nextUrl.pathname === '/redirect-middleware-to-dashboard') { return NextResponse.redirect(new URL('/dashboard', request.url)) } diff --git a/test/e2e/app-dir/app/next.config.js b/test/e2e/app-dir/app/next.config.js index c9f390fc829e4..0dcacc5e94534 100644 --- a/test/e2e/app-dir/app/next.config.js +++ b/test/e2e/app-dir/app/next.config.js @@ -12,6 +12,11 @@ module.exports = { source: '/rewritten-to-dashboard', destination: '/dashboard', }, + { + source: '/hooks/use-selected-layout-segment/rewritten', + destination: + '/hooks/use-selected-layout-segment/first/slug3/second/catch/all', + }, ], } }, diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index cc6b318efe65b..7fac781429347 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -1194,6 +1194,37 @@ describe('app dir', () => { expect(el.attr('data-query')).toBe('query') }) }) + + describe('useSelectedLayoutSegment', () => { + it.each` + path | outerLayout | innerLayout + ${'/hooks/use-selected-layout-segment/first'} | ${['first']} | ${[]} + ${'/hooks/use-selected-layout-segment/first/slug1'} | ${['first', 'slug1']} | ${['slug1']} + ${'/hooks/use-selected-layout-segment/first/slug2/second'} | ${['first', 'slug2', '(group)', 'second']} | ${['slug2', '(group)', 'second']} + ${'/hooks/use-selected-layout-segment/first/slug2/second/a/b'} | ${['first', 'slug2', '(group)', 'second', 'a/b']} | ${['slug2', '(group)', 'second', 'a/b']} + ${'/hooks/use-selected-layout-segment/rewritten'} | ${['first', 'slug3', '(group)', 'second', 'catch/all']} | ${['slug3', '(group)', 'second', 'catch/all']} + ${'/hooks/use-selected-layout-segment/rewritten-middleware'} | ${['first', 'slug3', '(group)', 'second', 'catch/all']} | ${['slug3', '(group)', 'second', 'catch/all']} + `( + 'should have the correct layout segments at $path', + async ({ path, outerLayout, innerLayout }) => { + const html = await renderViaHTTP(next.url, path) + const $ = cheerio.load(html) + + expect(JSON.parse($('#outer-layout').text())).toEqual(outerLayout) + expect(JSON.parse($('#inner-layout').text())).toEqual(innerLayout) + } + ) + + it('should return an empty array in pages', async () => { + const html = await renderViaHTTP( + next.url, + '/hooks/use-selected-layout-segment/first/slug2/second/a/b' + ) + const $ = cheerio.load(html) + + expect(JSON.parse($('#page-layout-segments').text())).toEqual([]) + }) + }) }) if (isDev) {