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) {