+23
-19
src/App.tsx
+23
-19
src/App.tsx
···
9
9
import Firefly from "./components/Firefly";
10
10
import NotificationContainer from "./components/common/NotificationContainer";
11
11
import ErrorBoundary from "./components/common/ErrorBoundary";
12
+
import AriaLiveAnnouncer from "./components/common/AriaLiveAnnouncer";
12
13
import { SearchResultSkeleton } from "./components/common/LoadingSkeleton";
13
14
import { DEFAULT_SETTINGS } from "./types/settings";
14
15
import type { UserSettings, SearchResult } from "./types";
···
48
49
logout,
49
50
} = useAuth();
50
51
51
-
// Notifications hook (replaces alerts)
52
-
const { notifications, removeNotification, success, error, info } =
53
-
useNotifications();
52
+
// Notifications hook (only for errors now)
53
+
const { notifications, removeNotification, error } = useNotifications();
54
+
55
+
// Aria-live announcements for non-error feedback (invisible, screen-reader only)
56
+
const [ariaAnnouncement, setAriaAnnouncement] = useState("");
54
57
55
58
// Theme hook
56
59
const { isDark, reducedMotion, toggleTheme, toggleMotion } = useTheme();
···
169
172
setSearchResults([]);
170
173
setCurrentPlatform("tiktok");
171
174
setCurrentStep("home");
172
-
info("No previous results found.");
175
+
// No visual feedback needed - empty state will show in UI
176
+
setAriaAnnouncement("No previous results found.");
173
177
return;
174
178
}
175
179
···
194
198
195
199
setSearchResults(loadedResults);
196
200
setCurrentStep("results");
197
-
success(`Loaded ${loadedResults.length} results from previous upload`);
201
+
// Announce to screen readers only - visual feedback is navigation to results page
202
+
setAriaAnnouncement(
203
+
`Loaded ${loadedResults.length} results from previous upload`,
204
+
);
198
205
} catch (err) {
199
206
console.error("Failed to load upload:", err);
200
207
error("Failed to load previous upload. Please try again.");
201
208
setCurrentStep("home");
202
209
}
203
210
},
204
-
[setStatusMessage, setCurrentStep, setSearchResults, info, error, success],
211
+
[setStatusMessage, setCurrentStep, setSearchResults, setAriaAnnouncement, error],
205
212
);
206
213
207
214
// Login handler
···
231
238
setSearchResults([]);
232
239
setCurrentPlatform("tiktok");
233
240
setSavedUploads(new Set());
234
-
success("Logged out successfully");
241
+
// No visual feedback needed - user sees login page
242
+
setAriaAnnouncement("Logged out successfully");
235
243
} catch (err) {
236
244
error("Failed to logout. Please try again.");
237
245
}
238
-
}, [logout, setSearchResults, success, error]);
246
+
}, [logout, setSearchResults, setAriaAnnouncement, error]);
239
247
240
248
return (
241
249
<ErrorBoundary>
242
250
<div className="min-h-screen relative overflow-hidden">
243
-
{/* Notification Container */}
251
+
{/* Notification Container - errors only */}
244
252
<NotificationContainer
245
253
notifications={notifications}
246
254
onRemove={removeNotification}
247
255
/>
256
+
257
+
{/* Invisible announcer for screen readers - non-error feedback */}
258
+
<AriaLiveAnnouncer message={ariaAnnouncement} politeness="polite" />
259
+
260
+
{/* Status message for screen readers - loading/progress updates */}
261
+
<AriaLiveAnnouncer message={statusMessage} politeness="polite" />
248
262
249
263
{/* Firefly particles - only render if motion not reduced */}
250
264
{!reducedMotion && (
···
258
272
))}
259
273
</div>
260
274
)}
261
-
262
-
{/* Status message for screen readers */}
263
-
<div
264
-
role="status"
265
-
aria-live="polite"
266
-
aria-atomic="true"
267
-
className="sr-only"
268
-
>
269
-
{statusMessage}
270
-
</div>
271
275
272
276
{/* Skip to main content link */}
273
277
<a
+45
src/components/common/AriaLiveAnnouncer.tsx
+45
src/components/common/AriaLiveAnnouncer.tsx
···
1
+
import React, { useEffect, useState } from "react";
2
+
3
+
interface AriaLiveAnnouncerProps {
4
+
message: string;
5
+
politeness?: "polite" | "assertive";
6
+
clearAfter?: number;
7
+
}
8
+
9
+
/**
10
+
* Invisible component that announces messages to screen readers
11
+
* without displaying visual toasts
12
+
*/
13
+
const AriaLiveAnnouncer: React.FC<AriaLiveAnnouncerProps> = ({
14
+
message,
15
+
politeness = "polite",
16
+
clearAfter = 5000,
17
+
}) => {
18
+
const [announcement, setAnnouncement] = useState("");
19
+
20
+
useEffect(() => {
21
+
if (message) {
22
+
// Set the message to trigger screen reader announcement
23
+
setAnnouncement(message);
24
+
25
+
// Clear after specified time to allow new announcements
26
+
if (clearAfter > 0) {
27
+
const timer = setTimeout(() => setAnnouncement(""), clearAfter);
28
+
return () => clearTimeout(timer);
29
+
}
30
+
}
31
+
}, [message, clearAfter]);
32
+
33
+
return (
34
+
<div
35
+
role="status"
36
+
aria-live={politeness}
37
+
aria-atomic="true"
38
+
className="sr-only"
39
+
>
40
+
{announcement}
41
+
</div>
42
+
);
43
+
};
44
+
45
+
export default AriaLiveAnnouncer;