Skip to content

Commit c1fc7dd

Browse files
bartlomiejuclaude
andcommitted
fix: active links consider query params and respect existing aria-current (#3474)
- matchesUrl() now accepts an optional currentSearch parameter; when a link's href contains query params that don't match the current page, it returns Ancestor instead of Current - setActiveUrl() skips links that already have a user-set aria-current attribute, allowing frameworks like daisyUI to manage tabs - Client-side updateLinks() also respects user-set aria-current Closes #3474 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 895bcac commit c1fc7dd

3 files changed

Lines changed: 32 additions & 6 deletions

File tree

packages/fresh/src/runtime/client/partials.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,15 @@ document.addEventListener("submit", async (e) => {
304304

305305
function updateLinks(url: URL) {
306306
document.querySelectorAll("a").forEach((link) => {
307-
const match = matchesUrl(url.pathname, link.href);
307+
// Don't override aria-current if it was explicitly set by the user
308+
// (detected by absence of data-current/data-ancestor attributes which
309+
// Fresh always sets alongside aria-current)
310+
const hasFreshAria = link.hasAttribute(DATA_CURRENT) ||
311+
link.hasAttribute(DATA_ANCESTOR);
312+
const hasUserAria = !hasFreshAria && link.hasAttribute("aria-current");
313+
if (hasUserAria) return;
314+
315+
const match = matchesUrl(url.pathname, link.href, url.search);
308316

309317
if (match === UrlMatchKind.Current) {
310318
link.setAttribute(DATA_CURRENT, "true");

packages/fresh/src/runtime/server/preact_hooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ options[OptionsType.VNODE] = (vnode) => {
132132
if (RENDER_STATE !== null) {
133133
RENDER_STATE.owners.set(vnode, RENDER_STATE!.ownerStack.at(-1)!);
134134
if (vnode.type === "a") {
135-
setActiveUrl(vnode, RENDER_STATE.ctx.url.pathname);
135+
setActiveUrl(vnode, RENDER_STATE.ctx.url.pathname, RENDER_STATE.ctx.url.search);
136136
}
137137
}
138138
assetHashingHook(vnode, BUILD_ID);

packages/fresh/src/runtime/shared_internal.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,15 @@ export const enum UrlMatchKind {
4242
Current,
4343
}
4444

45-
export function matchesUrl(current: string, needle: string): UrlMatchKind {
46-
let href = new URL(needle, "http://localhost").pathname;
45+
export function matchesUrl(
46+
current: string,
47+
needle: string,
48+
currentSearch?: string,
49+
): UrlMatchKind {
50+
const needleUrl = new URL(needle, "http://localhost");
51+
let href = needleUrl.pathname;
52+
const needleSearch = needleUrl.search;
53+
4754
if (href !== "/" && href.endsWith("/")) {
4855
href = href.slice(0, -1);
4956
}
@@ -53,6 +60,10 @@ export function matchesUrl(current: string, needle: string): UrlMatchKind {
5360
}
5461

5562
if (current === href) {
63+
// If the link has query params, only mark as current if they match
64+
if (needleSearch && currentSearch !== undefined && needleSearch !== currentSearch) {
65+
return UrlMatchKind.Ancestor;
66+
}
5667
return UrlMatchKind.Current;
5768
} else if (current.startsWith(href + "/") || href === "/") {
5869
return UrlMatchKind.Ancestor;
@@ -65,11 +76,18 @@ export function matchesUrl(current: string, needle: string): UrlMatchKind {
6576
* Mark active or ancestor link
6677
* Note: This function is used both on the server and the client
6778
*/
68-
export function setActiveUrl(vnode: VNode, pathname: string): void {
79+
export function setActiveUrl(
80+
vnode: VNode,
81+
pathname: string,
82+
search?: string,
83+
): void {
6984
const props = vnode.props as Record<string, unknown>;
7085
const hrefProp = props.href;
7186
if (typeof hrefProp === "string" && hrefProp.startsWith("/")) {
72-
const match = matchesUrl(pathname, hrefProp);
87+
// Don't override aria-current if it's already set by the user
88+
if (props["aria-current"] !== undefined) return;
89+
90+
const match = matchesUrl(pathname, hrefProp, search);
7391
if (match === UrlMatchKind.Current) {
7492
props[DATA_CURRENT] = "true";
7593
props["aria-current"] = "page";

0 commit comments

Comments
 (0)