Skip to content

ScrollView, HorizontalScrollView: do not ignore null contentOffset prop #28760

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

Closed
wants to merge 1 commit into from

Conversation

vonovak
Copy link
Collaborator

@vonovak vonovak commented Apr 26, 2020

Summary

motivation: I was just checking out 30cc158
and noticed that the commit, I believe, is missing logic for when contentOffset is actually null.

That is, consider you render ScrollView with contentOffset { x: 0, y: 100 } and then change that to null / undefined. I'd expect the content offset to invalidate (set to 0 - hope that's the default).

Changelog

[Android] [Fixed] - ScrollView, HorizontalScrollView: do not ignore null contentOffset prop

Test Plan

Tested locally within RNTester

code

const Ex = () => {
  let _scrollView: ?React.ElementRef<typeof ScrollView> = React.useRef(null);
  const [offset, setOffset] = React.useState({x: 0, y: 20});
  setTimeout(() => {
    setOffset(undefined);
  }, 2000);

  return (
    <View>
      <ScrollView
        ref={_scrollView}
        automaticallyAdjustContentInsets={false}
        onScroll={() => {
          console.log('onScroll!');
        }}
        contentOffset={offset}
        scrollEventThrottle={200}
        style={styles.scrollView}>
        {ITEMS.map(createItemRow)}
      </ScrollView>
      <Button
        label="Scroll to top"
        onPress={() => {
          nullthrows(_scrollView.current).scrollTo({y: 0});
        }}
      />
      <Button
        label="Scroll to bottom"
        onPress={() => {
          nullthrows(_scrollView.current).scrollToEnd({animated: true});
        }}
      />
      <Button
        label="Flash scroll indicators"
        onPress={() => {
          nullthrows(_scrollView.current).flashScrollIndicators();
        }}
      />
    </View>
  );
};

@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 Apr 26, 2020
@react-native-bot react-native-bot added Bug Platform: Android Android applications. labels Apr 26, 2020
@analysis-bot
Copy link

Platform Engine Arch Size (bytes) Diff
android hermes arm64-v8a 6,743,515 40
android hermes armeabi-v7a 6,408,491 47
android hermes x86 7,125,417 40
android hermes x86_64 7,015,801 55
android jsc arm64-v8a 8,912,035 27
android jsc armeabi-v7a 8,569,424 44
android jsc x86 8,737,078 55
android jsc x86_64 9,313,096 57

Base commit: 30cc158

@analysis-bot
Copy link

analysis-bot commented Apr 26, 2020

Platform Engine Arch Size (bytes) Diff
ios - universal 809,568 0

Base commit: 30cc158

@shergin shergin requested a review from JoshuaGross April 26, 2020 17:19
@JoshuaGross
Copy link
Contributor

Hi @vonovak - what does iOS do, will it reset to 0,0 if you pass in null from JS?

@JoshuaGross
Copy link
Contributor

Depending on what the behavior is for iOS, I actually don't think this is the correct thing to do. Passing in null equivalent to saying "delete this prop from the native side". I would intuitively expect that to actually cause no visible changes. If the engineer wants to reset the position to 0,0 I think those coordinates should be passed in directly.

Treating null as 0,0 also prevents us from doing the following in a typesafe manner: "Pass in coordinates if they're present, otherwise pass along no value, which will cause a noop". The only other way is to pass in condition ? { contentOffset: data } : {} and spread that into the element, which Flow and probably TypeScript don't like.

@vonovak
Copy link
Collaborator Author

vonovak commented May 27, 2020

@JoshuaGross Let me start with a premise that rendering the following should always render the same result (I hope that makes sense 😃 ). To give an example, imagine we render this ScrollView, then re-render it with different props and the last render looks again like this:

<ScrollView
  contentOffset={undefined}>
  {ITEMS.map(createItemRow)}
</ScrollView>

now if we take the example code I gave (I just changed null from the original post to undefined)

