From ae87e1e45919baaa204e246df98388443db84067 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Tue, 30 Mar 2021 18:58:24 -0700 Subject: [PATCH] [fix] ScrollView ref and methods Make sure the 'ref' for a ScrollView is a host node with React Native's proprietary and legacy instance methods attached. Fix #1957 --- .../ScrollView/__tests__/index-test.js | 48 ++++++++++++++++++ .../src/exports/ScrollView/index.js | 49 ++++++++++++++----- 2 files changed, 86 insertions(+), 11 deletions(-) diff --git a/packages/react-native-web/src/exports/ScrollView/__tests__/index-test.js b/packages/react-native-web/src/exports/ScrollView/__tests__/index-test.js index 8a93d818..f93e3c54 100644 --- a/packages/react-native-web/src/exports/ScrollView/__tests__/index-test.js +++ b/packages/react-native-web/src/exports/ScrollView/__tests__/index-test.js @@ -38,4 +38,52 @@ describe('components/ScrollView', () => { expect(onScroll).not.toBeCalled(); }); }); + + describe('prop "ref"', () => { + test('value is set', () => { + const ref = jest.fn(); + render(); + expect(ref).toBeCalled(); + }); + + test('is not called for prop changes', () => { + const ref = jest.fn(); + let rerender; + act(() => { + ({ rerender } = render()); + }); + expect(ref).toHaveBeenCalledTimes(1); + act(() => { + rerender(); + }); + expect(ref).toHaveBeenCalledTimes(1); + }); + + test('node has imperative methods', () => { + const ref = React.createRef(); + act(() => { + render(); + }); + const node = ref.current; + + // Did we get an HTMLElement? + expect(node.tagName === 'DIV').toBe(true); + // Does it have the "platform" methods? + expect(typeof node.measure === 'function').toBe(true); + expect(typeof node.measureLayout === 'function').toBe(true); + expect(typeof node.measureInWindow === 'function').toBe(true); + expect(typeof node.setNativeProps === 'function').toBe(true); + // Does it have the scrollview methods? + expect(typeof node.getScrollResponder === 'function').toBe(true); + expect(typeof node.getScrollableNode === 'function').toBe(true); + expect(typeof node.getInnerViewNode === 'function').toBe(true); + expect(typeof node.getInnerViewRef === 'function').toBe(true); + expect(typeof node.getNativeScrollRef === 'function').toBe(true); + expect(typeof node.scrollTo === 'function').toBe(true); + expect(typeof node.scrollToEnd === 'function').toBe(true); + expect(typeof node.flashScrollIndicators === 'function').toBe(true); + expect(typeof node.scrollResponderZoomTo === 'function').toBe(true); + expect(typeof node.scrollResponderScrollNativeHandleToKeyboard === 'function').toBe(true); + }); + }); }); diff --git a/packages/react-native-web/src/exports/ScrollView/index.js b/packages/react-native-web/src/exports/ScrollView/index.js index 4a826dfb..362da490 100644 --- a/packages/react-native-web/src/exports/ScrollView/index.js +++ b/packages/react-native-web/src/exports/ScrollView/index.js @@ -13,6 +13,7 @@ import type { ViewProps, ViewStyle } from '../View/types'; import createReactClass from 'create-react-class'; import dismissKeyboard from '../../modules/dismissKeyboard'; import invariant from 'fbjs/lib/invariant'; +import mergeRefs from '../../modules/mergeRefs'; import ScrollResponder from '../../modules/ScrollResponder'; import ScrollViewBase from './ScrollViewBase'; import StyleSheet from '../StyleSheet'; @@ -47,12 +48,6 @@ const ScrollView = ((createReactClass({ this.scrollResponderFlashScrollIndicators(); }, - setNativeProps(props: Object) { - if (this._scrollNodeRef) { - this._scrollNodeRef.setNativeProps(props); - } - }, - /** * Returns a reference to the underlying scroll responder, which supports * operations like `scrollTo`. All ScrollView-like components should @@ -67,10 +62,18 @@ const ScrollView = ((createReactClass({ return this._scrollNodeRef; }, + getInnerViewRef(): any { + return this._innerViewRef; + }, + getInnerViewNode(): any { return this._innerViewRef; }, + getNativeScrollRef(): any { + return this._scrollNodeRef; + }, + /** * Scrolls to a given x, y offset, either immediately or with a smooth animation. * Syntax: @@ -129,6 +132,7 @@ const ScrollView = ((createReactClass({ stickyHeaderIndices, pagingEnabled, /* eslint-disable */ + forwardedRef, keyboardDismissMode, onScroll, /* eslint-enable */ @@ -261,12 +265,29 @@ const ScrollView = ((createReactClass({ this.scrollResponderHandleScroll(e); }, - _setInnerViewRef(component) { - this._innerViewRef = component; + _setInnerViewRef(node) { + this._innerViewRef = node; }, - _setScrollNodeRef(component) { - this._scrollNodeRef = component; + _setScrollNodeRef(node) { + this._scrollNodeRef = node; + // ScrollView needs to add more methods to the hostNode in addition to those + // added by `usePlatformMethods`. This is temporarily until an API like + // `ScrollView.scrollTo(hostNode, { x, y })` is added to React Native. + if (node != null) { + node.getScrollResponder = this.getScrollResponder; + node.getInnerViewNode = this.getInnerViewNode; + node.getInnerViewRef = this.getInnerViewRef; + node.getNativeScrollRef = this.getNativeScrollRef; + node.getScrollableNode = this.getScrollableNode; + node.scrollTo = this.scrollTo; + node.scrollToEnd = this.scrollToEnd; + node.flashScrollIndicators = this.flashScrollIndicators; + node.scrollResponderZoomTo = this.scrollResponderZoomTo; + node.scrollResponderScrollNativeHandleToKeyboard = this.scrollResponderScrollNativeHandleToKeyboard; + } + const ref = mergeRefs(this.props.forwardedRef); + ref(node); } }): any): React.ComponentType); @@ -313,4 +334,10 @@ const styles = StyleSheet.create({ } }); -export default ScrollView; +const ForwardedScrollView = React.forwardRef((props, forwardedRef) => { + return ; +}); + +ForwardedScrollView.displayName = 'ScrollView'; + +export default ForwardedScrollView;