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

App Router: fix relative query/hash handling in next/link and router push/replace #49521

Merged
merged 4 commits into from May 28, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions packages/next/src/client/components/app-router.tsx
Expand Up @@ -211,7 +211,7 @@ function Router({
navigateType: 'push' | 'replace',
forceOptimisticNavigation: boolean
) => {
const url = new URL(addBasePath(href), location.origin)
const url = new URL(addBasePath(href), location.href)

return dispatch({
type: ACTION_NAVIGATE,
Expand Down Expand Up @@ -261,7 +261,7 @@ function Router({
if (isBot(window.navigator.userAgent)) {
return
}
const url = new URL(addBasePath(href), location.origin)
const url = new URL(addBasePath(href), location.href)
// External urls can't be prefetched in the same way.
if (isExternalURL(url)) {
return
Expand Down
3 changes: 3 additions & 0 deletions test/e2e/app-dir/navigation/app/hash/page.js
Expand Up @@ -20,6 +20,9 @@ export default function HashPage() {
<Link href="/hash#hash-300" id="link-to-300">
To 300
</Link>
<Link href="#hash-500" id="link-to-500">
To 500 (hash only)
</Link>
<Link href="/hash#top" id="link-to-top">
To Top
</Link>
Expand Down
@@ -0,0 +1,20 @@
'use client'

import { useRouter } from 'next/navigation'

export function RouterPushButton() {
const router = useRouter()

return (
<h3 id="h3">
<button
id="button-to-h3-hash-only"
onClick={() => {
router.push('#h3')
}}
>
To #h3, hash only
</button>
</h3>
)
}
@@ -0,0 +1,28 @@
import Link from 'next/link'
import { RouterPushButton } from './client-component'

export default function Page() {
return (
<>
<h1 id="h1">
<Link href="#h1" id="link-to-h1-hash-only">
To #h1, hash only
</Link>
</h1>

<p>
<Link href="?foo=1&bar=2" id="link-to-dummy-query">
Query only
</Link>
</p>

<h2 id="h2">
<Link href="?here=ok#h2" id="link-to-h2-with-hash-and-query">
To #h2, with both relative hash and query
</Link>
</h2>

<RouterPushButton />
</>
)
}
55 changes: 55 additions & 0 deletions test/e2e/app-dir/navigation/navigation.test.ts
Expand Up @@ -52,6 +52,7 @@ createNextDescribe(
await checkLink(50, 730)
await checkLink(160, 2270)
await checkLink(300, 4230)
await checkLink(500, 7030) // this one is hash only (`href="#hash-500"`)
await checkLink('top', 0)
await checkLink('non-existent', 0)
})
Expand Down Expand Up @@ -106,6 +107,60 @@ createNextDescribe(
})
})

describe('relative hashes and queries', () => {
const pathname = '/nested-relative-query-and-hash'

it('should work with a hash-only href', async () => {
const browser = await next.browser(pathname)
await browser.elementByCss('#link-to-h1-hash-only').click()

await check(() => browser.url(), next.url + pathname + '#h1')
})

it('should work with a hash-only `router.push(...)`', async () => {
const browser = await next.browser(pathname)
await browser.elementByCss('#button-to-h3-hash-only').click()

await check(() => browser.url(), next.url + pathname + '#h3')
})

it('should work with a query-only href', async () => {
const browser = await next.browser(pathname)
await browser.elementByCss('#link-to-dummy-query').click()

await check(() => browser.url(), next.url + pathname + '?foo=1&bar=2')
})

it('should work with both relative hashes and queries', async () => {
const browser = await next.browser(pathname)
await browser.elementByCss('#link-to-h2-with-hash-and-query').click()

await check(() => browser.url(), next.url + pathname + '?here=ok#h2')

// Only update hash
await browser.elementByCss('#link-to-h1-hash-only').click()
await check(() => browser.url(), next.url + pathname + '?here=ok#h1')

// Replace all with new query
await browser.elementByCss('#link-to-dummy-query').click()
await check(() => browser.url(), next.url + pathname + '?foo=1&bar=2')

// Add hash to existing query
await browser.elementByCss('#link-to-h1-hash-only').click()
await check(
() => browser.url(),
next.url + pathname + '?foo=1&bar=2#h1'
)

// Update hash again via `router.push(...)`
await browser.elementByCss('#button-to-h3-hash-only').click()
await check(
() => browser.url(),
next.url + pathname + '?foo=1&bar=2#h3'
)
})
})

describe('not-found', () => {
it('should trigger not-found in a server component', async () => {
const browser = await next.browser('/not-found/servercomponent')
Expand Down