/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format */ import type {TextStyleProp} from '../StyleSheet/StyleSheet'; import type {____TextStyle_Internal as TextStyleInternal} from '../StyleSheet/StyleSheetTypes'; import type {PressEvent} from '../Types/CoreEventTypes'; import type {NativeTextProps} from './TextNativeComponent'; import type {PressRetentionOffset, TextProps} from './TextProps'; import * as PressabilityDebug from '../Pressability/PressabilityDebug'; import usePressability from '../Pressability/usePressability'; import flattenStyle from '../StyleSheet/flattenStyle'; import processColor from '../StyleSheet/processColor'; import Platform from '../Utilities/Platform'; import TextAncestor from './TextAncestor'; import {NativeText, NativeVirtualText} from './TextNativeComponent'; import * as React from 'react'; import {useContext, useMemo, useState} from 'react'; type TextForwardRef = React.ElementRef< typeof NativeText | typeof NativeVirtualText, >; /** * Text is the fundamental component for displaying text. * * @see https://reactnative.dev/docs/text */ const Text: component( ref: React.RefSetter, ...props: TextProps ) = React.forwardRef( ( { accessible, accessibilityLabel, accessibilityState, allowFontScaling, 'aria-busy': ariaBusy, 'aria-checked': ariaChecked, 'aria-disabled': ariaDisabled, 'aria-expanded': ariaExpanded, 'aria-label': ariaLabel, 'aria-selected': ariaSelected, children, ellipsizeMode, disabled, id, nativeID, numberOfLines, onLongPress, onPress, onPressIn, onPressOut, onResponderGrant, onResponderMove, onResponderRelease, onResponderTerminate, onResponderTerminationRequest, onStartShouldSetResponder, pressRetentionOffset, selectable, selectionColor, suppressHighlighting, style, ...restProps }: TextProps, forwardedRef, ) => { const _accessibilityLabel = ariaLabel ?? accessibilityLabel; let _accessibilityState: ?TextProps['accessibilityState'] = accessibilityState; if ( ariaBusy != null || ariaChecked != null || ariaDisabled != null || ariaExpanded != null || ariaSelected != null ) { if (_accessibilityState != null) { _accessibilityState = { busy: ariaBusy ?? _accessibilityState.busy, checked: ariaChecked ?? _accessibilityState.checked, disabled: ariaDisabled ?? _accessibilityState.disabled, expanded: ariaExpanded ?? _accessibilityState.expanded, selected: ariaSelected ?? _accessibilityState.selected, }; } else { _accessibilityState = { busy: ariaBusy, checked: ariaChecked, disabled: ariaDisabled, expanded: ariaExpanded, selected: ariaSelected, }; } } const _accessibilityStateDisabled = _accessibilityState?.disabled; const _disabled = disabled ?? _accessibilityStateDisabled; const isPressable = (onPress != null || onLongPress != null || onStartShouldSetResponder != null) && _disabled !== true; // TODO: Move this processing to the view configuration. const _selectionColor = selectionColor != null ? processColor(selectionColor) : undefined; let _style = style; if (__DEV__) { if (PressabilityDebug.isEnabled() && onPress != null) { _style = [style, {color: 'magenta'}]; } } let _numberOfLines = numberOfLines; if (_numberOfLines != null && !(_numberOfLines >= 0)) { if (__DEV__) { console.error( `'numberOfLines' in must be a non-negative number, received: ${_numberOfLines}. The value will be set to 0.`, ); } _numberOfLines = 0; } let _selectable = selectable; let processedStyle = flattenStyle(_style); if (processedStyle != null) { let overrides: ?{...TextStyleInternal} = null; if (typeof processedStyle.fontWeight === 'number') { overrides = overrides || ({}: {...TextStyleInternal}); overrides.fontWeight = // $FlowFixMe[incompatible-cast] (processedStyle.fontWeight.toString(): TextStyleInternal['fontWeight']); } if (processedStyle.userSelect != null) { _selectable = userSelectToSelectableMap[processedStyle.userSelect]; overrides = overrides || ({}: {...TextStyleInternal}); overrides.userSelect = undefined; } if (processedStyle.verticalAlign != null) { overrides = overrides || ({}: {...TextStyleInternal}); overrides.textAlignVertical = verticalAlignToTextAlignVerticalMap[processedStyle.verticalAlign]; overrides.verticalAlign = undefined; } if (overrides != null) { // $FlowFixMe[incompatible-type] _style = [_style, overrides]; } } const _nativeID = id ?? nativeID; const hasTextAncestor = useContext(TextAncestor); if (hasTextAncestor) { if (isPressable) { return ( ); } return ( {children} ); } // If the disabled prop and accessibilityState.disabled are out of sync but not both in // falsy states we need to update the accessibilityState object to use the disabled prop. if ( _disabled !== _accessibilityStateDisabled && ((_disabled != null && _disabled !== false) || (_accessibilityStateDisabled != null && _accessibilityStateDisabled !== false)) ) { _accessibilityState = {..._accessibilityState, disabled: _disabled}; } const _accessible = Platform.select({ ios: accessible !== false, android: accessible == null ? onPress != null || onLongPress != null : accessible, default: accessible, }); let nativeText = null; if (isPressable) { nativeText = ( ); } else { nativeText = ( {children} ); } if (children == null) { return nativeText; } // If the children do not contain a JSX element it would not be possible to have a // nested `Text` component so we can skip adding the `TextAncestor` context wrapper // which has a performance overhead. Since we do this for performance reasons we need // to keep the check simple to avoid regressing overall perf. For this reason the // `children.length` constant is set to `3`, this should be a reasonable tradeoff // to capture the majority of `Text` uses but also not make this check too expensive. if (Array.isArray(children) && children.length <= 3) { let hasNonTextChild = false; for (let child of children) { if (child != null && typeof child === 'object') { hasNonTextChild = true; break; } } if (!hasNonTextChild) { return nativeText; } } else if (typeof children !== 'object') { return nativeText; } return ( {nativeText} ); }, ); Text.displayName = 'Text'; type TextPressabilityProps = $ReadOnly<{ onLongPress?: ?(event: PressEvent) => mixed, onPress?: ?(event: PressEvent) => mixed, onPressIn?: ?(event: PressEvent) => mixed, onPressOut?: ?(event: PressEvent) => mixed, onResponderGrant?: ?(event: PressEvent) => void, onResponderMove?: ?(event: PressEvent) => void, onResponderRelease?: ?(event: PressEvent) => void, onResponderTerminate?: ?(event: PressEvent) => void, onResponderTerminationRequest?: ?() => boolean, onStartShouldSetResponder?: ?() => boolean, pressRetentionOffset?: ?PressRetentionOffset, suppressHighlighting?: ?boolean, }>; /** * Hook that handles setting up Pressability of Text components. * * NOTE: This hook is relatively expensive so it should only be used absolutely necessary. */ function useTextPressability({ onLongPress, onPress, onPressIn, onPressOut, onResponderGrant, onResponderMove, onResponderRelease, onResponderTerminate, onResponderTerminationRequest, onStartShouldSetResponder, pressRetentionOffset, suppressHighlighting, }: TextPressabilityProps) { const [isHighlighted, setHighlighted] = useState(false); // Setup pressability config and wrap callbacks needs to track the highlight state. const config = useMemo(() => { let _onPressIn = onPressIn; let _onPressOut = onPressOut; // Updating isHighlighted causes unnecessary re-renders for platforms that don't use it // in the best case, and cause issues with text selection in the worst case. Forcing // the isHighlighted prop to false on all platforms except iOS. if (Platform.OS === 'ios') { _onPressIn = (event: PressEvent) => { setHighlighted(suppressHighlighting == null || !suppressHighlighting); onPressIn?.(event); }; _onPressOut = (event: PressEvent) => { setHighlighted(false); onPressOut?.(event); }; } return { disabled: false, pressRectOffset: pressRetentionOffset, onLongPress, onPress, onPressIn: _onPressIn, onPressOut: _onPressOut, }; }, [ pressRetentionOffset, onLongPress, onPress, onPressIn, onPressOut, suppressHighlighting, ]); // Init the pressability class const eventHandlers = usePressability(config); // Create NativeText event handlers which proxy events to pressability const eventHandlersForText = useMemo( () => eventHandlers == null ? null : { onResponderGrant(event: PressEvent) { eventHandlers.onResponderGrant(event); if (onResponderGrant != null) { onResponderGrant(event); } }, onResponderMove(event: PressEvent) { eventHandlers.onResponderMove(event); if (onResponderMove != null) { onResponderMove(event); } }, onResponderRelease(event: PressEvent) { eventHandlers.onResponderRelease(event); if (onResponderRelease != null) { onResponderRelease(event); } }, onResponderTerminate(event: PressEvent) { eventHandlers.onResponderTerminate(event); if (onResponderTerminate != null) { onResponderTerminate(event); } }, onClick: eventHandlers.onClick, onResponderTerminationRequest: onResponderTerminationRequest != null ? onResponderTerminationRequest : eventHandlers.onResponderTerminationRequest, onStartShouldSetResponder: onStartShouldSetResponder != null ? onStartShouldSetResponder : eventHandlers.onStartShouldSetResponder, }, [ eventHandlers, onResponderGrant, onResponderMove, onResponderRelease, onResponderTerminate, onResponderTerminationRequest, onStartShouldSetResponder, ], ); // Return the highlight state and NativeText event handlers return useMemo( () => [isHighlighted, eventHandlersForText], [isHighlighted, eventHandlersForText], ); } type NativePressableTextProps = $ReadOnly<{ textProps: NativeTextProps, textPressabilityProps: TextPressabilityProps, }>; /** * Wrap the NativeVirtualText component and initialize pressability. * * This logic is split out from the main Text component to enable the more * expensive pressability logic to be only initialized when needed. */ const NativePressableVirtualText: component( ref: React.RefSetter, ...props: NativePressableTextProps ) = React.forwardRef(({textProps, textPressabilityProps}, forwardedRef) => { const [isHighlighted, eventHandlersForText] = useTextPressability( textPressabilityProps, ); return ( ); }); /** * Wrap the NativeText component and initialize pressability. * * This logic is split out from the main Text component to enable the more * expensive pressability logic to be only initialized when needed. */ const NativePressableText: component( ref: React.RefSetter, ...props: NativePressableTextProps ) = React.forwardRef(({textProps, textPressabilityProps}, forwardedRef) => { const [isHighlighted, eventHandlersForText] = useTextPressability( textPressabilityProps, ); return ( ); }); const userSelectToSelectableMap = { auto: true, text: true, none: false, contain: true, all: true, }; const verticalAlignToTextAlignVerticalMap = { auto: 'auto', top: 'top', bottom: 'bottom', middle: 'center', }; module.exports = Text;