Skip to content

Commit d34834e

Browse files
committed
fix(a11y): reduce jsx-a11y warnings common 18→8, studio 45→31 (#386)
- Replace role="button" divs/spans with native <button> elements - Fix form label associations with aria-labelledby pattern - Add keyboard event handlers where missing - Wrap emojis with proper ARIA in test files - Remove redundant role="form" on native form element - Add aria-hidden + tabIndex=-1 on decorative canvas - Create update-max-warnings-a11y.ts script for maintaining baselines
1 parent c6fe97b commit d34834e

16 files changed

Lines changed: 142 additions & 65 deletions

File tree

web/packages/common/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"format:fix": "prettier --write './**/*.{js,jsx,ts,tsx}'",
1111
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
1212
"lint:fix": "eslint . --fix --report-unused-disable-directives --max-warnings 0",
13-
"lint:a11y": "eslint . --config ../../eslint.config.a11y.js --no-config-lookup --no-inline-config --max-warnings 18",
13+
"lint:a11y": "eslint . --config ../../eslint.config.a11y.js --no-config-lookup --no-inline-config --max-warnings 8",
14+
"update-warning-count:a11y": "tsx scripts/update-max-warnings-a11y.ts",
1415
"test": "vitest --run",
1516
"test:ci": "vitest run --coverage",
1617
"test:watch": "vitest watch",
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env node
2+
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
import { execSync } from 'child_process';
6+
import fs from 'fs';
7+
import path from 'path';
8+
9+
const pkgPath = path.resolve(process.cwd(), 'package.json');
10+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as {
11+
scripts: Record<string, string>;
12+
};
13+
14+
// Run ESLint with the a11y config and capture JSON output
15+
let output: string;
16+
try {
17+
output = execSync(
18+
'pnpm exec eslint . --config ../../eslint.config.a11y.js --no-config-lookup --no-inline-config --format json',
19+
{ encoding: 'utf8' }
20+
);
21+
} catch (e) {
22+
output = (e as { stdout: string }).stdout;
23+
}
24+
25+
const results: Array<{ warningCount: number }> = JSON.parse(output);
26+
const warningCount: number = results.reduce((sum, file) => sum + file.warningCount, 0);
27+
28+
// Find current max-warnings in lint:a11y script
29+
const a11yScript: string = pkg.scripts['lint:a11y'];
30+
if (!a11yScript) {
31+
console.error('No lint:a11y script found in package.json');
32+
process.exit(1);
33+
}
34+
35+
const maxWarningsRegex = /--max-warnings (\d+)/;
36+
const currentMax: number = parseInt(a11yScript.match(maxWarningsRegex)?.[1] || '0', 10);
37+
38+
if (warningCount !== currentMax) {
39+
pkg.scripts['lint:a11y'] = a11yScript.replace(maxWarningsRegex, `--max-warnings ${warningCount}`);
40+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
41+
// eslint-disable-next-line no-console
42+
console.log(`Updated lint:a11y max-warnings from ${currentMax} to ${warningCount}`);
43+
} else {
44+
// eslint-disable-next-line no-console
45+
console.log(`No update needed. Current warnings: ${warningCount}, max-warnings: ${currentMax}`);
46+
}

web/packages/common/src/components/DataView/StudioAppliedFilters.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ vi.mock('@nvidia/foundations-react-core', () => ({
2929
),
3030
Flex: ({ children }: React.PropsWithChildren) => <div>{children}</div>,
3131
Tag: ({ children, onClick }: React.PropsWithChildren<{ onClick?: () => void }>) => (
32-
<span role="button" onClick={onClick}>
32+
<button type="button" onClick={onClick}>
3333
{children}
34-
</span>
34+
</button>
3535
),
3636
}));
3737

