Skip to content

Commit caf881b

Browse files
Merge pull request Expensify#81738 from MobileMage/fix/81022-ios-tags-text-overlap
Fix iOS multi-level tags text overlap in WorkspaceTagsPage
2 parents 818bf17 + 4eee29e commit caf881b

4 files changed

Lines changed: 59 additions & 0 deletions

File tree

src/components/RenderHTML.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, {useMemo} from 'react';
22
import {RenderHTMLConfigProvider, RenderHTMLSource} from 'react-native-render-html';
33
import type {RenderersProps} from 'react-native-render-html';
4+
import useHasTextAncestor from '@hooks/useHasTextAncestor';
45
import useWindowDimensions from '@hooks/useWindowDimensions';
56
import Parser from '@libs/Parser';
67

@@ -22,6 +23,11 @@ type RenderHTMLProps = {
2223
// context to RenderHTMLSource components. See https://git.io/JRcZb
2324
// The provider is available at src/components/HTMLEngineProvider/
2425
function RenderHTML({html: htmlParam, onLinkPress, isSelectable}: RenderHTMLProps) {
26+
const hasTextAncestor = useHasTextAncestor();
27+
if (__DEV__ && hasTextAncestor) {
28+
throw new Error('RenderHTML must not be rendered inside a <Text> component, as it will break the layout on iOS. Render it as a sibling instead.');
29+
}
30+
2531
const {windowWidth} = useWindowDimensions();
2632
const html = useMemo(() => {
2733
return (
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {useContext} from 'react';
2+
// eslint-disable-next-line no-restricted-imports
3+
import {unstable_TextAncestorContext as TextAncestorContext} from 'react-native';
4+
5+
export default function useHasTextAncestor(): boolean {
6+
return useContext(TextAncestorContext);
7+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// TextAncestorContext is not available on web (react-native-web), so always return false.
2+
export default function useHasTextAncestor(): boolean {
3+
return false;
4+
}

tests/unit/RenderHTMLTest.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {render} from '@testing-library/react-native';
2+
import React from 'react';
3+
import {View} from 'react-native';
4+
import Text from '@components/Text';
5+
import RenderHTML from '@components/RenderHTML';
6+
7+
jest.mock('@hooks/useWindowDimensions', () => () => ({windowWidth: 400}));
8+
jest.mock('react-native-render-html', () => {
9+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
10+
const {View: MockView} = require('react-native');
11+
return {
12+
RenderHTMLConfigProvider: ({children}: {children: React.ReactNode}) => children,
13+
RenderHTMLSource: () => <MockView />,
14+
};
15+
});
16+
17+
const mockUseHasTextAncestor = jest.fn(() => false);
18+
jest.mock('@hooks/useHasTextAncestor', () => () => mockUseHasTextAncestor());
19+
20+
describe('RenderHTML', () => {
21+
it('throws when rendered inside a Text ancestor', () => {
22+
mockUseHasTextAncestor.mockReturnValue(true);
23+
expect(() =>
24+
render(
25+
<Text>
26+
<RenderHTML html="<p>test</p>" />
27+
</Text>,
28+
),
29+
).toThrow('RenderHTML must not be rendered inside a <Text> component');
30+
});
31+
32+
it('does not throw when rendered outside a Text ancestor', () => {
33+
mockUseHasTextAncestor.mockReturnValue(false);
34+
expect(() =>
35+
render(
36+
<View>
37+
<RenderHTML html="<p>test</p>" />
38+
</View>,
39+
),
40+
).not.toThrow();
41+
});
42+
});

0 commit comments

Comments
 (0)