Skip to content

Instantly share code, notes, and snippets.

@haileyok
Created October 4, 2024 20:05
Show Gist options
  • Save haileyok/dd1c79024dc72cc15101c11b89128e32 to your computer and use it in GitHub Desktop.
Save haileyok/dd1c79024dc72cc15101c11b89128e32 to your computer and use it in GitHub Desktop.
import React from 'react'
import {
Platform,
requireNativeComponent,
StyleSheet,
UIManager,
ViewStyle,
TextProps,
Text as RNText
} from 'react-native'
const LINKING_ERROR =
`The package 'react-native-uitextview' doesn't seem to be linked. Make sure: \n\n` +
Platform.select({ios: "- You have run 'pod install'\n", default: ''}) +
'- You rebuilt the app after installing the package\n' +
'- You are not using Expo Go\n'
// These props are for the main native wrapper component
export interface RNUITextViewProps extends TextProps {
children: React.ReactNode
style: ViewStyle[]
}
// These props are for each of the children native components
type RNUITextViewChildProps = TextProps & {
text: string
onTextPress?: (...args: any[]) => void
onTextLongPress?: (...args: any[]) => void
}
const RNUITextView =
UIManager.getViewManagerConfig?.('RNUITextView') != null
? requireNativeComponent<RNUITextViewProps>('RNUITextView')
: () => {
if (Platform.OS !== 'ios') return null
throw new Error(LINKING_ERROR)
}
export const RNUITextViewChild =
UIManager.getViewManagerConfig?.('RNUITextViewChild') != null
? requireNativeComponent<RNUITextViewChildProps>('RNUITextViewChild')
: () => {
if (Platform.OS !== 'ios') return null
throw new Error(LINKING_ERROR)
}
const TextAncestorContext = React.createContext<[boolean, ViewStyle]>([
false,
StyleSheet.create({})
])
const useTextAncestorContext = () => React.useContext(TextAncestorContext)
const textDefaults: TextProps = {
allowFontScaling: true,
selectable: true
}
function UITextViewChild({
style,
children,
...rest
}: TextProps & {
uiTextView?: boolean
}) {
const [isAncestor, rootStyle] = useTextAncestorContext()
// Flatten the styles, and apply the root styles when needed
const flattenedStyle = React.useMemo(
() => StyleSheet.flatten([rootStyle, style]),
[rootStyle, style]
)
if (!isAncestor) {
return (
<TextAncestorContext.Provider value={[true, flattenedStyle]}>
<RNUITextView
{...textDefaults}
{...rest}
ellipsizeMode={rest.ellipsizeMode ?? rest.lineBreakMode ?? 'tail'}
style={[flattenedStyle]}
onPress={undefined} // We want these to go to the children only
onLongPress={undefined}>
{parseChildren(children, flattenedStyle, rest)}
</RNUITextView>
</TextAncestorContext.Provider>
)
} else {
return parseChildren(children, flattenedStyle, rest)
}
}
function UITextViewInner(
props: TextProps & {
uiTextView?: boolean
}
) {
const [isAncestor] = useTextAncestorContext()
// Even if the uiTextView prop is set, we can still default to using
// normal selection (i.e. base RN text) if the text doesn't need to be
// selectable
if ((!props.selectable || !props.uiTextView) && !isAncestor) {
return <RNText {...props} />
}
return <UITextViewChild {...props} />
}
export function UITextView(props: TextProps & {uiTextView?: boolean}) {
if (Platform.OS !== 'ios') {
return <RNText {...props} />
}
return <UITextViewInner {...props} />
}
function isReactFragment(variableToInspect) {
if (variableToInspect.type) {
return variableToInspect.type === React.Fragment;
}
return variableToInspect === React.Fragment;
}
function parseChildren(children: React.ReactNode, flattenedStyle: ViewStyle, rest: TextProps) {
return React.Children.toArray(children).map((c, index) => {
if (isReactFragment(c)) {
const children = React.Children.toArray(c.props.children)
if (typeof children[0] === 'string') {
return (
<RNUITextViewChild
key={index}
style={flattenedStyle}
text={children[0].toString()}
{...rest}
/>
)
}
} else if (React.isValidElement(c)) {
return c
} else if (typeof c === 'string' || typeof c === 'number') {
return (
<RNUITextViewChild
key={index}
style={flattenedStyle}
text={c.toString()}
{...rest}
/>
)
}
return null
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment