fix(nav): use segment-boundary route matching to prevent false positives
Extracts isActiveRoute() into shared nav module. Matches exact path or path + '/' prefix, preventing /settings from matching /settings-advanced. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { desktopNavSections } from './nav';
|
import { desktopNavSections, isActiveRoute } from './nav';
|
||||||
|
|
||||||
let { appName, householdName }: { appName: string; householdName: string } = $props();
|
let { appName, householdName }: { appName: string; householdName: string } = $props();
|
||||||
</script>
|
</script>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
{section.title}
|
{section.title}
|
||||||
</p>
|
</p>
|
||||||
{#each section.items as item (item.href)}
|
{#each section.items as item (item.href)}
|
||||||
{@const active = $page.url.pathname.startsWith(item.href)}
|
{@const active = isActiveRoute(item.href, $page.url.pathname)}
|
||||||
<a
|
<a
|
||||||
href={item.href}
|
href={item.href}
|
||||||
aria-current={active ? 'page' : undefined}
|
aria-current={active ? 'page' : undefined}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { mobileNavItems } from './nav';
|
import { mobileNavItems, isActiveRoute } from './nav';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
class="fixed bottom-0 w-full flex justify-around bg-white border-t pb-[env(safe-area-inset-bottom,20px)] md:hidden"
|
class="fixed bottom-0 w-full flex justify-around bg-white border-t pb-[env(safe-area-inset-bottom,20px)] md:hidden"
|
||||||
>
|
>
|
||||||
{#each mobileNavItems as item (item.href)}
|
{#each mobileNavItems as item (item.href)}
|
||||||
{@const active = $page.url.pathname.startsWith(item.href)}
|
{@const active = isActiveRoute(item.href, $page.url.pathname)}
|
||||||
<a
|
<a
|
||||||
href={item.href}
|
href={item.href}
|
||||||
aria-current={active ? 'page' : undefined}
|
aria-current={active ? 'page' : undefined}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { mobileNavItems } from './nav';
|
import { mobileNavItems, isActiveRoute } from './nav';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
class="hidden md:flex lg:hidden gap-2 items-center p-2"
|
class="hidden md:flex lg:hidden gap-2 items-center p-2"
|
||||||
>
|
>
|
||||||
{#each mobileNavItems as item (item.href)}
|
{#each mobileNavItems as item (item.href)}
|
||||||
{@const active = $page.url.pathname.startsWith(item.href)}
|
{@const active = isActiveRoute(item.href, $page.url.pathname)}
|
||||||
<a
|
<a
|
||||||
href={item.href}
|
href={item.href}
|
||||||
aria-current={active ? 'page' : undefined}
|
aria-current={active ? 'page' : undefined}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { mobileNavItems, desktopNavSections } from './nav';
|
import { mobileNavItems, desktopNavSections, isActiveRoute } from './nav';
|
||||||
|
|
||||||
describe('nav config', () => {
|
describe('nav config', () => {
|
||||||
describe('mobileNavItems', () => {
|
describe('mobileNavItems', () => {
|
||||||
@@ -39,4 +39,22 @@ describe('nav config', () => {
|
|||||||
expect(labels).toEqual(['Mitglieder', 'Einstellungen']);
|
expect(labels).toEqual(['Mitglieder', 'Einstellungen']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isActiveRoute', () => {
|
||||||
|
it('matches exact route', () => {
|
||||||
|
expect(isActiveRoute('/planner', '/planner')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches sub-route', () => {
|
||||||
|
expect(isActiveRoute('/planner', '/planner/week')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not match route with similar prefix', () => {
|
||||||
|
expect(isActiveRoute('/settings', '/settings-advanced')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not match unrelated route', () => {
|
||||||
|
expect(isActiveRoute('/planner', '/recipes')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ export const mobileNavItems: NavItem[] = [
|
|||||||
{ href: '/settings', label: 'Einstellungen', icon: 'settings' }
|
{ href: '/settings', label: 'Einstellungen', icon: 'settings' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function isActiveRoute(href: string, pathname: string): boolean {
|
||||||
|
return pathname === href || pathname.startsWith(href + '/');
|
||||||
|
}
|
||||||
|
|
||||||
export const desktopNavSections: NavSection[] = [
|
export const desktopNavSections: NavSection[] = [
|
||||||
{
|
{
|
||||||
title: 'Plan',
|
title: 'Plan',
|
||||||
|
|||||||
Reference in New Issue
Block a user