Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/components/SelectionList/components/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {TextInputOptions} from '@components/SelectionList/types';
import Text from '@components/Text';
import BaseTextInput from '@components/TextInput';
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
import useStatusMessageAccessibilityAnnouncement from '@components/utils/useStatusMessageAccessibilityAnnouncement';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import mergeRefs from '@libs/mergeRefs';
Expand Down Expand Up @@ -66,9 +67,11 @@ function TextInput({
const styles = useThemeStyles();
const {translate} = useLocalize();
const {label, value, onChangeText, errorText, headerMessage, hint, disableAutoFocus, placeholder, maxLength, inputMode, ref: optionsRef, style, disableAutoCorrect} = options ?? {};
const resultsFound = headerMessage !== translate('common.noResultsFound');
const noResultsFoundText = translate('common.noResultsFound');
const isNoResultsFoundMessage = headerMessage === noResultsFoundText;
const noData = dataLength === 0 && !showLoadingPlaceholder;
const shouldShowHeaderMessage = !!headerMessage && (!isLoadingNewOptions || resultsFound || noData);
const shouldShowHeaderMessage = !!shouldShowTextInput && !!headerMessage && (!isLoadingNewOptions || !isNoResultsFoundMessage || noData);
const shouldAnnounceNoResults = shouldShowHeaderMessage && isNoResultsFoundMessage;

const focusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const mergedRef = mergeRefs<BaseTextInputRef>(ref, optionsRef);
Expand Down Expand Up @@ -106,6 +109,8 @@ function TextInput({
onFocusChange(false);
}, [onFocusChange]);

useStatusMessageAccessibilityAnnouncement(headerMessage, shouldAnnounceNoResults);

if (!shouldShowTextInput) {
return null;
}
Expand Down Expand Up @@ -140,7 +145,12 @@ function TextInput({
</View>
{shouldShowHeaderMessage && (
<View style={[styles.ph5, styles.pb5, style?.headerMessageStyle]}>
<Text style={[styles.textLabel, styles.colorMuted, styles.minHeight5]}>{headerMessage}</Text>
<Text
style={[styles.textLabel, styles.colorMuted, styles.minHeight5]}
accessibilityLiveRegion={shouldAnnounceNoResults ? 'polite' : undefined}
>
{headerMessage}
</Text>
</View>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {PressableWithFeedback} from '@components/Pressable';
import SectionList from '@components/SectionList';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useStatusMessageAccessibilityAnnouncement from '@components/utils/useStatusMessageAccessibilityAnnouncement';
import useActiveElementRole from '@hooks/useActiveElementRole';
import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
Expand Down Expand Up @@ -1018,11 +1019,22 @@ function BaseSelectionListWithSections<TItem extends ListItem>({
},
);

const noResultsFoundText = translate('common.noResultsFound');
const isNoResultsFoundMessage = headerMessage === noResultsFoundText;
const shouldShowHeaderMessage = !!headerMessage && (!isLoadingNewOptions || !isNoResultsFoundMessage || (flattenedSections.allOptions.length === 0 && !showLoadingPlaceholder));
const shouldAnnounceNoResults = shouldShowHeaderMessage && isNoResultsFoundMessage;

useStatusMessageAccessibilityAnnouncement(headerMessage, shouldAnnounceNoResults);

const headerMessageContent = () =>
(!isLoadingNewOptions || headerMessage !== translate('common.noResultsFound') || (flattenedSections.allOptions.length === 0 && !showLoadingPlaceholder)) &&
!!headerMessage && (
shouldShowHeaderMessage && (
<View style={headerMessageStyle ?? [styles.ph5, styles.pb5]}>
<Text style={[styles.textLabel, styles.colorMuted, styles.minHeight5]}>{headerMessage}</Text>
<Text
style={[styles.textLabel, styles.colorMuted, styles.minHeight5]}
accessibilityLiveRegion={shouldAnnounceNoResults ? 'polite' : undefined}
>
{headerMessage}
</Text>
</View>
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type {ReactNode} from 'react';
import {useEffect, useRef} from 'react';
import {AccessibilityInfo} from 'react-native';

const DELAY_FOR_ACCESSIBILITY_TREE_SYNC = 100;

function useStatusMessageAccessibilityAnnouncement(message: string | ReactNode, shouldAnnounceMessage: boolean) {
const previousAnnouncedMessageRef = useRef('');

useEffect(() => {
if (!shouldAnnounceMessage || typeof message !== 'string' || !message.trim()) {
previousAnnouncedMessageRef.current = '';
return;
}

if (previousAnnouncedMessageRef.current === message) {
return;
}

previousAnnouncedMessageRef.current = message;

// On iOS real devices, a brief delay helps the accessibility tree sync before announcing.
const timeout = setTimeout(() => {
AccessibilityInfo.announceForAccessibility(message);
}, DELAY_FOR_ACCESSIBILITY_TREE_SYNC);

return () => clearTimeout(timeout);
}, [message, shouldAnnounceMessage]);
}

export default useStatusMessageAccessibilityAnnouncement;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type {ReactNode} from 'react';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function useStatusMessageAccessibilityAnnouncement(_message: string | ReactNode, _shouldAnnounceMessage: boolean) {}

export default useStatusMessageAccessibilityAnnouncement;
Loading