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:
2026-04-02 14:00:18 +02:00
parent aeaca76534
commit bd8e901685
5 changed files with 29 additions and 7 deletions

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { desktopNavSections } from './nav';
import { desktopNavSections, isActiveRoute } from './nav';
let { appName, householdName }: { appName: string; householdName: string } = $props();
</script>
@@ -24,7 +24,7 @@
{section.title}
</p>
{#each section.items as item (item.href)}
{@const active = $page.url.pathname.startsWith(item.href)}
{@const active = isActiveRoute(item.href, $page.url.pathname)}
<a
href={item.href}
aria-current={active ? 'page' : undefined}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { mobileNavItems } from './nav';
import { mobileNavItems, isActiveRoute } from './nav';
</script>
<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"
>
{#each mobileNavItems as item (item.href)}
{@const active = $page.url.pathname.startsWith(item.href)}
{@const active = isActiveRoute(item.href, $page.url.pathname)}
<a
href={item.href}
aria-current={active ? 'page' : undefined}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { mobileNavItems } from './nav';
import { mobileNavItems, isActiveRoute } from './nav';
</script>
<nav
@@ -8,7 +8,7 @@
class="hidden md:flex lg:hidden gap-2 items-center p-2"
>
{#each mobileNavItems as item (item.href)}
{@const active = $page.url.pathname.startsWith(item.href)}
{@const active = isActiveRoute(item.href, $page.url.pathname)}
<a
href={item.href}
aria-current={active ? 'page' : undefined}

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { mobileNavItems, desktopNavSections } from './nav';
import { mobileNavItems, desktopNavSections, isActiveRoute } from './nav';
describe('nav config', () => {
describe('mobileNavItems', () => {
@@ -39,4 +39,22 @@ describe('nav config', () => {
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);
});
});
});

View File

@@ -16,6 +16,10 @@ export const mobileNavItems: NavItem[] = [
{ href: '/settings', label: 'Einstellungen', icon: 'settings' }
];
export function isActiveRoute(href: string, pathname: string): boolean {
return pathname === href || pathname.startsWith(href + '/');
}
export const desktopNavSections: NavSection[] = [
{
title: 'Plan',