Skip to content

Commit e061318

Browse files
Merge pull request #155 from heyoub/fix/aria-correctness
fix(web): repair invalid ARIA and make search/tag pickers real comboboxes
2 parents a24e5d2 + 908d4e1 commit e061318

33 files changed

Lines changed: 1253 additions & 196 deletions

‎apps/web/src/components/AppShell.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function AppShell() {
1111
{/* Skip to main content — must be the first focusable element */}
1212
<a
1313
href="#main-content"
14-
className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded focus:shadow-lg focus:outline-none"
14+
className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded focus:shadow-lg"
1515
>
1616
Skip to main content
1717
</a>

‎apps/web/src/components/ConnectGitHubBanner.tsx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export function ConnectGitHubBanner() {
5353
variant="ghost"
5454
size="sm"
5555
onClick={() => setDismissed(true)}
56-
aria-label="Dismiss"
5756
>
5857
Dismiss
5958
</Button>

‎apps/web/src/components/MarkdownEditor.tsx‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export function MarkdownEditor({
5353
required,
5454
}: MarkdownEditorProps) {
5555
const id = useId();
56+
const errorId = `${id}-error`;
5657
const textareaRef = useRef<HTMLTextAreaElement>(null);
5758
const [previewHtml, setPreviewHtml] = useState<string>('');
5859
const [previewLoading, setPreviewLoading] = useState(false);
@@ -170,11 +171,14 @@ export function MarkdownEditor({
170171
className="rounded-none border-0 focus-visible:ring-0 font-mono text-sm resize-y"
171172
style={{ minHeight }}
172173
aria-invalid={error ? 'true' : 'false'}
174+
aria-describedby={error ? errorId : undefined}
173175
/>
176+
{/* Deliberately not a live region: the preview is the whole document
177+
re-rendered on every debounce, so announcing it would read the
178+
entire text back on each pause in typing. */}
174179
<div
175180
className="p-3 bg-background text-sm overflow-auto"
176181
style={{ minHeight }}
177-
aria-live="polite"
178182
>
179183
{previewError ? (
180184
<p className="text-xs text-destructive">{previewError}</p>
@@ -192,7 +196,9 @@ export function MarkdownEditor({
192196
</div>
193197
<div className="flex items-center justify-between text-xs">
194198
{error ? (
195-
<span className="text-destructive">{error}</span>
199+
<span id={errorId} className="text-destructive">
200+
{error}
201+
</span>
196202
) : (
197203
<span className="text-muted-foreground">Markdown · supports GFM</span>
198204
)}

‎apps/web/src/components/NetworkErrorBanner.tsx‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,26 @@ import {
77
} from 'react';
88

99
interface NetworkErrorContextValue {
10-
showError: (message?: string) => void;
10+
/**
11+
* Show the banner. Pass `retry` when the failed work can be re-issued; the
12+
* button then reads "Retry" and runs it. Without one it reads "Dismiss".
13+
*/
14+
showError: (message?: string, retry?: () => void) => void;
1115
clearError: () => void;
1216
}
1317

18+
interface NetworkErrorState {
19+
message: string;
20+
retry?: () => void;
21+
}
22+
1423
const NetworkErrorContext = createContext<NetworkErrorContextValue | null>(null);
1524

1625
export function NetworkErrorProvider({ children }: { children: ReactNode }) {
17-
const [error, setError] = useState<string | null>(null);
26+
const [error, setError] = useState<NetworkErrorState | null>(null);
1827

19-
const showError = useCallback((message?: string) => {
20-
setError(message ?? 'Something went wrong. We are looking at it.');
28+
const showError = useCallback((message?: string, retry?: () => void) => {
29+
setError({ message: message ?? 'Something went wrong. We are looking at it.', retry });
2130
}, []);
2231

2332
const clearError = useCallback(() => {
@@ -32,13 +41,15 @@ export function NetworkErrorProvider({ children }: { children: ReactNode }) {
3241
className="bg-destructive text-destructive-foreground px-4 py-2 text-sm flex items-center justify-between"
3342
data-testid="network-error-banner"
3443
>
35-
<span>{error}</span>
44+
<span>{error.message}</span>
3645
<button
37-
onClick={clearError}
46+
onClick={() => {
47+
error.retry?.();
48+
clearError();
49+
}}
3850
className="ml-4 underline hover:no-underline"
39-
aria-label="Dismiss error"
4051
>
41-
Retry
52+
{error.retry ? 'Retry' : 'Dismiss'}
4253
</button>
4354
</div>
4455
)}

‎apps/web/src/components/Pagination.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export function Pagination({ page, totalPages, onPageChange, siblingCount = 1, c
6262
variant={p === page ? 'default' : 'outline'}
6363
size="sm"
6464
onClick={() => onPageChange(p)}
65+
aria-label={`Page ${p}`}
6566
aria-current={p === page ? 'page' : undefined}
6667
>
6768
{p}

‎apps/web/src/components/PersonAvatar.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export function PersonAvatar({ person, size = 32, asLink = true, className, titl
2424
/>
2525
) : (
2626
<span
27+
role="img"
2728
title={title ?? person.fullName}
2829
className={cn(
2930
'inline-flex items-center justify-center rounded-full font-medium',
@@ -43,7 +44,7 @@ export function PersonAvatar({ person, size = 32, asLink = true, className, titl
4344
if (!asLink || !person.slug || person.deactivated) return inner;
4445

4546
return (
46-
<Link to={`/members/${person.slug}`} aria-label={person.fullName}>
47+
<Link to={`/members/${person.slug}`}>
4748
{inner}
4849
</Link>
4950
);

0 commit comments

Comments
 (0)