fix(link): preserve native download clicks#1262
Conversation
Link currently treats anchors with the HTML download attribute like normal same-origin SPA navigations. That prevents the browser from performing its native download behavior and can also run onNavigate for a click that should never become client-side navigation. The click handler now bails out after user onClick and before preventDefault whenever the anchor has a download attribute, matching Next.js Link semantics. A focused regression test covers the default action, onNavigate, transition, and RSC navigate boundaries.
commit: |
|
/bigbonk review |
There was a problem hiding this comment.
Clean, well-scoped fix. The download guard matches Next.js semantics in both App Router (linkClicked in packages/next/src/client/app-dir/link.tsx) and Pages Router (linkClicked in packages/next/src/client/link.tsx), where the hasAttribute('download') check runs before e.preventDefault() and before onNavigate.
The placement after onClick / defaultPrevented and before the modifier-key check is correct — user onClick still fires, but SPA navigation and onNavigate are skipped.
Test is thorough: covers onClick firing, defaultPrevented staying false, onNavigate not called, startTransition not called, and navigate not called. The Next.js test reference comment is appreciated.
No issues found.
|
Approved. The fix is correct and well-tested. Summary of the review:
|
Overview
<Link download>click semantics.preventDefault()when the rendered anchor has thedownloadattribute.packages/vinext/src/shims/link.tsx,tests/link-navigation.test.tsWhy
next/linkextends an anchor, but an anchor withdownloadis not a client-side route transition. Next.js treats that click as browser-owned behavior: useronClickcan run, butonNavigateand router navigation must not.What changed
<Link href="/file.pdf" download>preventDefault()and entered SPA navigation.onClickon a download LinkonNavigateon a download LinkValidation
vp test run tests/link-navigation.test.ts -t "download links"failed before the production guard, then passed after it.vp test run tests/link-navigation.test.tsvp checkknip.References
download.onClickstate but notonNavigatestate.onClickbut notonNavigate.Risk
Low. The new guard only applies when the actual clicked anchor has the HTML
downloadattribute, and it executes after useronClicksopreventDefault()from user code still wins. It runs before router-specific App or Pages logic, so both routers share the same compatibility fix.