import React from 'react' import type { Ref, ForwardRefExoticComponent, RefAttributes } from "react"; import { forwardRef, useCallback, useMemo, useRef, useState } from "react"; import { type GestureResponderEvent, processColor } from "react-native"; // @ts-ignore import ReactNativeViewAttributes from "react-native/Libraries/Components/View/ReactNativeViewAttributes"; import Touchable from "react-native/Libraries/Components/Touchable/Touchable"; // @ts-ignore import createReactNativeComponentClass from "react-native/Libraries/Renderer/shims/createReactNativeComponentClass"; import nullthrows from "nullthrows"; import type { AnimateableTextProps } from "./TextProps"; import type { Text as IText } from "react-native"; const PRESS_RECT_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 }; const viewConfig = { validAttributes: { ...ReactNativeViewAttributes.UIView, isHighlighted: true, isPressable: true, numberOfLines: true, ellipsizeMode: true, allowFontScaling: true, dynamicTypeRamp: true, maxFontSizeMultiplier: true, disabled: true, selectable: true, selectionColor: true, adjustsFontSizeToFit: true, minimumFontScale: true, textBreakStrategy: true, onTextLayout: true, onInlineViewLayout: true, dataDetectorType: true, android_hyphenationFrequency: true, lineBreakStrategyIOS: true, text: true, }, directEventTypes: { topTextLayout: { registrationName: "onTextLayout" }, topInlineViewLayout: { registrationName: "onInlineViewLayout" }, }, uiViewClassName: "JBAnimatedText", }; const isTouchable = (props: AnimateableTextProps): boolean => props.onPress != null || props.onLongPress != null || props.onStartShouldSetResponder != null; const RCTText = createReactNativeComponentClass( viewConfig.uiViewClassName, () => viewConfig, ); const TouchableText = forwardRef( (props, forwardedRef) => { const [isHighlighted, setHighlighted] = useState(false); const touchableRef = useRef>({}); const hasAttachedTouchHandlers = useRef(false); const TouchableWithMixin = Touchable as typeof Touchable & { Mixin: Record void>; TOUCH_TARGET_DEBUG: boolean; }; const attachTouchHandlers = useCallback(() => { if (hasAttachedTouchHandlers.current) return; hasAttachedTouchHandlers.current = true; for (const key in TouchableWithMixin.Mixin) { if (typeof TouchableWithMixin.Mixin[key] === "function") { touchableRef.current[key] = TouchableWithMixin.Mixin[key].bind( touchableRef.current, ); } } touchableRef.current.touchableHandleActivePressIn = () => { if (!props.suppressHighlighting && isTouchable(props)) { setHighlighted(true); } }; touchableRef.current.touchableHandleActivePressOut = () => { if (!props.suppressHighlighting && isTouchable(props)) { setHighlighted(false); } }; touchableRef.current.touchableHandlePress = ( event: GestureResponderEvent, ) => { props.onPress?.(event); }; touchableRef.current.touchableHandleLongPress = ( event: GestureResponderEvent, ) => { props.onLongPress?.(event); }; touchableRef.current.touchableGetPressRectOffset = () => props.pressRetentionOffset ?? PRESS_RECT_OFFSET; }, [hasAttachedTouchHandlers, props, setHighlighted, touchableRef]); const responseHandlers = useMemo(() => { if (!isTouchable(props)) return undefined; return { onStartShouldSetResponder: (): boolean => { const shouldSet = (props.onStartShouldSetResponder?.() ?? false) || isTouchable(props); if (shouldSet) { attachTouchHandlers(); } return shouldSet; }, onResponderGrant: (event: GestureResponderEvent) => { nullthrows(touchableRef.current.touchableHandleResponderGrant)?.( event, ); props.onResponderGrant?.(event); }, onResponderMove: (event: GestureResponderEvent) => { nullthrows(touchableRef.current.touchableHandleResponderMove)?.( event, ); props.onResponderMove?.(event); }, onResponderRelease: (event: GestureResponderEvent) => { nullthrows(touchableRef.current.touchableHandleResponderRelease)?.( event, ); props.onResponderRelease?.(event); }, onResponderTerminate: (event: GestureResponderEvent) => { nullthrows(touchableRef.current.touchableHandleResponderTerminate)?.( event, ); props.onResponderTerminate?.(event); }, onResponderTerminationRequest: (): boolean => { const defaultRequest = nullthrows( touchableRef.current.touchableHandleResponderTerminationRequest, )(); return props.onResponderTerminationRequest?.() ?? defaultRequest; }, }; }, [props, attachTouchHandlers]); let selectionColor: (symbol & { __TYPE__: "Color" }) | undefined; if (props.selectionColor != null) { selectionColor = processColor(props.selectionColor) as | (symbol & { __TYPE__: "Color" }) | undefined; } const renderedProps: AnimateableTextProps & { ref: Ref; isHighlighted: boolean; selectionColor: (symbol & { __TYPE__: "Color" }) | undefined; } = { ...props, ...(responseHandlers ?? {}), ref: forwardedRef, isHighlighted, selectionColor, }; if (isTouchable(props)) { renderedProps.isHighlighted = isHighlighted; } if ( __DEV__ && TouchableWithMixin.TOUCH_TARGET_DEBUG && props.onPress != null ) { renderedProps.style = [props.style, { color: "magenta" }]; } return ; }, ); TouchableText.displayName = "TouchableText"; export const AnimateableText = TouchableText as unknown as ForwardRefExoticComponent< AnimateableTextProps & RefAttributes >;