[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.
This commit is contained in:
Nicolas Gallagher
2020-02-04 11:50:56 -08:00
parent 7d440c74f4
commit fc443c5abd
3 changed files with 61 additions and 58 deletions
@@ -7,6 +7,9 @@
* @flow strict * @flow strict
*/ */
import * as React from 'react'; import type { Context } from 'react';
const TextAncestorContext = React.createContext(false);
export default (TextAncestorContext: React.Context<boolean>); import { createContext } from 'react';
const TextAncestorContext = createContext(false);
export default (TextAncestorContext: Context<boolean>);
@@ -44,12 +44,12 @@ describe('components/View', () => {
}); });
describe('prop "hitSlop"', () => { describe('prop "hitSlop"', () => {
it('renders a span with negative position offsets', () => { test('renders a span with negative position offsets', () => {
const { container } = render(<View hitSlop={{ top: 10, bottom: 10, right: 5, left: 5 }} />); const { container } = render(<View hitSlop={{ top: 10, bottom: 10, right: 5, left: 5 }} />);
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();
}); });
it('handles partial offsets', () => { test('handles partial offsets', () => {
const { container } = render(<View hitSlop={{ top: 10 }} />); const { container } = render(<View hitSlop={{ top: 10 }} />);
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();
}); });
+53 -53
View File
@@ -10,73 +10,73 @@
import type { ViewProps } from './types'; import type { ViewProps } from './types';
import applyLayout from '../../modules/applyLayout';
import applyNativeMethods from '../../modules/applyNativeMethods';
import createElement from '../createElement'; import createElement from '../createElement';
import css from '../StyleSheet/css'; import css from '../StyleSheet/css';
import filterSupportedProps from './filterSupportedProps'; import filterSupportedProps from './filterSupportedProps';
import setAndForwardRef from '../../modules/setAndForwardRef';
import useElementLayout from '../../hooks/useElementLayout';
import usePlatformMethods from '../../hooks/usePlatformMethods';
import StyleSheet from '../StyleSheet'; import StyleSheet from '../StyleSheet';
import TextAncestorContext from '../Text/TextAncestorContext'; import TextAncestorContext from '../Text/TextAncestorContext';
import React from 'react'; import React, { forwardRef, useContext, useRef } from 'react';
export type { ViewProps }; export type { ViewProps };
const calculateHitSlopStyle = hitSlop => { function createHitSlopElement(hitSlop) {
const hitStyle = {}; const hitSlopStyle = {};
for (const prop in hitSlop) { for (const prop in hitSlop) {
if (hitSlop.hasOwnProperty(prop)) { if (hitSlop.hasOwnProperty(prop)) {
const value = hitSlop[prop]; const value = hitSlop[prop];
hitStyle[prop] = value > 0 ? -1 * value : 0; hitSlopStyle[prop] = value > 0 ? -1 * value : 0;
} }
} }
return hitStyle; return createElement('span', {
}; classList: [classes.hitSlop],
style: hitSlopStyle
class View extends React.Component<ViewProps> { });
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 <View>.`
);
}
});
}
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 (
<TextAncestorContext.Consumer>
{hasTextAncestor => this.renderView(hasTextAncestor)}
</TextAncestorContext.Consumer>
);
}
} }
const View = forwardRef<ViewProps, *>((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 <View>.`);
}
});
}
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({ const classes = css.create({
view: { view: {
alignItems: 'stretch', alignItems: 'stretch',
@@ -111,4 +111,4 @@ const styles = StyleSheet.create({
} }
}); });
export default applyLayout(applyNativeMethods(View)); export default View;