Repository navigation
Conversation
A nested Text or AnimatedText without a variant applied its fallback font unconditionally, overriding the font, weight, letter spacing and color it should have inherited from the enclosing text. React Native inherits these, so a nested one now declares only its own style. The shared context lives in its own module to keep the Text and AnimatedText imports one-directional. Fixes #4351.
|
A quick note on the Typography work: because a nested |
MikitasK
left a comment
There was a problem hiding this comment.
overall, these changes looks good to me 👌 the approach looks solid & aligns with RN's text inheritance behavior 👍
just a few point to address before merge:
Co-authored-by: Mikita Kliushun <nikita.kliushun@gmail.com>
Co-authored-by: Mikita Kliushun <nikita.kliushun@gmail.com>
|
@MikitasK thanks for the review, your proposed changes sounded good! |
|
@satya164 @artus9033 all yours |
satya164
left a comment
There was a problem hiding this comment.
please remove all LLM generated comments, they are either unnecessary or could be replaced with 1-2 lines describing the purpose.
only add comments where you think it's needed for understanding something and write it by hand.
for tests: add tests where
Textisn't a direct child and can be nested inside another component that's used insideText- child text also sets its variant and theme
this implementation also isn't accurate to what react native does. a View nested inside Text will reset the inheritance, though it might be a reasonable limitation.
| // Anything longer is assumed to contain an element, since an array is an | ||
| // object itself. | ||
| return typeof children === 'object'; |
There was a problem hiding this comment.
this doesn't make much sense. what makes children longer than 3 different?
| expect(screen.getByTestId('child-text')).not.toHaveStyle({ | ||
| color: LightTheme.colors.onSurface, | ||
| }); |
There was a problem hiding this comment.
shouldn't this inherit parent colors?
| it('text outside of another text still gets the default font', async () => { | ||
| await render(<Text testID="lone-text">{content}</Text>); | ||
| const { fontFamily, fontWeight } = LightTheme.fonts.default; | ||
|
|
||
| expect(screen.getByTestId('lone-text')).toHaveStyle({ | ||
| fontFamily, | ||
| fontWeight, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
doesn't this duplicate "renders v3 Text component without variant..."?
| it('nested text keeps applying its own style while inheriting the rest', async () => { | ||
| await render( | ||
| <Text variant="displayLarge"> | ||
| Parent{' '} | ||
| <Text testID="child-text" style={{ fontStyle: 'italic' }}> | ||
| child | ||
| </Text> | ||
| </Text> | ||
| ); | ||
|
|
||
| expect(screen.getByTestId('child-text')).toHaveStyle({ | ||
| fontStyle: 'italic', | ||
| }); | ||
| }); |
There was a problem hiding this comment.
this doesn't assert what everything the title says. it should just be consolidated into the above test
| it('text nested in animated text inherits instead of resetting', async () => { | ||
| await render( | ||
| <AnimatedText variant="displayLarge"> | ||
| Parent <Text testID="child-text">child</Text> | ||
| </AnimatedText> | ||
| ); | ||
| const { fontFamily, fontWeight } = LightTheme.fonts.default; | ||
|
|
||
| expect(screen.getByTestId('child-text')).not.toHaveStyle({ fontFamily }); | ||
| expect(screen.getByTestId('child-text')).not.toHaveStyle({ fontWeight }); | ||
| }); | ||
|
|
||
| it('animated text nested in text inherits instead of resetting', async () => { | ||
| await render( | ||
| <Text variant="displayLarge"> | ||
| Parent <AnimatedText testID="child-animated">child</AnimatedText> | ||
| </Text> | ||
| ); | ||
| // `AnimatedText` falls back to `bodyMedium` rather than the default font. | ||
| const { fontFamily, fontSize } = LightTheme.fonts.bodyMedium; | ||
|
|
||
| expect(screen.getByTestId('child-animated')).not.toHaveStyle({ fontFamily }); | ||
| expect(screen.getByTestId('child-animated')).not.toHaveStyle({ fontSize }); | ||
| }); | ||
|
|
||
| it('animated text outside of any text still gets its fallback font', async () => { | ||
| await render(<AnimatedText testID="lone-animated">{content}</AnimatedText>); | ||
| const { fontFamily, fontSize } = LightTheme.fonts.bodyMedium; | ||
|
|
||
| expect(screen.getByTestId('lone-animated')).toHaveStyle({ | ||
| fontFamily, | ||
| fontSize, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
not sure we need separate tests for animated text.
|
Review by Claude Screenshots of this code added to the Typography screen, dark theme: <Text variant="headlineSmall" style={{ color: 'tomato' }}>
A tomato <Text>plain child</Text>
</Text>
<Text variant="headlineSmall" style={{ color: 'tomato' }}>
B tomato <Text variant="bodyLarge">variant child</Text>
</Text>
<Text style={{ color: 'tomato', fontWeight: 'bold' }}>
C tomato bold <Text variant="titleLarge">variant child</Text>
</Text>
<Text variant="headlineSmall">
D parent <Text theme={{ colors: { onSurface: 'green' } }}>green theme child</Text>
</Text>
<Chip selected onPress={() => {}}><Text variant="labelLarge">Var</Text></Chip>
<Button mode="contained"><Text variant="labelLarge">Var</Text></Button>iOS
Android
Web
|
satya164
left a comment
There was a problem hiding this comment.
Review by Claude
See the inline comments. (via Claude, verify if it makes sense)
| } | ||
|
|
||
| return ( | ||
| element = ( |
There was a problem hiding this comment.
A nested Text with a variant still sets color: theme.colors.onSurface (and writingDirection and textAlign) on line 155, so it drops the color of the enclosing text. #4351 covers this case too: the child sets its own variant and should still inherit the rest.
For example, <Button mode="contained"><Text variant="labelLarge">Save</Text></Button> renders the label in onSurface on the primary container. In dark theme the label is almost invisible (the "Var" button):
When isNested is true, apply only the variant font:
style={
isNested
? textStyle
: [
styles.text,
{ writingDirection, color: theme.colors.onSurface },
textStyle,
]
}This keeps the style array of a top-level Text the same, so the snapshots do not change. AnimatedText has the same variant branch and needs the same change. (via Claude, verify if it makes sense)
| {...rest} | ||
| /> | ||
| ); | ||
| } else if (isNested) { |
There was a problem hiding this comment.
This branch also ignores the theme prop. On main, <Text variant="headlineSmall">Parent <Text theme={{ colors: { onSurface: "green" } }}>child</Text></Text> renders the child green. With this change it renders in the parent color (case D in the screenshot comment). themeOverrides in AnimatedText has the same problem. Skip the defaults only when no theme override is passed:
| } else if (isNested) { | |
| } else if (isNested && !initialTheme) { |
(via Claude, verify if it makes sense)



Motivation
Two related bugs in nested
Text, both reproduced onmainfirst.A variant parent lost its typography. When a
Textwith avariantwrapped anested Paper
Text, it replaced its own font with the child's style instead oflayering on top of it. With an unstyled child there was no child style to take
over, so the parent rendered with no typography at all. This reached shipped
components:
Chiprenders<Text variant="labelLarge">around its children, so<Chip><Text>label</Text></Chip>dropped the entire label font block(
fontSize: 14,fontWeight: 500,lineHeight: 20,letterSpacing: 0.1).A nested
Textdid not inherit. A nestedTextorAnimatedTextwithout avariantapplied its fallback font unconditionally, overriding the family,weight, letter spacing and color it should have inherited from the enclosing text.
On the approach: React Native's
Textalready inherits from an enclosingTextnatively, so nothing needs to merge styles. The bug was Paper overriding what
would otherwise be inherited. A nested component now declares only its own
style. The shared context therefore carries a boolean rather than a style, andlives in its own module so the
TextandAnimatedTextimports stayone-directional.
AnimatedTextwas broken in both directions and is fixed too. It falls back tobodyMediumwhereTextfalls back tofonts.default, so an animated childinside a
displayLargeparent was snapping tofontSize: 14. That fallbackinconsistency between the two components still exists, but is no longer
reachable through nesting.
Related issue
Fixes #4351.
#4356 is an earlier attempt at the same issue, taking the opposite approach of
merging the parent's style into the child. It also only publishes the parent
style from the variant-less branch, so a child under a variant parent would
still not inherit, and it is written against a version of
Text.tsxthat nolonger exists on
main.Test plan
Example app, Typography screen, new "Nested text" section with four cases:
unstyled child, styled child, child with its own variant, and a bold parent
whose nested child inherits the weight.
child-overrides-parent behaviour is preserved.
AnimatedTexthad no test coverage before this; it now has 3 tests.yarn lintandyarn typecheckclean.