/** * Il foglio che sale dal basso, con il velo scuro dietro. * * Un `Modal` con `animationType="slide"` fa scivolare tutto quello che * contiene, velo compreso: sull'iPhone si vedeva la patina nera scendere * dall'alto prima del foglio, e Paolo l'ha notato al primo tocco. Qui il velo * compare subito e a muoversi e' solo il foglio, come fanno i fogli di * sistema. Il `Modal` resta, con l'animazione spenta: e' lui che sta sopra * tutto e che risponde al tasto indietro di Android. * * Chi lo usa passa il contenuto del foglio gia' vestito (fondo, angoli, * padding): qui si decide solo come entra e come esce. */ import { useEffect, useRef, useState } from 'react'; import type { ReactNode } from 'react'; import { Animated, Easing, KeyboardAvoidingView, Modal, Platform, Pressable, StyleSheet, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; export type PropsFoglio = { visibile: boolean; /** Tocco sul velo, tasto indietro di Android. */ onChiudi: () => void; /** Vero quando dentro c'e' un campo con autoFocus: il foglio si scansa dalla tastiera. */ evitaTastiera?: boolean; /** * Chiamata quando il foglio e' sparito del tutto. Serve a chi deve * mostrare un Alert dopo: su iOS un Alert presentato mentre il Modal sta * ancora andando via non compare mai. */ onChiuso?: () => void; children: ReactNode; }; /** Da quanto sotto entra il foglio: piu' della sua altezza, cosi' parte fuori schermo. */ const CORSA = 600; export function Foglio({ visibile, onChiudi, evitaTastiera = false, onChiuso, children }: PropsFoglio) { // Il Modal resta montato finche' il foglio non e' uscito del tutto: se // sparisse insieme a `visibile`, l'uscita non si vedrebbe. const [montato, setMontato] = useState(visibile); const scorrimento = useRef(new Animated.Value(CORSA)).current; // L'ingresso parte da `onShow` del Modal e non da qui: montare il Modal e // animare nello stesso giro non funziona, la vista nativa non c'e' ancora // e il foglio restava fermo fuori schermo, con le righe sotto il bordo. const entra = () => { Animated.timing(scorrimento, { toValue: 0, duration: 280, easing: Easing.out(Easing.cubic), useNativeDriver: true, }).start(); }; useEffect(() => { if (visibile) { scorrimento.setValue(CORSA); setMontato(true); } else { Animated.timing(scorrimento, { toValue: CORSA, duration: 200, easing: Easing.in(Easing.cubic), useNativeDriver: true, }).start(({ finished }) => { if (!finished) return; setMontato(false); // Su iOS «sparito» lo dice il Modal con onDismiss; su Android quel // callback non arriva, e sparito e' adesso. if (Platform.OS !== 'ios') onChiuso?.(); }); } }, [visibile, scorrimento, onChiuso]); const Contenitore = evitaTastiera ? KeyboardAvoidingView : View; // Con la tastiera su, un foglio alto arriva fino in cima: senza questo // margine il titolo finiva sotto la Dynamic Island. const sopra = useSafeAreaInsets().top; return ( {children} ); } const stili = StyleSheet.create({ fondale: { flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(0, 0, 0, 0.4)' }, // Il tetto e il flexShrink lasciano che sia il contenuto a stringersi quando // la tastiera toglie spazio, non i tasti in fondo. foglio: { maxHeight: '100%', flexShrink: 1 }, });