feat(nav): add MobileTabBar with active state and safe-area padding
Fixed vitest resolve conditions to use browser entry for Svelte 5. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
22
frontend/src/lib/nav/MobileTabBar.svelte
Normal file
22
frontend/src/lib/nav/MobileTabBar.svelte
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { mobileNavItems } from './nav';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav
|
||||||
|
aria-label="Hauptnavigation"
|
||||||
|
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)}
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
aria-current={active ? 'page' : undefined}
|
||||||
|
class="flex flex-col items-center gap-1 py-2 px-3 rounded-[var(--radius-md)] text-[10px] font-[var(--font-sans)] min-h-[44px] min-w-[44px] {active
|
||||||
|
? 'bg-[var(--green-tint)] text-[var(--green-dark)] font-medium'
|
||||||
|
: ''}"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
47
frontend/src/lib/nav/MobileTabBar.test.ts
Normal file
47
frontend/src/lib/nav/MobileTabBar.test.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { render, screen } from '@testing-library/svelte';
|
||||||
|
import MobileTabBar from './MobileTabBar.svelte';
|
||||||
|
|
||||||
|
vi.mock('$app/stores', () => {
|
||||||
|
const { readable } = require('svelte/store');
|
||||||
|
return {
|
||||||
|
page: readable({ url: new URL('http://localhost/planner') })
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MobileTabBar', () => {
|
||||||
|
it('renders 4 nav items', () => {
|
||||||
|
render(MobileTabBar);
|
||||||
|
const links = screen.getAllByRole('link');
|
||||||
|
expect(links).toHaveLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correct labels', () => {
|
||||||
|
render(MobileTabBar);
|
||||||
|
expect(screen.getByText('Planer')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Rezepte')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Einkauf')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Einstellungen')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('links have correct hrefs', () => {
|
||||||
|
render(MobileTabBar);
|
||||||
|
const links = screen.getAllByRole('link');
|
||||||
|
expect(links[0]).toHaveAttribute('href', '/planner');
|
||||||
|
expect(links[1]).toHaveAttribute('href', '/recipes');
|
||||||
|
expect(links[2]).toHaveAttribute('href', '/shopping');
|
||||||
|
expect(links[3]).toHaveAttribute('href', '/settings');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks active item with aria-current="page"', () => {
|
||||||
|
render(MobileTabBar);
|
||||||
|
const plannerLink = screen.getByRole('link', { name: /planer/i });
|
||||||
|
expect(plannerLink).toHaveAttribute('aria-current', 'page');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('non-active items do not have aria-current', () => {
|
||||||
|
render(MobileTabBar);
|
||||||
|
const recipesLink = screen.getByRole('link', { name: /rezepte/i });
|
||||||
|
expect(recipesLink).not.toHaveAttribute('aria-current');
|
||||||
|
});
|
||||||
|
});
|
||||||
16
frontend/vite.config.ts
Normal file
16
frontend/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tailwindcss(), sveltekit()],
|
||||||
|
test: {
|
||||||
|
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: ['src/test-setup.ts']
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
conditions: ['browser']
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user