From 82ea7e31234febc8c60a1daec59094546b66b009 Mon Sep 17 00:00:00 2001 From: truph01 Date: Tue, 17 Mar 2026 11:45:17 +0700 Subject: [PATCH 1/6] fix: The fields of code are not announced --- src/components/MagicCodeInput.tsx | 31 ++++++++++++++++++++++++++++++- src/languages/de.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + 11 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index b43b0e6ecb2fd..97fc305928bf2 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -1,7 +1,7 @@ import type {ForwardedRef, KeyboardEvent} from 'react'; import React, {useEffect, useImperativeHandle, useRef, useState} from 'react'; import type {FocusEvent, TextInput as RNTextInput, TextInputKeyPressEvent} from 'react-native'; -import {StyleSheet, View} from 'react-native'; +import {AccessibilityInfo, Platform, StyleSheet, View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import Animated, {useAnimatedStyle, useSharedValue, withDelay, withRepeat, withSequence, withTiming} from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; @@ -213,6 +213,25 @@ function MagicCodeInput({ editIndex.current = index; }; + const [announcement, setAnnouncement] = useState(''); + useEffect(() => { + if (focusedIndex === undefined) { + setAnnouncement(''); + return; + } + const message = translate('common.enterDigitLabel', {digitIndex: focusedIndex + 1, totalDigits: maxLength}); + if (Platform.OS === 'web') { + // Toggle invisible zero-width space to force aria-live to re-announce identical content + setAnnouncement((prev) => (prev === message ? `${message}\u200B` : message)); + } else { + // accessibilityLiveRegion covers Android; iOS needs an explicit announcement + if (Platform.OS === 'ios') { + AccessibilityInfo.announceForAccessibility(message); + } + setAnnouncement(message); + } + }, [focusedIndex, maxLength, translate]); + useImperativeHandle(ref, () => ({ focus() { focusMagicCodeInput(); @@ -512,6 +531,8 @@ function MagicCodeInput({ return ( @@ -533,6 +554,7 @@ function MagicCodeInput({ accessibilityElementsHidden importantForAccessibility="no-hide-descendants" accessible={false} + aria-hidden > {!!char && {char}} @@ -544,6 +566,13 @@ function MagicCodeInput({ ); })} + + {announcement} + {!!errorText && ( = { concierge: {sidePanelGreeting: 'Hallo, wie kann ich helfen?', showHistory: 'Verlauf anzeigen'}, duplicateReport: 'Duplizierten Bericht', approver: 'Genehmiger', + enterDigitLabel: ({digitIndex, totalDigits}: {digitIndex: number; totalDigits: number}) => `Ziffer ${digitIndex} von ${totalDigits} eingeben`, }, socials: { podcast: 'Folgen Sie uns auf Podcast', diff --git a/src/languages/en.ts b/src/languages/en.ts index 9e1b3c985c71b..1bb56f4b7ec28 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -543,6 +543,7 @@ const translations = { vacationDelegate: 'Vacation delegate', expensifyLogo: 'Expensify logo', approver: 'Approver', + enterDigitLabel: ({digitIndex, totalDigits}: {digitIndex: number; totalDigits: number}) => `enter digit ${digitIndex} of ${totalDigits}`, }, socials: { podcast: 'Follow us on Podcast', diff --git a/src/languages/es.ts b/src/languages/es.ts index 28c768351a01a..ace057ecbae6b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -430,6 +430,7 @@ const translations: TranslationDeepObject = { vacationDelegate: 'Delegado de vacaciones', expensifyLogo: 'Logo de Expensify', approver: 'Aprobador', + enterDigitLabel: ({digitIndex, totalDigits}: {digitIndex: number; totalDigits: number}) => `introducir dígito ${digitIndex} de ${totalDigits}`, }, socials: { podcast: 'Síguenos en Podcast', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index e3141cb9aa261..46359e074351f 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -522,6 +522,7 @@ const translations: TranslationDeepObject = { concierge: {sidePanelGreeting: 'Bonjour, comment puis-je vous aider ?', showHistory: 'Afficher l’historique'}, duplicateReport: 'Note de frais en double', approver: 'Approbateur', + enterDigitLabel: ({digitIndex, totalDigits}: {digitIndex: number; totalDigits: number}) => `saisir le chiffre ${digitIndex} sur ${totalDigits}`, }, socials: { podcast: 'Suivez-nous sur Podcast', diff --git a/src/languages/it.ts b/src/languages/it.ts index 2814a3f55957f..93bcd51ca342e 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -522,6 +522,7 @@ const translations: TranslationDeepObject = { concierge: {sidePanelGreeting: 'Ciao, come posso aiutarti?', showHistory: 'Mostra cronologia'}, duplicateReport: 'Report duplicato', approver: 'Approvante', + enterDigitLabel: ({digitIndex, totalDigits}: {digitIndex: number; totalDigits: number}) => `inserire la cifra ${digitIndex} di ${totalDigits}`, }, socials: { podcast: 'Seguici su Podcast', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 1fb0fb5313e16..bb215c90c2bc1 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -521,6 +521,7 @@ const translations: TranslationDeepObject = { concierge: {sidePanelGreeting: 'こんにちは、どのようにお手伝いできますか?', showHistory: '履歴を表示'}, duplicateReport: 'レポートを複製', approver: '承認者', + enterDigitLabel: ({digitIndex, totalDigits}: {digitIndex: number; totalDigits: number}) => `${totalDigits}桁中${digitIndex}桁目を入力`, }, socials: { podcast: 'ポッドキャストでフォロー', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 5bee400ffe6f5..7cb39134acce3 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -521,6 +521,7 @@ const translations: TranslationDeepObject = { concierge: {sidePanelGreeting: 'Hoi, waarmee kan ik je helpen?', showHistory: 'Geschiedenis weergeven'}, duplicateReport: 'Dubbel rapport', approver: 'Fiatteur', + enterDigitLabel: ({digitIndex, totalDigits}: {digitIndex: number; totalDigits: number}) => `voer cijfer ${digitIndex} van ${totalDigits} in`, }, socials: { podcast: 'Volg ons op Podcast', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 3bea0ab6b5ffd..9ab3fb5a05d18 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -521,6 +521,7 @@ const translations: TranslationDeepObject = { concierge: {sidePanelGreeting: 'Cześć, w czym mogę pomóc?', showHistory: 'Pokaż historię'}, duplicateReport: 'Zduplikowany raport', approver: 'Osoba zatwierdzająca', + enterDigitLabel: ({digitIndex, totalDigits}: {digitIndex: number; totalDigits: number}) => `wprowadź cyfrę ${digitIndex} z ${totalDigits}`, }, socials: { podcast: 'Śledź nas na Podcast', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index e2c3fb53a3f5f..dafcfa48fe36c 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -520,6 +520,7 @@ const translations: TranslationDeepObject = { concierge: {sidePanelGreeting: 'Oi, como posso ajudar?', showHistory: 'Mostrar histórico'}, duplicateReport: 'Duplicar relatório', approver: 'Aprovador', + enterDigitLabel: ({digitIndex, totalDigits}: {digitIndex: number; totalDigits: number}) => `inserir dígito ${digitIndex} de ${totalDigits}`, }, socials: { podcast: 'Siga-nos no Podcast', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 96415f4fa273d..1b38c06d54b92 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -517,6 +517,7 @@ const translations: TranslationDeepObject = { concierge: {sidePanelGreeting: '你好,我能帮你做什么?', showHistory: '显示历史'}, duplicateReport: '重复报销单', approver: '审批人', + enterDigitLabel: ({digitIndex, totalDigits}: {digitIndex: number; totalDigits: number}) => `输入第 ${digitIndex} 位数字,共 ${totalDigits} 位`, }, socials: { podcast: '在播客上关注我们', From 9b447c35f4b83eddc90ce407992218feb9cd36b2 Mon Sep 17 00:00:00 2001 From: truph01 Date: Tue, 17 Mar 2026 15:48:16 +0700 Subject: [PATCH 2/6] fix: apply melvin suggestions --- src/components/MagicCodeInput.tsx | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index 97fc305928bf2..ed1a0a0f6cb95 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -1,10 +1,11 @@ import type {ForwardedRef, KeyboardEvent} from 'react'; import React, {useEffect, useImperativeHandle, useRef, useState} from 'react'; import type {FocusEvent, TextInput as RNTextInput, TextInputKeyPressEvent} from 'react-native'; -import {AccessibilityInfo, Platform, StyleSheet, View} from 'react-native'; +import {StyleSheet, View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import Animated, {useAnimatedStyle, useSharedValue, withDelay, withRepeat, withSequence, withTiming} from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; +import useAccessibilityAnnouncement from '@hooks/useAccessibilityAnnouncement'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -170,6 +171,7 @@ function MagicCodeInput({ const [focusedIndex, setFocusedIndex] = useState(autoFocus ? 0 : undefined); const editIndex = useRef(0); const [wasSubmitted, setWasSubmitted] = useState(false); + const [announcement, setAnnouncement] = useState(''); const shouldFocusLast = useRef(false); const inputWidth = useRef(0); const lastFocusedIndex = useRef(0); @@ -213,25 +215,16 @@ function MagicCodeInput({ editIndex.current = index; }; - const [announcement, setAnnouncement] = useState(''); useEffect(() => { if (focusedIndex === undefined) { setAnnouncement(''); return; } - const message = translate('common.enterDigitLabel', {digitIndex: focusedIndex + 1, totalDigits: maxLength}); - if (Platform.OS === 'web') { - // Toggle invisible zero-width space to force aria-live to re-announce identical content - setAnnouncement((prev) => (prev === message ? `${message}\u200B` : message)); - } else { - // accessibilityLiveRegion covers Android; iOS needs an explicit announcement - if (Platform.OS === 'ios') { - AccessibilityInfo.announceForAccessibility(message); - } - setAnnouncement(message); - } + setAnnouncement(translate('common.enterDigitLabel', {digitIndex: focusedIndex + 1, totalDigits: maxLength})); }, [focusedIndex, maxLength, translate]); + useAccessibilityAnnouncement(announcement, announcement.length > 0, {shouldAnnounceOnNative: true}); + useImperativeHandle(ref, () => ({ focus() { focusMagicCodeInput(); From 5cdb1050def331b7b6037e74132ad0092d485b0b Mon Sep 17 00:00:00 2001 From: truph01 Date: Thu, 19 Mar 2026 16:55:51 +0700 Subject: [PATCH 3/6] fix: apply reviewer suggestion --- src/components/MagicCodeInput.tsx | 19 ++----------------- .../useAccessibilityAnnouncement/index.ts | 7 ++++++- .../useAccessibilityAnnouncement/types.ts | 1 + 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index ed1a0a0f6cb95..02898dc71c515 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -171,7 +171,6 @@ function MagicCodeInput({ const [focusedIndex, setFocusedIndex] = useState(autoFocus ? 0 : undefined); const editIndex = useRef(0); const [wasSubmitted, setWasSubmitted] = useState(false); - const [announcement, setAnnouncement] = useState(''); const shouldFocusLast = useRef(false); const inputWidth = useRef(0); const lastFocusedIndex = useRef(0); @@ -215,15 +214,8 @@ function MagicCodeInput({ editIndex.current = index; }; - useEffect(() => { - if (focusedIndex === undefined) { - setAnnouncement(''); - return; - } - setAnnouncement(translate('common.enterDigitLabel', {digitIndex: focusedIndex + 1, totalDigits: maxLength})); - }, [focusedIndex, maxLength, translate]); - - useAccessibilityAnnouncement(announcement, announcement.length > 0, {shouldAnnounceOnNative: true}); + const announcement = focusedIndex !== undefined ? translate('common.enterDigitLabel', {digitIndex: focusedIndex + 1, totalDigits: maxLength}) : undefined; + useAccessibilityAnnouncement(announcement, !!announcement, {shouldAnnounceOnNative: true, shouldAnnounceOnWeb: true, shouldAnnouncePolite: true}); useImperativeHandle(ref, () => ({ focus() { @@ -559,13 +551,6 @@ function MagicCodeInput({ ); })} - - {announcement} - {!!errorText && ( { @@ -63,6 +64,10 @@ function useAccessibilityAnnouncement(message: string | ReactNode, shouldAnnounc const timer = setTimeout(() => { const node = document.createElement('div'); node.setAttribute('role', 'alert'); + if (shouldAnnouncePolite) { + node.setAttribute('aria-live', 'polite'); + } + node.textContent = message; container.appendChild(node); }, ANNOUNCEMENT_DELAY_MS); @@ -71,7 +76,7 @@ function useAccessibilityAnnouncement(message: string | ReactNode, shouldAnnounc clearTimeout(timer); prevShouldAnnounceRef.current = false; }; - }, [message, shouldAnnounceMessage, shouldAnnounceOnWeb]); + }, [message, shouldAnnounceMessage, shouldAnnounceOnWeb, shouldAnnouncePolite]); } export default useAccessibilityAnnouncement; diff --git a/src/hooks/useAccessibilityAnnouncement/types.ts b/src/hooks/useAccessibilityAnnouncement/types.ts index fa7a8e4c1c537..740ac0cf9410e 100644 --- a/src/hooks/useAccessibilityAnnouncement/types.ts +++ b/src/hooks/useAccessibilityAnnouncement/types.ts @@ -1,6 +1,7 @@ type UseAccessibilityAnnouncementOptions = { shouldAnnounceOnNative?: boolean; shouldAnnounceOnWeb?: boolean; + shouldAnnouncePolite?: boolean; }; export default UseAccessibilityAnnouncementOptions; From df79038d3a03475b4eb5cf4a36e893d404318a05 Mon Sep 17 00:00:00 2001 From: truph01 Date: Fri, 20 Mar 2026 01:48:27 +0700 Subject: [PATCH 4/6] fix: remove shouldAnnouncePolite prop --- src/components/MagicCodeInput.tsx | 2 +- src/hooks/useAccessibilityAnnouncement/index.ts | 7 +------ src/hooks/useAccessibilityAnnouncement/types.ts | 1 - 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index 02898dc71c515..205989ad4a2cf 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -215,7 +215,7 @@ function MagicCodeInput({ }; const announcement = focusedIndex !== undefined ? translate('common.enterDigitLabel', {digitIndex: focusedIndex + 1, totalDigits: maxLength}) : undefined; - useAccessibilityAnnouncement(announcement, !!announcement, {shouldAnnounceOnNative: true, shouldAnnounceOnWeb: true, shouldAnnouncePolite: true}); + useAccessibilityAnnouncement(announcement, !!announcement, {shouldAnnounceOnNative: true, shouldAnnounceOnWeb: true}); useImperativeHandle(ref, () => ({ focus() { diff --git a/src/hooks/useAccessibilityAnnouncement/index.ts b/src/hooks/useAccessibilityAnnouncement/index.ts index 14b8250af51d2..bc104b6f9d467 100644 --- a/src/hooks/useAccessibilityAnnouncement/index.ts +++ b/src/hooks/useAccessibilityAnnouncement/index.ts @@ -40,7 +40,6 @@ function getWrapper(): HTMLDivElement { function useAccessibilityAnnouncement(message: string | ReactNode, shouldAnnounceMessage: boolean, options?: UseAccessibilityAnnouncementOptions) { const shouldAnnounceOnWeb = options?.shouldAnnounceOnWeb ?? false; - const shouldAnnouncePolite = options?.shouldAnnouncePolite ?? false; const prevShouldAnnounceRef = useRef(false); useEffect(() => { @@ -64,10 +63,6 @@ function useAccessibilityAnnouncement(message: string | ReactNode, shouldAnnounc const timer = setTimeout(() => { const node = document.createElement('div'); node.setAttribute('role', 'alert'); - if (shouldAnnouncePolite) { - node.setAttribute('aria-live', 'polite'); - } - node.textContent = message; container.appendChild(node); }, ANNOUNCEMENT_DELAY_MS); @@ -76,7 +71,7 @@ function useAccessibilityAnnouncement(message: string | ReactNode, shouldAnnounc clearTimeout(timer); prevShouldAnnounceRef.current = false; }; - }, [message, shouldAnnounceMessage, shouldAnnounceOnWeb, shouldAnnouncePolite]); + }, [message, shouldAnnounceMessage, shouldAnnounceOnWeb]); } export default useAccessibilityAnnouncement; diff --git a/src/hooks/useAccessibilityAnnouncement/types.ts b/src/hooks/useAccessibilityAnnouncement/types.ts index 740ac0cf9410e..fa7a8e4c1c537 100644 --- a/src/hooks/useAccessibilityAnnouncement/types.ts +++ b/src/hooks/useAccessibilityAnnouncement/types.ts @@ -1,7 +1,6 @@ type UseAccessibilityAnnouncementOptions = { shouldAnnounceOnNative?: boolean; shouldAnnounceOnWeb?: boolean; - shouldAnnouncePolite?: boolean; }; export default UseAccessibilityAnnouncementOptions; From f91eed73e16c8889dfc1b1ac755ea64dc8de7443 Mon Sep 17 00:00:00 2001 From: truph01 Date: Fri, 20 Mar 2026 01:51:55 +0700 Subject: [PATCH 5/6] fix: lint --- src/components/MagicCodeInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index 205989ad4a2cf..0452c6b9db121 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -471,7 +471,7 @@ function MagicCodeInput({ {/* Android does not handle touch on invisible Views so I created a wrapper around invisible TextInput just to handle taps */} Date: Tue, 24 Mar 2026 02:46:28 +0700 Subject: [PATCH 6/6] fix: conflicts --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index c58bf3cb2b5af..d6051eb539e52 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit c58bf3cb2b5af52846f68f0901a4a3db2fb9909b +Subproject commit d6051eb539e52a75a250bec7f9439bfb846cee13