Fix mobile nav icon accessible names

This commit is contained in:
rcourtman 2026-04-26 12:53:11 +01:00
parent 5ce4308748
commit 250bcb1f47
3 changed files with 44 additions and 3 deletions

View file

@ -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

View file

@ -37,7 +37,7 @@ export function MobileNavBar(props: MobileNavBarProps) {
enabled: platform.enabled,
})}
>
<span class="relative flex items-center justify-center">
<span aria-hidden="true" class="relative flex items-center justify-center">
<Icon class={tabIconClass} />
</span>
<span class="whitespace-nowrap">{platform.label}</span>
@ -72,7 +72,9 @@ export function MobileNavBar(props: MobileNavBarProps) {
})}
>
<span class="relative flex items-center justify-center">
<Icon class={tabIconClass} />
<span aria-hidden="true" class="inline-flex items-center justify-center">
<Icon class={tabIconClass} />
</span>
<Show when={alertBadges()}>
{(badges) => (
<span class="absolute -right-2 -top-1 flex items-center gap-1">

View file

@ -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) => <span class={pro
const StorageIcon: Component<{ class?: string }> = (props) => <span class={props.class}>ST</span>;
const AlertsIcon: Component<{ class?: string }> = (props) => <span class={props.class}>AL</span>;
const SettingsIcon: Component<{ class?: string }> = (props) => <span class={props.class}>SE</span>;
const PatrolIcon: Component<{ class?: string }> = (props) => (
<svg aria-label="Pulse Patrol" class={props.class} viewBox="0 0 24 24">
<title>Pulse Patrol</title>
<circle cx="12" cy="12" r="8" />
</svg>
);
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(() => (
<MobileNavBar
activeTab={() => '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();