import { ReactNode, useEffect, useRef, useState } from 'react'; import { Animated, Easing, Keyboard, Modal, Platform, Pressable, StyleProp, View, ViewStyle, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useColors } from '../lib/theme'; /** * Universal-Bottom-Sheet für Forms mit TextInput. * * Pattern (verifiziert auf PostCommentsSheet + EditMailAccountSheet): * * 1. Outer-Animated.View hat animated `height` (JS-driver) — Sheet WÄCHST * bei Tastatur-Open um genau die Tastatur-Höhe. * 2. Inner-Animated.View hat `transform: translateY` (Native-driver) — * Slide-In/Out smooth. Driver-Mix-Trennung verhindert * "Style property 'height' is not supported by native animated module"-Crash. * 3. iOS: `paddingBottom: keyboardHeight` shifted Form innerhalb des * gewachsenen Sheets über die Tastatur. Android: `windowSoftInputMode=adjustResize` * im Manifest schrumpft das Window selbst. * 4. Flex-Spacer drückt `children` (Form) automatisch an den Sheet-Bottom-Edge — * sitzt direkt über der Tastatur ohne Gap. * * Anti-Pattern (siehe `docs/internal/RECOVERY_LOG_2026-05-10.md` §7.2): * - `useKeyboardAnimation()` aus `react-native-keyboard-controller` liefert * in iOS-Modals keine Höhe (separate UIWindow). Hier: plain RN * `Keyboard.addListener` für die Höhe. * - `Animated.subtract`/`marginBottom: keyboardHeight` mischen JS+Native-Driver * auf demselben View → Bouncing oder Crash. * * Usage: * ```tsx * } * > * * * * * * ``` */ export interface KeyboardAwareSheetProps { visible: boolean; onClose: () => void; /** Sheet-Höhe wenn Tastatur zu. Eng auf Inhalt zuschneiden — typisch 220-340px. */ collapsedHeight: number; /** Optionaler Header (Cancel/Title-Row). Rendert direkt unter dem Drag-Handle. */ header?: ReactNode; /** Form-Inhalt. Wird per Flex-Spacer an den Sheet-Bottom gedrückt — sitzt * damit direkt über der Tastatur sobald die offen ist. */ children: ReactNode; /** Default true — Tap auf Backdrop schließt das Sheet. */ dismissOnBackdrop?: boolean; /** Default true — kleiner Drag-Handle ganz oben am Sheet. */ showDragHandle?: boolean; /** Default true — fügt unten eine Safe-Area-Spacer-Höhe ein (insets.bottom). */ showSafeAreaSpacer?: boolean; /** Default true — interner Flex-Spacer drückt children zum Sheet-Bottom. * Auf false setzen wenn der Inhalt seine eigene Scroll-/Flex-Logik hat * (z.B. ScrollView mit Provider-Grid, Listen). */ pushChildrenToBottom?: boolean; /** Border-Radius oben. Default 20. */ topRadius?: number; /** Optional zusätzlicher Style für den Sheet-Container. */ containerStyle?: StyleProp; } export function KeyboardAwareSheet({ visible, onClose, collapsedHeight, header, children, dismissOnBackdrop = true, showDragHandle = true, showSafeAreaSpacer = true, pushChildrenToBottom = true, topRadius = 20, containerStyle, }: KeyboardAwareSheetProps) { const colors = useColors(); const insets = useSafeAreaInsets(); const slideY = useRef(new Animated.Value(collapsedHeight)).current; const backdropOpacity = useRef(new Animated.Value(0)).current; const sheetHeight = useRef(new Animated.Value(collapsedHeight)).current; const [keyboardHeight, setKeyboardHeight] = useState(0); // Slide-In + Backdrop-Fade bei `visible=true` useEffect(() => { if (visible) { slideY.setValue(collapsedHeight); backdropOpacity.setValue(0); Animated.parallel([ Animated.timing(slideY, { toValue: 0, duration: 280, easing: Easing.out(Easing.cubic), useNativeDriver: true, }), Animated.timing(backdropOpacity, { toValue: 1, duration: 220, useNativeDriver: true, }), ]).start(); } }, [visible, slideY, backdropOpacity, collapsedHeight]); // Sheet-Höhe wächst/schrumpft mit Tastatur useEffect(() => { const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'; const showSub = Keyboard.addListener(showEvent, (e) => { const h = e.endCoordinates.height; setKeyboardHeight(h); Animated.timing(sheetHeight, { toValue: collapsedHeight + h, duration: Platform.OS === 'ios' ? (e.duration ?? 250) : 220, easing: Easing.out(Easing.cubic), useNativeDriver: false, }).start(); }); const hideSub = Keyboard.addListener(hideEvent, (e) => { setKeyboardHeight(0); Animated.timing(sheetHeight, { toValue: collapsedHeight, duration: Platform.OS === 'ios' ? (e?.duration ?? 250) : 220, easing: Easing.out(Easing.cubic), useNativeDriver: false, }).start(); }); return () => { showSub.remove(); hideSub.remove(); }; }, [sheetHeight, collapsedHeight]); return ( {/* Backdrop */} {dismissOnBackdrop && } {/* Outer: animated height (JS-driver) */} {/* Inner: animated transform (Native-driver). Driver-Mix vermeiden durch zwei verschachtelte Animated.Views. */} {showDragHandle && ( )} {header} {pushChildrenToBottom ? ( <> {/* Flex-Spacer drückt children an den Sheet-Bottom */} {children} ) : ( {children} )} {showSafeAreaSpacer && } ); }