From 50305804edadcab19775d32f16f51cbab3d68144 Mon Sep 17 00:00:00 2001 From: Jason Zheng Date: Thu, 10 Sep 2026 16:16:07 -0700 Subject: [PATCH 1/4] Update descriptions in KeyboardAvoidingViewExample (#58430) Summary: In the RNTester KeyboardAvoidingView example, fix minor spacing issues and add descriptions for "Keyboard Avoiding View with enabled={false}" and "Keyboard Avoiding View with contentContainerStyle". Changelog: [Internal] Reviewed By: vzaidman Differential Revision: D119204437 --- .../KeyboardAvoidingView/KeyboardAvoidingViewExample.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js b/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js index 9bf56b7f6a16..3e7c9a73378f 100644 --- a/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js +++ b/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js @@ -251,7 +251,7 @@ exports.examples = [ { title: 'Keyboard Avoiding View with different behaviors', description: - ('Specify how to react to the presence of the keyboard. Android and iOS both interact' + + ('Specify how to react to the presence of the keyboard. Android and iOS both interact ' + 'with this prop differently. On both iOS and Android, setting behavior is recommended.') as string, render(): React.Node { return ; @@ -260,7 +260,7 @@ exports.examples = [ { title: 'Keyboard Avoiding View with keyboardVerticalOffset={distance}', description: - ('This is the distance between the top of the user screen and the react native' + + ('This is the distance between the top of the user screen and the React Native ' + 'view, may be non-zero in some use cases. Defaults to 0.') as string, render(): React.Node { return ; @@ -268,12 +268,15 @@ exports.examples = [ }, { title: 'Keyboard Avoiding View with enabled={false}', + description: 'Disable the KeyboardAvoidingView.' as string, render(): React.Node { return ; }, }, { title: 'Keyboard Avoiding View with contentContainerStyle', + description: + 'Specify the style of the content container View when behavior is set to position.' as string, render(): React.Node { return ; }, From bdd34a65135e872ef44760a8775ef60aaef2fcc6 Mon Sep 17 00:00:00 2001 From: Jason Zheng Date: Thu, 10 Sep 2026 16:16:07 -0700 Subject: [PATCH 2/4] Fix CloseButton alignment in KeyboardAvoidingViewExample (#58431) Summary: The RNTester KeyboardAvoidingView example had alignment issues with the close button on top of the example form. When KeyboardAvoidingView's behavior was toggled to "position", a small amount of right padding is added to the close button, making it appear non-flush with the right edge of the form. This impacted both the "Keyboard Avoiding View with different behaviors" screen and the "Keyboard Avoiding View with contentContainerStyle" screen. Changelog: [Internal] Reviewed By: vzaidman Differential Revision: D119204438 --- .../KeyboardAvoidingViewExample.js | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js b/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js index 3e7c9a73378f..ac3144ee3ba2 100644 --- a/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js +++ b/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js @@ -47,22 +47,13 @@ const TextInputForm = () => { ); }; -const CloseButton = ( - props: - {behavior: any, setModalOpen: any} | {behavior: string, setModalOpen: any}, -) => { +const CloseButton = (props: {setModalOpen: boolean => void}) => { return ( - - props.setModalOpen(false)} - style={styles.closeButton}> - Close - - + props.setModalOpen(false)} + style={styles.closeButton}> + Close + ); }; @@ -114,7 +105,7 @@ const KeyboardAvoidingViewBehaviour = () => { - + @@ -140,7 +131,7 @@ const KeyboardAvoidingDisabled = () => { enabled={false} behavior={'height'} style={styles.container}> - + @@ -162,7 +153,7 @@ const KeyboardAvoidingVerticalOffset = () => { keyboardVerticalOffset={20} behavior={'padding'} style={styles.container}> - + @@ -185,7 +176,7 @@ const KeyboardAvoidingContentContainerStyle = () => { behavior={'position'} style={styles.container} contentContainerStyle={styles.contentContainer}> - + @@ -205,9 +196,11 @@ const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', - alignItems: 'center', + alignSelf: 'center', paddingHorizontal: 20, paddingTop: 20, + width: '100%', + maxWidth: 340, }, contentContainer: { paddingTop: 20, @@ -217,13 +210,9 @@ const styles = StyleSheet.create({ borderRadius: 5, borderWidth: 1, height: 44, - width: 300, marginBottom: 20, paddingHorizontal: 10, }, - closeView: { - alignSelf: 'stretch', - }, pillStyle: { padding: 10, marginHorizontal: 5, @@ -233,8 +222,7 @@ const styles = StyleSheet.create({ borderColor: 'blue', }, closeButton: { - flexDirection: 'row', - justifyContent: 'flex-end', + alignSelf: 'flex-end', marginVertical: 10, padding: 10, }, From de7598585f50935869bca2f811948fbc4d508012 Mon Sep 17 00:00:00 2001 From: Jason Zheng Date: Thu, 10 Sep 2026 16:16:07 -0700 Subject: [PATCH 3/4] Add ScrollView example in KeyboardAvoidingViewExample (#58432) Summary: Add a new RNTester example in KeyboardAvoidingView for using the component with a ScrollView. This reproduces the issue raised in [#16826](https://github.com/react/react-native/issues/16826) where keyboard avoiding behavior fails for multi-line text fields in a ScrollView on iOS. As this is a common use case, the example acts as a reference to show intended behavior and prevent future regressions. Changelog: [Internal] Differential Revision: D119204439 --- .../KeyboardAvoidingViewExample.js | 167 ++++++++++++++---- 1 file changed, 128 insertions(+), 39 deletions(-) diff --git a/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js b/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js index ac3144ee3ba2..750496f9a369 100644 --- a/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js +++ b/packages/rn-tester/js/examples/KeyboardAvoidingView/KeyboardAvoidingViewExample.js @@ -20,6 +20,7 @@ import { KeyboardAvoidingView, Modal, Pressable, + ScrollView, StyleSheet, Text, TextInput, @@ -57,54 +58,67 @@ const CloseButton = (props: {setModalOpen: boolean => void}) => { ); }; -const KeyboardAvoidingViewBehaviour = () => { - const [modalOpen, setModalOpen] = useState(false); - const [behavior, setBehavior] = useState('padding'); +type KeyboardAvoidingBehavior = 'padding' | 'position' | 'height'; + +const BEHAVIORS: Array = [ + 'padding', + 'position', + 'height', +]; + +const BEHAVIOR_DESCRIPTIONS: {[KeyboardAvoidingBehavior]: string} = { + padding: 'Sets bottom padding equal to the keyboard overlap.', + position: 'Offsets the content container up by the keyboard overlap.', + height: 'Shrinks the view height by the keyboard overlap.', +}; + +const BehaviorPicker = (props: { + behavior: KeyboardAvoidingBehavior, + setBehavior: KeyboardAvoidingBehavior => void, +}) => { return ( - - - {/* $FlowFixMe[incompatible-type] Natural Inference rollout. See - * https://fburl.com/workplace/6291gfvu */} - - - setBehavior('padding')} - style={[ - styles.pillStyle, - {backgroundColor: behavior === 'padding' ? 'blue' : 'white'}, - ]}> - - Padding - - - setBehavior('position')} - style={[ - styles.pillStyle, - {backgroundColor: behavior === 'position' ? 'blue' : 'white'}, - ]}> - - Position - - + + + {BEHAVIORS.map(behavior => { + const selected = props.behavior === behavior; + return ( setBehavior('height')} + key={behavior} + onPress={() => props.setBehavior(behavior)} style={[ styles.pillStyle, - {backgroundColor: behavior === 'height' ? 'blue' : 'white'}, + {backgroundColor: selected ? 'blue' : 'white'}, ]}> - Height + {behavior} - + ); + })} + + + {BEHAVIOR_DESCRIPTIONS[props.behavior]} + + + ); +}; + +const KeyboardAvoidingViewBehaviour = () => { + const [modalOpen, setModalOpen] = useState(false); + const [behavior, setBehavior] = useState('padding'); + return ( + + + + @@ -189,6 +203,46 @@ const KeyboardAvoidingContentContainerStyle = () => { ); }; +const KeyboardAvoidingScrollView = () => { + const [modalOpen, setModalOpen] = useState(false); + const [behavior, setBehavior] = useState('padding'); + return ( + + + + + + + + + {Array.from({length: 10}, (_, index) => ( + + Item {index + 1} + + ))} + + + + + + + setModalOpen(true)}> + Open Example + + + + ); +}; + const styles = StyleSheet.create({ outerContainer: { flex: 1, @@ -206,13 +260,34 @@ const styles = StyleSheet.create({ paddingTop: 20, backgroundColor: '#abdebf', }, + scrollViewContainer: { + flex: 1, + paddingTop: 100, + }, + scrollViewContent: { + paddingBottom: 60, + paddingHorizontal: 20, + }, + fillerItem: { + backgroundColor: '#eeeeee', + borderRadius: 8, + paddingVertical: 16, + paddingHorizontal: 12, + marginBottom: 20, + alignItems: 'center', + }, textInput: { borderRadius: 5, borderWidth: 1, - height: 44, + minHeight: 44, marginBottom: 20, paddingHorizontal: 10, }, + multilineTextInput: { + minHeight: 88, + paddingVertical: 10, + textAlignVertical: 'top', + }, pillStyle: { padding: 10, marginHorizontal: 5, @@ -230,6 +305,11 @@ const styles = StyleSheet.create({ fontWeight: '500', color: 'blue', }, + behaviorDescription: { + textAlign: 'center', + alignSelf: 'center', + width: 200, + }, }); exports.title = 'KeyboardAvoidingView'; @@ -269,4 +349,13 @@ exports.examples = [ return ; }, }, + { + title: 'Keyboard Avoiding View with ScrollView', + description: + ('A ScrollView with filler content and TextInputs at the bottom inside the scrollable content. ' + + 'TextInputs should still be visible when focused.') as string, + render(): React.Node { + return ; + }, + }, ] as Array; From f361a14dc6625eea8afe34592ed7b7dde33cedcd Mon Sep 17 00:00:00 2001 From: Jason Zheng Date: Thu, 10 Sep 2026 16:16:07 -0700 Subject: [PATCH 4/4] Fix KeyboardAvoidingView behavior for multi-line TextInputs on iOS (#58436) Summary: Fixes the issue described in [#16826](https://github.com/react/react-native/issues/16826), where KeyboardAvoidingView does not reveal multi-line TextInputs when used with an enclosing ScrollView on iOS. ### Root Cause When a text input is focused on iOS, UIKit reveals the input by applying a scroll operation on the innermost ScrollView. When a single-line UITextField is inside a ScrollView, this scrolls the ScrollView itself, revealing the input correctly. However, the multi-line UITextView itself extends UIScrollView. When a UITextView is inside a ScrollView, UIKit applies the scroll operation only on the input and not the surrounding ScrollView. If the input is in a position where it is covered by the keyboard, it will remain covered afterwards. Community members have found a workaround by setting `scrollEnabled={false}` on multi-line inputs. This disables scrolling on the UITextView, so the scroll operation is correctly applied on the surrounding ScrollView. However, this workaround comes with obvious downsides, and solutions offered by third-party libraries are often used instead. ### This PR Modify RCTUITextView to override the `scrollRectToVisible(_:animated:)` method which is what UIKit uses to implicitly reveal the text view. The scrolling operation is performed on the text view like before, but we then forward the same operation to the nearest scrollable ancestor (such as a surrounding ScrollView). This ensures that the text view itself is always scrolled into view as scrolling is always applied to the parent. ### Limitations The main limitation is that we rely on UIKit to reveal the text input, since KeyboardAvoidingView itself implements no scrolling logic. This also leads to some discrepancy in behavior with Android. On Android, the entire text box is revealed on focus, whereas on iOS with this fix, only the caret line is revealed. We can introduce additional logic in RCTUITextView to reveal the entire text view, but this won't apply to text views with `scrollEnabled={false}` which will still only reveal the caret line using UIKit's default behavior. Native approaches on iOS generally involve manually handling the keyboard avoiding behavior by adding keyboard event listeners and manually triggering `scrollView.scrollRectToVisible()` when receiving the `keyboardDidShow()` notification. We can do something similar in `RCTScrollViewComponentView`, but this explicitly violates the contract there which prohibits behavior that contradicts a standard UIScrollView. In contrast, this approach aligns with what is already being used to reveal both UITextField and UITextView with `scrollEnabled={false}`. Changelog: [iOS][Fixed] Reveal multi-line text inputs when using KeyboardAvoidingView with a ScrollView Differential Revision: D119204440 --- .../Text/TextInput/Multiline/RCTUITextView.mm | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm index a803387b98bf..763867ecd85d 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm @@ -213,6 +213,25 @@ - (void)scrollRangeToVisible:(NSRange)range [super scrollRangeToVisible:range]; } +// Since UITextView is a UIScrollView, UIKit only scrolls this view to reveal the cursor. If UITextView is obscured when +// enclosed in a ScrollView, the cursor is never revealed. Therefore we forward the reveal upward to the nearest +// scrollable ancestor. +- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated +{ + [super scrollRectToVisible:rect animated:animated]; + + if (!self.isFirstResponder) { + return; + } + + UIScrollView *scrollableAncestor = [self nearestScrollableAncestor]; + if (scrollableAncestor == nil) { + return; + } + + [scrollableAncestor scrollRectToVisible:[scrollableAncestor convertRect:rect fromView:self] animated:animated]; +} + - (void)paste:(id)sender { _textWasPasted = YES; @@ -379,4 +398,14 @@ - (CGRect)caretRectForPosition:(UITextPosition *)position #pragma mark - Utility Methods +- (nullable UIScrollView *)nearestScrollableAncestor +{ + for (UIView *superview = self.superview; superview != nil; superview = superview.superview) { + if ([superview isKindOfClass:[UIScrollView class]] && ((UIScrollView *)superview).isScrollEnabled) { + return (UIScrollView *)superview; + } + } + return nil; +} + @end