From 06c05e744d8af9582bde348210f254d76dae48b9 Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Wed, 9 May 2018 00:47:55 -0700 Subject: [PATCH] RN: Cleanup `Text` Implementation Reviewed By: sahrens, TheSavior Differential Revision: D7901531 fbshipit-source-id: dfaba402c1c26e34e9d2df01f2bbb8c26dfcd17e --- Libraries/Text/Text.js | 304 ++++++++++++++++++++++------------------- 1 file changed, 164 insertions(+), 140 deletions(-) diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js index 1727e8f26..adde97175 100644 --- a/Libraries/Text/Text.js +++ b/Libraries/Text/Text.js @@ -7,6 +7,7 @@ * @flow * @format */ + 'use strict'; const React = require('React'); @@ -18,17 +19,31 @@ const Touchable = require('Touchable'); const UIManager = require('UIManager'); const createReactNativeComponentClass = require('createReactNativeComponentClass'); +const nullthrows = require('fbjs/lib/nullthrows'); const processColor = require('processColor'); import type {PressEvent} from 'CoreEventTypes'; import type {PressRetentionOffset, TextProps} from 'TextProps'; +type ResponseHandlers = $ReadOnly<{| + onStartShouldSetResponder: () => boolean, + onResponderGrant: (event: SyntheticEvent<>, dispatchID: string) => void, + onResponderMove: (event: SyntheticEvent<>) => void, + onResponderRelease: (event: SyntheticEvent<>) => void, + onResponderTerminate: (event: SyntheticEvent<>) => void, + onResponderTerminationRequest: () => boolean, +|}>; + +type Props = TextProps; + type State = {| touchable: {| touchState: ?string, responderID: ?number, |}, isHighlighted: boolean, + createResponderHandlers: () => ResponseHandlers, + responseHandlers: ?ResponseHandlers, |}; const PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; @@ -55,7 +70,7 @@ const viewConfig = { * * See https://facebook.github.io/react-native/docs/text.html */ -class Text extends ReactNative.NativeComponent { +class Text extends ReactNative.NativeComponent { static propTypes = TextPropTypes; static defaultProps = { @@ -64,176 +79,185 @@ class Text extends ReactNative.NativeComponent { ellipsizeMode: 'tail', }; + touchableGetPressRectOffset: ?() => PressRetentionOffset; + touchableHandleActivePressIn: ?() => void; + touchableHandleActivePressOut: ?() => void; + touchableHandleLongPress: ?(event: PressEvent) => void; + touchableHandlePress: ?(event: PressEvent) => void; + touchableHandleResponderGrant: ?( + event: SyntheticEvent<>, + dispatchID: string, + ) => void; + touchableHandleResponderMove: ?(event: SyntheticEvent<>) => void; + touchableHandleResponderRelease: ?(event: SyntheticEvent<>) => void; + touchableHandleResponderTerminate: ?(event: SyntheticEvent<>) => void; + touchableHandleResponderTerminationRequest: ?() => boolean; + state = { ...Touchable.Mixin.touchableGetInitialState(), isHighlighted: false, + createResponderHandlers: this._createResponseHandlers.bind(this), + responseHandlers: null, }; - viewConfig = viewConfig; - - _handlers: ?Object; - - _hasPressHandler(): boolean { - return !!this.props.onPress || !!this.props.onLongPress; + static getDerivedStateFromProps(nextProps: Props, prevState: State): ?State { + return prevState.responseHandlers == null && isTouchable(nextProps) + ? { + ...prevState, + responseHandlers: prevState.createResponderHandlers(), + } + : null; } - /** - * These are assigned lazily the first time the responder is set to make plain - * text nodes as cheap as possible. - */ - touchableHandleActivePressIn: ?Function; - touchableHandleActivePressOut: ?Function; - touchableHandlePress: ?Function; - touchableHandleLongPress: ?Function; - touchableHandleResponderGrant: ?Function; - touchableHandleResponderMove: ?Function; - touchableHandleResponderRelease: ?Function; - touchableHandleResponderTerminate: ?Function; - touchableHandleResponderTerminationRequest: ?Function; - touchableGetPressRectOffset: ?Function; - render(): React.Element { - let newProps = this.props; - if (this.props.onStartShouldSetResponder || this._hasPressHandler()) { - if (!this._handlers) { - this._handlers = { - onStartShouldSetResponder: (): boolean => { - const shouldSetFromProps = - this.props.onStartShouldSetResponder && - this.props.onStartShouldSetResponder(); - const setResponder = shouldSetFromProps || this._hasPressHandler(); - if (setResponder && !this.touchableHandleActivePressIn) { - // Attach and bind all the other handlers only the first time a touch - // actually happens. - for (const key in Touchable.Mixin) { - if (typeof Touchable.Mixin[key] === 'function') { - (this: any)[key] = Touchable.Mixin[key].bind(this); - } - } - this.touchableHandleActivePressIn = () => { - if ( - this.props.suppressHighlighting || - !this._hasPressHandler() - ) { - return; - } - this.setState({ - isHighlighted: true, - }); - }; + static viewConfig = viewConfig; - this.touchableHandleActivePressOut = () => { - if ( - this.props.suppressHighlighting || - !this._hasPressHandler() - ) { - return; - } - this.setState({ - isHighlighted: false, - }); - }; - - this.touchableHandlePress = (e: PressEvent) => { - this.props.onPress && this.props.onPress(e); - }; - - this.touchableHandleLongPress = (e: PressEvent) => { - this.props.onLongPress && this.props.onLongPress(e); - }; - - this.touchableGetPressRectOffset = function(): PressRetentionOffset { - return this.props.pressRetentionOffset || PRESS_RECT_OFFSET; - }; - } - return setResponder; - }, - onResponderGrant: function(e: SyntheticEvent<>, dispatchID: string) { - // $FlowFixMe TouchableMixin handlers couldn't actually be null - this.touchableHandleResponderGrant(e, dispatchID); - this.props.onResponderGrant && - this.props.onResponderGrant.apply(this, arguments); - }.bind(this), - onResponderMove: function(e: SyntheticEvent<>) { - // $FlowFixMe TouchableMixin handlers couldn't actually be null - this.touchableHandleResponderMove(e); - this.props.onResponderMove && - this.props.onResponderMove.apply(this, arguments); - }.bind(this), - onResponderRelease: function(e: SyntheticEvent<>) { - // $FlowFixMe TouchableMixin handlers couldn't actually be null - this.touchableHandleResponderRelease(e); - this.props.onResponderRelease && - this.props.onResponderRelease.apply(this, arguments); - }.bind(this), - onResponderTerminate: function(e: SyntheticEvent<>) { - // $FlowFixMe TouchableMixin handlers couldn't actually be null - this.touchableHandleResponderTerminate(e); - this.props.onResponderTerminate && - this.props.onResponderTerminate.apply(this, arguments); - }.bind(this), - onResponderTerminationRequest: function(): boolean { - // Allow touchable or props.onResponderTerminationRequest to deny - // the request - // $FlowFixMe TouchableMixin handlers couldn't actually be null - var allowTermination = this.touchableHandleResponderTerminationRequest(); - if (allowTermination && this.props.onResponderTerminationRequest) { - allowTermination = this.props.onResponderTerminationRequest.apply( - this, - arguments, - ); - } - return allowTermination; - }.bind(this), - }; - } - newProps = { - ...this.props, - ...this._handlers, + render(): React.Node { + let props = this.props; + if (isTouchable(props)) { + props = { + ...props, + ...this.state.responseHandlers, isHighlighted: this.state.isHighlighted, }; } - if (newProps.selectionColor != null) { - newProps = { - ...newProps, - selectionColor: processColor(newProps.selectionColor), + if (props.selectionColor != null) { + props = { + ...props, + selectionColor: processColor(props.selectionColor), }; } - if (Touchable.TOUCH_TARGET_DEBUG && newProps.onPress) { - newProps = { - ...newProps, - style: [this.props.style, {color: 'magenta'}], - }; + if (__DEV__) { + if (Touchable.TOUCH_TARGET_DEBUG && props.onPress != null) { + props = { + ...props, + style: [props.style, {color: 'magenta'}], + }; + } } return ( {hasTextAncestor => hasTextAncestor ? ( - + ) : ( - + ) } ); } + + _createResponseHandlers(): ResponseHandlers { + return { + onStartShouldSetResponder: (): boolean => { + const {onStartShouldSetResponder} = this.props; + const shouldSetResponder = + (onStartShouldSetResponder == null + ? false + : onStartShouldSetResponder()) || isTouchable(this.props); + + if (shouldSetResponder) { + this._attachTouchHandlers(); + } + return shouldSetResponder; + }, + onResponderGrant: (event: SyntheticEvent<>, dispatchID: string): void => { + nullthrows(this.touchableHandleResponderGrant)(event, dispatchID); + if (this.props.onResponderGrant != null) { + this.props.onResponderGrant.apply(this, arguments); + } + }, + onResponderMove: (event: SyntheticEvent<>): void => { + nullthrows(this.touchableHandleResponderMove)(event); + if (this.props.onResponderMove != null) { + this.props.onResponderMove.apply(this, arguments); + } + }, + onResponderRelease: (event: SyntheticEvent<>): void => { + nullthrows(this.touchableHandleResponderRelease)(event); + if (this.props.onResponderRelease != null) { + this.props.onResponderRelease.apply(this, arguments); + } + }, + onResponderTerminate: (event: SyntheticEvent<>): void => { + nullthrows(this.touchableHandleResponderTerminate)(event); + if (this.props.onResponderTerminate != null) { + this.props.onResponderTerminate.apply(this, arguments); + } + }, + onResponderTerminationRequest: (): boolean => { + const {onResponderTerminationRequest} = this.props; + if (!nullthrows(this.touchableHandleResponderTerminationRequest)()) { + return false; + } + if (onResponderTerminationRequest == null) { + return true; + } + return onResponderTerminationRequest(); + }, + }; + } + + /** + * Lazily attaches Touchable.Mixin handlers. + */ + _attachTouchHandlers(): void { + if (this.touchableGetPressRectOffset != null) { + return; + } + for (const key in Touchable.Mixin) { + if (typeof Touchable.Mixin[key] === 'function') { + (this: any)[key] = Touchable.Mixin[key].bind(this); + } + } + this.touchableHandleActivePressIn = (): void => { + if (!this.props.suppressHighlighting && isTouchable(this.props)) { + this.setState({isHighlighted: true}); + } + }; + this.touchableHandleActivePressOut = (): void => { + if (!this.props.suppressHighlighting && isTouchable(this.props)) { + this.setState({isHighlighted: false}); + } + }; + this.touchableHandlePress = (event: PressEvent): void => { + if (this.props.onPress != null) { + this.props.onPress(event); + } + }; + this.touchableHandleLongPress = (event: PressEvent): void => { + if (this.props.onLongPress != null) { + this.props.onLongPress(event); + } + }; + this.touchableGetPressRectOffset = (): PressRetentionOffset => + this.props.pressRetentionOffset == null + ? PRESS_RECT_OFFSET + : this.props.pressRetentionOffset; + } } -var RCTText = createReactNativeComponentClass( +const isTouchable = (props: Props): boolean => + props.onPress != null || + props.onLongPress != null || + props.onStartShouldSetResponder != null; + +const RCTText = createReactNativeComponentClass( viewConfig.uiViewClassName, () => viewConfig, ); -var RCTVirtualText = RCTText; -if (UIManager.RCTVirtualText) { - RCTVirtualText = createReactNativeComponentClass('RCTVirtualText', () => ({ - validAttributes: { - ...ReactNativeViewAttributes.UIView, - isHighlighted: true, - }, - uiViewClassName: 'RCTVirtualText', - })); -} +const RCTVirtualText = + UIManager.RCTVirtualText == null + ? RCTText + : createReactNativeComponentClass('RCTVirtualText', () => ({ + validAttributes: { + ...ReactNativeViewAttributes.UIView, + isHighlighted: true, + }, + uiViewClassName: 'RCTVirtualText', + })); module.exports = Text;