From 4055da637c16ad87cd936f4c36257a25046b7443 Mon Sep 17 00:00:00 2001 From: "Nyoman Jyotisa (via MelvinBot)" Date: Fri, 28 Aug 2026 03:33:44 +0000 Subject: [PATCH 1/9] Fix: make company card CSV import help links reliably tappable on Android Co-authored-by: Nyoman Jyotisa --- .../addNew/ImportFromFileStep.tsx | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx b/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx index debea4119c4a..f9e8b53fca67 100644 --- a/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx +++ b/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx @@ -1,11 +1,12 @@ import Button from '@components/ButtonComposed'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import {PressableWithoutFeedback} from '@components/Pressable'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; -import TextLink from '@components/TextLink'; +import useEnvironment from '@hooks/useEnvironment'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -19,6 +20,7 @@ import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/t import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; import {setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards'; +import {openLink} from '@userActions/Link'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -47,6 +49,7 @@ const CSV_TEMPLATE_CONTENT = [ function ImportFromFileStep() { const {translate} = useLocalize(); const styles = useThemeStyles(); + const {environmentURL} = useEnvironment(); const {isOffline} = useNetwork(); const icons = useMemoizedLazyExpensifyIcons(['Download']); const route = useRoute>(); @@ -63,6 +66,18 @@ function ImportFromFileStep() { localFileDownload(CSV_TEMPLATE_FILE_NAME, CSV_TEMPLATE_CONTENT, translate); }; + // The help text mixes plain copy with two tappable links (a client-side template download and an external help guide). + // On Android, a link nested inline inside a becomes a ClickableSpan whose touch area is limited to the glyph bounds, + // which makes it unreliable to tap (e.g. at the minimum device font size). Rendering each link as its own PressableWithoutFeedback + // gives it a real native touch target, while splitting the plain copy into words keeps the paragraph flowing/wrapping naturally. + const createFileFeedHelpTextSegments: Array<{text: string; onPress?: () => void; role?: React.ComponentProps['role']}> = [ + {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionStart')}, + {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.templateLink'), onPress: downloadTemplate, role: CONST.ROLE.BUTTON}, + {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionMiddle')}, + {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.helpGuideLink'), onPress: () => openLink(CONST.COMPANY_CARDS_CREATE_FILE_FEED_HELP_URL, environmentURL), role: CONST.ROLE.LINK}, + {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionEnd')}, + ]; + const navigateToImport = () => { if (!companyCardLayoutName.trim()) { setHasError(true); @@ -89,13 +104,35 @@ function ImportFromFileStep() { contentContainerStyle={styles.flexGrow1} addBottomSafeAreaPadding > - - {translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionStart')} - {translate('workspace.companyCards.addNewCard.createFileFeedHelpText.templateLink')} - {translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionMiddle')} - {translate('workspace.companyCards.addNewCard.createFileFeedHelpText.helpGuideLink')} - {translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionEnd')} - + + {createFileFeedHelpTextSegments.map((segment, segmentIndex) => { + if (segment.onPress) { + return ( + // eslint-disable-next-line react/no-array-index-key + + + {segment.text} + + + + ); + } + return (segment.text.match(/\S+\s*/g) ?? []).map((word, wordIndex) => ( + + {word} + + )); + })} + Date: Fri, 28 Aug 2026 03:40:45 +0000 Subject: [PATCH 2/9] Fix: format ImportFromFileStep to satisfy oxfmt (line length) Co-authored-by: Nyoman Jyotisa --- .../workspace/companyCards/addNew/ImportFromFileStep.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx b/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx index f9e8b53fca67..b061187c9c8d 100644 --- a/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx +++ b/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx @@ -74,7 +74,11 @@ function ImportFromFileStep() { {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionStart')}, {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.templateLink'), onPress: downloadTemplate, role: CONST.ROLE.BUTTON}, {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionMiddle')}, - {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.helpGuideLink'), onPress: () => openLink(CONST.COMPANY_CARDS_CREATE_FILE_FEED_HELP_URL, environmentURL), role: CONST.ROLE.LINK}, + { + text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.helpGuideLink'), + onPress: () => openLink(CONST.COMPANY_CARDS_CREATE_FILE_FEED_HELP_URL, environmentURL), + role: CONST.ROLE.LINK, + }, {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionEnd')}, ]; From 1209fac208de5442eee2ddfc33e3cfcd8e45ef98 Mon Sep 17 00:00:00 2001 From: "Nyoman Jyotisa (via MelvinBot)" Date: Fri, 28 Aug 2026 03:53:55 +0000 Subject: [PATCH 3/9] Fix: add sentryLabel to PressableWithoutFeedback links to satisfy ESLint Co-authored-by: Nyoman Jyotisa --- .../companyCards/addNew/ImportFromFileStep.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx b/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx index b061187c9c8d..d9c9cccc3725 100644 --- a/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx +++ b/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx @@ -70,14 +70,20 @@ function ImportFromFileStep() { // On Android, a link nested inline inside a becomes a ClickableSpan whose touch area is limited to the glyph bounds, // which makes it unreliable to tap (e.g. at the minimum device font size). Rendering each link as its own PressableWithoutFeedback // gives it a real native touch target, while splitting the plain copy into words keeps the paragraph flowing/wrapping naturally. - const createFileFeedHelpTextSegments: Array<{text: string; onPress?: () => void; role?: React.ComponentProps['role']}> = [ + const createFileFeedHelpTextSegments: Array<{text: string; onPress?: () => void; role?: React.ComponentProps['role']; sentryLabel?: string}> = [ {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionStart')}, - {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.templateLink'), onPress: downloadTemplate, role: CONST.ROLE.BUTTON}, + { + text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.templateLink'), + onPress: downloadTemplate, + role: CONST.ROLE.BUTTON, + sentryLabel: 'ImportFromFileStep-TemplateLink', + }, {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionMiddle')}, { text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.helpGuideLink'), onPress: () => openLink(CONST.COMPANY_CARDS_CREATE_FILE_FEED_HELP_URL, environmentURL), role: CONST.ROLE.LINK, + sentryLabel: 'ImportFromFileStep-HelpGuideLink', }, {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionEnd')}, ]; @@ -117,6 +123,7 @@ function ImportFromFileStep() { From 865a6bab571544570997f15ea4b238fb94eb67af Mon Sep 17 00:00:00 2001 From: "Nyoman Jyotisa (via MelvinBot)" Date: Fri, 28 Aug 2026 05:13:05 +0000 Subject: [PATCH 4/9] Address review: preserve locale spacing and add href to help guide link Co-authored-by: Nyoman Jyotisa --- .../addNew/ImportFromFileStep.tsx | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx b/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx index d9c9cccc3725..97b87e537993 100644 --- a/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx +++ b/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx @@ -70,7 +70,13 @@ function ImportFromFileStep() { // On Android, a link nested inline inside a becomes a ClickableSpan whose touch area is limited to the glyph bounds, // which makes it unreliable to tap (e.g. at the minimum device font size). Rendering each link as its own PressableWithoutFeedback // gives it a real native touch target, while splitting the plain copy into words keeps the paragraph flowing/wrapping naturally. - const createFileFeedHelpTextSegments: Array<{text: string; onPress?: () => void; role?: React.ComponentProps['role']; sentryLabel?: string}> = [ + const createFileFeedHelpTextSegments: Array<{ + text: string; + onPress?: React.ComponentProps['onPress']; + href?: string; + role?: React.ComponentProps['role']; + sentryLabel?: string; + }> = [ {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionStart')}, { text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.templateLink'), @@ -81,7 +87,13 @@ function ImportFromFileStep() { {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionMiddle')}, { text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.helpGuideLink'), - onPress: () => openLink(CONST.COMPANY_CARDS_CREATE_FILE_FEED_HELP_URL, environmentURL), + // Pass href so the link renders as a real anchor on web (native link behavior: hover URL, open in a new tab, etc.), + // while onPress preventDefault()s the anchor's default navigation and routes through openLink on every platform. + href: CONST.COMPANY_CARDS_CREATE_FILE_FEED_HELP_URL, + onPress: (event) => { + event?.preventDefault(); + openLink(CONST.COMPANY_CARDS_CREATE_FILE_FEED_HELP_URL, environmentURL); + }, role: CONST.ROLE.LINK, sentryLabel: 'ImportFromFileStep-HelpGuideLink', }, @@ -118,22 +130,24 @@ function ImportFromFileStep() { {createFileFeedHelpTextSegments.map((segment, segmentIndex) => { if (segment.onPress) { return ( - // eslint-disable-next-line react/no-array-index-key - - - {segment.text} - - - + + {segment.text} + ); } - return (segment.text.match(/\S+\s*/g) ?? []).map((word, wordIndex) => ( + // Keep each word (with its own leading/trailing whitespace) as a separate node so the paragraph wraps in the + // flexWrap row. Preserving the segment's own spacing means locales that don't use spaces around the links + // (e.g. Japanese, Chinese) aren't given extra spaces the translation never intended. + return (segment.text.match(/\s*\S+\s*/g) ?? []).map((word, wordIndex) => ( Date: Fri, 28 Aug 2026 08:15:19 +0000 Subject: [PATCH 5/9] Address review: use stable segment key and justify word-level index key Co-authored-by: Nyoman Jyotisa --- .../companyCards/addNew/ImportFromFileStep.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx b/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx index 97b87e537993..95900017d102 100644 --- a/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx +++ b/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx @@ -127,12 +127,12 @@ function ImportFromFileStep() { addBottomSafeAreaPadding > - {createFileFeedHelpTextSegments.map((segment, segmentIndex) => { + {createFileFeedHelpTextSegments.map((segment) => { if (segment.onPress) { return ( ( {word} From 6a6ed3cdc4e61fb4a42d32dd20c6a126cc1f594e Mon Sep 17 00:00:00 2001 From: "Nyoman Jyotisa (via MelvinBot)" Date: Fri, 28 Aug 2026 10:46:19 +0000 Subject: [PATCH 6/9] Address review: render help-text segments explicitly and add inline-link coverage Co-authored-by: Nyoman Jyotisa --- .../addNew/ImportFromFileStep.tsx | 107 ++++++++---------- tests/unit/ImportFromFileStepTest.tsx | 107 ++++++++++++++++++ 2 files changed, 152 insertions(+), 62 deletions(-) create mode 100644 tests/unit/ImportFromFileStepTest.tsx diff --git a/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx b/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx index 95900017d102..cc032d79349d 100644 --- a/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx +++ b/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx @@ -70,35 +70,22 @@ function ImportFromFileStep() { // On Android, a link nested inline inside a becomes a ClickableSpan whose touch area is limited to the glyph bounds, // which makes it unreliable to tap (e.g. at the minimum device font size). Rendering each link as its own PressableWithoutFeedback // gives it a real native touch target, while splitting the plain copy into words keeps the paragraph flowing/wrapping naturally. - const createFileFeedHelpTextSegments: Array<{ - text: string; - onPress?: React.ComponentProps['onPress']; - href?: string; - role?: React.ComponentProps['role']; - sentryLabel?: string; - }> = [ - {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionStart')}, - { - text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.templateLink'), - onPress: downloadTemplate, - role: CONST.ROLE.BUTTON, - sentryLabel: 'ImportFromFileStep-TemplateLink', - }, - {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionMiddle')}, - { - text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.helpGuideLink'), - // Pass href so the link renders as a real anchor on web (native link behavior: hover URL, open in a new tab, etc.), - // while onPress preventDefault()s the anchor's default navigation and routes through openLink on every platform. - href: CONST.COMPANY_CARDS_CREATE_FILE_FEED_HELP_URL, - onPress: (event) => { - event?.preventDefault(); - openLink(CONST.COMPANY_CARDS_CREATE_FILE_FEED_HELP_URL, environmentURL); - }, - role: CONST.ROLE.LINK, - sentryLabel: 'ImportFromFileStep-HelpGuideLink', - }, - {text: translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionEnd')}, - ]; + const renderPlainCopy = (text: string) => + // Keep each word (with its own leading/trailing whitespace) as a separate node so the paragraph wraps in the flexWrap row. + // Preserving the run's own spacing means locales that don't use spaces around the links (e.g. Japanese, Chinese) aren't + // given extra spaces the translation never intended. + (text.match(/\s*\S+\s*/g) ?? []).map((word, wordIndex) => ( + + {word} + + )); const navigateToImport = () => { if (!companyCardLayoutName.trim()) { @@ -127,39 +114,35 @@ function ImportFromFileStep() { addBottomSafeAreaPadding > - {createFileFeedHelpTextSegments.map((segment) => { - if (segment.onPress) { - return ( - - {segment.text} - - ); - } - // Keep each word (with its own leading/trailing whitespace) as a separate node so the paragraph wraps in the - // flexWrap row. Preserving the segment's own spacing means locales that don't use spaces around the links - // (e.g. Japanese, Chinese) aren't given extra spaces the translation never intended. - return (segment.text.match(/\s*\S+\s*/g) ?? []).map((word, wordIndex) => ( - - {word} - - )); - })} + {renderPlainCopy(translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionStart'))} + + {translate('workspace.companyCards.addNewCard.createFileFeedHelpText.templateLink')} + + {renderPlainCopy(translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionMiddle'))} + { + event?.preventDefault(); + openLink(CONST.COMPANY_CARDS_CREATE_FILE_FEED_HELP_URL, environmentURL); + }} + style={styles.dInlineFlex} + > + {translate('workspace.companyCards.addNewCard.createFileFeedHelpText.helpGuideLink')} + + {renderPlainCopy(translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionEnd'))} { + // jest.requireActual returns `any` for the untyped React Navigation module + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actualNav = jest.requireActual('@react-navigation/native'); + + // Spreading the untyped requireActual result is intentional for this navigation mock + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...actualNav, + useNavigation: () => ({ + navigate: jest.fn(), + goBack: jest.fn(), + addListener: () => jest.fn(), + isFocused: () => true, + }), + useIsFocused: () => true, + useFocusEffect: jest.fn(), + usePreventRemove: jest.fn(), + useRoute: () => ({key: 'test-route', name: 'Workspace_Company_Cards_Add_New', params: {policyID: POLICY_ID}}), + }; +}); + +jest.mock('@libs/Navigation/Navigation', () => ({ + navigate: jest.fn(), + goBack: jest.fn(), + getActiveRoute: jest.fn(() => ''), + getActiveRouteWithoutParams: jest.fn(() => ''), + getTopmostReportId: jest.fn(() => undefined), + isNavigationReady: jest.fn(() => Promise.resolve()), + setNavigationActionToMicrotaskQueue: jest.fn(), + removeScreenFromNavigationState: jest.fn(), + dismissModal: jest.fn(), +})); + +function renderImportFromFileStep() { + return render( + + + , + ); +} + +describe('ImportFromFileStep inline help links', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + await act(async () => { + await Onyx.clear(); + await waitForBatchedUpdatesWithAct(); + }); + renderImportFromFileStep(); + await waitForBatchedUpdatesWithAct(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders the template link as a Pressable button that triggers the client-side CSV download', () => { + const templateLink = screen.getByTestId('ImportFromFileStep-TemplateLink'); + + // A Pressable-backed link (not a bare inline /ClickableSpan) so it gets a real native touch target on Android. + expect(templateLink).toHaveProp('role', CONST.ROLE.BUTTON); + + fireEvent.press(templateLink); + expect(localFileDownload).toHaveBeenCalledTimes(1); + }); + + it('renders the help guide link as a Pressable that keeps its href and opens the help guide', () => { + const openLinkSpy = jest.spyOn(Link, 'openLink').mockImplementation(() => {}); + const helpGuideLink = screen.getByTestId('ImportFromFileStep-HelpGuideLink'); + + // Retains href so web renders a real (native link behavior), while still routing through onPress on every platform. + expect(helpGuideLink).toHaveProp('role', CONST.ROLE.LINK); + expect(helpGuideLink).toHaveProp('href', CONST.COMPANY_CARDS_CREATE_FILE_FEED_HELP_URL); + + fireEvent.press(helpGuideLink); + expect(openLinkSpy).toHaveBeenCalledWith(CONST.COMPANY_CARDS_CREATE_FILE_FEED_HELP_URL, expect.any(String)); + }); +}); From 3ccd53ac8bd3173feb154b0786f9ea492a42d214 Mon Sep 17 00:00:00 2001 From: "Nyoman Jyotisa (via MelvinBot)" Date: Fri, 28 Aug 2026 11:07:30 +0000 Subject: [PATCH 7/9] Extract renderPlainCopy into a WrappingText component Co-authored-by: Nyoman Jyotisa --- .../addNew/ImportFromFileStep.tsx | 29 +++----------- .../companyCards/addNew/WrappingText.tsx | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+), 24 deletions(-) create mode 100644 src/pages/workspace/companyCards/addNew/WrappingText.tsx diff --git a/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx b/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx index cc032d79349d..6a07d5261373 100644 --- a/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx +++ b/src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx @@ -31,6 +31,8 @@ import {useRoute} from '@react-navigation/native'; import React, {useState} from 'react'; import {View} from 'react-native'; +import WrappingText from './WrappingText'; + // cspell:disable // Example CSV shared with customers so they can see how to structure a company card import file. // Sourced from the Expensify Classic "Manage Company Cards" help article. @@ -66,27 +68,6 @@ function ImportFromFileStep() { localFileDownload(CSV_TEMPLATE_FILE_NAME, CSV_TEMPLATE_CONTENT, translate); }; - // The help text mixes plain copy with two tappable links (a client-side template download and an external help guide). - // On Android, a link nested inline inside a becomes a ClickableSpan whose touch area is limited to the glyph bounds, - // which makes it unreliable to tap (e.g. at the minimum device font size). Rendering each link as its own PressableWithoutFeedback - // gives it a real native touch target, while splitting the plain copy into words keeps the paragraph flowing/wrapping naturally. - const renderPlainCopy = (text: string) => - // Keep each word (with its own leading/trailing whitespace) as a separate node so the paragraph wraps in the flexWrap row. - // Preserving the run's own spacing means locales that don't use spaces around the links (e.g. Japanese, Chinese) aren't - // given extra spaces the translation never intended. - (text.match(/\s*\S+\s*/g) ?? []).map((word, wordIndex) => ( - - {word} - - )); - const navigateToImport = () => { if (!companyCardLayoutName.trim()) { setHasError(true); @@ -114,7 +95,7 @@ function ImportFromFileStep() { addBottomSafeAreaPadding > - {renderPlainCopy(translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionStart'))} + {translate('workspace.companyCards.addNewCard.createFileFeedHelpText.templateLink')} - {renderPlainCopy(translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionMiddle'))} + {translate('workspace.companyCards.addNewCard.createFileFeedHelpText.helpGuideLink')} - {renderPlainCopy(translate('workspace.companyCards.addNewCard.createFileFeedHelpText.instructionEnd'))} + becomes a ClickableSpan whose touch area is limited to the glyph bounds, +// which makes it unreliable to tap (e.g. at the minimum device font size). The links are therefore rendered as their own +// PressableWithoutFeedback nodes inside a flexWrap row, and this component renders the plain copy between them as a sequence +// of per-word nodes so the paragraph still flows and wraps naturally across the row. +function WrappingText({text}: WrappingTextProps) { + const styles = useThemeStyles(); + + // Keep each word (with its own leading/trailing whitespace) as a separate node so the paragraph wraps in the flexWrap row. + // Preserving the run's own spacing means locales that don't use spaces around the links (e.g. Japanese, Chinese) aren't + // given extra spaces the translation never intended. + return (text.match(/\s*\S+\s*/g) ?? []).map((word, wordIndex) => ( + + {word} + + )); +} + +WrappingText.displayName = 'WrappingText'; + +export default WrappingText; +export type {WrappingTextProps}; From 7596d452053881bb6f942a6ee54b14854edbb6ce Mon Sep 17 00:00:00 2001 From: "Nyoman Jyotisa (via MelvinBot)" Date: Fri, 28 Aug 2026 12:05:31 +0000 Subject: [PATCH 8/9] Fix WrappingText.tsx Oxfmt import order and knip unused export, add doc header Co-authored-by: Nyoman Jyotisa --- .../companyCards/addNew/WrappingText.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/pages/workspace/companyCards/addNew/WrappingText.tsx b/src/pages/workspace/companyCards/addNew/WrappingText.tsx index c8d0fa6e8d85..2e045bc5471c 100644 --- a/src/pages/workspace/companyCards/addNew/WrappingText.tsx +++ b/src/pages/workspace/companyCards/addNew/WrappingText.tsx @@ -1,19 +1,25 @@ -import React from 'react'; - import Text from '@components/Text'; import useThemeStyles from '@hooks/useThemeStyles'; +import React from 'react'; + type WrappingTextProps = { /** Plain copy to render, split into per-word nodes so it wraps naturally inside a flexWrap row. */ text: string; }; -// The help text mixes plain copy with tappable links (e.g. a client-side template download and an external help guide). -// On Android, a link nested inline inside a becomes a ClickableSpan whose touch area is limited to the glyph bounds, -// which makes it unreliable to tap (e.g. at the minimum device font size). The links are therefore rendered as their own -// PressableWithoutFeedback nodes inside a flexWrap row, and this component renders the plain copy between them as a sequence -// of per-word nodes so the paragraph still flows and wraps naturally across the row. +/** + * Renders plain copy as a sequence of per-word nodes so it flows and wraps naturally inside a + * flexWrap row alongside tappable link nodes. + * + * The company-card CSV import help text mixes plain copy with tappable links (a client-side template + * download and an external help guide). On Android, a link nested inline inside a becomes a + * ClickableSpan whose touch area is limited to the glyph bounds, which makes it unreliable to tap (e.g. + * at the minimum device font size). The links are therefore rendered as their own PressableWithoutFeedback + * nodes inside a flexWrap row, and this component renders the plain copy between them as per-word + * nodes so the paragraph still flows and wraps naturally across the row. + */ function WrappingText({text}: WrappingTextProps) { const styles = useThemeStyles(); @@ -37,4 +43,3 @@ function WrappingText({text}: WrappingTextProps) { WrappingText.displayName = 'WrappingText'; export default WrappingText; -export type {WrappingTextProps}; From 1f8e1e8c9b53db79a4747ad4cf646a4dc7b6de29 Mon Sep 17 00:00:00 2001 From: "Nyoman Jyotisa (via MelvinBot)" Date: Fri, 28 Aug 2026 12:31:14 +0000 Subject: [PATCH 9/9] Add file description header above WrappingTextProps Co-authored-by: Nyoman Jyotisa --- src/pages/workspace/companyCards/addNew/WrappingText.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/workspace/companyCards/addNew/WrappingText.tsx b/src/pages/workspace/companyCards/addNew/WrappingText.tsx index 2e045bc5471c..a6b5e663aec3 100644 --- a/src/pages/workspace/companyCards/addNew/WrappingText.tsx +++ b/src/pages/workspace/companyCards/addNew/WrappingText.tsx @@ -4,6 +4,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import React from 'react'; +// Renders plain paragraph copy as per-word nodes so it flows and wraps naturally inside a flexWrap +// row alongside tappable link nodes. Used by the company-card CSV import help text (see JSDoc below). + type WrappingTextProps = { /** Plain copy to render, split into per-word nodes so it wraps naturally inside a flexWrap row. */ text: string;