fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)

Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:

- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/

The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.

Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.

Test suite stays at 1315/1315 vitest. typescript clean.

Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 12:37:22 +02:00
parent ecf49be18c
commit c8ea9ec0a0
172 changed files with 727 additions and 614 deletions

View File

@@ -46,7 +46,7 @@ export function ActiveDealsTile() {
Active deals
</p>
{isLoading ? (
<Skeleton className="mt-1 h-7 w-12" />
<Skeleton className="mt-1 h-7 w-12" aria-hidden />
) : (
<p className="text-2xl font-bold leading-tight text-foreground">
{data?.activeInterests ?? 0}

View File

@@ -56,7 +56,7 @@ export function BerthStatusChart() {
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-[240px] w-full" />
<Skeleton className="h-[240px] w-full" aria-hidden />
) : chartData.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">No berths yet.</p>
) : (

View File

@@ -114,18 +114,18 @@ export function ChartCard({
aria-label="Chart options"
data-testid="chart-menu"
>
<MoreHorizontal className="h-4 w-4" />
<MoreHorizontal className="h-4 w-4" aria-hidden />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-44">
{toCsv ? (
<DropdownMenuItem onSelect={onDownloadCsv}>
<Download className="mr-2 h-4 w-4" />
<Download className="mr-2 h-4 w-4" aria-hidden />
Download CSV
</DropdownMenuItem>
) : null}
<DropdownMenuItem onSelect={onDownloadPng}>
<ImageIcon className="mr-2 h-4 w-4" />
<ImageIcon className="mr-2 h-4 w-4" aria-hidden />
Download PNG
</DropdownMenuItem>
</DropdownMenuContent>

View File

@@ -41,7 +41,7 @@ export function CustomizeWidgetsMenu() {
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm" className="gap-1.5">
<LayoutGrid className="h-4 w-4" />
<LayoutGrid className="h-4 w-4" aria-hidden />
Customize
</Button>
</DialogTrigger>

View File

@@ -69,9 +69,9 @@ export function HotDealsCard() {
<CardContent>
{isLoading ? (
<div className="space-y-3">
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-full" aria-hidden />
<Skeleton className="h-12 w-full" aria-hidden />
<Skeleton className="h-12 w-full" aria-hidden />
</div>
) : deals.length === 0 ? (
<p className="py-4 text-center text-sm text-muted-foreground">

View File

@@ -26,8 +26,8 @@ function KpiTileSkeleton() {
return (
<div className="relative overflow-hidden rounded-xl border border-border bg-card p-3 shadow-sm sm:p-5">
<div className="absolute inset-x-0 top-0 h-1 bg-muted" aria-hidden />
<Skeleton className="h-3 w-20" />
<Skeleton className="mt-2 h-6 w-24 sm:mt-3 sm:h-7" />
<Skeleton className="h-3 w-20" aria-hidden />
<Skeleton className="mt-2 h-6 w-24 sm:mt-3 sm:h-7" aria-hidden />
</div>
);
}

View File

@@ -82,7 +82,7 @@ export function MyRemindersRail() {
<CardHeader className="flex flex-row items-start justify-between gap-2 space-y-0 pb-3">
<div className="space-y-0.5">
<CardTitle className="flex items-center gap-1.5 text-base">
<AlarmClock className="size-4" />
<AlarmClock className="size-4" aria-hidden />
Reminders
</CardTitle>
{overdueCount > 0 ? (
@@ -150,7 +150,10 @@ export function MyRemindersRail() {
? 'in ' + formatDistanceToNowStrict(due)
: 'now'}
</span>
<ChevronRight className="size-3.5 shrink-0 text-muted-foreground/60 transition-transform group-hover:translate-x-0.5" />
<ChevronRight
className="size-3.5 shrink-0 text-muted-foreground/60 transition-transform group-hover:translate-x-0.5"
aria-hidden
/>
</Link>
</li>
);

View File

@@ -38,7 +38,7 @@ export function PipelineValueTile() {
Pipeline value
</p>
{isLoading ? (
<Skeleton className="mt-1 h-7 w-24" />
<Skeleton className="mt-1 h-7 w-24" aria-hidden />
) : (
<p
className="truncate text-2xl font-bold leading-tight text-foreground"

View File

@@ -46,9 +46,9 @@ export function SourceConversionChart() {
<CardContent>
{isLoading ? (
<div className="space-y-3">
<Skeleton className="h-8 w-full" />
<Skeleton className="h-8 w-full" />
<Skeleton className="h-8 w-full" />
<Skeleton className="h-8 w-full" aria-hidden />
<Skeleton className="h-8 w-full" aria-hidden />
<Skeleton className="h-8 w-full" aria-hidden />
</div>
) : rows.length === 0 ? (
<p className="py-4 text-center text-sm text-muted-foreground">

View File

@@ -103,7 +103,7 @@ export function TimezoneDriftBanner() {
return (
<div className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-amber-300 bg-amber-50 px-4 py-2.5 text-sm text-amber-900 sm:flex-nowrap">
<div className="flex items-start gap-2 min-w-0">
<Clock className="mt-0.5 h-4 w-4 shrink-0" />
<Clock className="mt-0.5 h-4 w-4 shrink-0" aria-hidden />
<div className="min-w-0">
<p className="font-medium leading-tight">
You appear to be in{' '}

View File

@@ -52,7 +52,7 @@ export function WebsiteGlanceTile() {
Website today
</div>
{loading ? (
<Skeleton className="mt-2 h-7 w-20" />
<Skeleton className="mt-2 h-7 w-20" aria-hidden />
) : (
<div className="mt-1 flex items-baseline gap-2 text-lg font-semibold tabular-nums sm:mt-2 sm:text-2xl">
{today.toLocaleString()}