From 7fd7ba9a67d50b159acb91d3e23ec5ff4e2e8145 Mon Sep 17 00:00:00 2001 From: Maruf Sharifi Date: Sun, 22 Feb 2026 14:43:16 +0430 Subject: [PATCH 1/2] fix: remove duplicate focusable elements to improve accessibility --- .../implementation/index.native.tsx | 1 - .../BaseTextInput/implementation/index.tsx | 5 +- .../TextInput/TextInputLabel/index.native.tsx | 3 ++ .../TextInput/TextInputLabel/index.tsx | 4 ++ .../TextInput/TextInputMeasurement/index.tsx | 12 +++++ src/pages/signin/SignInPageLayout/Footer.tsx | 49 ++++++++----------- .../FooterRow/index.native.tsx | 47 ++++++++++++++++++ .../SignInPageLayout/FooterRow/index.tsx | 33 +++++++++++++ src/pages/signin/SignInPageLayout/types.ts | 8 ++- tests/ui/AddressPageTest.tsx | 7 +-- tests/ui/components/TextInputLabel.tsx | 4 +- 11 files changed, 136 insertions(+), 37 deletions(-) create mode 100644 src/pages/signin/SignInPageLayout/FooterRow/index.native.tsx create mode 100644 src/pages/signin/SignInPageLayout/FooterRow/index.tsx diff --git a/src/components/TextInput/BaseTextInput/implementation/index.native.tsx b/src/components/TextInput/BaseTextInput/implementation/index.native.tsx index 481720fe4eea0..6c28bf261db45 100644 --- a/src/components/TextInput/BaseTextInput/implementation/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/implementation/index.native.tsx @@ -300,7 +300,6 @@ function BaseTextInput({ // When autoGrowHeight is true we calculate the width for the text input, so it will break lines properly // or if multiline is not supplied we calculate the text input height, using onLayout. onLayout={onLayout} - accessibilityLabel={accessibilityLabel} style={[ autoGrowHeight && !isAutoGrowHeightMarkdown && diff --git a/src/components/TextInput/BaseTextInput/implementation/index.tsx b/src/components/TextInput/BaseTextInput/implementation/index.tsx index 5daafe3e0a262..98957a726e9c1 100644 --- a/src/components/TextInput/BaseTextInput/implementation/index.tsx +++ b/src/components/TextInput/BaseTextInput/implementation/index.tsx @@ -337,7 +337,7 @@ function BaseTextInput({ role={CONST.ROLE.PRESENTATION} onPress={onPress} tabIndex={-1} - accessibilityLabel={accessibilityLabel} + accessible={false} // When autoGrowHeight is true we calculate the width for the text input, so it will break lines properly // or if multiline is not supplied we calculate the text input height, using onLayout. onLayout={onLayout} @@ -487,7 +487,8 @@ function BaseTextInput({ readOnly={isReadOnly} defaultValue={defaultValue} markdownStyle={markdownStyle} - accessibilityLabel={inputProps.accessibilityLabel} + accessibilityLabel={inputProps.accessibilityLabel ?? accessibilityLabel} + keyboardType={inputProps.keyboardType} /> {!!suffixCharacter && ( diff --git a/src/components/TextInput/TextInputLabel/index.native.tsx b/src/components/TextInput/TextInputLabel/index.native.tsx index ebd2ee79b72df..fd9f5431c12eb 100644 --- a/src/components/TextInput/TextInputLabel/index.native.tsx +++ b/src/components/TextInput/TextInputLabel/index.native.tsx @@ -12,6 +12,9 @@ function TextInputLabel({label, labelScale, labelTranslateY, isMultiline}: TextI return ( {label} diff --git a/src/components/TextInput/TextInputMeasurement/index.tsx b/src/components/TextInput/TextInputMeasurement/index.tsx index c86ff46cf0498..644ff331edf4e 100644 --- a/src/components/TextInput/TextInputMeasurement/index.tsx +++ b/src/components/TextInput/TextInputMeasurement/index.tsx @@ -36,6 +36,10 @@ function TextInputMeasurement({ onSetTextInputWidth(e.nativeEvent.layout.width); onSetTextInputHeight(e.nativeEvent.layout.height); }} + accessible={false} + accessibilityElementsHidden + importantForAccessibility="no" + aria-hidden > {/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */} {value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder} @@ -68,6 +76,10 @@ function TextInputMeasurement({ styles.hiddenElementOutsideOfWindow, styles.visibilityHidden, ]} + accessible={false} + accessibilityElementsHidden + importantForAccessibility="no" + aria-hidden onLayout={(e) => { if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) { return; diff --git a/src/pages/signin/SignInPageLayout/Footer.tsx b/src/pages/signin/SignInPageLayout/Footer.tsx index 33620b5480688..4522d411ac4e1 100644 --- a/src/pages/signin/SignInPageLayout/Footer.tsx +++ b/src/pages/signin/SignInPageLayout/Footer.tsx @@ -5,8 +5,6 @@ import SignInGradient from '@assets/images/home-fade-gradient--mobile.svg'; import Hoverable from '@components/Hoverable'; import ImageSVG from '@components/ImageSVG'; import Text from '@components/Text'; -import type {LinkProps, PressProps} from '@components/TextLink'; -import TextLink from '@components/TextLink'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -18,14 +16,11 @@ import Socials from '@pages/signin/Socials'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; -import type {SignInPageLayoutProps} from './types'; +import FooterRow from './FooterRow'; +import type {FooterColumnRow, SignInPageLayoutProps} from './types'; type FooterProps = Pick; -type FooterColumnRow = (LinkProps | PressProps) & { - translationPath: TranslationPaths; -}; - type FooterColumnData = { translationPath: TranslationPaths; rows: FooterColumnRow[]; @@ -183,27 +178,25 @@ function Footer({navigateFocus}: FooterProps) { {translate(column.translationPath)} - {column.rows.map(({href, onPress, translationPath}) => ( - - {(hovered) => ( - - {onPress ? ( - - {translate(translationPath)} - - ) : ( - - {translate(translationPath)} - - )} - - )} + {column.rows.map((row) => ( + + {(hovered) => + row.onPress ? ( + + ) : ( + + ) + } ))} {i === 2 && ( diff --git a/src/pages/signin/SignInPageLayout/FooterRow/index.native.tsx b/src/pages/signin/SignInPageLayout/FooterRow/index.native.tsx new file mode 100644 index 0000000000000..a723a68fbe9c7 --- /dev/null +++ b/src/pages/signin/SignInPageLayout/FooterRow/index.native.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import type {GestureResponderEvent, StyleProp, TextStyle} from 'react-native'; +import {PressableWithoutFeedback} from '@components/Pressable'; +import Text from '@components/Text'; +import useEnvironment from '@hooks/useEnvironment'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {FooterColumnRow} from '@pages/signin/SignInPageLayout/types'; +import {openLink as openLinkUtil} from '@userActions/Link'; +import CONST from '@src/CONST'; + +type FooterRowProps = FooterColumnRow & { + text: string; + style: StyleProp; +}; + +function FooterRow({href, onPress, translationPath, text, style}: FooterRowProps) { + const styles = useThemeStyles(); + const {environmentURL} = useEnvironment(); + + return ( + { + if (onPress) { + onPress({} as GestureResponderEvent); + return; + } + if (href) { + openLinkUtil(href, environmentURL); + } + }} + > + + {text} + + + ); +} + +export default FooterRow; diff --git a/src/pages/signin/SignInPageLayout/FooterRow/index.tsx b/src/pages/signin/SignInPageLayout/FooterRow/index.tsx new file mode 100644 index 0000000000000..4b2f167b212f5 --- /dev/null +++ b/src/pages/signin/SignInPageLayout/FooterRow/index.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import type {StyleProp, TextStyle} from 'react-native'; +import TextLink from '@components/TextLink'; +import type {FooterColumnRow} from '@pages/signin/SignInPageLayout/types'; + +type FooterRowProps = FooterColumnRow & { + text: string; + style: StyleProp; +}; + +function FooterRow({href, onPress, text, style}: FooterRowProps) { + if (onPress) { + return ( + + {text} + + ); + } + + return ( + + {text} + + ); +} + +export default FooterRow; diff --git a/src/pages/signin/SignInPageLayout/types.ts b/src/pages/signin/SignInPageLayout/types.ts index 9c31cb9b486ad..2546335f483d6 100644 --- a/src/pages/signin/SignInPageLayout/types.ts +++ b/src/pages/signin/SignInPageLayout/types.ts @@ -1,5 +1,7 @@ import type React from 'react'; import type {ForwardedRef} from 'react'; +import type {LinkProps, PressProps} from '@components/TextLink'; +import type {TranslationPaths} from '@src/languages/types'; type SignInPageLayoutProps = { /** The children to show inside the layout */ @@ -35,4 +37,8 @@ type SignInPageLayoutRef = { scrollPageToTop: (animated?: boolean) => void; }; -export type {SignInPageLayoutRef, SignInPageLayoutProps}; +type FooterColumnRow = (LinkProps | PressProps) & { + translationPath: TranslationPaths; +}; + +export type {SignInPageLayoutRef, SignInPageLayoutProps, FooterColumnRow}; diff --git a/tests/ui/AddressPageTest.tsx b/tests/ui/AddressPageTest.tsx index b10f1ebd181fa..f1b3263d71551 100644 --- a/tests/ui/AddressPageTest.tsx +++ b/tests/ui/AddressPageTest.tsx @@ -74,12 +74,13 @@ describe('AddressPageTest', () => { renderPage(SCREENS.SETTINGS.PROFILE.ADDRESS); await waitForBatchedUpdatesWithAct(); - const state = screen.getAllByLabelText('State / Province'); - expect(state.at(1)?.props.value).toEqual('Test'); + const stateInput = screen.getByLabelText('State / Province'); + expect(stateInput.props.value).toEqual('Test'); Navigation.setParams({ country: 'VN', }); await waitForBatchedUpdatesWithAct(); - expect(state?.at(1)?.props.value).toEqual('Test'); + const stateInputAfterParams = screen.getByLabelText('State / Province'); + expect(stateInputAfterParams.props.value).toEqual('Test'); }); }); diff --git a/tests/ui/components/TextInputLabel.tsx b/tests/ui/components/TextInputLabel.tsx index 93d542e2b824b..05fae399cd433 100644 --- a/tests/ui/components/TextInputLabel.tsx +++ b/tests/ui/components/TextInputLabel.tsx @@ -26,7 +26,7 @@ describe('TextInputLabel', () => { labelScale, }); // Find the Animated.Text component by its text content - const labelElement = screen.getByText(longLabel); + const labelElement = screen.getByText(longLabel, {includeHiddenElements: true}); // Verify the component renders the correct text expect(labelElement).toBeTruthy(); // Verify the props for shortening behavior @@ -44,7 +44,7 @@ describe('TextInputLabel', () => { labelScale, }); // Find the Animated.Text component by its text content - const labelElement = screen.getByText(label); + const labelElement = screen.getByText(label, {includeHiddenElements: true}); // Verify the component renders the correct text expect(labelElement).toBeTruthy(); // Verify that numberOfLines and ellipsizeMode are undefined From b47ab6e6ed21aa445eaaf0fb1cffec32180f3dc4 Mon Sep 17 00:00:00 2001 From: Maruf Sharifi Date: Mon, 23 Feb 2026 09:54:05 +0430 Subject: [PATCH 2/2] fixed test failure --- tests/ui/IOURequestStepHoursTest.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ui/IOURequestStepHoursTest.tsx b/tests/ui/IOURequestStepHoursTest.tsx index 20a8d4d6490ad..f4ade54be0c6c 100644 --- a/tests/ui/IOURequestStepHoursTest.tsx +++ b/tests/ui/IOURequestStepHoursTest.tsx @@ -166,8 +166,8 @@ describe('IOURequestStepHours', () => { await waitForBatchedUpdatesWithAct(); - // NumberWithSymbolForm should display the existing hours value - expect(screen.getByText(String(existingHours))).toBeDefined(); + // NumberWithSymbolForm should prefill the input with existing hours value + expect(screen.getByDisplayValue(String(existingHours))).toBeDefined(); }); });