Text.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @flow
  8. * @format
  9. */
  10. 'use strict';
  11. const DeprecatedTextPropTypes = require('../DeprecatedPropTypes/DeprecatedTextPropTypes');
  12. const React = require('react');
  13. const ReactNativeViewAttributes = require('../Components/View/ReactNativeViewAttributes');
  14. const TextAncestor = require('./TextAncestor');
  15. const Touchable = require('../Components/Touchable/Touchable');
  16. const UIManager = require('../ReactNative/UIManager');
  17. const createReactNativeComponentClass = require('../Renderer/shims/createReactNativeComponentClass');
  18. const nullthrows = require('nullthrows');
  19. const processColor = require('../StyleSheet/processColor');
  20. import type {PressEvent} from '../Types/CoreEventTypes';
  21. import type {HostComponent} from '../Renderer/shims/ReactNativeTypes';
  22. import type {PressRetentionOffset, TextProps} from './TextProps';
  23. type ResponseHandlers = $ReadOnly<{|
  24. onStartShouldSetResponder: () => boolean,
  25. onResponderGrant: (event: PressEvent, dispatchID: string) => void,
  26. onResponderMove: (event: PressEvent) => void,
  27. onResponderRelease: (event: PressEvent) => void,
  28. onResponderTerminate: (event: PressEvent) => void,
  29. onResponderTerminationRequest: () => boolean,
  30. |}>;
  31. type Props = $ReadOnly<{|
  32. ...TextProps,
  33. forwardedRef: ?React.Ref<'RCTText' | 'RCTVirtualText'>,
  34. |}>;
  35. type State = {|
  36. touchable: {|
  37. touchState: ?string,
  38. responderID: ?number,
  39. |},
  40. isHighlighted: boolean,
  41. createResponderHandlers: () => ResponseHandlers,
  42. responseHandlers: ?ResponseHandlers,
  43. |};
  44. const PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
  45. const viewConfig = {
  46. validAttributes: {
  47. ...ReactNativeViewAttributes.UIView,
  48. isHighlighted: true,
  49. numberOfLines: true,
  50. ellipsizeMode: true,
  51. allowFontScaling: true,
  52. maxFontSizeMultiplier: true,
  53. disabled: true,
  54. selectable: true,
  55. selectionColor: true,
  56. adjustsFontSizeToFit: true,
  57. minimumFontScale: true,
  58. textBreakStrategy: true,
  59. onTextLayout: true,
  60. onInlineViewLayout: true,
  61. dataDetectorType: true,
  62. },
  63. directEventTypes: {
  64. topTextLayout: {
  65. registrationName: 'onTextLayout',
  66. },
  67. topInlineViewLayout: {
  68. registrationName: 'onInlineViewLayout',
  69. },
  70. },
  71. uiViewClassName: 'RCTText',
  72. };
  73. /**
  74. * A React component for displaying text.
  75. *
  76. * See https://reactnative.dev/docs/text.html
  77. */
  78. class TouchableText extends React.Component<Props, State> {
  79. static defaultProps = {
  80. accessible: true,
  81. allowFontScaling: true,
  82. ellipsizeMode: 'tail',
  83. };
  84. touchableGetPressRectOffset: ?() => PressRetentionOffset;
  85. touchableHandleActivePressIn: ?() => void;
  86. touchableHandleActivePressOut: ?() => void;
  87. touchableHandleLongPress: ?(event: PressEvent) => void;
  88. touchableHandlePress: ?(event: PressEvent) => void;
  89. touchableHandleResponderGrant: ?(
  90. event: PressEvent,
  91. dispatchID: string,
  92. ) => void;
  93. touchableHandleResponderMove: ?(event: PressEvent) => void;
  94. touchableHandleResponderRelease: ?(event: PressEvent) => void;
  95. touchableHandleResponderTerminate: ?(event: PressEvent) => void;
  96. touchableHandleResponderTerminationRequest: ?() => boolean;
  97. state = {
  98. ...Touchable.Mixin.touchableGetInitialState(),
  99. isHighlighted: false,
  100. createResponderHandlers: this._createResponseHandlers.bind(this),
  101. responseHandlers: null,
  102. };
  103. static getDerivedStateFromProps(
  104. nextProps: Props,
  105. prevState: State,
  106. ): $Shape<State> | null {
  107. return prevState.responseHandlers == null && isTouchable(nextProps)
  108. ? {
  109. responseHandlers: prevState.createResponderHandlers(),
  110. }
  111. : null;
  112. }
  113. static viewConfig = viewConfig;
  114. render(): React.Node {
  115. let props = this.props;
  116. if (isTouchable(props)) {
  117. props = {
  118. ...props,
  119. ...this.state.responseHandlers,
  120. isHighlighted: this.state.isHighlighted,
  121. };
  122. }
  123. if (props.selectionColor != null) {
  124. props = {
  125. ...props,
  126. selectionColor: processColor(props.selectionColor),
  127. };
  128. }
  129. if (__DEV__) {
  130. if (Touchable.TOUCH_TARGET_DEBUG && props.onPress != null) {
  131. props = {
  132. ...props,
  133. style: [props.style, {color: 'magenta'}],
  134. };
  135. }
  136. }
  137. return (
  138. <TextAncestor.Consumer>
  139. {hasTextAncestor =>
  140. hasTextAncestor ? (
  141. <RCTVirtualText {...props} ref={props.forwardedRef} />
  142. ) : (
  143. <TextAncestor.Provider value={true}>
  144. <RCTText {...props} ref={props.forwardedRef} />
  145. </TextAncestor.Provider>
  146. )
  147. }
  148. </TextAncestor.Consumer>
  149. );
  150. }
  151. _createResponseHandlers(): ResponseHandlers {
  152. return {
  153. onStartShouldSetResponder: (): boolean => {
  154. const {onStartShouldSetResponder} = this.props;
  155. const shouldSetResponder =
  156. (onStartShouldSetResponder == null
  157. ? false
  158. : onStartShouldSetResponder()) || isTouchable(this.props);
  159. if (shouldSetResponder) {
  160. this._attachTouchHandlers();
  161. }
  162. return shouldSetResponder;
  163. },
  164. onResponderGrant: (event: PressEvent, dispatchID: string): void => {
  165. nullthrows(this.touchableHandleResponderGrant)(event, dispatchID);
  166. if (this.props.onResponderGrant != null) {
  167. this.props.onResponderGrant.call(this, event, dispatchID);
  168. }
  169. },
  170. onResponderMove: (event: PressEvent): void => {
  171. nullthrows(this.touchableHandleResponderMove)(event);
  172. if (this.props.onResponderMove != null) {
  173. this.props.onResponderMove.call(this, event);
  174. }
  175. },
  176. onResponderRelease: (event: PressEvent): void => {
  177. nullthrows(this.touchableHandleResponderRelease)(event);
  178. if (this.props.onResponderRelease != null) {
  179. this.props.onResponderRelease.call(this, event);
  180. }
  181. },
  182. onResponderTerminate: (event: PressEvent): void => {
  183. nullthrows(this.touchableHandleResponderTerminate)(event);
  184. if (this.props.onResponderTerminate != null) {
  185. this.props.onResponderTerminate.call(this, event);
  186. }
  187. },
  188. onResponderTerminationRequest: (): boolean => {
  189. const {onResponderTerminationRequest} = this.props;
  190. if (!nullthrows(this.touchableHandleResponderTerminationRequest)()) {
  191. return false;
  192. }
  193. if (onResponderTerminationRequest == null) {
  194. return true;
  195. }
  196. return onResponderTerminationRequest();
  197. },
  198. };
  199. }
  200. /**
  201. * Lazily attaches Touchable.Mixin handlers.
  202. */
  203. _attachTouchHandlers(): void {
  204. if (this.touchableGetPressRectOffset != null) {
  205. return;
  206. }
  207. for (const key in Touchable.Mixin) {
  208. if (typeof Touchable.Mixin[key] === 'function') {
  209. (this: any)[key] = Touchable.Mixin[key].bind(this);
  210. }
  211. }
  212. this.touchableHandleActivePressIn = (): void => {
  213. if (!this.props.suppressHighlighting && isTouchable(this.props)) {
  214. this.setState({isHighlighted: true});
  215. }
  216. };
  217. this.touchableHandleActivePressOut = (): void => {
  218. if (!this.props.suppressHighlighting && isTouchable(this.props)) {
  219. this.setState({isHighlighted: false});
  220. }
  221. };
  222. this.touchableHandlePress = (event: PressEvent): void => {
  223. if (this.props.onPress != null) {
  224. this.props.onPress(event);
  225. }
  226. };
  227. this.touchableHandleLongPress = (event: PressEvent): void => {
  228. if (this.props.onLongPress != null) {
  229. this.props.onLongPress(event);
  230. }
  231. };
  232. this.touchableGetPressRectOffset = (): PressRetentionOffset =>
  233. this.props.pressRetentionOffset == null
  234. ? PRESS_RECT_OFFSET
  235. : this.props.pressRetentionOffset;
  236. }
  237. }
  238. const isTouchable = (props: Props): boolean =>
  239. props.onPress != null ||
  240. props.onLongPress != null ||
  241. props.onStartShouldSetResponder != null;
  242. const RCTText = createReactNativeComponentClass(
  243. viewConfig.uiViewClassName,
  244. () => viewConfig,
  245. );
  246. const RCTVirtualText =
  247. UIManager.getViewManagerConfig('RCTVirtualText') == null
  248. ? RCTText
  249. : createReactNativeComponentClass('RCTVirtualText', () => ({
  250. validAttributes: {
  251. ...ReactNativeViewAttributes.UIView,
  252. isHighlighted: true,
  253. maxFontSizeMultiplier: true,
  254. },
  255. uiViewClassName: 'RCTVirtualText',
  256. }));
  257. const Text = (
  258. props: TextProps,
  259. forwardedRef: ?React.Ref<'RCTText' | 'RCTVirtualText'>,
  260. ) => {
  261. return <TouchableText {...props} forwardedRef={forwardedRef} />;
  262. };
  263. const TextToExport = React.forwardRef(Text);
  264. TextToExport.displayName = 'Text';
  265. // TODO: Deprecate this.
  266. /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an error
  267. * found when Flow v0.89 was deployed. To see the error, delete this comment
  268. * and run Flow. */
  269. TextToExport.propTypes = DeprecatedTextPropTypes;
  270. type TextStatics = $ReadOnly<{|
  271. propTypes: typeof DeprecatedTextPropTypes,
  272. |}>;
  273. module.exports = ((TextToExport: any): React.AbstractComponent<
  274. TextProps,
  275. React.ElementRef<HostComponent<TextProps>>,
  276. > &
  277. TextStatics);