From b8cc41818539d02711c7282b6a9f6ec7797a1348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Wed, 29 Apr 2026 11:40:16 +0200 Subject: [PATCH 1/9] POC adding new ul and li renderers --- .../BaseHTMLEngineProvider.tsx | 9 +++++++++ .../HTMLRenderers/ULRenderer.tsx | 19 +++++++++++++++++++ .../HTMLEngineProvider/HTMLRenderers/index.ts | 3 +++ src/components/RenderHTML.tsx | 3 +++ src/languages/en.ts | 6 +++--- 5 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index 40207e0e3894..2e549d9bfe72 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -186,6 +186,15 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim tagName: 'bullet-item', contentModel: HTMLContentModel.block, }), + ul: HTMLElementModel.fromCustomModel({ + tagName: 'ul', + contentModel: HTMLContentModel.block, + mixedUAStyles: {marginVertical: 12}, + }), + li: HTMLElementModel.fromCustomModel({ + tagName: 'li', + contentModel: HTMLContentModel.block, + }), 'sparkles-icon': HTMLElementModel.fromCustomModel({ tagName: 'sparkles-icon', contentModel: HTMLContentModel.mixed, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx new file mode 100644 index 000000000000..e908e75410c5 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {CustomRendererProps, TBlock} from 'react-native-render-html'; +import {TNodeChildrenRenderer} from 'react-native-render-html'; + +/** + * Bypasses the library's internal ULRenderer (which wraps children in MarkedListItem) + * and renders ul as a plain block container — matching how bullet-list works. + * The actual bullet markers are drawn by the li renderer (BulletItemRenderer). + */ +function ULRenderer({tnode, style}: CustomRendererProps) { + return ( + + + + ); +} + +export default ULRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts index 3d91670c38e7..2031cd8de1bb 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts @@ -19,6 +19,7 @@ import ShortMentionRenderer from './ShortMentionRenderer'; import SparklesIconRenderer from './SparklesIconRenderer'; import TaskTitleRenderer from './TaskTitleRenderer'; import TransactionHistoryLinkRenderer from './TransactionHistoryLinkRenderer'; +import ULRenderer from './ULRenderer'; import UserDetailsRenderer from './UserDetailsRenderer'; import VideoRenderer from './VideoRenderer'; @@ -30,6 +31,8 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = { a: AnchorRenderer, code: CodeRenderer, img: ImageRenderer, + li: BulletItemRenderer, + ul: ULRenderer, video: VideoRenderer, // Custom tag renderers diff --git a/src/components/RenderHTML.tsx b/src/components/RenderHTML.tsx index ababd16463b8..ddd1ff67c368 100644 --- a/src/components/RenderHTML.tsx +++ b/src/components/RenderHTML.tsx @@ -6,6 +6,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import Parser from '@libs/Parser'; import BulletItemRenderer from './HTMLEngineProvider/HTMLRenderers/BulletItemRenderer'; import SparklesIconRenderer from './HTMLEngineProvider/HTMLRenderers/SparklesIconRenderer'; +import ULRenderer from './HTMLEngineProvider/HTMLRenderers/ULRenderer'; type LinkPressHandler = NonNullable['onPress']; @@ -55,6 +56,8 @@ function RenderHTML({html: htmlParam, onLinkPress, isSelectable}: RenderHTMLProp /* eslint-disable @typescript-eslint/naming-convention */ 'bullet-item': BulletItemRenderer, 'sparkles-icon': SparklesIconRenderer, + li: BulletItemRenderer, + ul: ULRenderer, }; const htmlSource = ( diff --git a/src/languages/en.ts b/src/languages/en.ts index e9730712658c..3a4acf369ceb 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9079,7 +9079,7 @@ const translations = { resetDomain: 'Reset domain', resetDomainExplanation: ({domainName}: {domainName?: string}) => `Please type ${domainName} to confirm the domain reset.`, enterDomainName: 'Enter your domain name here', - resetDomainInfo: `This action is permanent and the following data will be deleted:
Company card connections and any unreported expenses from those cardsSAML and group settings All accounts, workspaces, reports, expenses, and other data will remain.

Note: You can clear this domain from your domains list by removing the associated email from your contact methods.`, + resetDomainInfo: `This action is permanent and the following data will be deleted:
  • Company card connections and any unreported expenses from those cards
  • SAML and group settings
All accounts, workspaces, reports, expenses, and other data will remain.

Note: You can clear this domain from your domains list by removing the associated email from your contact methods.`, }, domainMembers: 'Domain members', members: { @@ -9102,8 +9102,8 @@ const translations = { other: 'Close accounts safely', }), closeAccountInfo: () => ({ - one: 'We recommend closing the account safely to skip closing it in case there are: Pending approvalsActive reimbursementsNo alternative login methodsOtherwise, you can ignore the safety precautions above and force close the selected account.', - other: 'We recommend closing the accounts safely to skip closing it in case there are: Pending approvalsActive reimbursementsNo alternative login methodsOtherwise, you can ignore the safety precautions above and force close the selected accounts.', + one: 'We recommend closing the account safely to skip closing it in case there are:
  • Pending approvals
  • Active reimbursements
  • No alternative login methods


Otherwise, you can ignore the safety precautions above and force close the selected account.', + other: 'We recommend closing the accounts safely to skip closing it in case there are:
  • Pending approvals
  • Active reimbursements
  • No alternative login methods
Otherwise, you can ignore the safety precautions above and force close the selected accounts.', }), closeAccount: () => ({ one: 'Close account', From 90452d0a9286031f3d72d08168a46b3cc605ac95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Wed, 29 Apr 2026 11:54:26 +0200 Subject: [PATCH 2/9] strip extra brs, revert translation --- src/components/RenderHTML.tsx | 3 +++ src/languages/en.ts | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/RenderHTML.tsx b/src/components/RenderHTML.tsx index ddd1ff67c368..86bf9b181705 100644 --- a/src/components/RenderHTML.tsx +++ b/src/components/RenderHTML.tsx @@ -41,6 +41,9 @@ function RenderHTML({html: htmlParam, onLinkPress, isSelectable}: RenderHTMLProp // Remove double tag if exists and keep the outermost tag (always the original tag). .replaceAll(/(]*>)(?:]*>)+/g, '$1') .replaceAll(/(<\/emoji[^>]*>)(?:<\/emoji[^>]*>)+/g, '$1') + // Strip orphaned
tags inside
    that would render as extra empty bullets + .replace(/\s*(<\/ul>)/gi, '$1') + .replace(/(<\/li>)\s*\s*(?=<(?:li|\/ul)>)/gi, '$1') ); }, [htmlParam]); diff --git a/src/languages/en.ts b/src/languages/en.ts index 3a4acf369ceb..e9730712658c 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9079,7 +9079,7 @@ const translations = { resetDomain: 'Reset domain', resetDomainExplanation: ({domainName}: {domainName?: string}) => `Please type ${domainName} to confirm the domain reset.`, enterDomainName: 'Enter your domain name here', - resetDomainInfo: `This action is permanent and the following data will be deleted:
    • Company card connections and any unreported expenses from those cards
    • SAML and group settings
    All accounts, workspaces, reports, expenses, and other data will remain.

    Note: You can clear this domain from your domains list by removing the associated email from your contact methods.`, + resetDomainInfo: `This action is permanent and the following data will be deleted:
    Company card connections and any unreported expenses from those cardsSAML and group settings All accounts, workspaces, reports, expenses, and other data will remain.

    Note: You can clear this domain from your domains list by removing the associated email from your contact methods.`, }, domainMembers: 'Domain members', members: { @@ -9102,8 +9102,8 @@ const translations = { other: 'Close accounts safely', }), closeAccountInfo: () => ({ - one: 'We recommend closing the account safely to skip closing it in case there are:
    • Pending approvals
    • Active reimbursements
    • No alternative login methods


    Otherwise, you can ignore the safety precautions above and force close the selected account.', - other: 'We recommend closing the accounts safely to skip closing it in case there are:
    • Pending approvals
    • Active reimbursements
    • No alternative login methods
    Otherwise, you can ignore the safety precautions above and force close the selected accounts.', + one: 'We recommend closing the account safely to skip closing it in case there are: Pending approvalsActive reimbursementsNo alternative login methodsOtherwise, you can ignore the safety precautions above and force close the selected account.', + other: 'We recommend closing the accounts safely to skip closing it in case there are: Pending approvalsActive reimbursementsNo alternative login methodsOtherwise, you can ignore the safety precautions above and force close the selected accounts.', }), closeAccount: () => ({ one: 'Close account', From d6ec5b14ca8d692d158d97d7236a617423838a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Wed, 29 Apr 2026 12:11:25 +0200 Subject: [PATCH 3/9] fix lint --- src/components/RenderHTML.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/RenderHTML.tsx b/src/components/RenderHTML.tsx index 86bf9b181705..421382aabdd3 100644 --- a/src/components/RenderHTML.tsx +++ b/src/components/RenderHTML.tsx @@ -42,8 +42,8 @@ function RenderHTML({html: htmlParam, onLinkPress, isSelectable}: RenderHTMLProp .replaceAll(/(]*>)(?:]*>)+/g, '$1') .replaceAll(/(<\/emoji[^>]*>)(?:<\/emoji[^>]*>)+/g, '$1') // Strip orphaned
    tags inside
      that would render as extra empty bullets - .replace(/\s*(<\/ul>)/gi, '$1') - .replace(/(<\/li>)\s*\s*(?=<(?:li|\/ul)>)/gi, '$1') + .replaceAll(/\s*(<\/ul>)/gi, '$1') + .replaceAll(/(<\/li>)\s*\s*(?=<(?:li|\/ul)>)/gi, '$1') ); }, [htmlParam]); From 820e699e6c22ff502d70f7fc197a57a2abf3cd06 Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 6 May 2026 11:16:17 +0200 Subject: [PATCH 4/9] test: add BulletListRendererTest --- tests/unit/BulletListRendererTest.tsx | 90 +++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/unit/BulletListRendererTest.tsx diff --git a/tests/unit/BulletListRendererTest.tsx b/tests/unit/BulletListRendererTest.tsx new file mode 100644 index 000000000000..39cb8b94040a --- /dev/null +++ b/tests/unit/BulletListRendererTest.tsx @@ -0,0 +1,90 @@ +import {render, screen} from '@testing-library/react-native'; +import React from 'react'; +import type {CustomRendererProps, TBlock} from 'react-native-render-html'; +import BulletItemRenderer from '@components/HTMLEngineProvider/HTMLRenderers/BulletItemRenderer'; +import ULRenderer from '@components/HTMLEngineProvider/HTMLRenderers/ULRenderer'; +import RenderHTML from '@components/RenderHTML'; +import CONST from '@src/CONST'; + +jest.mock('@hooks/useWindowDimensions', () => () => ({windowWidth: 400})); +jest.mock('@hooks/useHasTextAncestor', () => () => false); + +// Capture the html string ultimately passed to react-native-render-html so we can +// assert the orphaned
      stripping happens before the library sees the HTML. +const capturedSource: {html?: string} = {}; +jest.mock('react-native-render-html', () => { + const ReactModule = jest.requireActual('react'); + const {View: MockView, Text: MockText} = jest.requireActual<{View: React.ComponentType; Text: React.ComponentType}>('react-native'); + return { + RenderHTMLConfigProvider: ({children}: {children: React.ReactNode}) => children, + RenderHTMLSource: ({source}: {source: {html?: string}}) => { + capturedSource.html = source?.html; + return ReactModule.createElement(MockView); + }, + TNodeChildrenRenderer: ({tnode}: {tnode?: {mockText?: string}}) => ReactModule.createElement(MockText, null, tnode?.mockText ?? ''), + }; +}); + +// Bypass ExpensiMark in these tests — we want to assert the regex stripping logic +// in RenderHTML, not the upstream parser's behavior. +jest.mock('@libs/Parser', () => ({ + __esModule: true, + default: {replace: (html: string) => html}, +})); + +const buildTNode = (text = '') => ({mockText: text}) as unknown as CustomRendererProps['tnode']; + +describe('Bullet list rendering', () => { + beforeEach(() => { + capturedSource.html = undefined; + }); + + describe('ULRenderer', () => { + it('renders its children', () => { + render( + // @ts-expect-error — only the props read by the renderer are needed for this test + , + ); + expect(screen.getByText('child content')).toBeTruthy(); + }); + }); + + describe('BulletItemRenderer (used for both
    • and )', () => { + it('renders a bullet marker next to the item content', () => { + // @ts-expect-error — only the props read by the renderer are needed for this test + render(); + expect(screen.getByText(CONST.DOT_SEPARATOR)).toBeTruthy(); + expect(screen.getByText('First item')).toBeTruthy(); + }); + }); + + describe('RenderHTML strips orphaned
      tags inside
        ', () => { + it('strips
        immediately before
      ', () => { + render(); + expect(capturedSource.html).toBe('
      • One
      • Two
      '); + }); + + it('strips
      (no slash) immediately before
    ', () => { + render(); + expect(capturedSource.html).toBe('
    • One
    • Two
    '); + }); + + it('strips
    appearing between and the next
  • ', () => { + render(); + expect(capturedSource.html).toBe('
    • One
    • Two
    '); + }); + + it('leaves a valid
      /
    • list untouched', () => { + render(); + expect(capturedSource.html).toBe('
      • One
      • Two
      '); + }); + + it('does not strip
      outside of
        lists', () => { + render(); + expect(capturedSource.html).toBe('

        line1
        line2

        '); + }); + }); +}); From bb1e818dcf1eee43f638082034362df127011965 Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 6 May 2026 11:44:30 +0200 Subject: [PATCH 5/9] test: add case with
        inside
      • --- tests/unit/BulletListRendererTest.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit/BulletListRendererTest.tsx b/tests/unit/BulletListRendererTest.tsx index 39cb8b94040a..4012f574bebf 100644 --- a/tests/unit/BulletListRendererTest.tsx +++ b/tests/unit/BulletListRendererTest.tsx @@ -86,5 +86,10 @@ describe('Bullet list rendering', () => { render(); expect(capturedSource.html).toBe('

        line1
        line2

        '); }); + + it('preserves
        that lives inside
      • as an in-bullet line break', () => { + render(); + expect(capturedSource.html).toBe('
        • One
          still one
        • Two
        '); + }); }); }); From d46cead464395e36f62b598106ac463651b89a27 Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 6 May 2026 12:09:03 +0200 Subject: [PATCH 6/9] feat: add gap between list items in ULRenderer --- src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx index e908e75410c5..aa6d62c45a6d 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx @@ -10,7 +10,7 @@ import {TNodeChildrenRenderer} from 'react-native-render-html'; */ function ULRenderer({tnode, style}: CustomRendererProps) { return ( - + ); From ab7b6cd32f32aa87a161210c14f4c908aa5cdb3d Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 7 May 2026 10:38:40 +0200 Subject: [PATCH 7/9] fix: don't render bullets in ordered list --- .../BaseHTMLEngineProvider.tsx | 4 --- .../HTMLRenderers/BulletItemRenderer.tsx | 4 +-- .../HTMLRenderers/ULRenderer.tsx | 28 +++++++++++++++--- .../HTMLEngineProvider/HTMLRenderers/index.ts | 1 - src/components/RenderHTML.tsx | 1 - tests/unit/BulletListRendererTest.tsx | 29 ++++++++++++++++--- 6 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index 5eaf270be3ab..b2708101bd24 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -191,10 +191,6 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim contentModel: HTMLContentModel.block, mixedUAStyles: {marginVertical: 12}, }), - li: HTMLElementModel.fromCustomModel({ - tagName: 'li', - contentModel: HTMLContentModel.block, - }), 'sparkles-icon': HTMLElementModel.fromCustomModel({ tagName: 'sparkles-icon', contentModel: HTMLContentModel.mixed, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/BulletItemRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/BulletItemRenderer.tsx index 540a48015bba..21cc276781f0 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/BulletItemRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/BulletItemRenderer.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import type {CustomRendererProps, TBlock} from 'react-native-render-html'; +import type {TNode} from 'react-native-render-html'; import {TNodeChildrenRenderer} from 'react-native-render-html'; import Text from '@components/Text'; import useTheme from '@hooks/useTheme'; @@ -8,7 +8,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -function BulletItemRenderer({tnode}: CustomRendererProps) { +function BulletItemRenderer({tnode}: {tnode: TNode}) { const styles = useThemeStyles(); const theme = useTheme(); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx index aa6d62c45a6d..1060dc7f4c24 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx @@ -1,17 +1,37 @@ import React from 'react'; import {View} from 'react-native'; import type {CustomRendererProps, TBlock} from 'react-native-render-html'; -import {TNodeChildrenRenderer} from 'react-native-render-html'; +import {TNodeRenderer} from 'react-native-render-html'; +import BulletItemRenderer from './BulletItemRenderer'; /** * Bypasses the library's internal ULRenderer (which wraps children in MarkedListItem) - * and renders ul as a plain block container — matching how bullet-list works. - * The actual bullet markers are drawn by the li renderer (BulletItemRenderer). + * and renders
          as a plain block container that draws bullet markers around each + * direct
        • child — matching how / render.
        • is left + * unregistered globally so that
          1. still uses the library's default numeric markers. */ function ULRenderer({tnode, style}: CustomRendererProps) { return ( - + {tnode.children.map((child, index) => { + const key = `${child.tagName ?? 'node'}-${index}`; + if (child.tagName === 'li') { + return ( + + ); + } + return ( + + ); + })} ); } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts index 2031cd8de1bb..ef1d630ce007 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts @@ -31,7 +31,6 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = { a: AnchorRenderer, code: CodeRenderer, img: ImageRenderer, - li: BulletItemRenderer, ul: ULRenderer, video: VideoRenderer, diff --git a/src/components/RenderHTML.tsx b/src/components/RenderHTML.tsx index 421382aabdd3..4ae470aada04 100644 --- a/src/components/RenderHTML.tsx +++ b/src/components/RenderHTML.tsx @@ -59,7 +59,6 @@ function RenderHTML({html: htmlParam, onLinkPress, isSelectable}: RenderHTMLProp /* eslint-disable @typescript-eslint/naming-convention */ 'bullet-item': BulletItemRenderer, 'sparkles-icon': SparklesIconRenderer, - li: BulletItemRenderer, ul: ULRenderer, }; diff --git a/tests/unit/BulletListRendererTest.tsx b/tests/unit/BulletListRendererTest.tsx index 4012f574bebf..8e18194d3d53 100644 --- a/tests/unit/BulletListRendererTest.tsx +++ b/tests/unit/BulletListRendererTest.tsx @@ -22,6 +22,7 @@ jest.mock('react-native-render-html', () => { return ReactModule.createElement(MockView); }, TNodeChildrenRenderer: ({tnode}: {tnode?: {mockText?: string}}) => ReactModule.createElement(MockText, null, tnode?.mockText ?? ''), + TNodeRenderer: ({tnode}: {tnode?: {mockText?: string}}) => ReactModule.createElement(MockText, null, tnode?.mockText ?? ''), }; }); @@ -33,6 +34,10 @@ jest.mock('@libs/Parser', () => ({ })); const buildTNode = (text = '') => ({mockText: text}) as unknown as CustomRendererProps['tnode']; +const buildULTNode = (children: Array<{tagName: string; text: string}>) => + ({ + children: children.map((child) => ({tagName: child.tagName, mockText: child.text})), + }) as unknown as CustomRendererProps['tnode']; describe('Bullet list rendering', () => { beforeEach(() => { @@ -40,21 +45,37 @@ describe('Bullet list rendering', () => { }); describe('ULRenderer', () => { - it('renders its children', () => { + it('wraps each
          2. child with a bullet marker', () => { render( // @ts-expect-error — only the props read by the renderer are needed for this test , ); - expect(screen.getByText('child content')).toBeTruthy(); + expect(screen.getAllByText(CONST.DOT_SEPARATOR)).toHaveLength(2); + expect(screen.getByText('One')).toBeTruthy(); + expect(screen.getByText('Two')).toBeTruthy(); + }); + + it('renders non-
          3. children with the default node renderer', () => { + render( + // @ts-expect-error — only the props read by the renderer are needed for this test + , + ); + expect(screen.queryByText(CONST.DOT_SEPARATOR)).toBeNull(); + expect(screen.getByText('stray child')).toBeTruthy(); }); }); describe('BulletItemRenderer (used for both
          4. and )', () => { it('renders a bullet marker next to the item content', () => { - // @ts-expect-error — only the props read by the renderer are needed for this test render(); expect(screen.getByText(CONST.DOT_SEPARATOR)).toBeTruthy(); expect(screen.getByText('First item')).toBeTruthy(); From dcc81fdb1bb5e05242c11864388e743e77c0b517 Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 7 May 2026 11:30:37 +0200 Subject: [PATCH 8/9] fix: list content not visible on mobile --- .../HTMLEngineProvider/HTMLRenderers/BulletItemRenderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/BulletItemRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/BulletItemRenderer.tsx index 21cc276781f0..bad8b5579b37 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/BulletItemRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/BulletItemRenderer.tsx @@ -13,7 +13,7 @@ function BulletItemRenderer({tnode}: {tnode: TNode}) { const theme = useTheme(); return ( - + {CONST.DOT_SEPARATOR} From adf4a336a682a53bf4fd2a0a2816912265d4c835 Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 7 May 2026 11:40:54 +0200 Subject: [PATCH 9/9] fix: address codex review --- src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx | 3 ++- .../HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index b2708101bd24..13dc7fbaaebb 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -189,7 +189,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim ul: HTMLElementModel.fromCustomModel({ tagName: 'ul', contentModel: HTMLContentModel.block, - mixedUAStyles: {marginVertical: 12}, + mixedUAStyles: styles.mv3, }), 'sparkles-icon': HTMLElementModel.fromCustomModel({ tagName: 'sparkles-icon', @@ -200,6 +200,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim styles.taskTitleMenuItem, styles.formError, styles.mb0, + styles.mv3, styles.colorMuted, styles.mutedNormalTextLabel, styles.productTrainingTooltipText, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx index 1060dc7f4c24..c42c714c053f 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ULRenderer.tsx @@ -2,6 +2,7 @@ import React from 'react'; import {View} from 'react-native'; import type {CustomRendererProps, TBlock} from 'react-native-render-html'; import {TNodeRenderer} from 'react-native-render-html'; +import useThemeStyles from '@hooks/useThemeStyles'; import BulletItemRenderer from './BulletItemRenderer'; /** @@ -11,8 +12,9 @@ import BulletItemRenderer from './BulletItemRenderer'; * unregistered globally so that
            1. still uses the library's default numeric markers. */ function ULRenderer({tnode, style}: CustomRendererProps) { + const styles = useThemeStyles(); return ( - + {tnode.children.map((child, index) => { const key = `${child.tagName ?? 'node'}-${index}`; if (child.tagName === 'li') {