Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {FlatList} from 'react-native-gesture-handler';
import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import ColorSchemeWrapper from '@components/ColorSchemeWrapper';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import {hasHoverSupport} from '@libs/DeviceCapabilities';
Expand All @@ -23,6 +25,7 @@ function BaseAutoCompleteSuggestions<TSuggestion>({
measuredHeightOfSuggestionRows,
}: ExternalProps<TSuggestion>) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const StyleUtils = useStyleUtils();
const rowHeight = useSharedValue(0);
const prevRowHeightRef = useRef<number>(measuredHeightOfSuggestionRows);
Expand All @@ -41,6 +44,7 @@ function BaseAutoCompleteSuggestions<TSuggestion>({
onLongPress={() => {}}
accessibilityLabel={accessibilityLabelExtractor(item, index)}
role={CONST.ROLE.MENUITEM}
sentryLabel="AutoCompleteSuggestion-Item"
>
{renderSuggestionMenuItem(item, index)}
</PressableWithFeedback>
Expand Down Expand Up @@ -92,13 +96,15 @@ function BaseAutoCompleteSuggestions<TSuggestion>({
return (
<Animated.View
style={[styles.autoCompleteSuggestionsContainer, animatedStyles]}
accessibilityLiveRegion="polite"
onPointerDown={(e) => {
if (hasHoverSupport()) {
return;
}
e.preventDefault();
}}
>
<Text style={styles.visuallyHidden}>{translate('common.suggestionsAvailable', suggestions.length)}</Text>
<ColorSchemeWrapper>
<FlatList
ref={scrollRef}
Expand Down
28 changes: 28 additions & 0 deletions src/components/SelectionList/BaseSelectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, use
import type {TextInputKeyPressEvent} from 'react-native';
import {Keyboard, View} from 'react-native';
import OptionsListSkeletonView from '@components/OptionsListSkeletonView';
import Text from '@components/Text';
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
import useActiveElementRole from '@hooks/useActiveElementRole';
import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
import useDebounce from '@hooks/useDebounce';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useKeyboardState from '@hooks/useKeyboardState';
import useLocalize from '@hooks/useLocalize';
import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useScrollEnabled from '@hooks/useScrollEnabled';
import useSingleExecution from '@hooks/useSingleExecution';
Expand Down Expand Up @@ -95,6 +97,7 @@ function BaseSelectionList<TItem extends ListItem>({
setShouldDisableHoverStyle = () => {},
}: SelectionListProps<TItem>) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const isFocused = useIsFocused();
const scrollEnabled = useScrollEnabled();
const {singleExecution} = useSingleExecution();
Expand All @@ -111,6 +114,23 @@ function BaseSelectionList<TItem extends ListItem>({

const initialFocusedIndex = useMemo(() => data.findIndex((i) => i.keyForList === initiallyFocusedItemKey), [data, initiallyFocusedItemKey]);
const [itemsToHighlight, setItemsToHighlight] = useState<Set<string> | null>(null);
const [announcedResultCount, setAnnouncedResultCount] = useState<string>('');
const announceTimeoutRef = useRef<NodeJS.Timeout | null>(null);

// Debounced announcement of result count for screen readers
useEffect(() => {
if (!shouldShowTextInput) {
return;
}

announceTimeoutRef.current = setTimeout(() => {
setAnnouncedResultCount(translate('common.resultsAvailable', data.length));
}, CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME);

return () => {
clearTimeout(announceTimeoutRef.current ?? undefined);
};
}, [data.length, shouldShowTextInput, translate]);

const isItemSelected = useCallback(
(item: TItem) => item.isSelected ?? ((isSelected?.(item) ?? selectedItems.includes(item.keyForList)) && canSelectMultiple),
Expand Down Expand Up @@ -545,6 +565,14 @@ function BaseSelectionList<TItem extends ListItem>({
return (
<View style={[styles.flex1, addBottomSafeAreaPadding && !hasFooter && paddingBottomStyle, style?.containerStyle]}>
{textInputComponent({shouldBeInsideList: false})}
{shouldShowTextInput && (
<Text
style={styles.visuallyHidden}
accessibilityLiveRegion="polite"
>
{announcedResultCount}
</Text>
)}
{data.length === 0 && (shouldShowLoadingPlaceholder || shouldShowListEmptyContent) ? (
renderListEmptyContent()
) : (
Expand Down
20 changes: 15 additions & 5 deletions src/components/ValidateCodeCountdown/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, {useEffect, useImperativeHandle, useRef, useState} from 'react';
import {View} from 'react-native';
import RenderHTML from '@components/RenderHTML';
import useAccessibilityAnnouncement from '@hooks/useAccessibilityAnnouncement';
import useLocalize from '@hooks/useLocalize';
import CONST from '@src/CONST';
import type {ValidateCodeCountdownProps} from './types';
Expand Down Expand Up @@ -27,12 +29,20 @@ function ValidateCodeCountdown({onCountdownFinish, ref}: ValidateCodeCountdownPr
};
}, [onCountdownFinish, timeRemaining]);

// Announce countdown start/reset for screen readers
useAccessibilityAnnouncement(translate('validateCodeForm.timeRemainingAnnouncement', {timeRemaining: CONST.REQUEST_CODE_DELAY}), timeRemaining === CONST.REQUEST_CODE_DELAY);

// Announce expiration for screen readers
useAccessibilityAnnouncement(translate('validateCodeForm.timeExpiredAnnouncement'), timeRemaining === 0);

return (
<RenderHTML
html={translate('validateCodeForm.requestNewCode', {
timeRemaining: `00:${String(timeRemaining).padStart(2, '0')}`,
})}
/>
<View accessibilityLiveRegion="polite">
<RenderHTML
html={translate('validateCodeForm.requestNewCode', {
timeRemaining: `00:${String(timeRemaining).padStart(2, '0')}`,
})}
/>
</View>
);
}

Expand Down
6 changes: 5 additions & 1 deletion src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,9 @@ const translations: TranslationDeepObject<typeof en> = {
send: 'Senden',
na: 'k. A.',
noResultsFound: 'Keine Ergebnisse gefunden',
noResultsFoundMatching: (searchString: string) => `Keine Ergebnisse gefunden für „${searchString}“`,
noResultsFoundMatching: (searchString: string) => `Keine Ergebnisse gefunden für „${searchString}”`,
resultsAvailable: (count: number) => `${count} ${count === 1 ? 'Ergebnis verfügbar' : 'Ergebnisse verfügbar'}`,
suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'Vorschlag verfügbar' : 'Vorschläge verfügbar'}`,
recentDestinations: 'Letzte Ziele',
timePrefix: 'Es ist',
conjunctionFor: 'für',
Expand Down Expand Up @@ -2693,6 +2695,8 @@ ${amount} für ${merchant} – ${date}`,
requiredWhen2FAEnabled: 'Erforderlich, wenn 2FA aktiviert ist',
requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Fordere einen neuen Code an in <a>${timeRemaining}</a>`,
requestNewCodeAfterErrorOccurred: 'Neuen Code anfordern',
timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Verbleibende Zeit: ${timeRemaining} Sekunden`,
timeExpiredAnnouncement: 'Die Zeit ist abgelaufen',
error: {
pleaseFillMagicCode: 'Bitte gib deinen Magic Code ein',
incorrectMagicCode: 'Falscher oder ungültiger Magic-Code. Bitte versuche es erneut oder fordere einen neuen Code an.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ const translations = {
na: 'N/A',
noResultsFound: 'No results found',
noResultsFoundMatching: (searchString: string) => `No results found matching "${searchString}"`,
resultsAvailable: (count: number) => `${count} ${count === 1 ? 'result' : 'results'} available`,
suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'suggestion' : 'suggestions'} available`,
recentDestinations: 'Recent destinations',
timePrefix: "It's",
conjunctionFor: 'for',
Expand Down Expand Up @@ -2728,6 +2730,8 @@ const translations = {
requiredWhen2FAEnabled: 'Required when 2FA is enabled',
requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Request a new code in <a>${timeRemaining}</a>`,
requestNewCodeAfterErrorOccurred: 'Request a new code',
timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Time remaining: ${timeRemaining} seconds`,
timeExpiredAnnouncement: 'The time has expired',
error: {
pleaseFillMagicCode: 'Please enter your magic code',
incorrectMagicCode: 'Incorrect or invalid magic code. Please try again or request a new code.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ const translations: TranslationDeepObject<typeof en> = {
na: 'N/A',
noResultsFound: 'No se han encontrado resultados',
noResultsFoundMatching: (searchString: string) => `No se encontraron resultados que coincidan con "${searchString}"`,
resultsAvailable: (count: number) => `${count} ${count === 1 ? 'resultado disponible' : 'resultados disponibles'}`,
suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'sugerencia disponible' : 'sugerencias disponibles'}`,
recentDestinations: 'Destinos recientes',
timePrefix: 'Son las',
conjunctionFor: 'para',
Expand Down Expand Up @@ -2556,6 +2558,8 @@ ${amount} para ${merchant} - ${date}`,
requiredWhen2FAEnabled: 'Obligatorio cuando A2F está habilitado',
requestNewCode: ({timeRemaining}) => `Pedir un código nuevo en <a>${timeRemaining}</a>`,
requestNewCodeAfterErrorOccurred: 'Solicitar un nuevo código',
timeRemainingAnnouncement: ({timeRemaining}) => `Tiempo restante: ${timeRemaining} segundos`,
timeExpiredAnnouncement: 'El tiempo ha expirado',
error: {
pleaseFillMagicCode: 'Por favor, introduce el código mágico.',
incorrectMagicCode: 'Código mágico incorrecto o no válido. Inténtalo de nuevo o solicita otro código.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ const translations: TranslationDeepObject<typeof en> = {
na: 'N/D',
noResultsFound: 'Aucun résultat trouvé',
noResultsFoundMatching: (searchString: string) => `Aucun résultat trouvé correspondant à « ${searchString} »`,
resultsAvailable: (count: number) => `${count} ${count === 1 ? 'résultat disponible' : 'résultats disponibles'}`,
suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'suggestion disponible' : 'suggestions disponibles'}`,
recentDestinations: 'Destinations récentes',
timePrefix: "C'est",
conjunctionFor: 'pour',
Expand Down Expand Up @@ -2698,6 +2700,8 @@ ${amount} pour ${merchant} - ${date}`,
requiredWhen2FAEnabled: 'Obligatoire lorsque l’authentification à deux facteurs est activée',
requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Demander un nouveau code dans <a>${timeRemaining}</a>`,
requestNewCodeAfterErrorOccurred: 'Demander un nouveau code',
timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Temps restant : ${timeRemaining} secondes`,
timeExpiredAnnouncement: 'Le temps est écoulé',
error: {
pleaseFillMagicCode: 'Veuillez saisir votre code magique',
incorrectMagicCode: 'Code magique incorrect ou non valide. Veuillez réessayer ou demander un nouveau code.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ const translations: TranslationDeepObject<typeof en> = {
na: 'N/D',
noResultsFound: 'Nessun risultato trovato',
noResultsFoundMatching: (searchString: string) => `Nessun risultato trovato per "${searchString}"`,
resultsAvailable: (count: number) => `${count} ${count === 1 ? 'risultato disponibile' : 'risultati disponibili'}`,
suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'suggerimento disponibile' : 'suggerimenti disponibili'}`,
recentDestinations: 'Destinazioni recenti',
timePrefix: 'È',
conjunctionFor: 'per',
Expand Down Expand Up @@ -2687,6 +2689,8 @@ ${amount} per ${merchant} - ${date}`,
requiredWhen2FAEnabled: 'Obbligatorio quando l’autenticazione a due fattori è abilitata',
requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Richiedi un nuovo codice tra <a>${timeRemaining}</a>`,
requestNewCodeAfterErrorOccurred: 'Richiedi un nuovo codice',
timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Tempo rimanente: ${timeRemaining} secondi`,
timeExpiredAnnouncement: 'Il tempo è scaduto',
error: {
pleaseFillMagicCode: 'Inserisci il tuo codice magico',
incorrectMagicCode: 'Codice magico errato o non valido. Riprova o richiedi un nuovo codice.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ const translations: TranslationDeepObject<typeof en> = {
na: '該当なし',
noResultsFound: '結果が見つかりません',
noResultsFoundMatching: (searchString: string) => `"${searchString}" に一致する結果は見つかりませんでした`,
resultsAvailable: (count: number) => `${count}件の結果が利用可能`,
suggestionsAvailable: (count: number) => `${count}件の候補が利用可能`,
recentDestinations: '最近の宛先',
timePrefix: 'それは',
conjunctionFor: '〜用',
Expand Down Expand Up @@ -2667,6 +2669,8 @@ ${date} の ${merchant} への ${amount}`,
requiredWhen2FAEnabled: '2要素認証が有効な場合は必須',
requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `<a>${timeRemaining}</a> 後に新しいコードをリクエストする`,
requestNewCodeAfterErrorOccurred: '新しいコードをリクエスト',
timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `残り時間: ${timeRemaining}秒`,
timeExpiredAnnouncement: '時間切れです',
error: {
pleaseFillMagicCode: 'マジックコードを入力してください',
incorrectMagicCode: '魔法コードが間違っているか無効です。もう一度お試しいただくか、新しいコードをリクエストしてください。',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ const translations: TranslationDeepObject<typeof en> = {
na: 'n.v.t.',
noResultsFound: 'Geen resultaten gevonden',
noResultsFoundMatching: (searchString: string) => `Geen resultaten gevonden voor "${searchString}"`,
resultsAvailable: (count: number) => `${count} ${count === 1 ? 'resultaat beschikbaar' : 'resultaten beschikbaar'}`,
suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'suggestie beschikbaar' : 'suggesties beschikbaar'}`,
recentDestinations: 'Recente bestemmingen',
timePrefix: 'Het is',
conjunctionFor: 'voor',
Expand Down Expand Up @@ -2686,6 +2688,8 @@ ${amount} voor ${merchant} - ${date}`,
requiredWhen2FAEnabled: 'Vereist wanneer 2FA is ingeschakeld',
requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Vraag een nieuwe code aan over <a>${timeRemaining}</a>`,
requestNewCodeAfterErrorOccurred: 'Een nieuwe code aanvragen',
timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Resterende tijd: ${timeRemaining} seconden`,
timeExpiredAnnouncement: 'De tijd is verstreken',
error: {
pleaseFillMagicCode: 'Voer je magische code in',
incorrectMagicCode: 'Onjuiste of ongeldige magische code. Probeer het opnieuw of vraag een nieuwe code aan.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ const translations: TranslationDeepObject<typeof en> = {
na: 'ND dotyczy',
noResultsFound: 'Nie znaleziono wyników',
noResultsFoundMatching: (searchString: string) => `Nie znaleziono wyników pasujących do „${searchString}”`,
resultsAvailable: (count: number) => `${count} ${count === 1 ? 'wynik dostępny' : 'wyników dostępnych'}`,
suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'sugestia dostępna' : 'sugestii dostępnych'}`,
recentDestinations: 'Ostatnie miejsca docelowe',
timePrefix: 'To jest',
conjunctionFor: 'dla',
Expand Down Expand Up @@ -2679,6 +2681,8 @@ ${amount} dla ${merchant} - ${date}`,
requiredWhen2FAEnabled: 'Wymagane, gdy włączone jest 2FA',
requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Poproś o nowy kod za <a>${timeRemaining}</a>`,
requestNewCodeAfterErrorOccurred: 'Poproś o nowy kod',
timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Pozostały czas: ${timeRemaining} sekund`,
timeExpiredAnnouncement: 'Czas minął',
error: {
pleaseFillMagicCode: 'Wprowadź swój magiczny kod',
incorrectMagicCode: 'Nieprawidłowy lub niepoprawny kod magiczny. Spróbuj ponownie lub poproś o nowy kod.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ const translations: TranslationDeepObject<typeof en> = {
na: 'N/D',
noResultsFound: 'Nenhum resultado encontrado',
noResultsFoundMatching: (searchString: string) => `Nenhum resultado encontrado para "${searchString}"`,
resultsAvailable: (count: number) => `${count} ${count === 1 ? 'resultado disponível' : 'resultados disponíveis'}`,
suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'sugestão disponível' : 'sugestões disponíveis'}`,
recentDestinations: 'Destinos recentes',
timePrefix: 'É',
conjunctionFor: 'para',
Expand Down Expand Up @@ -2679,6 +2681,8 @@ ${amount} para ${merchant} - ${date}`,
requiredWhen2FAEnabled: 'Obrigatório quando a 2FA estiver ativada',
requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Solicite um novo código em <a>${timeRemaining}</a>`,
requestNewCodeAfterErrorOccurred: 'Solicitar um novo código',
timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Tempo restante: ${timeRemaining} segundos`,
timeExpiredAnnouncement: 'O tempo expirou',
error: {
pleaseFillMagicCode: 'Insira seu código mágico',
incorrectMagicCode: 'Código mágico incorreto ou inválido. Tente novamente ou solicite um novo código.',
Expand Down
6 changes: 5 additions & 1 deletion src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,9 @@ const translations: TranslationDeepObject<typeof en> = {
send: '发送',
na: '不适用',
noResultsFound: '未找到结果',
noResultsFoundMatching: (searchString: string) => `未找到与“${searchString}”匹配的结果`,
noResultsFoundMatching: (searchString: string) => `未找到与”${searchString}”匹配的结果`,
resultsAvailable: (count: number) => `${count}个结果可用`,
suggestionsAvailable: (count: number) => `${count}个建议可用`,
recentDestinations: '最近目的地',
timePrefix: '它是',
conjunctionFor: '用于',
Expand Down Expand Up @@ -2627,6 +2629,8 @@ ${amount},商户:${merchant} - 日期:${date}`,
requiredWhen2FAEnabled: '启用双重验证时必填',
requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `在<a>${timeRemaining}</a>后请求新代码`,
requestNewCodeAfterErrorOccurred: '请求新验证码',
timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `剩余时间: ${timeRemaining}秒`,
timeExpiredAnnouncement: '时间已过期',
error: {
pleaseFillMagicCode: '请输入你的魔法验证码',
incorrectMagicCode: '魔术验证码不正确或无效。请重试或请求新的验证码。',
Expand Down
Loading
Loading