const Ex = () => {
  const [offset, setOffset] = React.useState({x: 0, y: 20});
  setTimeout(() => {
    setOffset(undefined);
  }, 2000);

  return (
    <View>
      <ScrollView
        contentOffset={offset}
        style={styles.scrollView}>
        {ITEMS.map(createItemRow)}
      </ScrollView>
    </View>
  );
};

first, we start with contentOffset={{x: 0, y: 20}}, then we re-render with contentOffset={undefined}. At this point,

public void setContentOffset(ReactHorizontalScrollView view, ReadableMap value) {

will be called with null because we effectively removed the contentOffset prop. With code from master, nothing will happen. That breaks the initial premise which is why I think the behavior is incorrect. I think if developer wants to control the offset "outside of react" they should use imperative scrollTo* methods.

edit: iOS will scroll to top for both setOffset(undefined) and setOffset(null). So this should unify the behavior.

It's rather interesting on iOS, I don't know the internals, but if I pass contentOffset={undefined}, that undefined is treated as nil in ObjectiveC and is transformed into CGPoint (x = 0, y = 0).

@vonovak
Copy link
Collaborator Author

vonovak commented Jun 5, 2020

@JoshuaGross would you please re-review? Thanks!

@vonovak
Copy link
Collaborator Author

vonovak commented Jun 29, 2020

@JoshuaGross sorry to ping you again, would you please take a look? The PR unifies behavior between ios and android. Thank you!

@JoshuaGross
Copy link
Contributor

Thanks for clarifying that this will unify behavior! I will test a bit internally.

Copy link
Contributor

@facebook-github-bot facebook-github-bot left a comment

Choose a reason for hiding this comment

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

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

@react-native-bot
Copy link
Collaborator

This pull request was successfully merged by @vonovak in 9e85b7a.

When will my fix make it into a release? | Upcoming Releases

@react-native-bot react-native-bot added the Merged This PR has been merged. label Jun 30, 2020
kelset pushed a commit that referenced this pull request Nov 27, 2020
…` prop (#28760)

Summary:
motivation: I was just checking out 30cc158
and noticed that the commit, I believe, is missing logic for when `contentOffset` is actually `null`.

That is, consider you render `ScrollView` with `contentOffset` { x: 0, y: 100 } and then change that to null / undefined. I'd expect the content offset to invalidate (set to 0 - hope that's the default).

## Changelog

<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://github.com/facebook/react-native/wiki/Changelog
-->

[Android] [Fixed] - ScrollView, HorizontalScrollView: do not ignore `null` `contentOffset` prop
Pull Request resolved: #28760

Test Plan:
Tested locally within RNTester

<details><summary>code</summary>
<p>

```js
const Ex = () => {
  let _scrollView: ?React.ElementRef<typeof ScrollView> = React.useRef(null);
  const [offset, setOffset] = React.useState({x: 0, y: 20});
  setTimeout(() => {
    setOffset(undefined);
  }, 2000);

  return (
    <View>
      <ScrollView
        ref={_scrollView}
        automaticallyAdjustContentInsets={false}
        onScroll={() => {
          console.log('onScroll!');
        }}
        contentOffset={offset}
        scrollEventThrottle={200}
        style={styles.scrollView}>
        {ITEMS.map(createItemRow)}
      </ScrollView>
      <Button
        label="Scroll to top"
        onPress={() => {
          nullthrows(_scrollView.current).scrollTo({y: 0});
        }}
      />
      <Button
        label="Scroll to bottom"
        onPress={() => {
          nullthrows(_scrollView.current).scrollToEnd({animated: true});
        }}
      />
      <Button
        label="Flash scroll indicators"
        onPress={() => {
          nullthrows(_scrollView.current).flashScrollIndicators();
        }}
      />
    </View>
  );
};
```

</p>
</details>

Reviewed By: shergin

Differential Revision: D22298676

Pulled By: JoshuaGross

fbshipit-source-id: e411ba4c8a276908e354d59085d164a38ae253c0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug 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. Platform: Android Android applications.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants