From ebfe9d57834695dbb6271e20ffa3ddf3f14c1c07 Mon Sep 17 00:00:00 2001 From: Jakub Magiera Date: Wed, 8 Jul 2026 15:40:27 +0200 Subject: [PATCH 01/13] render report name as HTML conditionally --- .../MoneyRequestConfirmationList/sections/ReportField.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList/sections/ReportField.tsx b/src/components/MoneyRequestConfirmationList/sections/ReportField.tsx index 379a8574dc3c..5ab075d1325f 100644 --- a/src/components/MoneyRequestConfirmationList/sections/ReportField.tsx +++ b/src/components/MoneyRequestConfirmationList/sections/ReportField.tsx @@ -7,6 +7,7 @@ import useReportAttributes from '@hooks/useReportAttributes'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import Parser from '@libs/Parser'; import {getReportName} from '@libs/ReportNameUtils'; import {generateReportID, getOutstandingReportsForUser, isMoneyRequestReport, isReportOutstanding} from '@libs/ReportUtils'; @@ -115,6 +116,8 @@ function ReportField({selectedParticipants, iouType, reportID, reportActionID, a return name; })(); + const shouldRenderReportNameAsHTML = Parser.isHTML(reportName); + const outstandingReports = useOutstandingReports(undefined, isFromGlobalCreate && !isPerDiemRequest ? undefined : policyID, ownerAccountID, false); // When creating an expense in an individual report, the report field becomes read-only // since the destination is already determined and there's no need to show a selectable list. @@ -134,7 +137,7 @@ function ReportField({selectedParticipants, iouType, reportID, reportActionID, a Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_REPORT.getRoute(action, iouType, transactionID, selectedReportID, Navigation.getActiveRoute(), reportActionID)); }} interactive={shouldReportBeEditable} - shouldRenderAsHTML + shouldRenderAsHTML={shouldRenderReportNameAsHTML} sentryLabel={CONST.SENTRY_LABEL.REQUEST_CONFIRMATION_LIST.REPORT_FIELD} /> ); From 8a501ab69ca509ef59321242abfcac2360aac551 Mon Sep 17 00:00:00 2001 From: Jakub Magiera Date: Tue, 14 Jul 2026 14:53:08 +0200 Subject: [PATCH 02/13] render report name as HTML conditionally --- src/components/MenuItem.tsx | 12 +++++++----- .../sections/ReportField.tsx | 5 +---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index a02078362778..e5b19964919b 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -748,9 +748,11 @@ function MenuItem({ return Parser.replace(helperText, {shouldEscapeText}); }, [helperText, shouldParseHelperText, shouldEscapeText]); + const shouldRenderTitleAsHTML = shouldRenderAsHTML && !!title && Parser.isHTML(title); + const processedTitle = useMemo(() => { let titleToWrap = ''; - if (shouldRenderAsHTML) { + if (shouldRenderTitleAsHTML) { titleToWrap = title ?? ''; } @@ -764,7 +766,7 @@ function MenuItem({ } return titleToWrap ? `${titleToWrap}` : ''; - }, [title, shouldRenderAsHTML, shouldParseTitle, characterLimit, shouldTruncateTitle, html]); + }, [title, shouldRenderTitleAsHTML, shouldParseTitle, characterLimit, shouldTruncateTitle, html]); const processedHelperText = useMemo(() => { let textToWrap = ''; @@ -783,7 +785,7 @@ function MenuItem({ return ( - {!!title && (shouldRenderAsHTML || (shouldParseTitle && !!html.length)) && ( + {!!title && (shouldRenderTitleAsHTML || (shouldParseTitle && !!html.length)) && ( )} - {!shouldRenderAsHTML && !shouldParseTitle && !!title && ( + {!shouldRenderTitleAsHTML && !shouldParseTitle && !!title && ( ); From 37e5fd5c968448106cc3d553b51508aad9ed5209 Mon Sep 17 00:00:00 2001 From: Jakub Magiera Date: Fri, 17 Jul 2026 08:34:32 +0200 Subject: [PATCH 03/13] add tests for HTML rendering, title truncation and styling consistency --- tests/ui/components/MenuItemTest.tsx | 86 ++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/tests/ui/components/MenuItemTest.tsx b/tests/ui/components/MenuItemTest.tsx index 71aaa4b5c858..e4e1c0483733 100644 --- a/tests/ui/components/MenuItemTest.tsx +++ b/tests/ui/components/MenuItemTest.tsx @@ -1,5 +1,6 @@ import {render, screen} from '@testing-library/react-native'; +import HTMLEngineProvider from '@components/HTMLEngineProvider'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import MenuItem from '@components/MenuItem'; @@ -8,9 +9,11 @@ import getPlatform from '@libs/getPlatform'; import CONST from '@src/CONST'; +import type {TextStyle} from 'react-native'; import type {SvgProps} from 'react-native-svg'; import React from 'react'; +import {StyleSheet} from 'react-native'; import {translateLocal} from '../../utils/TestHelper'; @@ -41,6 +44,89 @@ describe('MenuItem', () => { mockedGetOperatingSystem.mockReturnValue(CONST.OS.WINDOWS); }); + describe('HTML rendering', () => { + it('truncates a plain-text title when HTML rendering is requested', () => { + render( + + + + + , + ); + + expect(screen.getByText('0123456789...')).toBeOnTheScreen(); + }); + + it('uses the same default line limit for plain-text and HTML titles', () => { + const plainTitle = 'Weekly expenses for the marketing team'; + const htmlTitle = 'Weekly expenses for marketing & sales'; + + const {rerender} = render( + + + + + , + ); + const plainLimit = (screen.getByText(plainTitle).props as {numberOfLines?: number}).numberOfLines; + + rerender( + + + + + , + ); + const htmlLimit = (screen.getByText('Weekly expenses for marketing & sales').props as {numberOfLines?: number}).numberOfLines; + + expect(htmlLimit).toBe(plainLimit); + }); + + it('uses the same font style for plain-text and HTML titles', () => { + const getFontStyle = (text: string) => { + const style = StyleSheet.flatten(screen.getByText(text).props.style as TextStyle); + return {fontFamily: style.fontFamily, fontWeight: style.fontWeight}; + }; + + const {rerender} = render( + + + + + , + ); + const plainFontStyle = getFontStyle('Weekly expenses'); + + rerender( + + + + + , + ); + const htmlFontStyle = getFontStyle("Bob's expenses"); + + expect(htmlFontStyle).toEqual(plainFontStyle); + }); + }); + describe('accessibility label with NewWindow icon', () => { it('appends "Opens in a new tab" to the accessibility label when iconRight is NewWindow', () => { const customLabel = 'Open external link'; From 5394e446d26524ed255fc0d6c11bd803f07030d2 Mon Sep 17 00:00:00 2001 From: Jakub Magiera Date: Fri, 17 Jul 2026 08:35:06 +0200 Subject: [PATCH 04/13] update conditions for rendering HTML titles --- src/components/MenuItem.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index e5b19964919b..a5a64a7103ed 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -752,7 +752,7 @@ function MenuItem({ const processedTitle = useMemo(() => { let titleToWrap = ''; - if (shouldRenderTitleAsHTML) { + if (shouldRenderAsHTML) { titleToWrap = title ?? ''; } @@ -766,7 +766,9 @@ function MenuItem({ } return titleToWrap ? `${titleToWrap}` : ''; - }, [title, shouldRenderTitleAsHTML, shouldParseTitle, characterLimit, shouldTruncateTitle, html]); + }, [title, shouldRenderAsHTML, shouldParseTitle, characterLimit, shouldTruncateTitle, html]); + + const processedPlainTextTitle = !shouldRenderAsHTML || shouldRenderTitleAsHTML || shouldParseTitle ? '' : Parser.htmlToText(processedTitle); const processedHelperText = useMemo(() => { let textToWrap = ''; @@ -1043,12 +1045,16 @@ function MenuItem({ style={[styles.flexRow, styles.alignItemsCenter, styles.mw100, titleWrapperStyle]} fsClass={forwardedFSClass} > - {!!title && (shouldRenderTitleAsHTML || (shouldParseTitle && !!html.length)) && ( + {!!title && (shouldRenderAsHTML || (shouldParseTitle && !!html.length)) && ( - + {shouldRenderTitleAsHTML || shouldParseTitle ? ( + + ) : ( + {processedPlainTextTitle} + )} )} - {!shouldRenderTitleAsHTML && !shouldParseTitle && !!title && ( + {!shouldRenderAsHTML && !shouldParseTitle && !!title && ( Date: Fri, 17 Jul 2026 11:15:03 +0200 Subject: [PATCH 05/13] revert adding tests --- tests/ui/components/MenuItemTest.tsx | 86 ---------------------------- 1 file changed, 86 deletions(-) diff --git a/tests/ui/components/MenuItemTest.tsx b/tests/ui/components/MenuItemTest.tsx index e4e1c0483733..71aaa4b5c858 100644 --- a/tests/ui/components/MenuItemTest.tsx +++ b/tests/ui/components/MenuItemTest.tsx @@ -1,6 +1,5 @@ import {render, screen} from '@testing-library/react-native'; -import HTMLEngineProvider from '@components/HTMLEngineProvider'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import MenuItem from '@components/MenuItem'; @@ -9,11 +8,9 @@ import getPlatform from '@libs/getPlatform'; import CONST from '@src/CONST'; -import type {TextStyle} from 'react-native'; import type {SvgProps} from 'react-native-svg'; import React from 'react'; -import {StyleSheet} from 'react-native'; import {translateLocal} from '../../utils/TestHelper'; @@ -44,89 +41,6 @@ describe('MenuItem', () => { mockedGetOperatingSystem.mockReturnValue(CONST.OS.WINDOWS); }); - describe('HTML rendering', () => { - it('truncates a plain-text title when HTML rendering is requested', () => { - render( - - - - - , - ); - - expect(screen.getByText('0123456789...')).toBeOnTheScreen(); - }); - - it('uses the same default line limit for plain-text and HTML titles', () => { - const plainTitle = 'Weekly expenses for the marketing team'; - const htmlTitle = 'Weekly expenses for marketing & sales'; - - const {rerender} = render( - - - - - , - ); - const plainLimit = (screen.getByText(plainTitle).props as {numberOfLines?: number}).numberOfLines; - - rerender( - - - - - , - ); - const htmlLimit = (screen.getByText('Weekly expenses for marketing & sales').props as {numberOfLines?: number}).numberOfLines; - - expect(htmlLimit).toBe(plainLimit); - }); - - it('uses the same font style for plain-text and HTML titles', () => { - const getFontStyle = (text: string) => { - const style = StyleSheet.flatten(screen.getByText(text).props.style as TextStyle); - return {fontFamily: style.fontFamily, fontWeight: style.fontWeight}; - }; - - const {rerender} = render( - - - - - , - ); - const plainFontStyle = getFontStyle('Weekly expenses'); - - rerender( - - - - - , - ); - const htmlFontStyle = getFontStyle("Bob's expenses"); - - expect(htmlFontStyle).toEqual(plainFontStyle); - }); - }); - describe('accessibility label with NewWindow icon', () => { it('appends "Opens in a new tab" to the accessibility label when iconRight is NewWindow', () => { const customLabel = 'Open external link'; From aa15fb5328276ee7562bcd56dcac9f671c9d474c Mon Sep 17 00:00:00 2001 From: Jakub Magiera <97454980+jmgraa@users.noreply.github.com> Date: Mon, 20 Jul 2026 08:25:45 +0200 Subject: [PATCH 06/13] simplify condition for processedPlainTextTitle Co-authored-by: Dariusz Biela --- src/components/MenuItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index a5a64a7103ed..c8f1345d42ab 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -768,7 +768,7 @@ function MenuItem({ return titleToWrap ? `${titleToWrap}` : ''; }, [title, shouldRenderAsHTML, shouldParseTitle, characterLimit, shouldTruncateTitle, html]); - const processedPlainTextTitle = !shouldRenderAsHTML || shouldRenderTitleAsHTML || shouldParseTitle ? '' : Parser.htmlToText(processedTitle); + const processedPlainTextTitle = shouldRenderAsHTML && !shouldRenderTitleAsHTML && !shouldParseTitle ? Parser.htmlToText(processedTitle) : ''; const processedHelperText = useMemo(() => { let textToWrap = ''; From 57b822a9dcae06a3b507c9e75749fea8b945dffa Mon Sep 17 00:00:00 2001 From: Jakub Magiera <97454980+jmgraa@users.noreply.github.com> Date: Mon, 20 Jul 2026 08:26:47 +0200 Subject: [PATCH 07/13] add explanation comment to shouldRenderAsHTML Co-authored-by: Dariusz Biela --- src/components/MenuItem.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index c8f1345d42ab..1033ae8d9d53 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -296,7 +296,8 @@ type MenuItemBaseProps = ForwardedFSClassProps & brickRoadIndicator?: ValueOf; /** Should render the content in HTML format */ - shouldRenderAsHTML?: boolean; + /** Should render the content in HTML format. A title without HTML content is rendered as plain text even when this is set. */ +shouldRenderAsHTML?: boolean; /** Whether or not the text should be escaped */ shouldEscapeText?: boolean; From 98704c0f36a6586f37601e082b68271c8c682212 Mon Sep 17 00:00:00 2001 From: Jakub Magiera Date: Mon, 20 Jul 2026 08:34:06 +0200 Subject: [PATCH 08/13] remove redundant processedPlainTextTitle variable --- src/components/MenuItem.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 1033ae8d9d53..fc4d7bfd45c9 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -296,8 +296,8 @@ type MenuItemBaseProps = ForwardedFSClassProps & brickRoadIndicator?: ValueOf; /** Should render the content in HTML format */ - /** Should render the content in HTML format. A title without HTML content is rendered as plain text even when this is set. */ -shouldRenderAsHTML?: boolean; + /** Should render the content in HTML format. A title without HTML content is rendered as plain text even when this is set. */ + shouldRenderAsHTML?: boolean; /** Whether or not the text should be escaped */ shouldEscapeText?: boolean; @@ -769,8 +769,6 @@ function MenuItem({ return titleToWrap ? `${titleToWrap}` : ''; }, [title, shouldRenderAsHTML, shouldParseTitle, characterLimit, shouldTruncateTitle, html]); - const processedPlainTextTitle = shouldRenderAsHTML && !shouldRenderTitleAsHTML && !shouldParseTitle ? Parser.htmlToText(processedTitle) : ''; - const processedHelperText = useMemo(() => { let textToWrap = ''; @@ -1051,7 +1049,7 @@ function MenuItem({ {shouldRenderTitleAsHTML || shouldParseTitle ? ( ) : ( - {processedPlainTextTitle} + {Parser.htmlToText(processedTitle)} )} )} From 39e136bd2dd2e5a69e3c3d9150b523ba5c44b84b Mon Sep 17 00:00:00 2001 From: Jakub Magiera Date: Mon, 20 Jul 2026 08:40:43 +0200 Subject: [PATCH 09/13] explain plain-text title rendering --- src/components/MenuItem.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index fc4d7bfd45c9..4a296050a605 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -1046,6 +1046,9 @@ function MenuItem({ > {!!title && (shouldRenderAsHTML || (shouldParseTitle && !!html.length)) && ( + {/* Use Text instead of RenderHTML when the title is plain text. + Titles with shouldRenderAsHTML use baseFontStyle, which differs from combinedTitleTextStyle below. + */} {shouldRenderTitleAsHTML || shouldParseTitle ? ( ) : ( From f896edbcbf38403f806beaa478070157ca61fd00 Mon Sep 17 00:00:00 2001 From: Jakub Magiera Date: Mon, 20 Jul 2026 12:52:30 +0200 Subject: [PATCH 10/13] remove additional comment --- src/components/MenuItem.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 4a296050a605..f24901077212 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -295,7 +295,6 @@ type MenuItemBaseProps = ForwardedFSClassProps & /** The type of brick road indicator to show. */ brickRoadIndicator?: ValueOf; - /** Should render the content in HTML format */ /** Should render the content in HTML format. A title without HTML content is rendered as plain text even when this is set. */ shouldRenderAsHTML?: boolean; From a8229ce1b65d5ea7db132586ccf81c7f035cdf00 Mon Sep 17 00:00:00 2001 From: Jakub Magiera Date: Mon, 20 Jul 2026 13:12:59 +0200 Subject: [PATCH 11/13] preserve Android LTR conversion for plain HTML titles --- src/components/MenuItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index f24901077212..35d1f71f06a1 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -1051,7 +1051,7 @@ function MenuItem({ {shouldRenderTitleAsHTML || shouldParseTitle ? ( ) : ( - {Parser.htmlToText(processedTitle)} + {convertToLTR(Parser.htmlToText(processedTitle))} )} )} From 80e5f14018b4260dc19608efe53758fd4c7b059f Mon Sep 17 00:00:00 2001 From: Jakub Magiera Date: Mon, 20 Jul 2026 13:28:32 +0200 Subject: [PATCH 12/13] skip RenderHTML for plain MenuItem titles only --- src/components/MenuItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 35d1f71f06a1..12def19db2e2 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -785,7 +785,7 @@ function MenuItem({ return ( Date: Wed, 22 Jul 2026 08:45:02 +0200 Subject: [PATCH 13/13] revert shouldParseFullTitle condition --- src/components/MenuItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 7bbfbad77bc9..da019c2cd02e 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -789,7 +789,7 @@ function MenuItem({ return (