Files
edgeever/apps/mobile/src/components/LocalizedText.tsx
T
tianma-if 494cce5e84 fix(mobile): harden iPadOS 26.5 paths against Fabric/WebView crashes
Detail preview now uses static HTML WebView instead of Dom TipTap. Stagger
TextInput and editor host mount/teardown for edit, Me, create, and back so
Fabric ShadowNode measure and Hermes GC no longer SIGSEGV on iPadOS 26.5.
2026-08-06 17:41:30 +08:00

105 lines
4.1 KiB
TypeScript

import { Children, forwardRef, isValidElement, type ComponentRef, type ReactNode } from "react";
import {
Alert as NativeAlert,
Platform,
Pressable as NativePressable,
StyleSheet,
Text as NativeText,
TextInput as NativeTextInput,
type PressableProps,
type TextInputProps,
type TextProps,
type TextStyle,
} from "react-native";
import { translateCurrentMobileText, useMobileLocale } from "../lib/mobile-locale";
import { presentAppDialog } from "./app-dialog-controller";
/**
* iPadOS 26.5 + Fabric: measuring TextInput with the variable system font
* (UIFont systemFontOfSize:weight: → CopyVariationAxes) can SIGSEGV. Force a
* non-variable UIKit face and drop weight so layout never hits that path.
*/
const IOS_SAFE_TEXT_INPUT_STYLE: TextStyle = Platform.OS === "ios"
? {
fontFamily: "Helvetica",
fontWeight: "400",
}
: {};
const translateChildren = (children: ReactNode, translate: (value: string) => string): ReactNode =>
Children.map(children, (child) => {
if (typeof child === "string") {
return translate(child);
}
// Keep numeric/boolean leaves as plain strings so Fabric's AttributedString
// cache never receives mixed non-text host children under Text.
if (typeof child === "number" || typeof child === "boolean") {
return String(child);
}
if (isValidElement(child) && child.type === NativeText) {
return child;
}
return child;
});
export const Text = forwardRef<ComponentRef<typeof NativeText>, TextProps>(({ children, ...props }, ref) => {
const { translate } = useMobileLocale();
return (
<NativeText allowFontScaling {...props} ref={ref}>
{translateChildren(children, translate)}
</NativeText>
);
});
Text.displayName = "LocalizedText";
export const TextInput = forwardRef<ComponentRef<typeof NativeTextInput>, TextInputProps>(({ accessibilityHint, accessibilityLabel, placeholder, value, defaultValue, style, ...props }, ref) => {
const { translate } = useMobileLocale();
// Fabric TextInput measure on iPadOS 26.5 corrupts when value is non-string.
const safeValue = value === undefined || value === null ? value : String(value);
const safeDefault = defaultValue === undefined || defaultValue === null ? defaultValue : String(defaultValue);
return (
<NativeTextInput
{...props}
accessibilityHint={typeof accessibilityHint === "string" ? translate(accessibilityHint) : accessibilityHint}
accessibilityLabel={typeof accessibilityLabel === "string" ? translate(accessibilityLabel) : accessibilityLabel}
defaultValue={safeDefault}
placeholder={placeholder ? translate(placeholder) : placeholder}
ref={ref}
// Safe face last so callers cannot reintroduce variable system-font weights.
style={style ? StyleSheet.flatten([style, IOS_SAFE_TEXT_INPUT_STYLE]) : IOS_SAFE_TEXT_INPUT_STYLE}
value={safeValue}
/>
);
});
TextInput.displayName = "LocalizedTextInput";
export const Pressable = forwardRef<ComponentRef<typeof NativePressable>, PressableProps>(
({ accessibilityHint, accessibilityLabel, ...props }, ref) => (
<NativePressable
{...props}
accessibilityHint={typeof accessibilityHint === "string" ? translateCurrentMobileText(accessibilityHint) : accessibilityHint}
accessibilityLabel={typeof accessibilityLabel === "string" ? translateCurrentMobileText(accessibilityLabel) : accessibilityLabel}
ref={ref}
/>
)
);
Pressable.displayName = "LocalizedPressable";
export const Alert = {
alert: (...[title, message, buttons, options]: Parameters<typeof NativeAlert.alert>) => {
const translatedTitle = translateCurrentMobileText(title);
const translatedMessage = message ? translateCurrentMobileText(message) : message;
const translatedButtons = buttons?.map((button) => ({
...button,
text: button.text ? translateCurrentMobileText(button.text) : button.text,
}));
if (presentAppDialog({ title: translatedTitle, message: translatedMessage, buttons: translatedButtons, options })) {
return;
}
NativeAlert.alert(translatedTitle, translatedMessage, translatedButtons, options);
},
};