Skip to content

feat(ios): honor textAlignVertical on Paragraph and multiline TextInput#56774

Open
quantizor wants to merge 4 commits into
react:mainfrom
quantizor:feat-ios-text-align-vertical
Open

feat(ios): honor textAlignVertical on Paragraph and multiline TextInput#56774
quantizor wants to merge 4 commits into
react:mainfrom
quantizor:feat-ios-text-align-vertical

Conversation

@quantizor

@quantizor quantizor commented May 11, 2026

Copy link
Copy Markdown
Contributor

Summary:

textAlignVertical (and the CSS-style verticalAlign) has worked on Android since #34567, but iOS silently ignores it on both <Text> and multiline <TextInput>: every value paints content at the top of the host view. This PR closes both halves of the gap so a fixed-height Text or multiline TextInput with verticalAlign: 'middle' (or 'bottom') renders the same on iOS as on Android. Single-line TextInput (UITextField) already centers natively and is intentionally not routed.

Scope: New Architecture only. This wires the Fabric iOS renderer (the default since 0.76): the Fabric Paragraph path in RCTTextLayoutManager and the Fabric multiline TextInput path in RCTTextInputComponentView / RCTUITextView. The legacy (Paper) iOS <Text> and <TextInput> managers (RCTTextViewManager / RCTBaseTextInputViewManager) are intentionally not wired, so apps still on the old renderer are unaffected. Thanks to @popsiclelmlm for flagging the ambiguity.

Computation matches Android's existing TextLayoutManager.getVerticalOffset exactly: 0 for auto / top, (boxHeight - textHeight) / 2 for center, boxHeight - textHeight for bottom, and 0 when content overflows the box. This is the CSS Box Alignment Level 3 align-content algorithm with safe overflow on a block container.

Paragraph (<Text>)

The offset is applied at three sites in RCTTextLayoutManager.mm:

  • drawAttributedString shifts the paint origin for the background pass, glyph pass, and highlight rect path.
  • getEventEmitterWithAttributeString reverse-offsets the incoming touch point so hit-testing still resolves the correct character.
  • getRectWithAttributedString forward-offsets the enumerated link/button rects so VoiceOver focuses on the right region.

Keeping all three in lockstep avoids the class of bug where a centered link "renders below the touch target" or accessibility reads the wrong frame. Measurement is unchanged: alignment shifts the line-box stack inside the box, it doesn't alter intrinsic text size.

Multiline TextInput

The offset is applied via UIScrollView.contentInset.top inside RCTUITextView.layoutSubviews. This is the right hook on iOS: it shifts the default scroll origin without interfering with text layout, user scrolling, or RN's existing textContainerInset wiring (which is used for padding). When content grows past the bounds, the inset clamps to 0 and normal scrolling takes over.

RCTTextInputComponentView.updateProps reads paragraphAttributes.textAlignVertical (already parsed by BaseTextInputProps), maps the C++ enum to an ObjC bridge enum (RCTUITextViewTextAlignmentVertical), and pushes it to the backed RCTUITextView. The mapping is also re-applied when the backed view switches from single-line to multiline so the new view picks up the alignment on the same render. The default Auto / Top case short-circuits before reading contentSize so apps that don't use the feature pay zero per-layout overhead.

No public header changes; ABI is preserved.

Changelog:

[IOS] [ADDED] - Honor textAlignVertical (and the equivalent verticalAlign style) on <Text> and multiline <TextInput> on the New Architecture

Test Plan:

Tested in a Fabric / Hermes V1 / new-arch Expo app on iPhone 17 simulator (iOS 26.4) at all three values, for both Text and multiline TextInput:

vertical-align Result
top (default) Content flush to top of host view (unchanged)
middle / center Content centered in host view
bottom Content flush to bottom of host view

For Text: decorations (underline, strike) follow the glyphs in all three positions. Touching anywhere on a centered or bottom-aligned link still emits the correct press event. Overflow (text taller than the box) falls back to top to avoid clipping content.

For multiline TextInput: the caret and placeholder sit at the requested position before any text is typed. Typing extends content naturally; once the content fills past the fixed height, scrolling takes over and the alignment offset clamps to top.

Related

Honor ParagraphAttributes.textAlignVertical when drawing the Fabric
Paragraph view on iOS. Mirrors Android's existing offset computation:
center the line-box stack vertically when 'center' is requested, push
to the bottom when 'bottom' is requested, fall back to top when the
text overflows. Shifts paint origin, highlight rects, hit-test point,
and link/button accessibility rects in lockstep.

Equivalent to CSS Box Alignment Level 3 align-content:
{start,center,end} with safe overflow on a block container.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 11, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label May 11, 2026
quantizor added 2 commits May 11, 2026 14:15
Extend the textAlignVertical fix from Paragraph to multiline TextInput
so a tall fixed-height multiline field can sit centered or bottom-aligned
on iOS the same way it already does on Android. Single-line TextInput
(UITextField) centers natively, so this only routes when the backed view
is the multiline UITextView.

Offset is applied via UIScrollView's contentInset.top inside
RCTUITextView.layoutSubviews, which is the spec-correct surface: it
shifts the default scroll origin without interfering with text layout or
user scrolling. When content exceeds bounds the inset falls back to 0
(safe overflow per CSS Box Alignment L3 align-content).
Rename the ObjC property to `textAlignVertical` so the bridge matches
the JS prop and the C++ paragraph attribute exactly. The enum type stays
`RCTUITextViewTextAlignmentVertical` since it mirrors the C++ enum
class name.

Skip the contentSize read for the default Auto / Top case so the new
code adds zero per-layout work for apps that don't opt in.
@quantizor quantizor changed the title feat(ios): honor textAlignVertical on Paragraph feat(ios): honor textAlignVertical on Paragraph and multiline TextInput May 11, 2026
Adds RCTUITextView.textAlignVertical to the committed Apple C++ API
snapshots so validate_cxx_api_snapshots passes.
@meta-codesync

meta-codesync Bot commented May 14, 2026

Copy link
Copy Markdown

@CalixTang has imported this pull request. If you are a Meta employee, you can view this in D105240559.

@popsiclelmlm popsiclelmlm left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed write-up. One scope issue stood out while checking this against current main: the multiline TextInput half appears to only be wired for Fabric.

The PR adds RCTUITextView.textAlignVertical, but the only native prop bridge I see is in React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm. The legacy/Paper path still goes through Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm and RCTBaseTextInputShadowView.mm, and those do not export/apply textAlignVertical to the backed RCTUITextView. Similarly, Paper <Text> is still driven through RCTTextViewManager / RCTTextShadowView, not this Fabric text layout manager path.

If the intent is Fabric-only, could you scope the changelog/body/test plan to new architecture so users do not expect Paper iOS Text/TextInput parity? If this is meant as a general iOS fix, I think it needs the legacy manager/shadow-view path wired too, or at least a note explaining why Paper is intentionally excluded.

@quantizor

Copy link
Copy Markdown
Contributor Author

Good catch, and yes: this is Fabric-only on purpose, we're not chasing the Paper renderer here. I've scoped the summary, changelog, and test plan to the New Architecture so nobody reads it as Paper parity.

The surfaces this targets are the Fabric Paragraph path (RCTTextLayoutManager) and the Fabric multiline TextInput path (RCTTextInputComponentView / RCTUITextView). Single-line TextInput (UITextField) already centers natively and is left alone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants