Skip to content
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

Closed
wants to merge 26 commits into from

Conversation

fabOnReact
Copy link
Contributor

@fabOnReact fabOnReact commented Jul 29, 2023

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).

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):

Before After
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):

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
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

@facebook-github-bot facebook-github-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 Jul 29, 2023
@analysis-bot
Copy link

analysis-bot commented Jul 29, 2023

Platform Engine Arch Size (bytes) Diff
android hermes arm64-v8a 17,885,642 -1
android hermes armeabi-v7a n/a --
android hermes x86 n/a --
android hermes x86_64 n/a --
android jsc arm64-v8a 21,240,040 +1
android jsc armeabi-v7a n/a --
android jsc x86 n/a --
android jsc x86_64 n/a --

Base commit: 475a156
Branch: main

@fabOnReact fabOnReact changed the title Fix input field scrolls up when tap enter in 1st line of text Fix iOS TextInput scrolls up when tap on 1st line of text Jul 29, 2023
@fabOnReact fabOnReact changed the title Fix iOS TextInput scrolls up when tap on 1st line of text Fix Multiline TextInput with a fixed height scrolls to the bottom when prepending new lines to the text Jul 29, 2023
@fabOnReact fabOnReact marked this pull request as ready for review July 29, 2023 07:33
@facebook-github-bot facebook-github-bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Jul 29, 2023
@NickGerleman
Copy link
Contributor

Would this also prevent scroll when adding to the end of the list?

@fabOnReact fabOnReact marked this pull request as draft August 17, 2023 11:48
@fabOnReact
Copy link
Contributor Author

Thanks for the code review. I moved the PR to draft. I'm working on a solution for this issue.

@fabOnReact
Copy link
Contributor Author

Simulator.Screen.Recording.-.iPhone.14.Pro.-.2023-08-19.at.09.35.06.mp4

@fabOnReact
Copy link
Contributor Author

Thanks for the code-review @NickGerleman. I updated the PR and I included a video test in this comment.

@fabOnReact fabOnReact marked this pull request as ready for review August 20, 2023 03:10
Comment on lines 597 to 606
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;
}
}
Copy link
Contributor

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?

Copy link
Contributor Author

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?

https://github.com/facebook/react-native/pull/38679/files#diff-65bdfd8888173755da0fd38dae8c70ba1561c11ec453e690195a2f327fa6b62bR603

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.

  1. 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).
  2. Changing the AttributedText (text) attributes (alignment) triggers a mutation of the attributed text (_textOf)
  3. Mutating the AttributedString causes the cursor to move to the end of the string
  4. 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.

@facebook-github-bot
Copy link
Contributor

@NickGerleman has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

@fabOnReact
Copy link
Contributor Author

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.

@NickGerleman
Copy link
Contributor

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.

@fabOnReact fabOnReact marked this pull request as ready for review February 19, 2024 03:17
@fabOnReact
Copy link
Contributor Author

Thanks @NickGerleman. I replied to your code-review and further improved the solution as by Meta requirement.
#38679 (comment)
#38679 (comment)

@NickGerleman
Copy link
Contributor

Thanks @NickGerleman. I replied to your code-review and further improved the solution as by Meta requirement. #38679 (comment) #38679 (comment)

I will take another look at this soon (still need to write the accompanying internal tests first).

@NickGerleman
Copy link
Contributor

Thanks @NickGerleman. I replied to your code-review and further improved the solution as by Meta requirement. #38679 (comment) #38679 (comment)

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.

@facebook-github-bot
Copy link
Contributor

@NickGerleman has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

@facebook-github-bot facebook-github-bot added the Merged This PR has been merged. label Mar 28, 2024
@facebook-github-bot
Copy link
Contributor

@NickGerleman merged this pull request in f6badca.

Titozzz pushed a commit that referenced this pull request Jun 18, 2024
…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
Titozzz pushed a commit that referenced this pull request Jun 18, 2024
…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
Titozzz pushed a commit that referenced this pull request Jun 18, 2024
…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
This was referenced Jun 28, 2024
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. Merged This PR has been merged. 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.

6 participants