Skip to content

Commit

Permalink
Full remaining path in selected layout segment (vercel#41562)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
Hannes Bornö authored and Kikobeats committed Oct 24, 2022
1 parent c63907d commit 18c916a
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 7 deletions.
42 changes: 36 additions & 6 deletions 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,
Expand Down Expand Up @@ -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)
}
@@ -0,0 +1,11 @@
'use client'

import { useSelectedLayoutSegment } from 'next/navigation'

export default function Page() {
const selectedLayoutSegment = useSelectedLayoutSegment()

return (
<p id="page-layout-segments">{JSON.stringify(selectedLayoutSegment)}</p>
)
}
@@ -0,0 +1,3 @@
export default function Page() {
return null
}
@@ -0,0 +1,3 @@
export default function Page() {
return null
}
@@ -0,0 +1,14 @@
'use client'

import { useSelectedLayoutSegment } from 'next/navigation'

export default function Layout({ children }) {
const selectedLayoutSegment = useSelectedLayoutSegment()

return (
<>
<p id="inner-layout">{JSON.stringify(selectedLayoutSegment)}</p>
{children}
</>
)
}
@@ -0,0 +1,3 @@
export default function Page() {
return null
}
@@ -0,0 +1,14 @@
'use client'

import { useSelectedLayoutSegment } from 'next/navigation'

export default function Layout({ children }) {
const selectedLayoutSegment = useSelectedLayoutSegment()

return (
<>
<p id="outer-layout">{JSON.stringify(selectedLayoutSegment)}</p>
{children}
</>
)
}
@@ -1,4 +1,3 @@
'use client'
// TODO-APP: enable once test is not skipped.
// import { useSelectedLayoutSegment } from 'next/navigation'

Expand Down
12 changes: 12 additions & 0 deletions test/e2e/app-dir/app/middleware.js
Expand Up @@ -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))
}
Expand Down
5 changes: 5 additions & 0 deletions test/e2e/app-dir/app/next.config.js
Expand Up @@ -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',
},
],
}
},
Expand Down
31 changes: 31 additions & 0 deletions test/e2e/app-dir/index.test.ts
Expand Up @@ -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) {
Expand Down

0 comments on commit 18c916a

Please sign in to comment.