Skip to content

Commit 2d4892f

Browse files
committed
fix: WCAG 2.1 AA accessibility audit — contrast fixes, ARIA labels, keyboard nav, mobile a11y & ACCESSIBILITY.md
1 parent 42ee1ac commit 2d4892f

22 files changed

Lines changed: 886 additions & 127 deletions

ACCESSIBILITY.md

Lines changed: 319 additions & 0 deletions
Large diffs are not rendered by default.

apps/mobile/src/components/CardPickerSheet.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ const CardPickerSheet = React.forwardRef<BottomSheetModal, Props>(
9696
]}
9797
onPress={() => onSelect(card.id)}
9898
disabled={isSelected}
99+
accessibilityLabel={isSelected ? `${card.title} is currently selected` : `Select card ${card.title}`}
100+
accessibilityRole="button"
101+
accessibilityState={{ disabled: isSelected, selected: isSelected }}
102+
accessibilityHint={isSelected ? undefined : "Switches the active context card shown on your profile and QR code"}
99103
>
100104
<Text
101105
style={[
@@ -187,7 +191,7 @@ const styles = StyleSheet.create({
187191
borderRadius: 5,
188192
},
189193
morePlatforms: {
190-
fontSize: 10,
194+
fontSize: FONT_SIZE.micro,
191195
color: COLORS.textMuted,
192196
fontWeight: '700',
193197
},

apps/mobile/src/screens/CardsScreen.tsx

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,10 @@ export default function CardsScreen() {
167167
<Text style={styles.title}>Context Cards</Text>
168168
<TouchableOpacity
169169
style={styles.addButton}
170-
onPress={() => setShowCreate(true)}>
170+
onPress={() => setShowCreate(true)}
171+
accessibilityLabel="Create a new context card"
172+
accessibilityRole="button"
173+
accessibilityHint="Opens a form to create a new context card">
171174
<Text style={styles.addButtonText}>+ New Card</Text>
172175
</TouchableOpacity>
173176
</View>
@@ -192,7 +195,7 @@ export default function CardsScreen() {
192195
<View style={styles.chip} />
193196
<Text style={styles.brandText}>DevCard</Text>
194197
</View>
195-
<Text style={styles.contactless}>📶</Text>
198+
<Text style={styles.contactless} accessibilityElementsHidden importantForAccessibility="no">📶</Text>
196199
</View>
197200

198201
{/* Card Center: Title */}
@@ -207,7 +210,7 @@ export default function CardsScreen() {
207210
<Text style={styles.userName}>{user?.displayName || 'Card Holder'}</Text>
208211
<Text style={styles.cardId}>{Math.random().toString(36).substring(2, 6).toUpperCase()} {Math.random().toString(36).substring(2, 6).toUpperCase()}</Text>
209212
</View>
210-
<View style={styles.platformIcons}>
213+
<View style={styles.platformIcons} accessibilityLabel={`Linked platforms: ${item.links.map(l => PLATFORMS[l.platform]?.name || l.platform).join(', ')}`}>
211214
{item.links.slice(0, 3).map(link => (
212215
<View key={link.id} style={[styles.platformDot, { backgroundColor: PLATFORMS[link.platform]?.color || COLORS.primary }]} />
213216
))}
@@ -224,15 +227,25 @@ export default function CardsScreen() {
224227
{/* Card Actions Below the Card */}
225228
<View style={styles.actionRow}>
226229
{!item.isDefault ? (
227-
<TouchableOpacity onPress={() => setDefault(item.id)} style={styles.actionBtn}>
230+
<TouchableOpacity
231+
onPress={() => setDefault(item.id)}
232+
style={styles.actionBtn}
233+
accessibilityLabel={`Set ${item.title} as primary card`}
234+
accessibilityRole="button"
235+
accessibilityHint="Makes this card the default card shared via your QR code and profile link">
228236
<Text style={styles.actionBtnText}>Set as Primary</Text>
229237
</TouchableOpacity>
230238
) : (
231-
<View style={styles.activeBadge}>
239+
<View style={styles.activeBadge} accessibilityLabel="This is your active card">
232240
<Text style={styles.activeBadgeText}>ACTIVE CARD</Text>
233241
</View>
234242
)}
235-
<TouchableOpacity onPress={() => deleteCard(item.id)} style={styles.deleteBtn}>
243+
<TouchableOpacity
244+
onPress={() => deleteCard(item.id)}
245+
style={styles.deleteBtn}
246+
accessibilityLabel={`Delete card ${item.title}`}
247+
accessibilityRole="button"
248+
accessibilityHint="Removes this card from your list of context cards">
236249
<Text style={styles.deleteBtnText}>Delete</Text>
237250
</TouchableOpacity>
238251
</View>
@@ -258,6 +271,8 @@ export default function CardsScreen() {
258271
placeholderTextColor={COLORS.textMuted}
259272
value={newTitle}
260273
onChangeText={setNewTitle}
274+
accessibilityLabel="Card title text input"
275+
accessibilityHint="Type a title for your context card"
261276
/>
262277
<Text style={styles.selectLabel}>Select platforms to include:</Text>
263278
{allLinks.length === 0 ? (
@@ -270,7 +285,11 @@ export default function CardsScreen() {
270285
<TouchableOpacity
271286
key={link.id}
272287
style={[styles.linkOption, selectedLinkIds.includes(link.id) && styles.linkSelected]}
273-
onPress={() => toggleLink(link.id)}>
288+
onPress={() => toggleLink(link.id)}
289+
accessibilityLabel={`Include ${PLATFORMS[link.platform]?.name || link.platform} connection for ${link.username}`}
290+
accessibilityRole="checkbox"
291+
accessibilityState={{ checked: selectedLinkIds.includes(link.id) }}
292+
accessibilityHint="Toggles inclusion of this connection in the context card">
274293
<View style={[styles.dot, { backgroundColor: PLATFORMS[link.platform]?.color || COLORS.primary }]} />
275294
<Text style={styles.linkOptionText}>
276295
{PLATFORMS[link.platform]?.name || link.platform}{link.username}
@@ -279,12 +298,20 @@ export default function CardsScreen() {
279298
</TouchableOpacity>
280299
))
281300
)}
282-
<TouchableOpacity style={styles.submitBtn} onPress={createCard}>
301+
<TouchableOpacity
302+
style={styles.submitBtn}
303+
onPress={createCard}
304+
accessibilityLabel="Create card"
305+
accessibilityRole="button"
306+
accessibilityHint="Saves the context card with the selected connections">
283307
<Text style={styles.submitBtnText}>Create Card</Text>
284308
</TouchableOpacity>
285309
<TouchableOpacity
286310
style={styles.cancelBtn}
287-
onPress={() => { setShowCreate(false); setNewTitle(''); setSelectedLinkIds([]); }}>
311+
onPress={() => { setShowCreate(false); setNewTitle(''); setSelectedLinkIds([]); }}
312+
accessibilityLabel="Cancel card creation"
313+
accessibilityRole="button"
314+
accessibilityHint="Closes the create card modal without saving">
288315
<Text style={styles.cancelBtnText}>Cancel</Text>
289316
</TouchableOpacity>
290317
</View>
@@ -421,7 +448,7 @@ const styles = StyleSheet.create({
421448
},
422449
brandText: {
423450
color: 'rgba(255,255,255,0.6)',
424-
fontSize: 12,
451+
fontSize: FONT_SIZE.xs,
425452
fontWeight: '700',
426453
letterSpacing: 1,
427454
textTransform: 'uppercase',
@@ -434,13 +461,13 @@ const styles = StyleSheet.create({
434461
marginTop: SPACING.md,
435462
},
436463
premiumCardTitle: {
437-
fontSize: 28,
464+
fontSize: FONT_SIZE.xxl,
438465
fontWeight: '800',
439466
color: COLORS.white,
440467
letterSpacing: 0.5,
441468
},
442469
cardType: {
443-
fontSize: 8,
470+
fontSize: FONT_SIZE.nano,
444471
color: 'rgba(255,255,255,0.4)',
445472
fontWeight: '700',
446473
letterSpacing: 2,
@@ -455,14 +482,14 @@ const styles = StyleSheet.create({
455482
flex: 1,
456483
},
457484
userName: {
458-
fontSize: 14,
485+
fontSize: FONT_SIZE.sm,
459486
color: 'rgba(255,255,255,0.8)',
460487
fontWeight: '600',
461488
textTransform: 'uppercase',
462489
letterSpacing: 1,
463490
},
464491
cardId: {
465-
fontSize: 10,
492+
fontSize: FONT_SIZE.micro,
466493
color: 'rgba(255,255,255,0.3)',
467494
fontFamily: 'monospace',
468495
marginTop: 2,
@@ -478,7 +505,7 @@ const styles = StyleSheet.create({
478505
borderRadius: 5,
479506
},
480507
morePlatforms: {
481-
fontSize: 10,
508+
fontSize: FONT_SIZE.micro,
482509
color: 'rgba(255,255,255,0.5)',
483510
fontWeight: '700',
484511
},
@@ -501,7 +528,7 @@ const styles = StyleSheet.create({
501528
},
502529
actionBtnText: {
503530
color: COLORS.primary,
504-
fontSize: 12,
531+
fontSize: FONT_SIZE.xs,
505532
fontWeight: '600',
506533
},
507534
activeBadge: {
@@ -511,7 +538,7 @@ const styles = StyleSheet.create({
511538
},
512539
activeBadgeText: {
513540
color: COLORS.success,
514-
fontSize: 10,
541+
fontSize: FONT_SIZE.micro,
515542
fontWeight: '800',
516543
letterSpacing: 1,
517544
},
@@ -521,7 +548,7 @@ const styles = StyleSheet.create({
521548
},
522549
deleteBtnText: {
523550
color: 'rgba(239, 68, 68, 0.6)',
524-
fontSize: 12,
551+
fontSize: FONT_SIZE.xs,
525552
fontWeight: '600',
526553
},
527554
});

apps/mobile/src/screens/ConnectPlatformsScreen.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,16 @@ export const ConnectPlatformsScreen: React.FC<Props> = ({ navigation: _navigatio
112112

113113
{isConnected ? (
114114
<View style={styles.connectedState}>
115-
<View style={styles.statusBadge}>
115+
<View style={styles.statusBadge} accessibilityLabel={`${name} is currently connected`}>
116116
<Icon name="check-circle" size={14} color={COLORS.success} />
117117
<Text style={styles.statusText}>Connected</Text>
118118
</View>
119119
<TouchableOpacity
120120
style={styles.disconnectBtn}
121121
onPress={() => handleDisconnect(platformId)}
122+
accessibilityLabel={`Disconnect from ${name}`}
123+
accessibilityRole="button"
124+
accessibilityHint={`Removes authorization and disconnects your ${name} account`}
122125
>
123126
<Text style={styles.disconnectBtnText}>Disconnect</Text>
124127
</TouchableOpacity>
@@ -127,6 +130,9 @@ export const ConnectPlatformsScreen: React.FC<Props> = ({ navigation: _navigatio
127130
<TouchableOpacity
128131
style={styles.connectBtn}
129132
onPress={() => handleConnect(platformId)}
133+
accessibilityLabel={`Connect ${name}`}
134+
accessibilityRole="button"
135+
accessibilityHint={`Redirects to authenticate and connect your ${name} account`}
130136
>
131137
<Text style={styles.connectBtnText}>Connect {name}</Text>
132138
</TouchableOpacity>

apps/mobile/src/screens/DevCardViewScreen.tsx

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -339,9 +339,13 @@ export default function DevCardViewScreen({ navigation, route }: Props) {
339339
return (
340340
<SafeAreaView style={styles.container}>
341341
<View style={styles.errorState}>
342-
<Text style={styles.errorEmoji}>😕</Text>
342+
<Text style={styles.errorEmoji} accessibilityElementsHidden>😕</Text>
343343
<Text style={styles.errorText}>User not found</Text>
344-
<TouchableOpacity onPress={() => navigation.goBack()}>
344+
<TouchableOpacity
345+
onPress={() => navigation.goBack()}
346+
accessibilityLabel="Go back to the previous screen"
347+
accessibilityRole="button"
348+
>
345349
<Text style={styles.backLink}>Go Back</Text>
346350
</TouchableOpacity>
347351
</View>
@@ -354,8 +358,14 @@ export default function DevCardViewScreen({ navigation, route }: Props) {
354358
<StatusBar barStyle="light-content" backgroundColor={COLORS.bgPrimary} />
355359

356360
{/* Close Button */}
357-
<TouchableOpacity style={styles.closeBtn} onPress={() => navigation.goBack()}>
358-
<Text style={styles.closeBtnText}></Text>
361+
<TouchableOpacity
362+
style={styles.closeBtn}
363+
onPress={() => navigation.goBack()}
364+
accessibilityLabel="Close"
365+
accessibilityRole="button"
366+
accessibilityHint="Returns to the previous screen"
367+
>
368+
<Text style={styles.closeBtnText} aria-hidden></Text>
359369
</TouchableOpacity>
360370

361371
<ScrollView contentContainerStyle={styles.scrollContent}>
@@ -432,6 +442,18 @@ export default function DevCardViewScreen({ navigation, route }: Props) {
432442
const state = followStates[link.id] || 'idle';
433443
const btnColor = getButtonColor(link, state);
434444
const isDone = state === 'success';
445+
const actionLabel = getButtonLabel(link);
446+
// Build a clear, human-readable label for screen readers
447+
const a11yLabel = isDone
448+
? `${platform?.name || link.platform} — connected as ${link.username}`
449+
: `${actionLabel} ${platform?.name || link.platform}${link.username}`;
450+
const a11yHint = isDone
451+
? 'Long press to reset connection status'
452+
: platform?.followStrategy === 'webview'
453+
? 'Opens an in-app browser to connect'
454+
: platform?.followStrategy === 'copy'
455+
? 'Copies the username to your clipboard'
456+
: 'Opens the profile in your browser';
435457
return (
436458
<TouchableOpacity
437459
key={link.id}
@@ -454,7 +476,12 @@ export default function DevCardViewScreen({ navigation, route }: Props) {
454476
}
455477
}}
456478
activeOpacity={isDone ? 0.9 : 0.8}
457-
disabled={state === 'loading'}>
479+
disabled={state === 'loading'}
480+
accessibilityLabel={a11yLabel}
481+
accessibilityRole="button"
482+
accessibilityHint={a11yHint}
483+
accessibilityState={{ disabled: state === 'loading', selected: isDone }}
484+
>
458485

459486
{/* Icon */}
460487
<View style={[
@@ -548,7 +575,7 @@ const styles = StyleSheet.create({
548575
},
549576
brandRow: { flexDirection: 'row', alignItems: 'center', gap: 7 },
550577
miniChip: { width: 28, height: 18, borderRadius: 4, opacity: 0.7 },
551-
brandText: { color: 'rgba(255,255,255,0.45)', fontSize: 9, fontWeight: '800', letterSpacing: 2.5 },
578+
brandText: { color: 'rgba(255,255,255,0.45)', fontSize: FONT_SIZE.nano + 1, fontWeight: '800', letterSpacing: 2.5 },
552579
cardMid: { flexDirection: 'row', alignItems: 'center', gap: SPACING.md },
553580
avatarRing: {
554581
borderRadius: 38,
@@ -565,18 +592,18 @@ const styles = StyleSheet.create({
565592
profileRole: {
566593
fontSize: 11, color: 'rgba(255,255,255,0.55)', fontWeight: '500', lineHeight: 15,
567594
},
568-
pronouns: { fontSize: 10, color: COLORS.textMuted, fontStyle: 'italic' },
595+
pronouns: { fontSize: FONT_SIZE.micro, color: COLORS.textMuted, fontStyle: 'italic' },
569596
cardBottom: { gap: SPACING.xs },
570597
cardDivider: {
571598
height: 1, backgroundColor: 'rgba(255,255,255,0.06)', marginBottom: 2,
572599
},
573-
bioText: { fontSize: 10.5, color: 'rgba(255,255,255,0.38)', lineHeight: 15 },
600+
bioText: { fontSize: FONT_SIZE.micro + 0.5, color: 'rgba(255,255,255,0.38)', lineHeight: 15 },
574601
cardBadge: {
575602
alignSelf: 'flex-start',
576603
paddingHorizontal: 8, paddingVertical: 3, borderRadius: 4,
577604
borderWidth: 1,
578605
},
579-
badgeText: { fontSize: 8, fontWeight: '900', letterSpacing: 1.5 },
606+
badgeText: { fontSize: FONT_SIZE.nano, fontWeight: '900', letterSpacing: 1.5 },
580607

581608
// ─── Tiles ───
582609
tilesSection: { gap: SPACING.sm },

0 commit comments

Comments
 (0)