Skip to content

Commit 96b71ef

Browse files
Merge pull request #157 from heyoub/fix/a11y-mechanical
fix(web): mechanical a11y fixes — breadcrumbs, headings, names, announcements
2 parents e061318 + e8e9bc6 commit 96b71ef

35 files changed

Lines changed: 922 additions & 157 deletions

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,13 @@ function UpdateCard({ update }: { update: ProjectUpdateResponse }) {
3333
Update #{update.number}
3434
</Link>
3535
</div>
36-
<span title={formatAbsoluteDate(update.createdAt)} className="text-xs text-muted-foreground">
36+
<time
37+
dateTime={update.createdAt}
38+
title={formatAbsoluteDate(update.createdAt)}
39+
className="text-xs text-muted-foreground"
40+
>
3741
{formatRelativeTime(update.createdAt)}
38-
</span>
42+
</time>
3943
</div>
4044

4145
{update.author && (
@@ -69,9 +73,9 @@ function BuzzCard({ buzz }: { buzz: ProjectBuzzResponse }) {
6973
{buzz.project.title}
7074
</Link>
7175
<span> · Buzz · </span>
72-
<span title={formatAbsoluteDate(buzz.publishedAt)}>
76+
<time dateTime={buzz.publishedAt} title={formatAbsoluteDate(buzz.publishedAt)}>
7377
{formatAbsoluteDate(buzz.publishedAt, { month: 'short', day: 'numeric', year: 'numeric' })}
74-
</span>
78+
</time>
7579
</span>
7680
</div>
7781

@@ -89,6 +93,7 @@ function BuzzCard({ buzz }: { buzz: ProjectBuzzResponse }) {
8993
<h3 className="text-base font-semibold mb-0.5">
9094
<a href={buzz.url} target="_blank" rel="noopener noreferrer" className="hover:text-primary">
9195
{buzz.headline}
96+
<span className="sr-only"> (opens in new tab)</span>
9297
</a>
9398
</h3>
9499
<p className="text-xs text-muted-foreground mb-2">{hostname}</p>

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export function AppFooter() {
182182
<a
183183
key={href}
184184
href={href}
185-
aria-label={label}
185+
aria-label={`${label} (opens in new tab)`}
186186
target="_blank"
187187
rel="noopener noreferrer"
188188
className="text-muted-foreground hover:text-foreground transition-colors"
@@ -206,6 +206,7 @@ export function AppFooter() {
206206
className="hover:text-foreground transition-colors"
207207
>
208208
Open source &mdash; view this site on GitHub
209+
<span className="sr-only"> (opens in new tab)</span>
209210
</a>
210211
</div>
211212
</div>

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

Lines changed: 108 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,10 @@ function AboutDropdown() {
182182
);
183183
}
184184

185+
// `block` so each link fills its row: inside the sheet's <li>s an inline
186+
// anchor would shrink the tap target to the width of its text.
185187
const navLinkClass = ({ isActive }: { isActive: boolean }) =>
186-
`text-sm font-medium transition-colors hover:text-primary ${
188+
`block text-sm font-medium transition-colors hover:text-primary ${
187189
isActive ? 'text-primary' : 'text-muted-foreground'
188190
}`;
189191

@@ -197,7 +199,7 @@ function GitHubLink() {
197199
href={GITHUB_URL}
198200
target="_blank"
199201
rel="noopener noreferrer"
200-
aria-label="Code for Philly on GitHub"
202+
aria-label="Code for Philly on GitHub (opens in new tab)"
201203
>
202204
<GitHubIcon />
203205
</a>
@@ -235,20 +237,33 @@ export function AppHeader() {
235237

236238
{/* Desktop content cluster. The parent gap is the only source of
237239
spacing between children — no per-child margins. */}
240+
{/* A nav is a list of destinations — the <ul>/<li> is what tells a
241+
screen reader how many there are and where you are in them. The
242+
flex/gap spacing moves to the <ul>; the <li>s contribute none. */}
238243
<nav
239244
aria-label="Primary navigation"
240-
className="hidden md:flex items-center gap-2 ml-4 flex-1"
245+
className="hidden md:block ml-4 flex-1"
241246
>
242-
<NavLink to="/projects" className={navLinkClass}>
243-
Projects
244-
</NavLink>
245-
<NavLink to="/help-wanted" className={navLinkClass}>
246-
Help Wanted
247-
</NavLink>
248-
<NavLink to="/members" className={navLinkClass}>
249-
Members
250-
</NavLink>
251-
<AboutDropdown />
247+
<ul className="flex items-center gap-2">
248+
<li>
249+
<NavLink to="/projects" className={navLinkClass}>
250+
Projects
251+
</NavLink>
252+
</li>
253+
<li>
254+
<NavLink to="/help-wanted" className={navLinkClass}>
255+
Help Wanted
256+
</NavLink>
257+
</li>
258+
<li>
259+
<NavLink to="/members" className={navLinkClass}>
260+
Members
261+
</NavLink>
262+
</li>
263+
<li>
264+
<AboutDropdown />
265+
</li>
266+
</ul>
252267
</nav>
253268

254269
{/* Desktop utility cluster: GitHub, search, auth, then the Volunteer
@@ -295,58 +310,93 @@ export function AppHeader() {
295310
</SheetHeader>
296311
{/* min-h-0 + overflow-y-auto so the list stays reachable on
297312
short viewports instead of overflowing the panel. */}
313+
{/* Three lists with the separators and the group heading
314+
between them, rather than one list interrupted by
315+
non-list children. The nav keeps the flex column so the
316+
gap-2 rhythm between groups is unchanged. */}
298317
<nav
299318
aria-label="Mobile navigation"
300319
className="flex flex-col gap-2 px-4 min-h-0 overflow-y-auto"
301320
>
302-
<NavLink to="/projects" className={navLinkClass}>
303-
Projects
304-
</NavLink>
305-
<NavLink to="/help-wanted" className={navLinkClass}>
306-
Help Wanted
307-
</NavLink>
308-
<NavLink to="/members" className={navLinkClass}>
309-
Members
310-
</NavLink>
321+
<ul className="flex flex-col gap-2">
322+
<li>
323+
<NavLink to="/projects" className={navLinkClass}>
324+
Projects
325+
</NavLink>
326+
</li>
327+
<li>
328+
<NavLink to="/help-wanted" className={navLinkClass}>
329+
Help Wanted
330+
</NavLink>
331+
</li>
332+
<li>
333+
<NavLink to="/members" className={navLinkClass}>
334+
Members
335+
</NavLink>
336+
</li>
337+
</ul>
311338
<Separator />
312-
<p className="text-xs text-muted-foreground font-medium uppercase tracking-wide">
339+
{/* SheetTitle ("Menu") renders a Radix DialogTitle, i.e. an
340+
h2 — so this group label is an h3, not a styled <p>. */}
341+
<h3 className="text-xs text-muted-foreground font-medium uppercase tracking-wide">
313342
About
314-
</p>
315-
<NavLink to="/pages/mission" className={navLinkClass}>
316-
Mission
317-
</NavLink>
318-
<NavLink to="/pages/leadership" className={navLinkClass}>
319-
Leadership
320-
</NavLink>
321-
<NavLink to="/pages/code-of-conduct" className={navLinkClass}>
322-
Code of Conduct
323-
</NavLink>
324-
<NavLink to="/pages/hackathons" className={navLinkClass}>
325-
Hackathons
326-
</NavLink>
327-
<NavLink to="/sponsor" className={navLinkClass}>
328-
Sponsor
329-
</NavLink>
330-
<a
331-
href="mailto:hello@codeforphilly.org"
332-
className="text-sm font-medium text-muted-foreground hover:text-primary"
333-
onClick={() => setMobileOpen(false)}
334-
>
335-
Contact
336-
</a>
343+
</h3>
344+
<ul className="flex flex-col gap-2">
345+
<li>
346+
<NavLink to="/pages/mission" className={navLinkClass}>
347+
Mission
348+
</NavLink>
349+
</li>
350+
<li>
351+
<NavLink to="/pages/leadership" className={navLinkClass}>
352+
Leadership
353+
</NavLink>
354+
</li>
355+
<li>
356+
<NavLink to="/pages/code-of-conduct" className={navLinkClass}>
357+
Code of Conduct
358+
</NavLink>
359+
</li>
360+
<li>
361+
<NavLink to="/pages/hackathons" className={navLinkClass}>
362+
Hackathons
363+
</NavLink>
364+
</li>
365+
<li>
366+
<NavLink to="/sponsor" className={navLinkClass}>
367+
Sponsor
368+
</NavLink>
369+
</li>
370+
<li>
371+
<a
372+
href="mailto:hello@codeforphilly.org"
373+
className="block text-sm font-medium text-muted-foreground hover:text-primary"
374+
onClick={() => setMobileOpen(false)}
375+
>
376+
Contact
377+
</a>
378+
</li>
379+
</ul>
337380
<Separator />
338-
<a
339-
href={GITHUB_URL}
340-
target="_blank"
341-
rel="noopener noreferrer"
342-
className="text-sm font-medium text-muted-foreground hover:text-primary"
343-
onClick={() => setMobileOpen(false)}
344-
>
345-
GitHub
346-
</a>
347-
<NavLink to="/volunteer" className={navLinkClass}>
348-
Volunteer
349-
</NavLink>
381+
<ul className="flex flex-col gap-2">
382+
<li>
383+
<a
384+
href={GITHUB_URL}
385+
target="_blank"
386+
rel="noopener noreferrer"
387+
className="block text-sm font-medium text-muted-foreground hover:text-primary"
388+
onClick={() => setMobileOpen(false)}
389+
>
390+
GitHub
391+
<span className="sr-only"> (opens in new tab)</span>
392+
</a>
393+
</li>
394+
<li>
395+
<NavLink to="/volunteer" className={navLinkClass}>
396+
Volunteer
397+
</NavLink>
398+
</li>
399+
</ul>
350400
</nav>
351401
<Separator />
352402
<div className="px-4 pb-4">

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function Breadcrumbs({ items }: BreadcrumbsProps) {
2121
{items.map((item, index) => {
2222
const isLast = index === items.length - 1;
2323
return (
24-
<li key={item.label} className="flex items-center gap-1">
24+
<li key={`${index}-${item.label}`} className="flex items-center gap-1">
2525
{index > 0 && (
2626
<span aria-hidden="true" className="text-muted-foreground/50">
2727
&rsaquo;

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export function ConnectGitHubBanner() {
3030
if (dismissed) return null;
3131

3232
return (
33+
<>
3334
<div
3435
role="region"
3536
aria-label="Connect GitHub"
@@ -58,5 +59,14 @@ export function ConnectGitHubBanner() {
5859
</Button>
5960
</div>
6061
</div>
62+
{/* The banner mounts only once auth resolves, and a landmark arriving
63+
after first paint is never announced on its own — so mirror the
64+
headline in a live region (the Sponsor / ProfileEdit idiom). It stays
65+
a region rather than becoming one: a late-mounted status container
66+
is not read reliably either, and it would swallow the two buttons. */}
67+
<span role="status" className="sr-only">
68+
Connect your GitHub account
69+
</span>
70+
</>
6171
);
6272
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useState, type ReactNode } from 'react';
22
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
33
import { TagChip } from '@/components/TagChip';
44
import { Link } from 'react-router';
@@ -12,6 +12,8 @@ interface FacetSidebarProps {
1212
tabs?: Array<'topic' | 'tech' | 'event'>;
1313
limit?: number;
1414
className?: string;
15+
/** Extra filter controls rendered inside the same `Filters` landmark. */
16+
children?: ReactNode;
1517
}
1618

1719
const NS_LABELS = {
@@ -40,6 +42,7 @@ export function FacetSidebar({
4042
tabs = ['topic', 'tech', 'event'],
4143
limit = 10,
4244
className,
45+
children,
4346
}: FacetSidebarProps) {
4447
const [tab, setTab] = useState<string>(tabs[0] ?? 'topic');
4548
const activeTagSet = new Set(activeTags);
@@ -103,6 +106,7 @@ export function FacetSidebar({
103106
);
104107
})}
105108
</Tabs>
109+
{children}
106110
</aside>
107111
);
108112
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { PersonAvatar } from '@/components/PersonAvatar';
66
import { MarkdownView } from '@/components/MarkdownView';
77
import { ExpressInterestModal } from '@/components/modals/ExpressInterestModal';
88
import { useAuth } from '@/hooks/useAuth';
9-
import { formatRelativeTime } from '@/lib/time';
9+
import { formatAbsoluteDate, formatRelativeTime } from '@/lib/time';
1010
import type { HelpWantedRoleResponse } from '@/lib/api';
1111

1212
interface HelpWantedCardProps {
@@ -64,7 +64,12 @@ export function HelpWantedCard({ role, showProjectLink = true }: HelpWantedCardP
6464
<span>·</span>
6565
</>
6666
)}
67-
<span>posted {formatRelativeTime(role.createdAt)}</span>
67+
<span>
68+
posted{' '}
69+
<time dateTime={role.createdAt} title={formatAbsoluteDate(role.createdAt)}>
70+
{formatRelativeTime(role.createdAt)}
71+
</time>
72+
</span>
6873
</div>
6974

7075
{isSignedIn ? (

0 commit comments

Comments
 (0)