Skip to content

fix(Link): emit aria-current and match paths in inertia mode - #6809

Open
kodjosama wants to merge 4 commits into
nuxt:v4from
kodjosama:fix/inertia-link-aria-current
Open

fix(Link): emit aria-current and match paths in inertia mode#6809
kodjosama wants to merge 4 commits into
nuxt:v4from
kodjosama:fix/inertia-link-aria-current

Conversation

@kodjosama

@kodjosama kodjosama commented Aug 7, 2026

Copy link
Copy Markdown

🔗 Linked issue

Resolves #6803

❓ Type of change

  • 📖 Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

The inertia Link override diverges from the vue-router one in three ways. Every component that renders a link (UButton, UBreadcrumb, UNavigationMenu, UDropdownMenu, …) goes through it, so all three reach the whole library in Inertia mode.

1. aria-current was never emitted. isLinkActive only fed linkClass and the active slot prop. vue-router/Link.vue binds it (:189, :206), and the prop's own JSDoc says it is "passed to the attribute aria-current". An Inertia app moving its navigation onto Nuxt UI silently lost the current-page announcement.

2. ariaCurrentValue leaked onto the DOM. It was missing from the reactiveOmit list, so useForwardProps passed it to ULinkBase, which doesn't declare it either, and it reached the DOM as ariacurrentvalue="page". vue-router omits it from its list too, but there it's harmless: RouterLink declares ariaCurrentValue as a real prop and consumes it. InertiaLink doesn't. Because the prop carries a default, the attribute appeared even on components rendering no link at all — <UButton>no link</UButton> rendered <button ariacurrentvalue="page">.

3. Active matching used a raw prefix against a query-carrying URL. page.url.startsWith(href) meant a query string defeated the match, and /inventory reported active on /inventory-adjustments. Now compared on the path, matching the target path or a descendant of it. exact is untouched and still compares the raw url.

What drives aria-current

This PR first mirrored vue-router's condition — exact && isExactActive — on the grounds that parity is the point of the report. I no longer think that's right, and I'd rather say why than quietly ship it.

Requiring exact makes the accessible behaviour opt-in: a nav link to the page you are on stays silent unless the caller passes an extra prop. And exact can't simply be switched on, because it doubles as the scope of the active class — turning it on to get the announcement also narrows the highlight that a sidebar entry needs while browsing its section.

Binding it to isLinkActive instead is worse. An app that migrated its primitives to Nuxt UI hit this in production: form Cancel buttons became links to their section index, and each one started claiming to be the current page — /inventory/create marking its /inventory link with aria-current.

So the two questions are separated: isLinkActive keeps section semantics for the class, and a new isLinkCurrentPage answers "is this the page being rendered" by comparing paths. Only the latter drives aria-current.

This leaves vue-router as the odd one out, which is the opposite of where the report started, so it deserves your call rather than mine. vue-router/Link.vue has the same exact && requirement and, I'd argue, the same problem — isExactActive is already the right signal there without the extra prop. I deliberately did not touch it in this PR: it changes behaviour for existing Nuxt apps and that's a bigger decision than a bug fix. Happy to include it here, or open a separate PR, whichever you prefer.

One correction to the issue

I listed to="/" being active everywhere as a defect. With segment-aware matching that's just / being a parent of every route, which is what RouterLink does too — so this PR keeps it, and exact remains the way to make a home link exclusive. The real defect there was segment-blindness, which is fixed.

Tests

The overrides had no test project at all: the vue project resolves #components to [...vueRouterOverrides, ...vueComponents, ...components], so the inertia directory was never loaded by anything. That's how three defects coexisted here.

This adds a vue-inertia project mirroring the vue one with router: 'inertia', plus a stub for @inertiajs/vue3 (an optional peer dependency, not installed in this repo) and a router-less mountSuspended. 15 behavioural specs, including the Cancel-button case: a link to the section index from a page inside it stays active but must not announce itself as current.

Each guard is mutation-checked — reverting a fix must turn the suite red:

Mutation Result
drive aria-current from isLinkActive 1 failed (the Cancel-button case)
re-require exact for aria-current 5 failed
remove aria-current entirely 5 failed
remove ariaCurrentValue from reactiveOmit 2 failed
restore page.url.startsWith(href) 1 failed

Verification

pnpm vitest run --project vue-inertia → 15 passed. pnpm lint clean on the touched files.

On the full suite I see pre-existing failures in DropdownMenu, Select, ContextMenu and ContentSearch that also fail on a clean checkout of v4 in my environment and flake between runs. Comparing the failing-test sets with and without this branch, the modified tree's set is a strict subset of the clean tree's — nothing fails that didn't already.

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

The inertia override diverges from the vue-router one in three ways, and
since every component that renders a link goes through it, all three reach
the whole library in inertia mode.

`isLinkActive` only fed `linkClass` and the slot prop, so the anchor never
received `aria-current` — the vue-router variant binds it, and the prop's
own documentation says it is "passed to the attribute aria-current". An
Inertia app moving its navigation onto Nuxt UI lost the current-page
announcement.

`ariaCurrentValue` was missing from the `reactiveOmit` list, so it was
forwarded to `ULinkBase` and landed on the DOM as `ariacurrentvalue`.
`vue-router` omits it too, but there `RouterLink` declares it as a prop and
consumes it; `InertiaLink` does not. Because the prop carries a default the
attribute appeared even on components rendering no link at all.

Active matching ran `page.url.startsWith(href)` against a url that carries
the query string, on raw string prefixes, so a query defeated the match and
`/inventory` reported active on `/inventory-adjustments`. Compare on the
path, ignoring query and hash, and treat a match as the target path or a
descendant of it — `/` stays a parent of every route, as with RouterLink.

