Skip to content

Commit 0a415ed

Browse files
authored
Merge pull request Expensify#82964 from marufsharifi/fix/screen-reader-error-message-field-association
Fix: Associate error message with form field for screen readers
2 parents 68a869e + ab27f06 commit 0a415ed

2 files changed

Lines changed: 23 additions & 9 deletions

File tree

src/components/FormHelpMessage.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ function FormHelpMessage({
5454
const icons = useMemoizedLazyExpensifyIcons(['DotIndicator', 'Exclamation']);
5555
const isWeb = getPlatform() === CONST.PLATFORM.WEB;
5656
const shouldAnnounceError = isError && typeof message === 'string' && !!message && !shouldRenderMessageAsHTML && children == null;
57+
const shouldUseSeparateWebLiveAnnouncement = isWeb && !!nativeID && shouldAnnounceError;
58+
const visibleMessageRole = shouldUseSeparateWebLiveAnnouncement || !shouldAnnounceError ? undefined : CONST.ROLE.ALERT;
59+
const visibleMessageLiveRegion = shouldUseSeparateWebLiveAnnouncement || !shouldAnnounceError ? undefined : 'assertive';
5760

5861
const HTMLMessage = useMemo(() => {
5962
if (typeof message !== 'string' || !shouldRenderMessageAsHTML) {
@@ -78,10 +81,7 @@ function FormHelpMessage({
7881
}
7982

8083
return (
81-
<View
82-
style={[styles.flexRow, styles.alignItemsCenter, styles.mt2, styles.mb1, style]}
83-
nativeID={nativeID}
84-
>
84+
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mt2, styles.mb1, style]}>
8585
{isError && shouldShowRedDotIndicator && (
8686
<View
8787
accessible
@@ -109,15 +109,27 @@ function FormHelpMessage({
109109
) : (
110110
<Text
111111
style={[isError ? styles.formError : styles.formHelp, styles.mb0]}
112-
role={shouldAnnounceError ? CONST.ROLE.ALERT : undefined}
112+
nativeID={nativeID}
113+
role={visibleMessageRole}
113114
// TalkBack on some Android versions skips role-only alert announcements,
114115
// so keep native accessibilityRole/live-region as a platform fallback.
115116
accessibilityRole={!isWeb && shouldAnnounceError ? CONST.ROLE.ALERT : undefined}
116-
accessibilityLiveRegion={shouldAnnounceError ? 'assertive' : undefined}
117+
accessibilityLiveRegion={visibleMessageLiveRegion}
117118
>
118119
{message}
119120
</Text>
120121
))}
122+
{shouldUseSeparateWebLiveAnnouncement && (
123+
// Keep a separate live region for immediate web announcements without
124+
// changing the visible described text Safari relies on when refocusing inputs.
125+
<Text
126+
style={styles.hiddenElementOutsideOfWindow}
127+
role={CONST.ROLE.ALERT}
128+
accessibilityLiveRegion="assertive"
129+
>
130+
{message}
131+
</Text>
132+
)}
121133
</View>
122134
</View>
123135
);

src/components/TextInput/BaseTextInput/implementation/index.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,8 @@ function BaseTextInput({
327327
// This is workaround for https://github.com/Expensify/App/issues/47939: in case when user is using Chrome on Android we set inputMode to 'search' to disable autocomplete bar above the keyboard.
328328
// If we need some other inputMode (eg. 'decimal'), then the autocomplete bar will show, but we can do nothing about it as it's a known Chrome bug.
329329
const inputMode = inputProps.inputMode ?? (isMobileChrome() ? 'search' : undefined);
330-
const accessibilityLabel = [label, hint, errorText ? translate('common.yourReviewIsRequired') : ''].filter(Boolean).join(', ');
330+
const helpMessageTextID = `${helpMessageId}-text`;
331+
const accessibilityLabel = [label, hint, errorText].filter(Boolean).join(', ');
331332
const loadingSpinnerReasonAttributes: SkeletonSpanReasonAttributes = {
332333
context: 'BaseTextInput.isLoading',
333334
isLoading: !!inputProps.isLoading,
@@ -496,7 +497,8 @@ function BaseTextInput({
496497
markdownStyle={markdownStyle}
497498
accessibilityLabel={inputProps.accessibilityLabel ?? accessibilityLabel}
498499
keyboardType={inputProps.keyboardType}
499-
aria-describedby={inputHelpText ? helpMessageId : undefined}
500+
aria-describedby={inputHelpText ? helpMessageTextID : undefined}
501+
aria-invalid={errorText ? true : undefined}
500502
/>
501503
{!!suffixCharacter && (
502504
<View style={[styles.textInputSuffixWrapper, suffixContainerStyle]}>
@@ -570,7 +572,7 @@ function BaseTextInput({
570572
</PressableWithoutFeedback>
571573
{!!inputHelpText && (
572574
<FormHelpMessage
573-
nativeID={helpMessageId}
575+
nativeID={helpMessageTextID}
574576
isError={!!errorText}
575577
message={inputHelpText}
576578
/>

0 commit comments

Comments
 (0)