feat: TextField component#4909
Conversation
|
Hey @wonderlul, thank you for your pull request 🤗. The documentation from this branch can be viewed here. |
146de2a to
a1a20fd
Compare
| supportingTextProps?.style, | ||
| ]; | ||
|
|
||
| const $inputStyles: StyleProp<TextStyle> = [ |
There was a problem hiding this comment.
At least on Android there's a visual bug with the opacity of the input text.
When disabled, the opacity on the input text appears more than it should be (near invisible).
The fix I found was to extract a $inputWrapperStyles and move the { flex: 1 }, disabled && $disabledStyle to it then place the RN TextInput from TextField.tsx inside of a <View style={$inputWrapperStyles}>.
|
Here's a comment from my friend Claude about duplicate code:
|
| Block | Lines each |
|---|---|
| Props destructuring | ~13 |
isRTL extraction |
1 |
getLabelColor(...) call |
~5 |
getSupportingTextColor(...) call |
~5 |
$animatedLabelTextStyles array |
~9 |
$containerStyles array |
~4 |
$supportingTextStyles array |
~8 |
$counterStyles array |
~8 |
$prefixStyles array |
~6 |
$suffixStyles array |
~6 |
$leadingAccessoryStyles |
~3 |
$trailingAccessoryStyles |
~3 |
| Total | ~71 identical lines |
Outlined has 233 lines, filled has 280 -- so ~71/233 (30%) and ~71/280 (25%) respectively are shared logic.
What's genuinely variant: the $fieldStyles (filled adds backgroundColor), $outlineStyles (border vs bottom-bar), $animatedActiveOutlineStyles (filled-only), $disabledBackgroundStyles (undefined in outlined, overlay in filled), and the labelBackgroundColor extraction
(outlined only).
styles.ts -- low duplication (~10%)
$labelTextStyle is byte-for-byte identical in both files (5 lines). It belongs in the shared ../styles.ts.
Everything else differs meaningfully:
$fieldStyle: same keys,borderRadius(outlined) vsborderTopStart/EndRadius(filled)$outlineStyle: full-perimeter absolute (outlined) vs bottom-only absolute (filled)$containerStyle:alignItems: 'center'(outlined) vs'flex-end'(filled)$labelWrapperStyle: outlined addspaddingHorizontal, filled is empty$disabledBackgroundStyle: filled-only
constants.ts -- minimal duplication (~5%)
LABEL_START_OFFSET_WITHOUT_ACCESSORY resolves to TEXT_FIELD_INPUT_WRAPPER_PADDING_HORIZONTAL in both -- could live in shared ../constants.ts.
Everything else is variant: LABEL_PADDING_HORIZONTAL and the RTL translate constants are outlined-only; MULTILINE_PADDING_TOP is filled-only; ACTIVE_LABEL_TOP_POSITION uses different formulas; the opacity values differ (0.12 vs 0.04).
utils.ts -- 0% unifiable
Both export getOutlineColor but with incompatible signatures: outlined takes hasError: boolean, filled takes status?: 'error' | 'disabled'. Same name, different contract -- unifying them would require a signature change that ripples into both logic.ts files.
Overall
| File | Outlined lines | Filled lines | Duplicated | % |
|---|---|---|---|---|
logic.ts |
233 | 280 | ~71 | ~28% |
styles.ts |
45 | 54 | ~5 | ~10% |
constants.ts |
49 | 35 | ~2 | ~5% |
utils.ts |
39 | 34 | 0 | 0% |
| Total | 366 | 403 | ~78 | ~20% |
|
Some more issues I found:
Edit: the filled version lost the top rounded corners when in error mode and after toggling the "disabled" state. |
|
When the helper text is empty, the counter gets aligned to start. It should stay aligned to end. |
|
Error seems to take precedence over Disabled but I would expect that a disabled field is not editable even if it has "error". |
Multi-line fixed. I disagree with disallowing prefix + suffix. I think it's up to the developer to decide whether to use both, one or neither, but we shouldn't disable the option for developers. |
Yeah, this is on purpose — we’re using a clear order for those states. My thinking was: if there’s an error, the field shouldn’t really be blocked. You usually need to change the value to fix it, so disabling it felt backwards. If we’re okay with that idea, we can stick to this gradation and it keeps the code a lot simpler — less special cases. I know some developers expect disabled to always win even when error is set; if that’s what we want product-wise, we can always revisit it. Let me know. |
Motivation
File structure
The component is split by variant (
filled/andoutlined/) and a root that wires shared behavior. Each area keeps logic, styles, utils, and constants in separate files. That follows patterns already used elsewhere in the library, but goes one step further so responsibilities stay obvious: variant-specific layout and theming do not drown in shared code, and the public API file stays focused on behavior and types.LeftAccessory/RightAccessoryvsTextInputadornmentsTextInputcomposes leading and trailing content throughleftandright, which are built around icons and affixes (TextInput.Icon,TextInput.Affix) and internal adornment types.TextFieldinstead exposesLeftAccessoryandRightAccessoryas render props (component types). The field passes curated layout and state—notably the merged style for alignment with the field, plus status, multiline, and editable—so accessories stay aligned with the input without re-implementing field internals. That supports arbitrary leading/trailing UI (clear actions, custom buttons, non-icon content) while still inheriting the important structural styles from the component.filled/outlinedinstead offlat/outlinedMaterial Design 3 describes text fields in terms of
filledandoutlinedstyles. The existing TextInput API uses mode:'flat' | 'outlined', where “flat” corresponds to the filled look. The new component names variants filled and outlined so the public API matches MD3 language and is easier to map from the spec and design tools.Style overrides
TextField is built as a small stack of clear layers, and each layer can be adjusted without fighting the rest. The outer pressable wrapper, the field shell (border, background, row that includes accessories), and the inner content wrapper (label + TextInput) each accept dedicated style props (pressableStyle, fieldStyle, containerStyle). The underlying TextInput still uses the normal style prop (and the rest of TextInputProps) for typography, padding, and input-specific layout. Label and helper text can be customized through labelProps and helperProps (including style). Leading/trailing UI uses LeftAccessory / RightAccessory, which receive a prepared style from the field so custom content stays aligned while remaining fully under your control. Together, this gives predictable “override the part you mean” behavior instead of a single opaque style that’s hard to reason about.
TextField instead of TextInput
Material Design 3 uses the term text field for this control. Exporting a second “Paper” input named
TextInputwould also blur the built-in React NativeTextInputin imports and documentation. A dedicatedTextFieldname keeps the design-system component clearly namespaced and aligned with MD3, while the underlying control remains React Native’sTextInputwhere appropriate.Positioning relative to TextInput
TextFieldis intended as the modern replacement path for form text entry in react-native-paper: implementation is structured for clarity and maintainability, and it adopts MD3-oriented theming (including use of thePlatformColor APIwhere it fits platform tokens). Compared to the legacyTextInputstack, this design aims to be easier to follow, less ad hoc in how variants and layout are split, and more efficient in how styles and state are applied—giving teams a refreshed, spec-aligned building block for new work without forcing an immediate break for existingTextInputusers.Order of merging
#4901
Related issue
#4878
#4329
#4235
Test plan
Run the Example app.
filledoutlined