The overrides had no test project: the `vue` project resolves `#components`
to the vue-router overrides only, which is how three defects coexisted here.
Add a `vue-inertia` project mirroring it, with a stub for the optional
`@inertiajs/vue3` peer dependency.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 55789921-faac-450a-8c87-9cc48da040f4

📥 Commits

Reviewing files that changed from the base of the PR and between 81bc6e6 and f424e44.

📒 Files selected for processing (2)
  • src/runtime/vue/overrides/inertia/Link.vue
  • test/inertia/Link.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/runtime/vue/overrides/inertia/Link.vue

📝 Walkthrough

Walkthrough

The Inertia Link override now normalizes URLs before active-route comparison, removes query and hash components, handles trailing slashes, and enforces path-segment boundaries. It excludes ariaCurrentValue from forwarded props and applies it as aria-current for current-page links. New Vue Inertia test utilities, Vitest configuration, and component tests cover matching, prop handling, slots, and accessibility.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the Inertia Link fix for aria-current emission and path matching.
Description check ✅ Passed The description directly explains the three defects, the implementation choices, and the related tests.
Linked Issues check ✅ Passed The changes address issue #6803 by fixing aria-current, filtering ariaCurrentValue, and using normalized segment-aware path matching.
Out of Scope Changes check ✅ Passed The test utilities, Vitest project, and component tests support validation of the Inertia Link fixes and remain in scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/runtime/vue/overrides/inertia/Link.vue

Parsing error: Unexpected token )

test/inertia/Link.spec.ts

Parsing error: Unexpected token :


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/vue/overrides/inertia/Link.vue`:
- Line 199: Update src/runtime/vue/overrides/inertia/Link.vue at lines 199-199
and 216-216 to emit aria-current whenever isLinkExactActive is true, without
requiring the exact prop. Add a regression test in test/inertia/Link.spec.ts at
lines 14-23 covering an exactly matching to path with exact omitted and
verifying aria-current is emitted.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f0e0d269-abf5-42b5-83d8-4b0843240643

📥 Commits

Reviewing files that changed from the base of the PR and between 6add5fb and cd696d3.

📒 Files selected for processing (5)
  • src/runtime/vue/overrides/inertia/Link.vue
  • test/inertia/Link.spec.ts
  • test/utils/inertia.ts
  • test/utils/mount-inertia.ts
  • vitest.config.ts

Comment thread src/runtime/vue/overrides/inertia/Link.vue Outdated
@codspeed-hq

codspeed-hq Bot commented Aug 7, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 28.18%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 1 regressed benchmark
✅ 30 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
table (~13 slots) 292.7 µs 407.6 µs -28.18%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing kodjosama:fix/inertia-link-aria-current (800d5cd) with v4 (e7b126b)

Open in CodSpeed

@pkg-pr-new

pkg-pr-new Bot commented Aug 7, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/@nuxt/ui@6809

commit: 800d5cd

Binding the attribute to `exact && isExactActive`, as this PR first did to
match vue-router, makes the accessible behaviour opt-in: a navigation link
to the page you are on stays silent unless the caller also passes `exact`.
And `exact` cannot simply be switched on, because it doubles as the scope of
the active class — turning it on to get the announcement narrows the
highlight a sidebar entry needs.

Binding it to the active state instead is worse. An app that migrated its
primitives hit this: form Cancel buttons became links to their section
index, and each one claimed to be the current page — /inventory/create
marking its /inventory link with aria-current.

The two questions are different. `isLinkActive` keeps section semantics for
the class; a new `isLinkCurrentPage` answers "is this the page being
rendered" by comparing paths, and only that drives aria-current. `exact`
goes back to the raw url comparison it has always done, so nothing changes
for callers already using it.
@kodjosama

Copy link
Copy Markdown
Author

Updated: aria-current is now keyed on a new isLinkCurrentPage (path comparison) rather than on exact && isExactActive.

I originally mirrored the vue-router condition because parity is what this report is about, but that makes the accessible behaviour opt-in — a nav link to the page you're on stays silent unless the caller passes exact — and exact can't just be switched on, since it doubles as the scope of the active class.

The alternative, driving it from isLinkActive, is worse, and I have a concrete case rather than a hypothetical: an app that migrated its primitives to Nuxt UI had its form Cancel buttons become links to their section index, and each one started claiming to be the current page (/inventory/create marking its /inventory link with aria-current). That case is now a test.

So isLinkActive keeps section semantics for the class, and only isLinkCurrentPage drives the attribute. exact goes back to the raw url comparison it always did — no behaviour change for existing callers, which also drops the caveat I had flagged earlier.

This leaves vue-router as the one still requiring exact, which is the opposite of where this report started, so it's your call: I think isExactActive is already the right signal there without the extra prop, but changing it affects existing Nuxt apps, so I left it alone. Happy to include it in this PR or open a separate one.

@kodjosama

Copy link
Copy Markdown
Author

@codspeedbot fix this regression

`normalizePath` stripped the query and hash before its empty-path
fallback, so `?tab=orders` and `#section` both collapsed to `/`. That
made the non-exact branch of `isLinkActive` reduce to
`currentPath.startsWith('/')` — true for every url — so a query- or
fragment-only link rendered active on every page, and every sibling tab
in a switcher reported active at once.

Return `undefined` for a pathless href instead. `isLinkActive` already
guards on `!targetPath`; `isLinkCurrentPage` now guards on it too,
keeping `exact` a raw url comparison as before.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v4 #4488

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inertia Link override: no aria-current, invalid ariacurrentvalue attribute, and prefix-based active matching

2 participants