Skip to content

fix: TextInput styles can't crash a field, and NitroInput matches TextInput's layout - #70

Merged
ronickg merged 3 commits into
mainfrom
fix/style-typography
Oct 13, 2026
Merged

ronickg merged 3 commits into
mainfrom
fix/style-typography

Conversation

@ronickg

@ronickg ronickg commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Found by the visual regression run that compares NitroInput with RN's TextInput side by side, in a 5-page gallery on both platforms.

1. A TextInput style crashed a field

style={{ fontWeight: '700' }} on NitroInput, NitroNumber or NitroText threw:

Exception in HostFunction: NitroInputView.fontWeight: Value is a string, expected a number

Cause. Fabric hands a view its style's keys as props, and our native props had the same names as text style keys.

Fix.

  • The native props are renamed so none can share a name with a style key: textSize, textWeight, textFamily, textColor, textLetterSpacing, textLineHeight, textAlignment, textVerticalAlignment. The public props are unchanged.
  • PropNameCollisions.test now checks every native prop of every view against every React Native style key and view prop, with a check that it would have caught fontWeight.
  • A Harness test renders all three components with a full TextInput style on a device.
  • NitroInput now reads its typography from style like TextInput, and the props of the same name win. Its style is typed as TextInput's.

2. Layout differences from TextInput

Before Now
Android textAlign="center" / "right" on a plain field ignored: the text started at 1.9 dp applied: starts at 81.9 dp, TextInput at 81.1 dp
style padding or border on a self-sized field iOS: squeezed to "…"; Android: text not inset at all the field grows by the padding and the text is inset, as TextInput's
iOS single line with lineHeight={30} 24 pt (the font's own line box) 30 pt, as on Android

How the padding fix works:

  • JS resolves padding plus border per edge the way Yoga does, and adds them to the measured size.
  • It also sends them to native as textInset* props. React Native already insets the iOS view, but never passes padding to a Nitro view on Android, so the Android view now insets its text, overlay and frame itself.

Kept as they are, now written in the README:

  • Digits are always tabular. Android's Roboto has tabular digits anyway, and a field keeps its width whether it reflows or not.
  • Android gets no default padding. iOS's TextInput has none.

Tests

  • The gallery is pixel-identical before and after the rename.
  • After the layout fixes, only the three rows above changed; everything else on the five pages stayed put.
  • New Jest and Harness tests cover the style typography, Yoga's edge precedence (including right-to-left), a padded field growing by exactly its padding, and a lineHeight field being exactly that tall.
  • All Harness suites pass: iOS 147/147, Android 143/143.
  • Jest 189/189, typecheck passes.

🤖 Generated with Claude Code

Ronald Goedeke and others added 3 commits October 12, 2026 17:37
`TextInput` takes fontSize, fontWeight, fontFamily, color, letterSpacing,
lineHeight, textAlign and textAlignVertical from `style`. NitroInput read
them only as props and handed `style` to the view as it was, and Fabric
gives a view its style's keys as props: these share names with the
view's own, so `style={{ fontWeight: '700' }}` threw ("NitroInputView
.fontWeight: Value is a string, expected a number") and a numeric one was
fought over by the prop and the style.

The keys are now read from the flattened style under the props of the
same name (which win) and taken out of the style the view gets. textAlign
'start' is the start edge, 'end' the far one for the layout direction,
'justify' the start edge, as on a one-line TextInput.

Found by the visual comparison with TextInput.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fabric hands a view its style's keys as props. Each view had native
props named like text style keys (fontSize, fontWeight, fontFamily,
color, letterSpacing, lineHeight, textAlign, textAlignVertical), so a
TextInput style on NitroInput, NitroNumber or NitroText reached them:
`style={{ fontWeight: '700' }}` threw "Value is a string, expected a
number", and a numeric fontSize or color was fought over by the style
and the prop.

The native props are renamed (textSize, textWeight, textFamily,
textColor, textLetterSpacing, textLineHeight, textAlignment,
textVerticalAlignment), on all three views and the measure's sizing
struct; the components' public props keep their names. With that, the
style no longer has to be stripped of those keys, so it is not.
NitroInput's `style` is typed as TextInput's, and it reads its
typography from it (the props of the same name win).

Guards:
- PropNameCollisions.test now also reads every style key React Native
  knows (ReactNativeStyleAttributes) and fails if a native prop of any
  view shares one, with a check that it would have caught fontWeight.
- A Harness test renders all three with a full TextInput style (string
  fontWeight included) on a device.

The visual gallery is pixel-identical before and after the rename on
both platforms. All suites pass (iOS 145, Android 141); Jest 187.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three differences from TextInput found by comparing the two side by side
in a visual gallery on both platforms:

- Android ignored textAlign center / right on a plain field: the text
  alignment set when the field became plain overrode the gravity that
  textAlign set later in the same commit. Both are now applied whenever
  either changes.
- A style padding or border did not make a self-sized field bigger: an
  autoWidth field with paddingHorizontal 8 squeezed "blue" to "…" on iOS
  (React Native lays the content out inside the padding there), and on
  Android the padding never reached the view at all (Fabric does not hand
  a Nitro view its padding). JS now resolves padding plus border per edge
  as Yoga does and adds them to the measured size, and sends them as
  textInset* props, by which the Android view insets its edit text,
  overlay and frame. Percentages count as 0 (they need the parent).
- iOS sized a single line by the font's own line box whatever lineHeight
  said; it now uses lineHeight, as Android does (sizing only; the drawing
  is unchanged).

Kept, and written down in the README: digits are always tabular, and
Android gets no default padding.

The gallery, re-measured: Android's centred text starts where
TextInput's does (81.9 vs 81.1 dp; it was 1.9), the padded field is
inset like TextInput's on both, and iOS's lineHeight 30 field is 30
tall. Nothing else on the five pages moved. New Harness and Jest tests;
all suites pass (iOS 147, Android 143), Jest 189.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ronickg ronickg changed the title fix: a TextInput style can no longer crash a field fix: TextInput styles can't crash a field, and NitroInput matches TextInput's layout Oct 12, 2026
@ronickg
ronickg merged commit b316c29 into main Oct 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant