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">
|
||||
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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user