-
Notifications
You must be signed in to change notification settings - Fork 24.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix Multiline TextInput with a fixed height scrolls to the bottom when prepending new lines to the text #38679
Conversation
Base commit: 475a156 |
Would this also prevent scroll when adding to the end of the list? |
Thanks for the code review. I moved the PR to draft. I'm working on a solution for this issue. |
Simulator.Screen.Recording.-.iPhone.14.Pro.-.2023-08-19.at.09.35.06.mp4 |
Thanks for the code-review @NickGerleman. I updated the PR and I included a video test in this comment. |
BOOL previousScrollEnabled = _backedTextInputView.scrollEnabled; | ||
NSInteger previousOffsetStart = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument | ||
toPosition:selectedRange.start]; | ||
if (previousScrollEnabled && previousOffsetStart > 0 && attributedString.string.length >= previousOffsetStart) { | ||
NSString *lastChar = [attributedString.string substringWithRange:NSMakeRange(previousOffsetStart - 1, 1)]; | ||
NSInteger previousOffsetFromEnd = oldTextLength - previousOffsetStart; | ||
if (previousOffsetFromEnd > 0 && [lastChar isEqual:@"\n"]) { | ||
_backedTextInputView.scrollEnabled = NO; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you explain this heuristic? Is it specific to adding newlines?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @NickGerleman
Could you explain this heuristic?
Scrolling is temporarily disabled when prepending a new line character to the attributed string:
- the cursor is not at the end of the string (previousOffsetFromEnd == 0)
- the last typed character (lastChar) is a new line character
Is it specific to adding newlines?
Yes. It is specific to adding new lines.
It is iOS issue 652653, previously addressed with commit Maintain cursor position when the text changes in the text input.
- Adding a new line changes the text spans and paragraph alignment. The old attributed string may have three spans with different paragraph alignments. The new attributed string could have one span with paragraph alignment 4 (see the test case below for a breakdown).
- Changing the AttributedText (text) attributes (alignment) triggers a mutation of the attributed text (_textOf)
- Mutating the AttributedString causes the cursor to move to the end of the string
- Moving the cursor to the end of the string causes the TextView to scroll to the end
https://developer.apple.com/forums/thread/652653
https://stackoverflow.com/questions/34914948/how-to-stop-cursor-changing-position-when-i-setattributedtext-in-uitextview-dele
Reproducing the issue on an iOS App without react-native
2023-06-17.18-16-16.mp4
Sourcecode available at Expensify/App#19507 (comment).
Details of the changes in the AttributedString attributes (paragraph alignment) when adding new line
Before one of the AttributedString spans has NSParagraphStyle Alignment 0. The string text is "1\n\n2"
.
CLICK TO OPEN ATTRIBUTED STRING DETAILS
Printing description of oldAttributedString->attributes:
Length 5 (8 blocks, 3 used, block 0 is at 0)
2 0x600002e70230 {
NSBackgroundColor = "UIExtendedGrayColorSpace 0 0";
NSColor = "UIExtendedGrayColorSpace 0 1";
NSFont = "<UICTFont: 0x130457dc0> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 14.00pt";
NSParagraphStyle = "Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection 0, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint ''";
}
1 0x600002f9d5e0 {
NSBackgroundColor = "UIExtendedGrayColorSpace 0 0";
NSColor = "UIExtendedGrayColorSpace 0 1";
NSFont = "<UICTFont: 0x130457dc0> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 14.00pt";
NSParagraphStyle = "Alignment 0, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection 0, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint ''";
}
2 0x600002e70230 {
NSBackgroundColor = "UIExtendedGrayColorSpace 0 0";
NSColor = "UIExtendedGrayColorSpace 0 1";
NSFont = "<UICTFont: 0x130457dc0> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 14.00pt";
NSParagraphStyle = "Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection 0, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint ''";
}
After The AttributedString has only one span. The span has NSParagraphStyle Alignment 4. The string text is "1\n\n2"
.
CLICK TO OPEN ATTRIBUTED STRING DETAILS
Printing description of attributedString->attributes:
Length 5 (2 blocks, 1 used, block 0 is at 0)
5 0x600002e70230 {
NSBackgroundColor = "UIExtendedGrayColorSpace 0 0";
NSColor = "UIExtendedGrayColorSpace 0 1";
NSFont = "<UICTFont: 0x130457dc0> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 14.00pt";
NSParagraphStyle = "Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection 0, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint ''";
}
I included a video of the two examples at this link.
@NickGerleman has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
Thanks @NickGerleman. Could you tell me what is the lint warning? I can fix it and push again the branch so it is easier to import. |
It's a formatting issue that I can fix before landing, but it is waiting on a reviewer of the imported PR. The best expert for iOS TextInput has been out of office. |
Don’t depend on external variables that can break this.
Thanks @NickGerleman. I replied to your code-review and further improved the solution as by Meta requirement. |
I will take another look at this soon (still need to write the accompanying internal tests first). |
Finally cleared some other commitments, so writing those tests now. |
@NickGerleman has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
@NickGerleman merged this pull request in f6badca. |
…n prepending new lines to the text (#38679) Summary: ### Please re-state the problem that we are trying to solve in this issue. Multiline TextInput with a fixed height will scroll to the bottom of the screen when prepending new lines to the text. ### What is the root cause of that problem? The issue is caused by iOS UITextView: - The cursor moves to the end of the text when prepending new lines. - Moving the cursor to the end of the text triggers the scroll to the bottom. The behavior was reproduced on an iOS App (without react-native). The example included below implements a Component RCTUITextView based on UITextView, which modifies the UITextView attributedText with the textViewDidChange callback (source code available in this [comment](Expensify/App#19507 (comment))). Adding a new line on top of the UITextView on iOS results in: Issue 1) The cursor moves to the end of TextInput text Issue 2) The TextInput scrolls to the bottom <details><summary>Reproducing the issue on an iOS App without react-native</summary> <p> <video src="https://user-images.githubusercontent.com/24992535/246601549-99f480f3-ce80-4678-9378-f71c8aa67e17.mp4" width="900" /> </p> </details> Issue 1) is already fixed in react-native, which restores the previous cursor position (on Fabric with [_setAttributedString](https://github.com/fabriziobertoglio1987/react-native/blob/71e7bbbc2cf21abacf7009e300f5bba737e20d17/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm#L600-L610)) after changing the text. Issue 2) needs to be fixed in react-native. ### What changes do you think we should make in order to solve the problem? Setting the correct TextInput scroll position after re-setting the cursor. ## Changelog: [IOS] [FIXED] - Fix Multiline TextInput with a fixed height scrolls to the bottom when changing AttributedText Pull Request resolved: #38679 Test Plan: Fabric (reproduces on controlled/not controlled TextInput example): | Before | After | | ----------- | ----------- | | <video src="https://github.com/facebook/react-native/assets/24992535/e06b31fe-407d-4897-b608-73e0cc0f224a" width="350" /> | <video src="https://github.com/facebook/react-native/assets/24992535/fa2eaa31-c616-43c5-9596-f84e7b70d80a" width="350" /> | Paper (reproduces only on controlled TextInput example): ```javascript function TextInputExample() { const [text, setText] = React.useState(''); return ( <View style={{marginTop: 200}}> <TextInput style={{height: 50, backgroundColor: 'white'}} multiline={true} value={text} onChangeText={text => { setText(text); }} /> </View> ); } ``` | Before | After | | ----------- | ----------- | | <video src="https://github.com/facebook/react-native/assets/24992535/6cb1f2de-717e-4dce-be0a-644f6a051c08" width="350" /> | <video src="https://github.com/facebook/react-native/assets/24992535/dee6edb6-76c6-48b0-b78f-99626235d30e" width="350" /> | Reviewed By: sammy-SC, cipolleschi Differential Revision: D48674090 Pulled By: NickGerleman fbshipit-source-id: 349e7b0910e314ec94b45b68c38571fed41ef117
…n prepending new lines to the text (#38679) Summary: ### Please re-state the problem that we are trying to solve in this issue. Multiline TextInput with a fixed height will scroll to the bottom of the screen when prepending new lines to the text. ### What is the root cause of that problem? The issue is caused by iOS UITextView: - The cursor moves to the end of the text when prepending new lines. - Moving the cursor to the end of the text triggers the scroll to the bottom. The behavior was reproduced on an iOS App (without react-native). The example included below implements a Component RCTUITextView based on UITextView, which modifies the UITextView attributedText with the textViewDidChange callback (source code available in this [comment](Expensify/App#19507 (comment))). Adding a new line on top of the UITextView on iOS results in: Issue 1) The cursor moves to the end of TextInput text Issue 2) The TextInput scrolls to the bottom <details><summary>Reproducing the issue on an iOS App without react-native</summary> <p> <video src="https://user-images.githubusercontent.com/24992535/246601549-99f480f3-ce80-4678-9378-f71c8aa67e17.mp4" width="900" /> </p> </details> Issue 1) is already fixed in react-native, which restores the previous cursor position (on Fabric with [_setAttributedString](https://github.com/fabriziobertoglio1987/react-native/blob/71e7bbbc2cf21abacf7009e300f5bba737e20d17/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm#L600-L610)) after changing the text. Issue 2) needs to be fixed in react-native. ### What changes do you think we should make in order to solve the problem? Setting the correct TextInput scroll position after re-setting the cursor. ## Changelog: [IOS] [FIXED] - Fix Multiline TextInput with a fixed height scrolls to the bottom when changing AttributedText Pull Request resolved: #38679 Test Plan: Fabric (reproduces on controlled/not controlled TextInput example): | Before | After | | ----------- | ----------- | | <video src="https://github.com/facebook/react-native/assets/24992535/e06b31fe-407d-4897-b608-73e0cc0f224a" width="350" /> | <video src="https://github.com/facebook/react-native/assets/24992535/fa2eaa31-c616-43c5-9596-f84e7b70d80a" width="350" /> | Paper (reproduces only on controlled TextInput example): ```javascript function TextInputExample() { const [text, setText] = React.useState(''); return ( <View style={{marginTop: 200}}> <TextInput style={{height: 50, backgroundColor: 'white'}} multiline={true} value={text} onChangeText={text => { setText(text); }} /> </View> ); } ``` | Before | After | | ----------- | ----------- | | <video src="https://github.com/facebook/react-native/assets/24992535/6cb1f2de-717e-4dce-be0a-644f6a051c08" width="350" /> | <video src="https://github.com/facebook/react-native/assets/24992535/dee6edb6-76c6-48b0-b78f-99626235d30e" width="350" /> | Reviewed By: sammy-SC, cipolleschi Differential Revision: D48674090 Pulled By: NickGerleman fbshipit-source-id: 349e7b0910e314ec94b45b68c38571fed41ef117
…n prepending new lines to the text (#38679) Summary: ### Please re-state the problem that we are trying to solve in this issue. Multiline TextInput with a fixed height will scroll to the bottom of the screen when prepending new lines to the text. ### What is the root cause of that problem? The issue is caused by iOS UITextView: - The cursor moves to the end of the text when prepending new lines. - Moving the cursor to the end of the text triggers the scroll to the bottom. The behavior was reproduced on an iOS App (without react-native). The example included below implements a Component RCTUITextView based on UITextView, which modifies the UITextView attributedText with the textViewDidChange callback (source code available in this [comment](Expensify/App#19507 (comment))). Adding a new line on top of the UITextView on iOS results in: Issue 1) The cursor moves to the end of TextInput text Issue 2) The TextInput scrolls to the bottom <details><summary>Reproducing the issue on an iOS App without react-native</summary> <p> <video src="https://user-images.githubusercontent.com/24992535/246601549-99f480f3-ce80-4678-9378-f71c8aa67e17.mp4" width="900" /> </p> </details> Issue 1) is already fixed in react-native, which restores the previous cursor position (on Fabric with [_setAttributedString](https://github.com/fabriziobertoglio1987/react-native/blob/71e7bbbc2cf21abacf7009e300f5bba737e20d17/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm#L600-L610)) after changing the text. Issue 2) needs to be fixed in react-native. ### What changes do you think we should make in order to solve the problem? Setting the correct TextInput scroll position after re-setting the cursor. ## Changelog: [IOS] [FIXED] - Fix Multiline TextInput with a fixed height scrolls to the bottom when changing AttributedText Pull Request resolved: #38679 Test Plan: Fabric (reproduces on controlled/not controlled TextInput example): | Before | After | | ----------- | ----------- | | <video src="https://github.com/facebook/react-native/assets/24992535/e06b31fe-407d-4897-b608-73e0cc0f224a" width="350" /> | <video src="https://github.com/facebook/react-native/assets/24992535/fa2eaa31-c616-43c5-9596-f84e7b70d80a" width="350" /> | Paper (reproduces only on controlled TextInput example): ```javascript function TextInputExample() { const [text, setText] = React.useState(''); return ( <View style={{marginTop: 200}}> <TextInput style={{height: 50, backgroundColor: 'white'}} multiline={true} value={text} onChangeText={text => { setText(text); }} /> </View> ); } ``` | Before | After | | ----------- | ----------- | | <video src="https://github.com/facebook/react-native/assets/24992535/6cb1f2de-717e-4dce-be0a-644f6a051c08" width="350" /> | <video src="https://github.com/facebook/react-native/assets/24992535/dee6edb6-76c6-48b0-b78f-99626235d30e" width="350" /> | Reviewed By: sammy-SC, cipolleschi Differential Revision: D48674090 Pulled By: NickGerleman fbshipit-source-id: 349e7b0910e314ec94b45b68c38571fed41ef117
Summary:
Please re-state the problem that we are trying to solve in this issue.
Multiline TextInput with a fixed height will scroll to the bottom of the screen when prepending new lines to the text.
What is the root cause of that problem?
The issue is caused by iOS UITextView:
The behavior was reproduced on an iOS App (without react-native).
The example included below implements a Component RCTUITextView based on UITextView, which modifies the UITextView attributedText with the textViewDidChange callback (source code available in this comment).
Adding a new line on top of the UITextView on iOS results in:
Issue 1) The cursor moves to the end of TextInput text
Issue 2) The TextInput scrolls to the bottom
Reproducing the issue on an iOS App without react-native
2023-06-17.18-16-16.mp4
Issue 1) is already fixed in react-native, which restores the previous cursor position (on Fabric with _setAttributedString) after changing the text.
Issue 2) needs to be fixed in react-native.
What changes do you think we should make in order to solve the problem?
Setting the correct TextInput scroll position after re-setting the cursor.
Changelog:
[IOS] [FIXED] - Fix Multiline TextInput with a fixed height scrolls to the bottom when changing AttributedText
Test Plan:
Fabric (reproduces on controlled/not controlled TextInput example):
Simulator.Screen.Recording.-.iPhone.14.Pro.-.2023-07-29.at.11.57.04.mp4
Simulator.Screen.Recording.-.iPhone.14.Pro.-.2023-07-29.at.11.55.11.mp4
Paper (reproduces only on controlled TextInput example):
Simulator.Screen.Recording.-.iPhone.14.Pro.-.2023-07-29.at.13.05.48.mp4
Simulator.Screen.Recording.-.iPhone.14.Pro.-.2023-07-29.at.13.09.48.mp4