RN: Cleanup `Text` Implementation

Reviewed By: sahrens, TheSavior

Differential Revision: D7901531

fbshipit-source-id: dfaba402c1c26e34e9d2df01f2bbb8c26dfcd17e
This commit is contained in:
Tim Yung 2018-05-09 00:47:55 -07:00 committed by Facebook Github Bot
parent a1f2076aae
commit 06c05e744d
1 changed files with 164 additions and 140 deletions

View File

@ -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<TextProps, State> {
class Text extends ReactNative.NativeComponent<Props, State> {
static propTypes = TextPropTypes;
static defaultProps = {
@ -64,176 +79,185 @@ class Text extends ReactNative.NativeComponent<TextProps, State> {
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<any> {
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 (
<TextAncestor.Consumer>
{hasTextAncestor =>
hasTextAncestor ? (
<RCTVirtualText {...newProps} />
<RCTVirtualText {...props} />
) : (
<TextAncestor.Provider value={true}>
<RCTText {...newProps} />
<RCTText {...props} />
</TextAncestor.Provider>
)
}
</TextAncestor.Consumer>
);
}
_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;