web/packages/common/src/components/DataView/StudioDataView.test.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,13 @@ vi.mock('@nemo/common/src/components/DataView/internal', () => ({
153153
TableContent: ({
154154
className,
155155
onClick,
156+
onKeyDown,
156157
}: {
157158
className?: string;
158159
onClick?: React.MouseEventHandler;
160+
onKeyDown?: React.KeyboardEventHandler;
159161
}) => (
160-
<table className={className} onClick={onClick}>
162+
<table className={className} onClick={onClick} onKeyDown={onKeyDown}>
161163
<tbody>
162164
{mockRenderedRows.map((cells, rowIdx) => (
163165
<tr key={rowIdx} data-index={rowIdx}>
@@ -168,7 +170,7 @@ vi.mock('@nemo/common/src/components/DataView/internal', () => ({
168170
<button type="button">Delete {mockFlatRows[rowIdx]?.item.name}</button>
169171
</td>
170172
<td>
171-
<a href="/details">Link {mockFlatRows[rowIdx]?.item.name}</a>
173+
<a href="/details">View {mockFlatRows[rowIdx]?.item.name}</a>
172174
</td>
173175
<td>
174176
<input type="text" defaultValue="edit" aria-label={`edit-${rowIdx}`} />

web/packages/common/src/components/DataView/useRowClick.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,14 @@ function RowKeyboardTarget({
7272
subIndex?: number;
7373
}) {
7474
return (
75-
<span
75+
<button
76+
type="button"
7677
className="sr-only"
77-
tabIndex={0}
78-
role="button"
7978
aria-label="Open row"
8079
data-row-click
8180
data-row-index={dataIndex}
8281
{...(subIndex !== undefined && { 'data-sub-index': subIndex })}
83-
onKeyDown={(e) => {
84-
if (e.key === 'Enter' || e.key === ' ') {
85-
e.preventDefault();
86-
onActivate();
87-
}
88-
}}
82+
onClick={onActivate}
8983
/>
9084
);
9185
}

web/packages/common/src/components/Nebula/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,12 @@ export const Nebula = ({
7676
className={`relative size-full min-h-[200px] min-w-[200px] ${className}`}
7777
data-testid="nv-nebula"
7878
>
79-
<canvas ref={handleRef} className="pointer-events-none absolute inset-[0]" />
79+
<canvas
80+
ref={handleRef}
81+
className="pointer-events-none absolute inset-[0]"
82+
aria-hidden="true"
83+
tabIndex={-1}
84+
/>
8085
</div>
8186
);
8287
};

web/packages/common/src/components/StatusBadge/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const StatusBadge = <T extends string = string>({
5353

5454
return (
5555
<Badge color={config.color} kind="solid">
56-
{Icon ? <Icon width="12px" height="12px" role="img" /> : null}
56+
{Icon ? <Icon width="12px" height="12px" aria-hidden="true" /> : null}
5757
{label}
5858
</Badge>
5959
);

web/packages/common/src/components/TableEmptyState/TableEmptyState.test.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ describe('TableEmptyState', () => {
1919
});
2020

2121
it('renders with custom icon', () => {
22-
const customIcon = <div data-testid="custom-icon">🔍</div>;
22+
const customIcon = (
23+
<div data-testid="custom-icon">
24+
<span role="img" aria-label="search icon">
25+
🔍
26+
</span>
27+
</div>
28+
);
2329

2430
render(<TableEmptyState header="Test Header" emptyMessage="Test Message" icon={customIcon} />);
2531

web/packages/common/src/components/UploadModal/SimpleFilesTable.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,14 +181,24 @@ export const SimpleFilesTable = () => {
181181
)}
182182
{trailingButton ? (
183183
<Flex justify="between" align="center">
184-
<Button kind="tertiary" asChild>
185-
<label htmlFor="upload-more-files">Upload More Files</label>
184+
<Button
185+
kind="tertiary"
186+
onClick={() => {
187+
document.getElementById('upload-more-files')?.click();
188+
}}
189+
>
190+
Upload More Files
186191
</Button>
187192
{trailingButton}
188193
</Flex>
189194
) : (
190-
<Button kind="tertiary" asChild>
191-
<label htmlFor="upload-more-files">Upload More Files</label>
195+
<Button
196+
kind="tertiary"
197+
onClick={() => {
198+
document.getElementById('upload-more-files')?.click();
199+
}}
200+
>
201+
Upload More Files
192202
</Button>
193203
)}
194204
<input
@@ -198,6 +208,7 @@ export const SimpleFilesTable = () => {
198208
onChange={handleFileChange}
199209
accept={acceptableFileTypes.join(',')}
200210
className="sr-only"
211+
aria-label="Upload more files"
201212
/>
202213
</Stack>
203214
);

web/packages/common/src/components/form/ControlledSearchableSelect/index.test.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,12 +336,26 @@ describe('ControlledSearchableSelect', () => {
336336
{
337337
value: 'apple',
338338
label: 'Apple',
339-
render: <span data-testid="custom-apple">🍎 Apple</span>,
339+
render: (
340+
<span data-testid="custom-apple">
341+
<span role="img" aria-label="apple">
342+
🍎
343+
</span>{' '}
344+
Apple
345+
</span>
346+
),
340347
},
341348
{
342349
value: 'banana',
343350
label: 'Banana',
344-
render: <span data-testid="custom-banana">🍌 Banana</span>,
351+
render: (
352+
<span data-testid="custom-banana">
353+
<span role="img" aria-label="banana">
354+
🍌
355+
</span>{' '}
356+
Banana
357+
</span>
358+
),
345359
},
346360
];
347361

0 commit comments

Comments
 (0)