fix: read toasts, the undo bar and the offline banner out on ios

This commit is contained in:
Linus Rath
2026-10-31 03:49:09 +01:00
parent ff5a8c91d6
commit d812caf025
5 changed files with 172 additions and 2 deletions
+7 -1
View File
@@ -6,6 +6,7 @@ import { useOutboxStore } from '../stores/outbox-store';
import { useLocaleStore } from '../stores/locale-store';
import { spacing, typography, type ThemePalette } from '../theme/tokens';
import { useColors } from '../theme/colors';
import { announceConnectivity } from '../lib/announce';
interface OfflineBannerProps {
/** Optional extra notice (e.g. "Showing cached mail"). */
@@ -19,6 +20,11 @@ export function OfflineBanner({ hint }: OfflineBannerProps) {
const t = useLocaleStore((s) => s.t);
// Pending offline mutations that will replay once we're back online.
const queued = useOutboxStore((s) => s.entries.length);
const offlineLabel = t('offline.banner', 'You are offline');
// VoiceOver hears the drop too (Android reads the live region below).
React.useEffect(() => {
announceConnectivity(online, offlineLabel);
}, [online, offlineLabel]);
if (online) return null;
const queuedHint = queued > 0
? t(
@@ -28,7 +34,7 @@ export function OfflineBanner({ hint }: OfflineBannerProps) {
)
: null;
const suffix = [hint, queuedHint].filter(Boolean).join(' · ');
const label = t('offline.banner', 'You are offline');
const label = offlineLabel;
return (
<View style={styles.bar} accessibilityRole="alert" accessibilityLiveRegion="polite">
<CloudOff size={14} color={c.warningForeground} />
+14 -1
View File
@@ -11,6 +11,7 @@ import { useEmailStore } from '../stores/email-store';
import { useSendUndoStore, restoreUndoneSend, type PendingUndoSend } from '../stores/send-undo-store';
import { useLocaleStore } from '../stores/locale-store';
import type { RootStackParamList } from '../navigation/types';
import { announce } from '../lib/announce';
const VISIBLE_MS = 5000;
@@ -70,6 +71,14 @@ export function UndoSnackbar() {
if (current) setShown(current);
}, [current]);
// Read the bar out on iOS, which has no live regions; Android reads the
// bar's live region. Once per entry, not on every countdown re-render.
const undoLabel = t('common.undo', 'Undo');
React.useEffect(() => {
if (!current) return;
announce(current.label);
}, [current?.createdAt, current?.label]);
// Drive the auto-dismiss off the entry's createdAt so re-renders during
// animation don't reset the timer, and so consecutive actions reset it.
React.useEffect(() => {
@@ -133,13 +142,15 @@ export function UndoSnackbar() {
{ left: frame.left, right: frame.right, bottom: insets.bottom + frame.bottom, opacity, transform: [{ translateY: slideY }] },
]}
>
<View style={styles.bar}>
<View style={styles.bar} accessibilityLiveRegion="polite">
<Text style={styles.label} numberOfLines={1}>{shown.label}</Text>
{shown.send && (
<Pressable
onPress={onSendNow}
hitSlop={8}
disabled={sendBusy}
accessibilityRole="button"
accessibilityLabel={t('email_composer.send_now', 'Send now')}
style={({ pressed }) => [styles.undoBtn, pressed && styles.undoBtnPressed]}
>
<Text style={styles.sendNowText}>{t('email_composer.send_now', 'Send now').toUpperCase()}</Text>
@@ -149,6 +160,8 @@ export function UndoSnackbar() {
onPress={() => { void onUndo(); }}
hitSlop={8}
disabled={sendBusy}
accessibilityRole="button"
accessibilityLabel={undoLabel}
style={({ pressed }) => [styles.undoBtn, pressed && styles.undoBtnPressed]}
>
<Text style={styles.undoText}>{t('common.undo', 'Undo').toUpperCase()}</Text>