diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index c5fc1ba9e..570099b74 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -174,6 +174,10 @@ work extends shared components instead of creating new local variants. durable plan-capacity explanation, over-plan reasoning, and upgrade/review actions belong in the `cloud-paid` plan surface rather than permanent banner-local prose. + Mobile navigation under the same shared boundary owns tab accessible names: + icon components may keep their standalone labels, but the nav must treat + those icons as decorative inside tab buttons so names come from the tab + label plus meaningful badge counts, not duplicated icon titles. 2. Route new top-level settings surfaces through the canonical settings shell instead of introducing page-local framing. Shared shells and primitives that need websocket or dark-mode context must diff --git a/frontend-modern/src/components/shared/MobileNavBar.tsx b/frontend-modern/src/components/shared/MobileNavBar.tsx index 2fc1228c7..29f25e3bc 100644 --- a/frontend-modern/src/components/shared/MobileNavBar.tsx +++ b/frontend-modern/src/components/shared/MobileNavBar.tsx @@ -37,7 +37,7 @@ export function MobileNavBar(props: MobileNavBarProps) { enabled: platform.enabled, })} > - + {platform.label} @@ -72,7 +72,9 @@ export function MobileNavBar(props: MobileNavBarProps) { })} > - + {(badges) => ( diff --git a/frontend-modern/src/components/shared/__tests__/MobileNavBar.test.tsx b/frontend-modern/src/components/shared/__tests__/MobileNavBar.test.tsx index ee32de159..3b9da7f4c 100644 --- a/frontend-modern/src/components/shared/__tests__/MobileNavBar.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/MobileNavBar.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from '@solidjs/testing-library'; +import { fireEvent, render, screen, waitFor, within } from '@solidjs/testing-library'; import { describe, expect, it, vi } from 'vitest'; import type { Component } from 'solid-js'; import mobileNavBarSource from '@/components/shared/MobileNavBar.tsx?raw'; @@ -16,6 +16,12 @@ const DashboardIcon: Component<{ class?: string }> = (props) => = (props) => ST; const AlertsIcon: Component<{ class?: string }> = (props) => AL; const SettingsIcon: Component<{ class?: string }> = (props) => SE; +const PatrolIcon: Component<{ class?: string }> = (props) => ( + + Pulse Patrol + + +); describe('MobileNavBar', () => { it('keeps the mobile nav on shell, runtime, and model owners', () => { @@ -37,6 +43,35 @@ describe('MobileNavBar', () => { expect(mobileNavBarModelSource).toContain('getMobileNavFadeState'); }); + it('keeps decorative icon labels out of mobile tab accessible names', () => { + render(() => ( + 'ai'} + platformTabs={() => []} + utilityTabs={() => [ + { + id: 'ai', + label: 'Patrol', + route: '/patrol', + tooltip: 'Continuous verification', + badge: null, + count: undefined, + breakdown: undefined, + icon: PatrolIcon, + }, + ]} + onPlatformClick={() => {}} + onUtilityClick={() => {}} + /> + )); + + const navList = screen.getByRole('tablist', { name: 'Mobile navigation' }); + const patrolButton = within(navList).getByRole('button', { name: 'Patrol' }); + + expect(patrolButton).toHaveAttribute('data-tab-id', 'ai'); + expect(within(navList).queryByRole('button', { name: 'Pulse Patrol Patrol' })).toBeNull(); + }); + it('orders tabs, renders alert badges, and shows fades from scroll state', async () => { const onPlatformClick = vi.fn(); const onUtilityClick = vi.fn();