From 47d57a0963c4f2509d10eb380d3d656b4bfb5308 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Mon, 4 May 2026 09:52:33 +0200 Subject: [PATCH] feat(ST-1134): MobileTabBar component (T-320) Bottom-fixed nav-bar met 3 lucide-iconen (ListTree/Activity/Settings). Verbergt Backlog/Solo-tabs als activeProductId null is. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/mobile/mobile-tab-bar.test.tsx | 57 ++++++++++++++++ components/mobile/mobile-tab-bar.tsx | 68 +++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 __tests__/components/mobile/mobile-tab-bar.test.tsx create mode 100644 components/mobile/mobile-tab-bar.tsx diff --git a/__tests__/components/mobile/mobile-tab-bar.test.tsx b/__tests__/components/mobile/mobile-tab-bar.test.tsx new file mode 100644 index 0000000..66d6170 --- /dev/null +++ b/__tests__/components/mobile/mobile-tab-bar.test.tsx @@ -0,0 +1,57 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi } from 'vitest' +import { render, screen } from '@testing-library/react' +import { MobileTabBar } from '@/components/mobile/mobile-tab-bar' + +let pathname = '/m/products/p1' +vi.mock('next/navigation', () => ({ + usePathname: () => pathname, +})) + +function setPathname(p: string) { pathname = p } + +describe('MobileTabBar', () => { + it('toont 3 tabs als activeProductId aanwezig is', () => { + setPathname('/m/products/p1') + render() + expect(screen.getByLabelText('Backlog')).toBeTruthy() + expect(screen.getByLabelText('Solo')).toBeTruthy() + expect(screen.getByLabelText('Settings')).toBeTruthy() + }) + + it('toont alleen Settings als activeProductId null is', () => { + setPathname('/m/settings') + render() + expect(screen.queryByLabelText('Backlog')).toBeNull() + expect(screen.queryByLabelText('Solo')).toBeNull() + expect(screen.getByLabelText('Settings')).toBeTruthy() + }) + + it('Backlog-tab is aria-current op /m/products/[id]', () => { + setPathname('/m/products/p1') + render() + expect(screen.getByLabelText('Backlog').getAttribute('aria-current')).toBe('page') + expect(screen.getByLabelText('Solo').getAttribute('aria-current')).toBeNull() + }) + + it('Solo-tab is aria-current op /m/products/[id]/solo', () => { + setPathname('/m/products/p1/solo') + render() + expect(screen.getByLabelText('Solo').getAttribute('aria-current')).toBe('page') + expect(screen.getByLabelText('Backlog').getAttribute('aria-current')).toBeNull() + }) + + it('Settings-tab is aria-current op /m/settings', () => { + setPathname('/m/settings') + render() + expect(screen.getByLabelText('Settings').getAttribute('aria-current')).toBe('page') + }) + + it('tap-targets >=44x44 (h-14 = 56px breedtevulling per tab)', () => { + setPathname('/m/products/p1') + render() + const tab = screen.getByLabelText('Backlog') + expect(tab.className).toContain('h-14') + expect(tab.className).toContain('flex-1') + }) +}) diff --git a/components/mobile/mobile-tab-bar.tsx b/components/mobile/mobile-tab-bar.tsx new file mode 100644 index 0000000..5845f81 --- /dev/null +++ b/components/mobile/mobile-tab-bar.tsx @@ -0,0 +1,68 @@ +'use client' + +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { ListTree, Activity, Settings } from 'lucide-react' +import { cn } from '@/lib/utils' + +interface MobileTabBarProps { + activeProductId: string | null +} + +export function MobileTabBar({ activeProductId }: MobileTabBarProps) { + const pathname = usePathname() + + const tabs: Array<{ href: string; icon: typeof ListTree; label: string; match: (p: string) => boolean }> = [] + + if (activeProductId) { + tabs.push( + { + href: `/m/products/${activeProductId}`, + icon: ListTree, + label: 'Backlog', + match: (p) => p === `/m/products/${activeProductId}`, + }, + { + href: `/m/products/${activeProductId}/solo`, + icon: Activity, + label: 'Solo', + match: (p) => p.startsWith(`/m/products/${activeProductId}/solo`), + }, + ) + } + + tabs.push({ + href: '/m/settings', + icon: Settings, + label: 'Settings', + match: (p) => p.startsWith('/m/settings'), + }) + + return ( + + ) +}