Skip to content

Commit 60b696f

Browse files
authored
Merge pull request #220 from Mosas2000/fix/accessibility-skip-focus-aria-labeling
Accessibility improvements: skip link, focus trap, ARIA live regions, and labeling
2 parents 0d741e1 + 42decd8 commit 60b696f

File tree

9 files changed

+135
-47
lines changed

9 files changed

+135
-47
lines changed

frontend/src/App.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ function App() {
8484

8585
return (
8686
<div className="min-h-screen flex flex-col bg-gray-50 dark:bg-gray-950 transition-colors">
87+
<a
88+
href="#main-content"
89+
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-amber-500 focus:text-black focus:rounded-lg focus:text-sm focus:font-semibold focus:outline-none focus:ring-2 focus:ring-amber-300"
90+
>
91+
Skip to main content
92+
</a>
8793
<OfflineBanner />
8894
<Header
8995
userData={userData}
@@ -95,7 +101,7 @@ function App() {
95101
notificationsLoading={notificationsLoading}
96102
/>
97103

98-
<main className="flex-1">
104+
<main id="main-content" className="flex-1">
99105
{userData ? (
100106
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8 animate-fade-in-up">
101107
{/* Navigation */}

frontend/src/components/Header.jsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default function Header({ userData, onAuth, authLoading, notifications, u
2828
const networkLabel = NETWORK_NAME.charAt(0).toUpperCase() + NETWORK_NAME.slice(1);
2929

3030
return (
31-
<nav className="sticky top-0 z-50 bg-gray-900/95 backdrop-blur-md border-b border-white/5">
31+
<nav className="sticky top-0 z-50 bg-gray-900/95 backdrop-blur-md border-b border-white/5" aria-label="Site header">
3232
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
3333
<div className="flex justify-between items-center h-16">
3434
{/* Logo */}
@@ -42,9 +42,15 @@ export default function Header({ userData, onAuth, authLoading, notifications, u
4242
/>
4343
<div className="flex items-center gap-2">
4444
<h1 className="text-lg font-black text-white tracking-tight">TipStream</h1>
45-
<div className="hidden sm:flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-white/5 border border-white/5">
46-
<span className={`h-1.5 w-1.5 rounded-full ${apiReachable === null ? 'bg-yellow-400 animate-pulse' : apiReachable ? 'bg-green-400 pulse-live' : 'bg-red-400'}`} />
45+
<div className="hidden sm:flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-white/5 border border-white/5" role="status" aria-label={`API status: ${apiReachable === null ? 'checking' : apiReachable ? 'connected' : 'disconnected'}`}>
46+
<span
47+
className={`h-1.5 w-1.5 rounded-full ${apiReachable === null ? 'bg-yellow-400 animate-pulse' : apiReachable ? 'bg-green-400 pulse-live' : 'bg-red-400'}`}
48+
aria-hidden="true"
49+
/>
4750
<span className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">{networkLabel}</span>
51+
<span className="sr-only">
52+
{apiReachable === null ? 'Checking connection' : apiReachable ? 'API connected' : 'API disconnected'}
53+
</span>
4854
</div>
4955
</div>
5056
</div>

frontend/src/components/NotificationBell.jsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,22 @@ export default function NotificationBell({ notifications, unreadCount, onMarkRea
3434
>
3535
<Bell className="w-5 h-5" aria-hidden="true" />
3636
{unreadCount > 0 && (
37-
<span className="absolute -top-0.5 -right-0.5 h-5 w-5 flex items-center justify-center text-[10px] font-bold text-white bg-red-500 rounded-full ring-2 ring-gray-900">
37+
<span
38+
className="absolute -top-0.5 -right-0.5 h-5 w-5 flex items-center justify-center text-[10px] font-bold text-white bg-red-500 rounded-full ring-2 ring-gray-900"
39+
aria-live="polite"
40+
aria-atomic="true"
41+
>
3842
{unreadCount > 9 ? '9+' : unreadCount}
3943
</span>
4044
)}
4145
</button>
4246

4347
{open && (
44-
<div className="absolute right-0 top-full mt-2 w-80 bg-white dark:bg-gray-900 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 z-50 overflow-hidden">
48+
<div
49+
className="absolute right-0 top-full mt-2 w-80 bg-white dark:bg-gray-900 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 z-50 overflow-hidden"
50+
role="region"
51+
aria-label="Notifications"
52+
>
4553
<div className="px-4 py-3 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between">
4654
<h3 className="font-bold text-sm text-gray-800 dark:text-gray-200">Notifications</h3>
4755
{notifications.length > 0 && (

frontend/src/components/RecentTips.jsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export default function RecentTips({ addToast }) {
185185
<div className="flex items-center gap-2">
186186
<h2 className="text-xl font-bold text-gray-900 dark:text-white">Live Feed</h2>
187187
<span className="flex items-center gap-1 px-2 py-0.5 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 rounded-full text-[10px] font-bold uppercase tracking-wider">
188-
<span className="h-1.5 w-1.5 bg-green-500 rounded-full animate-pulse" />Live
188+
<span className="h-1.5 w-1.5 bg-green-500 rounded-full animate-pulse" aria-hidden="true" />Live
189189
</span>
190190
{messagesLoading && (
191191
<span className="text-[10px] text-gray-400 dark:text-gray-500 font-medium">Loading messages...</span>
@@ -202,7 +202,8 @@ export default function RecentTips({ addToast }) {
202202
<div className="flex gap-2">
203203
<div className="relative flex-1">
204204
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" aria-hidden="true" />
205-
<input type="text" value={searchQuery} onChange={(e) => { setSearchQuery(e.target.value); setOffset(0); }}
205+
<label htmlFor="feed-search" className="sr-only">Search tips</label>
206+
<input id="feed-search" type="text" value={searchQuery} onChange={(e) => { setSearchQuery(e.target.value); setOffset(0); }}
206207
className="w-full pl-10 pr-4 py-2 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl text-sm text-gray-900 dark:text-white focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none placeholder-gray-400 dark:placeholder-gray-500"
207208
placeholder="Search by address or message..." />
208209
</div>
@@ -214,16 +215,19 @@ export default function RecentTips({ addToast }) {
214215
</div>
215216
{showFilters && (
216217
<div className="flex flex-wrap gap-3 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-xl border border-gray-100 dark:border-gray-700">
217-
{[['Min STX', minAmount, setMinAmount, '0'], ['Max STX', maxAmount, setMaxAmount, 'any']].map(([label, val, setter, ph]) => (
218-
<div key={label} className="flex items-center gap-2">
219-
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">{label}</label>
220-
<input type="number" value={val} onChange={(e) => { setter(e.target.value); setOffset(0); }}
221-
className="w-24 px-3 py-1.5 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg text-sm text-gray-900 dark:text-white outline-none focus:ring-2 focus:ring-amber-500" placeholder={ph} step="0.001" min="0" />
222-
</div>
223-
))}
224218
<div className="flex items-center gap-2">
225-
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">Sort</label>
226-
<select value={sortBy} onChange={(e) => { setSortBy(e.target.value); setOffset(0); }}
219+
<label htmlFor="feed-filter-min" className="text-xs font-medium text-gray-500 dark:text-gray-400">Min STX</label>
220+
<input id="feed-filter-min" type="number" value={minAmount} onChange={(e) => { setMinAmount(e.target.value); setOffset(0); }}
221+
className="w-24 px-3 py-1.5 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg text-sm text-gray-900 dark:text-white outline-none focus:ring-2 focus:ring-amber-500" placeholder="0" step="0.001" min="0" />
222+
</div>
223+
<div className="flex items-center gap-2">
224+
<label htmlFor="feed-filter-max" className="text-xs font-medium text-gray-500 dark:text-gray-400">Max STX</label>
225+
<input id="feed-filter-max" type="number" value={maxAmount} onChange={(e) => { setMaxAmount(e.target.value); setOffset(0); }}
226+
className="w-24 px-3 py-1.5 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg text-sm text-gray-900 dark:text-white outline-none focus:ring-2 focus:ring-amber-500" placeholder="any" step="0.001" min="0" />
227+
</div>
228+
<div className="flex items-center gap-2">
229+
<label htmlFor="feed-sort" className="text-xs font-medium text-gray-500 dark:text-gray-400">Sort</label>
230+
<select id="feed-sort" value={sortBy} onChange={(e) => { setSortBy(e.target.value); setOffset(0); }}
227231
className="px-3 py-1.5 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg text-sm text-gray-900 dark:text-white outline-none focus:ring-2 focus:ring-amber-500">
228232
<option value="newest">Newest first</option><option value="oldest">Oldest first</option>
229233
<option value="amount-high">Highest amount</option><option value="amount-low">Lowest amount</option>

frontend/src/components/SendTip.jsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export default function SendTip({ addToast }) {
197197
</p>
198198
</div>
199199
<button onClick={refetchBalance} disabled={balanceLoading}
200-
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-50" title="Refresh">
200+
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-50" title="Refresh balance" aria-label="Refresh balance">
201201
<svg className={`w-4 h-4 ${balanceLoading ? 'animate-spin' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
202202
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
203203
</svg>
@@ -208,26 +208,26 @@ export default function SendTip({ addToast }) {
208208
<div className="space-y-4">
209209
{/* Recipient */}
210210
<div>
211-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">Recipient Address</label>
212-
<input type="text" value={recipient} onChange={(e) => handleRecipientChange(e.target.value)}
211+
<label htmlFor="tip-recipient" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">Recipient Address</label>
212+
<input id="tip-recipient" type="text" value={recipient} onChange={(e) => handleRecipientChange(e.target.value)}
213213
className={`w-full px-4 py-2.5 border rounded-xl text-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 outline-none transition-all bg-white dark:bg-gray-800 dark:text-white ${recipientError ? 'border-red-300 dark:border-red-600' : 'border-gray-200 dark:border-gray-700'}`}
214214
placeholder="SP2..." />
215215
{recipientError && <p className="mt-1 text-xs text-red-500">{recipientError}</p>}
216216
</div>
217217

218218
{/* Amount */}
219219
<div>
220-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">Amount (STX)</label>
221-
<input type="number" value={amount} onChange={(e) => handleAmountChange(e.target.value)}
220+
<label htmlFor="tip-amount" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">Amount (STX)</label>
221+
<input id="tip-amount" type="number" value={amount} onChange={(e) => handleAmountChange(e.target.value)}
222222
className={`w-full px-4 py-2.5 border rounded-xl text-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 outline-none transition-all bg-white dark:bg-gray-800 dark:text-white ${amountError ? 'border-red-300 dark:border-red-600' : 'border-gray-200 dark:border-gray-700'}`}
223223
placeholder="0.5" step="0.001" min={MIN_TIP_STX} max={MAX_TIP_STX} />
224224
{amountError && <p className="mt-1 text-xs text-red-500">{amountError}</p>}
225225
</div>
226226

227227
{/* Message */}
228228
<div>
229-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">Message (optional)</label>
230-
<textarea value={message} onChange={(e) => setMessage(e.target.value)}
229+
<label htmlFor="tip-message" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">Message (optional)</label>
230+
<textarea id="tip-message" value={message} onChange={(e) => setMessage(e.target.value)}
231231
className="w-full px-4 py-2.5 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 dark:text-white rounded-xl text-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 outline-none transition-all resize-none"
232232
placeholder="Great work!" maxLength={280} rows={2} />
233233
<p className={`text-xs mt-1 text-right ${message.length >= 280 ? 'text-red-500' : 'text-gray-400'}`}>
@@ -237,8 +237,8 @@ export default function SendTip({ addToast }) {
237237

238238
{/* Category */}
239239
<div>
240-
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">Category</label>
241-
<select value={category} onChange={(e) => setCategory(Number(e.target.value))}
240+
<label htmlFor="tip-category" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">Category</label>
241+
<select id="tip-category" value={category} onChange={(e) => setCategory(Number(e.target.value))}
242242
className="w-full px-4 py-2.5 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 dark:text-white rounded-xl text-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 outline-none transition-all">
243243
{TIP_CATEGORIES.map((cat) => (
244244
<option key={cat.id} value={cat.id}>{cat.label}</option>

frontend/src/components/TipHistory.jsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -196,25 +196,31 @@ export default function TipHistory({ userAddress }) {
196196
{/* Tip list */}
197197
<div className="bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 p-5">
198198
<div className="flex flex-wrap items-center gap-2 mb-5">
199-
{['all', 'sent', 'received'].map((t) => (
200-
<button key={t} onClick={() => setTab(t)}
201-
className={`px-3 py-1.5 rounded-lg text-xs font-semibold capitalize transition-all ${tab === t ? 'bg-gray-900 dark:bg-amber-500 text-white dark:text-black' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'}`}>
202-
{t}
203-
</button>
204-
))}
205-
<select value={categoryFilter} onChange={(e) => setCategoryFilter(e.target.value)}
199+
<div role="tablist" aria-label="Filter by direction" className="flex items-center gap-2">
200+
{['all', 'sent', 'received'].map((t) => (
201+
<button key={t} onClick={() => setTab(t)}
202+
role="tab"
203+
aria-selected={tab === t}
204+
aria-controls="tip-history-panel"
205+
className={`px-3 py-1.5 rounded-lg text-xs font-semibold capitalize transition-all ${tab === t ? 'bg-gray-900 dark:bg-amber-500 text-white dark:text-black' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'}`}>
206+
{t}
207+
</button>
208+
))}
209+
</div>
210+
<label htmlFor="category-filter" className="sr-only">Filter by category</label>
211+
<select id="category-filter" value={categoryFilter} onChange={(e) => setCategoryFilter(e.target.value)}
206212
className="px-3 py-1.5 rounded-lg text-xs font-semibold bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-none outline-none">
207213
<option value="all">All Categories</option>
208214
{Object.entries(CATEGORY_LABELS).map(([id, label]) => (<option key={id} value={id}>{label}</option>))}
209215
</select>
210216
</div>
211217

212218
{filteredTips.length === 0 ? (
213-
<div className="text-center py-12 bg-gray-50 dark:bg-gray-800/50 rounded-xl border-2 border-dashed border-gray-200 dark:border-gray-700">
219+
<div id="tip-history-panel" role="tabpanel" className="text-center py-12 bg-gray-50 dark:bg-gray-800/50 rounded-xl border-2 border-dashed border-gray-200 dark:border-gray-700">
214220
<p className="text-gray-400">No tips found</p>
215221
</div>
216222
) : (
217-
<div className="space-y-2">
223+
<div id="tip-history-panel" role="tabpanel" className="space-y-2">
218224
{filteredTips.map((tip, i) => (
219225
<div key={tip.tipId || i} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800/50 hover:bg-white dark:hover:bg-gray-800 rounded-xl border border-transparent hover:border-gray-200 dark:hover:border-gray-700 transition-all">
220226
<div className="flex items-center gap-3">

0 commit comments

Comments
 (0)