From fc443c5abdafe40f2fe710d08e043891e06102a9 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Tue, 4 Feb 2020 11:50:56 -0800 Subject: [PATCH] [change] modernize View Rewrite View to use function components and hooks. The 'usePlatformMethods' hook also fixes a bug in the class-based implementation of 'setNativeProps' which was unable to correctly merge its styles with those provided via the component API. In the future, 'setNativeProps' will be removed from React Native anyway. See (3) in #1136 for more context. --- .../src/exports/Text/TextAncestorContext.js | 9 +- .../src/exports/View/__tests__/index-test.js | 4 +- .../src/exports/View/index.js | 106 +++++++++--------- 3 files changed, 61 insertions(+), 58 deletions(-) diff --git a/packages/react-native-web/src/exports/Text/TextAncestorContext.js b/packages/react-native-web/src/exports/Text/TextAncestorContext.js index 020069de..eb691f4c 100644 --- a/packages/react-native-web/src/exports/Text/TextAncestorContext.js +++ b/packages/react-native-web/src/exports/Text/TextAncestorContext.js @@ -7,6 +7,9 @@ * @flow strict */ -import * as React from 'react'; -const TextAncestorContext = React.createContext(false); -export default (TextAncestorContext: React.Context); +import type { Context } from 'react'; + +import { createContext } from 'react'; + +const TextAncestorContext = createContext(false); +export default (TextAncestorContext: Context); diff --git a/packages/react-native-web/src/exports/View/__tests__/index-test.js b/packages/react-native-web/src/exports/View/__tests__/index-test.js index cc5bf530..e6579565 100644 --- a/packages/react-native-web/src/exports/View/__tests__/index-test.js +++ b/packages/react-native-web/src/exports/View/__tests__/index-test.js @@ -44,12 +44,12 @@ describe('components/View', () => { }); describe('prop "hitSlop"', () => { - it('renders a span with negative position offsets', () => { + test('renders a span with negative position offsets', () => { const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); - it('handles partial offsets', () => { + test('handles partial offsets', () => { const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); diff --git a/packages/react-native-web/src/exports/View/index.js b/packages/react-native-web/src/exports/View/index.js index 43902f65..d37a6732 100644 --- a/packages/react-native-web/src/exports/View/index.js +++ b/packages/react-native-web/src/exports/View/index.js @@ -10,73 +10,73 @@ import type { ViewProps } from './types'; -import applyLayout from '../../modules/applyLayout'; -import applyNativeMethods from '../../modules/applyNativeMethods'; import createElement from '../createElement'; import css from '../StyleSheet/css'; import filterSupportedProps from './filterSupportedProps'; +import setAndForwardRef from '../../modules/setAndForwardRef'; +import useElementLayout from '../../hooks/useElementLayout'; +import usePlatformMethods from '../../hooks/usePlatformMethods'; import StyleSheet from '../StyleSheet'; import TextAncestorContext from '../Text/TextAncestorContext'; -import React from 'react'; +import React, { forwardRef, useContext, useRef } from 'react'; export type { ViewProps }; -const calculateHitSlopStyle = hitSlop => { - const hitStyle = {}; +function createHitSlopElement(hitSlop) { + const hitSlopStyle = {}; for (const prop in hitSlop) { if (hitSlop.hasOwnProperty(prop)) { const value = hitSlop[prop]; - hitStyle[prop] = value > 0 ? -1 * value : 0; + hitSlopStyle[prop] = value > 0 ? -1 * value : 0; } } - return hitStyle; -}; - -class View extends React.Component { - static displayName = 'View'; - - renderView(hasTextAncestor) { - const hitSlop = this.props.hitSlop; - const supportedProps = filterSupportedProps(this.props); - - if (process.env.NODE_ENV !== 'production') { - React.Children.toArray(this.props.children).forEach(item => { - if (typeof item === 'string') { - console.error( - `Unexpected text node: ${item}. A text node cannot be a child of a .` - ); - } - }); - } - - supportedProps.classList = [classes.view]; - supportedProps.ref = this.props.forwardedRef; - supportedProps.style = StyleSheet.compose( - hasTextAncestor && styles.inline, - this.props.style - ); - - if (hitSlop) { - const hitSlopStyle = calculateHitSlopStyle(hitSlop); - const hitSlopChild = createElement('span', { - classList: [classes.hitSlop], - style: hitSlopStyle - }); - supportedProps.children = React.Children.toArray([hitSlopChild, supportedProps.children]); - } - - return createElement('div', supportedProps); - } - - render() { - return ( - - {hasTextAncestor => this.renderView(hasTextAncestor)} - - ); - } + return createElement('span', { + classList: [classes.hitSlop], + style: hitSlopStyle + }); } +const View = forwardRef((props, ref) => { + const { forwardedRef, hitSlop, onLayout, style, ...rest } = props; + + if (process.env.NODE_ENV !== 'production') { + React.Children.toArray(props.children).forEach(item => { + if (typeof item === 'string') { + console.error(`Unexpected text node: ${item}. A text node cannot be a child of a .`); + } + }); + } + + const classList = [classes.view]; + const hasTextAncestor = useContext(TextAncestorContext); + const hostRef = useRef(null); + + const setRef = setAndForwardRef({ + getForwardedRef: () => forwardedRef, + setLocalRef: c => { + hostRef.current = c; + } + }); + + useElementLayout(hostRef, onLayout); + usePlatformMethods(hostRef, ref, classList, style); + + const supportedProps = filterSupportedProps(rest); + supportedProps.children = hitSlop + ? React.Children.toArray([createHitSlopElement(hitSlop), props.children]) + : props.children; + supportedProps.classList = classList; + supportedProps.ref = setRef; + supportedProps.style = StyleSheet.compose( + hasTextAncestor && styles.inline, + style + ); + + return createElement('div', supportedProps); +}); + +View.displayName = 'View'; + const classes = css.create({ view: { alignItems: 'stretch', @@ -111,4 +111,4 @@ const styles = StyleSheet.create({ } }); -export default applyLayout(applyNativeMethods(View)); +export default View;