Files
paseo/packages/app/src/components/markdown-text.ios.tsx
T
Mohamed Boudra a1581e66b0 fix(chat): keep large tables responsive on iOS
Selectable table cells each installed a window-level gesture recognizer, causing every touch to fan out across the entire table. Keep prose selectable while rendering iOS table text without selection.
2026-07-12 22:15:42 +02:00

112 lines
3.5 KiB
TypeScript

import { useMemo, type ReactNode } from "react";
import {
Text,
View,
type StyleProp,
type TextProps,
type TextStyle,
type ViewStyle,
} from "react-native";
import { UITextView } from "react-native-uitextview";
import { resolvePlainMarkdownTextStyle } from "@/components/markdown-text-style";
import {
iosMarkdownTextIsSelectable,
useMarkdownTextSurface,
} from "@/components/markdown-text-selection";
interface MarkdownTextSpanProps {
style?: StyleProp<TextStyle>;
monoSurface?: boolean;
children: ReactNode;
// Links route through this span too (see assistant-file-links/link.tsx). A
// plain <Text> nested in the paragraph UITextView is dropped, so the link
// must be a UITextView span to be visible. onPress is wired onto the leaf
// string children here: react-native-uitextview attaches it to the
// RNUITextViewChild nodes it builds from string content, which the native tap
// recognizer dispatches to. The link's handler reaches these leaf spans via
// AssistantLinkPressProvider (see assistant-file-links/link-press-context).
onPress?: TextProps["onPress"];
accessibilityRole?: TextProps["accessibilityRole"];
}
// Inline span backed by UITextView so iOS gets native word-selection handles.
// Used inside MarkdownParagraphView (which is also a UITextView on iOS); the
// library's TextAncestorContext hoists these into UITextViewChild nodes so
// selection drags can cross sibling spans (e.g. plain text → **bold** → code).
export function MarkdownTextSpan({
style,
children,
onPress,
accessibilityRole,
}: MarkdownTextSpanProps) {
const plainStyle = useMemo(() => resolvePlainMarkdownTextStyle(style), [style]);
const surface = useMarkdownTextSurface();
// Each selectable span creates a UIKit UITextView with a window-level tap recognizer.
// A large table would create one per cell and make every app touch fan out across them.
if (!iosMarkdownTextIsSelectable(surface)) {
return (
<Text
selectable={false}
style={plainStyle}
onPress={onPress}
accessibilityRole={accessibilityRole}
>
{children}
</Text>
);
}
return (
<UITextView
uiTextView
selectable
style={plainStyle}
onPress={onPress}
accessibilityRole={accessibilityRole}
>
{children}
</UITextView>
);
}
interface MarkdownParagraphViewProps {
paragraphStyle: ViewStyle;
containsImage?: boolean;
children: ReactNode;
}
const MARKDOWN_PARAGRAPH_RESET: ViewStyle = {};
// iOS-only: paragraph wraps in UITextView so the entire paragraph is one
// native text view. That's what unlocks cross-inline drag selection — handles
// can span every MarkdownTextSpan child inside this paragraph.
// ViewStyle is structurally compatible with the layout props paragraphs use
// (margin, padding, alignment); the cast lets the existing paragraphStyle
// flow through unchanged.
export function MarkdownParagraphView({
paragraphStyle,
containsImage = false,
children,
}: MarkdownParagraphViewProps) {
const textStyle = useMemo(
() =>
resolvePlainMarkdownTextStyle([
paragraphStyle,
MARKDOWN_PARAGRAPH_RESET,
] as StyleProp<TextStyle>),
[paragraphStyle],
);
const viewStyle = useMemo(() => [paragraphStyle, MARKDOWN_PARAGRAPH_RESET], [paragraphStyle]);
if (containsImage) {
return <View style={viewStyle}>{children}</View>;
}
return (
<UITextView uiTextView selectable style={textStyle}>
{children}
</UITextView>
);
}