fix(errors,a11y): review round 1 — drop redundant onErrors, stabilize the live region
Review found my C-3.23 additions double-notified: the global error modal (api.ts req) already surfaces 400/409/422/500 with the backend's specific message, so a caller toast on those codes is a second, weaker surface. Only 403/404 are caller-handled and silent — and only a YouTube WRITE action has a meaningful 403 (missing scope). So the correct set is just the ChannelPage unsubscribe handler (a YouTube write, 403 → "connect", matching subscribe). Reverted the redundant onErrors on block (a local DB op, not YouTube), SettingsPanel Plex watch toggle/reimport, and the DownloadCenter job action — the global modal already speaks for them. Removed the three now-unused keys. Toaster: moved the polite live region onto the stable, always-mounted container (a polite region must pre-exist to announce later insertions); error/fatal toasts keep role="alert", which announces on insertion regardless.
This commit is contained in:
@@ -120,7 +120,9 @@ export default function ChannelPage({
|
||||
qc.invalidateQueries({ queryKey: ["feed"] });
|
||||
qc.invalidateQueries({ queryKey: ["blocked-channels"] });
|
||||
},
|
||||
onError: (err) => notifyYouTubeActionError(err, t("channels.notify.blockFailed")),
|
||||
// No onError: block/unblock is a local DB op (not a YouTube write, so no 403 "connect" case),
|
||||
// and its 400/409/422/500 are already surfaced by the global error modal (see api.ts req) —
|
||||
// a caller toast here would just double it.
|
||||
});
|
||||
|
||||
const subscribe = useMutation({
|
||||
|
||||
@@ -681,7 +681,8 @@ export default function DownloadCenter() {
|
||||
? api.cancelDownload(id)
|
||||
: api.deleteDownload(id),
|
||||
onSuccess: invalidate,
|
||||
onError: () => notify({ level: "error", message: t("downloads.actionFailed") }),
|
||||
// No onError: a failed job action returns 400/409/422/500, already surfaced by the global
|
||||
// error modal (api.ts req) with the backend's specific message — a caller toast would double it.
|
||||
});
|
||||
|
||||
// Remove a shared-with-me item from your list (only your grant; the owner's file is untouched).
|
||||
|
||||
@@ -533,14 +533,15 @@ function PlexWatchSync() {
|
||||
}
|
||||
};
|
||||
|
||||
const onFail = () => notify({ level: "error", message: t("settings.plexSync.failed") });
|
||||
// No onError on these: they're Plex, not YouTube (notifyYouTubeActionError would wrongly say
|
||||
// "connect YouTube"), and their 409/500 are already surfaced by the global error modal — a caller
|
||||
// toast would double it. Actual import failures come back as 200 {error}, handled by announce.
|
||||
const toggle = useMutation({
|
||||
mutationFn: (next: boolean) => api.plexWatchSetLink(next),
|
||||
onSuccess: (res) => {
|
||||
qc.setQueryData(["plex-watch-link"], res);
|
||||
announce(res.import);
|
||||
},
|
||||
onError: onFail,
|
||||
});
|
||||
const reimport = useMutation({
|
||||
mutationFn: () => api.plexWatchImport(),
|
||||
@@ -548,7 +549,6 @@ function PlexWatchSync() {
|
||||
qc.setQueryData(["plex-watch-link"], res);
|
||||
announce(res.import);
|
||||
},
|
||||
onError: onFail,
|
||||
});
|
||||
|
||||
const busy = toggle.isPending || reimport.isPending;
|
||||
|
||||
@@ -16,17 +16,23 @@ export default function Toaster() {
|
||||
const toasts = useSyncExternalStore(subscribe, getActiveToasts, getActiveToasts);
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-4 left-4 z-overlay flex flex-col gap-2 w-80 max-w-[calc(100vw-2rem)]">
|
||||
// Stable, always-mounted polite live region — a polite region must PRE-EXIST in the DOM for a
|
||||
// screen reader to announce content later inserted into it (a region created together with its
|
||||
// first message is unreliably announced). Non-error toasts render inside here and are announced
|
||||
// politely; error/fatal toasts additionally carry role="alert", which announces assertively on
|
||||
// insertion regardless.
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className="absolute bottom-4 left-4 z-overlay flex flex-col gap-2 w-80 max-w-[calc(100vw-2rem)]"
|
||||
>
|
||||
{toasts.map((toast) => {
|
||||
const { icon: Icon, color, bar } = LEVEL_STYLE[toast.level];
|
||||
const urgent = toast.level === "error" || toast.level === "fatal";
|
||||
return (
|
||||
<div
|
||||
key={toast.id}
|
||||
// Announce toasts to screen readers — the toast is the app's main feedback channel.
|
||||
// Errors interrupt (assertive/alert); everything else waits its turn (polite/status).
|
||||
role={urgent ? "alert" : "status"}
|
||||
aria-live={urgent ? "assertive" : "polite"}
|
||||
{...(urgent ? { role: "alert" } : {})}
|
||||
className="toast-card glass relative overflow-hidden rounded-xl px-3 py-3 flex items-start gap-3 animate-[popIn_0.16s_ease]"
|
||||
>
|
||||
<Icon className={`w-5 h-5 shrink-0 mt-0.5 ${color}`} />
|
||||
|
||||
@@ -119,7 +119,6 @@
|
||||
"syncFailed": "Subscription sync failed",
|
||||
"unsubscribed": "Unsubscribed from {{name}} on YouTube",
|
||||
"unsubscribeFailed": "Unsubscribe failed",
|
||||
"blockFailed": "Couldn't update block state",
|
||||
"fullHistoryRequested": "Full history requested for {{count}} channels",
|
||||
"fullHistoryFailed": "Couldn't request full history",
|
||||
"needYouTube": "Connect your YouTube account to do this.",
|
||||
|
||||
@@ -197,6 +197,5 @@
|
||||
"source_not_ready": "The source download isn't ready to edit.",
|
||||
"generic": "That edit couldn't be created."
|
||||
}
|
||||
},
|
||||
"actionFailed": "Download action failed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
"imported": "Imported from Plex: {{watched}} watched, {{in_progress}} in progress.",
|
||||
"lastSync": "Last import: {{when}}",
|
||||
"never": "Not imported yet",
|
||||
"importing": "Importing your Plex watch history…",
|
||||
"failed": "Plex watch sync failed"
|
||||
"importing": "Importing your Plex watch history…"
|
||||
},
|
||||
"save": {
|
||||
"saving": "Saving…",
|
||||
|
||||
@@ -119,7 +119,6 @@
|
||||
"syncFailed": "A feliratkozások szinkronizálása sikertelen",
|
||||
"unsubscribed": "Leiratkozva a(z) {{name}} csatornáról a YouTube-on",
|
||||
"unsubscribeFailed": "A leiratkozás sikertelen",
|
||||
"blockFailed": "A tiltás állapotát nem sikerült frissíteni",
|
||||
"fullHistoryRequested": "Teljes előzmény kérve {{count}} csatornához",
|
||||
"fullHistoryFailed": "Nem sikerült teljes előzményt kérni",
|
||||
"needYouTube": "Ehhez csatlakoztasd a YouTube-fiókod.",
|
||||
|
||||
@@ -197,6 +197,5 @@
|
||||
"source_not_ready": "A forrásletöltés még nem szerkeszthető.",
|
||||
"generic": "Ezt a vágást nem sikerült létrehozni."
|
||||
}
|
||||
},
|
||||
"actionFailed": "A letöltési művelet sikertelen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
"imported": "Importálva a Plexből: {{watched}} megnézve, {{in_progress}} folyamatban.",
|
||||
"lastSync": "Utolsó import: {{when}}",
|
||||
"never": "Még nincs importálva",
|
||||
"importing": "Plex-előzmény importálása…",
|
||||
"failed": "A Plex-megtekintés szinkron sikertelen"
|
||||
"importing": "Plex-előzmény importálása…"
|
||||
},
|
||||
"save": {
|
||||
"saving": "Mentés…",
|
||||
|
||||
Reference in New Issue
Block a user