|
1 | 1 | import { ReactNode, useRef, useState } from 'react' |
2 | | -import { View, Text, KeyboardAvoidingView, Platform, Animated } from 'react-native' |
| 2 | +import { View, Text, Platform, Animated } from 'react-native' |
3 | 3 | import { LinearGradient } from 'expo-linear-gradient' |
4 | 4 | import { StatusBar } from 'expo-status-bar' |
5 | 5 | import { SafeAreaView } from 'react-native-safe-area-context' |
@@ -40,26 +40,38 @@ export function AuthScaffold({ |
40 | 40 | outputRange: [2, 1], |
41 | 41 | extrapolateRight: 'clamp', |
42 | 42 | }) |
| 43 | + // Stable scroll handler — recreating Animated.event each render re-attaches the |
| 44 | + // native scroll listener, which can interrupt an input's focus as the keyboard opens. |
| 45 | + const onScroll = useRef( |
| 46 | + Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], { |
| 47 | + useNativeDriver: Platform.OS !== 'web', // RN-web has no native driver |
| 48 | + }), |
| 49 | + ).current |
43 | 50 |
|
44 | 51 | return ( |
45 | | - <KeyboardAvoidingView |
46 | | - style={{ flex: 1, backgroundColor: colors.base }} |
47 | | - behavior={Platform.OS === 'ios' ? 'padding' : undefined} |
48 | | - > |
| 52 | + // Plain container — no KeyboardAvoidingView. Its 'padding' behavior fights the |
| 53 | + // ScrollView's own keyboard auto-scroll on iOS and can bounce the tapped input out |
| 54 | + // from under the touch (keyboard flashes up, then dismisses). Instead we let the |
| 55 | + // ScrollView inset itself for the keyboard natively via automaticallyAdjustKeyboardInsets. |
| 56 | + <View style={{ flex: 1, backgroundColor: colors.base }}> |
49 | 57 | {/* Hero is always the brand gradient (dark), so the status bar stays light. */} |
50 | 58 | <StatusBar style="light" /> |
51 | 59 | <Animated.ScrollView |
52 | 60 | contentContainerStyle={{ flexGrow: 1 }} |
53 | 61 | keyboardShouldPersistTaps="handled" |
| 62 | + automaticallyAdjustKeyboardInsets |
54 | 63 | showsVerticalScrollIndicator={false} |
55 | | - onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], { |
56 | | - useNativeDriver: Platform.OS !== 'web', // RN-web has no native driver |
57 | | - })} |
| 64 | + onScroll={onScroll} |
58 | 65 | scrollEventThrottle={16} |
59 | 66 | > |
60 | 67 | {/* HERO — gradient bg is absolute so it can stretch independently of the text */} |
61 | 68 | <View |
62 | | - onLayout={(e) => setHeroH(Math.max(1, Math.round(e.nativeEvent.layout.height)))} |
| 69 | + onLayout={(e) => { |
| 70 | + // Guard: only update on a real height change so the keyboard-open layout |
| 71 | + // pass doesn't trigger a needless re-render (another focus-stealer). |
| 72 | + const h = Math.max(1, Math.round(e.nativeEvent.layout.height)) |
| 73 | + setHeroH((prev) => (Math.abs(prev - h) > 1 ? h : prev)) |
| 74 | + }} |
63 | 75 | style={{ paddingBottom: 46 }} |
64 | 76 | > |
65 | 77 | <Animated.View |
@@ -172,6 +184,6 @@ export function AuthScaffold({ |
172 | 184 | {children} |
173 | 185 | </View> |
174 | 186 | </Animated.ScrollView> |
175 | | - </KeyboardAvoidingView> |
| 187 | + </View> |
176 | 188 | ) |
177 | 189 | } |
0 commit comments