fix: use react-native-keyboard-controller for reliable keyboard handling (#6)
Replace manual keyboard listeners and RN KeyboardAvoidingView with react-native-keyboard-controller which handles edge-to-edge correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
360310e99f
commit
f2fe141737
7 changed files with 56 additions and 73 deletions
|
|
@ -1,8 +1,9 @@
|
|||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import {
|
||||
View, Text, Pressable, useColorScheme, TextInput, Alert,
|
||||
Modal, KeyboardAvoidingView, Platform, ScrollView,
|
||||
Modal, Platform, ScrollView,
|
||||
} from 'react-native';
|
||||
import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
|
||||
import { useRouter } from 'expo-router';
|
||||
import {
|
||||
Plus, ChevronRight, Check, GripVertical,
|
||||
|
|
@ -249,8 +250,8 @@ export default function ListsScreen() {
|
|||
{/* Create/Edit Modal */}
|
||||
<Modal visible={showModal} transparent animationType="fade">
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
className="flex-1"
|
||||
behavior="padding"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Pressable onPress={() => setShowModal(false)} className="flex-1 justify-center bg-black/40 px-6">
|
||||
<Pressable
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { View, Text, Pressable, useColorScheme, TextInput, ScrollView, Alert, Modal, KeyboardAvoidingView, Platform, Switch, Linking, ActivityIndicator } from 'react-native';
|
||||
import { View, Text, Pressable, useColorScheme, TextInput, ScrollView, Alert, Modal, Platform, Switch, Linking, ActivityIndicator } from 'react-native';
|
||||
import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Sun, Moon, Smartphone, Plus, Trash2, Pencil, Bell, CalendarDays, Mail, RefreshCw } from 'lucide-react-native';
|
||||
import Constants from 'expo-constants';
|
||||
|
|
@ -352,8 +353,8 @@ export default function SettingsScreen() {
|
|||
{/* Tag Create/Edit Modal */}
|
||||
<Modal visible={showTagModal} transparent animationType="fade">
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
className="flex-1"
|
||||
behavior="padding"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Pressable onPress={() => setShowTagModal(false)} className="flex-1 justify-center bg-black/40 px-6">
|
||||
<Pressable
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useFonts, Inter_400Regular, Inter_500Medium, Inter_600SemiBold, Inter_7
|
|||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { useMigrations } from 'drizzle-orm/expo-sqlite/migrator';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { KeyboardProvider } from 'react-native-keyboard-controller';
|
||||
|
||||
import { db } from '@/src/db/client';
|
||||
import migrations from '@/src/db/migrations/migrations';
|
||||
|
|
@ -79,23 +80,25 @@ export default function RootLayout() {
|
|||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<ThemeProvider value={effectiveScheme === 'dark' ? SimplDarkTheme : SimplLightTheme}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen
|
||||
name="task/new"
|
||||
options={{ presentation: 'modal', headerShown: false }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="task/[id]"
|
||||
options={{ headerShown: false }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="list/[id]"
|
||||
options={{ headerShown: false }}
|
||||
/>
|
||||
</Stack>
|
||||
</ThemeProvider>
|
||||
<KeyboardProvider>
|
||||
<ThemeProvider value={effectiveScheme === 'dark' ? SimplDarkTheme : SimplLightTheme}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen
|
||||
name="task/new"
|
||||
options={{ presentation: 'modal', headerShown: false }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="task/[id]"
|
||||
options={{ headerShown: false }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="list/[id]"
|
||||
options={{ headerShown: false }}
|
||||
/>
|
||||
</Stack>
|
||||
</ThemeProvider>
|
||||
</KeyboardProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
useColorScheme,
|
||||
Alert,
|
||||
Platform,
|
||||
Keyboard,
|
||||
} from 'react-native';
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import {
|
||||
ArrowLeft, Plus, Trash2, Calendar, X, Repeat, Download,
|
||||
|
|
@ -81,22 +80,6 @@ export default function TaskDetailScreen() {
|
|||
const [lists, setLists] = useState<{ id: string; name: string; color: string | null; icon: string | null; isInbox: boolean }[]>([]);
|
||||
const [selectedListId, setSelectedListId] = useState<string>('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const scrollRef = useRef<ScrollView>(null);
|
||||
const subtaskFocused = useRef(false);
|
||||
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const showSub = Keyboard.addListener('keyboardDidShow', (e) => {
|
||||
setKeyboardHeight(e.endCoordinates.height);
|
||||
if (subtaskFocused.current) {
|
||||
setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), 100);
|
||||
}
|
||||
});
|
||||
const hideSub = Keyboard.addListener('keyboardDidHide', () => {
|
||||
setKeyboardHeight(0);
|
||||
});
|
||||
return () => { showSub.remove(); hideSub.remove(); };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isValidUUID(id)) {
|
||||
|
|
@ -226,7 +209,7 @@ export default function TaskDetailScreen() {
|
|||
</View>
|
||||
</View>
|
||||
|
||||
<ScrollView ref={scrollRef} className="flex-1 px-4 pt-4" keyboardShouldPersistTaps="handled">
|
||||
<KeyboardAwareScrollView className="flex-1 px-4 pt-4" keyboardShouldPersistTaps="handled" bottomOffset={20}>
|
||||
{/* Title */}
|
||||
<TextInput
|
||||
value={title}
|
||||
|
|
@ -415,8 +398,6 @@ export default function TaskDetailScreen() {
|
|||
value={newSubtask}
|
||||
onChangeText={setNewSubtask}
|
||||
onSubmitEditing={handleAddSubtask}
|
||||
onFocus={() => { subtaskFocused.current = true; }}
|
||||
onBlur={() => { subtaskFocused.current = false; }}
|
||||
placeholder={t('task.addSubtask')}
|
||||
placeholderTextColor={isDark ? '#A0A0A0' : '#6B6B6B'}
|
||||
className={`ml-2 flex-1 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
|
|
@ -424,8 +405,8 @@ export default function TaskDetailScreen() {
|
|||
/>
|
||||
</View>
|
||||
|
||||
<View style={{ height: keyboardHeight || 32 }} />
|
||||
</ScrollView>
|
||||
<View style={{ height: 32 }} />
|
||||
</KeyboardAwareScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
useColorScheme,
|
||||
Platform,
|
||||
Keyboard,
|
||||
} from 'react-native';
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import {
|
||||
X, Calendar, Repeat, Plus,
|
||||
|
|
@ -59,22 +58,6 @@ export default function NewTaskScreen() {
|
|||
const [saving, setSaving] = useState(false);
|
||||
const [pendingSubtasks, setPendingSubtasks] = useState<string[]>([]);
|
||||
const [newSubtask, setNewSubtask] = useState('');
|
||||
const scrollRef = useRef<ScrollView>(null);
|
||||
const subtaskFocused = useRef(false);
|
||||
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const showSub = Keyboard.addListener('keyboardDidShow', (e) => {
|
||||
setKeyboardHeight(e.endCoordinates.height);
|
||||
if (subtaskFocused.current) {
|
||||
setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), 100);
|
||||
}
|
||||
});
|
||||
const hideSub = Keyboard.addListener('keyboardDidHide', () => {
|
||||
setKeyboardHeight(0);
|
||||
});
|
||||
return () => { showSub.remove(); hideSub.remove(); };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getAllLists().then(setLists);
|
||||
|
|
@ -153,7 +136,7 @@ export default function NewTaskScreen() {
|
|||
</Pressable>
|
||||
</View>
|
||||
|
||||
<ScrollView ref={scrollRef} className="flex-1 px-4 pt-4" keyboardShouldPersistTaps="handled">
|
||||
<KeyboardAwareScrollView className="flex-1 px-4 pt-4" keyboardShouldPersistTaps="handled" bottomOffset={20}>
|
||||
{/* Title */}
|
||||
<TextInput
|
||||
autoFocus
|
||||
|
|
@ -385,8 +368,6 @@ export default function NewTaskScreen() {
|
|||
value={newSubtask}
|
||||
onChangeText={setNewSubtask}
|
||||
onSubmitEditing={handleAddPendingSubtask}
|
||||
onFocus={() => { subtaskFocused.current = true; }}
|
||||
onBlur={() => { subtaskFocused.current = false; }}
|
||||
placeholder={t('task.addSubtask')}
|
||||
placeholderTextColor={isDark ? '#A0A0A0' : '#6B6B6B'}
|
||||
className={`ml-2 flex-1 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
|
|
@ -394,8 +375,8 @@ export default function NewTaskScreen() {
|
|||
/>
|
||||
</View>
|
||||
|
||||
<View style={{ height: keyboardHeight || 32 }} />
|
||||
</ScrollView>
|
||||
<View style={{ height: 32 }} />
|
||||
</KeyboardAwareScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
19
package-lock.json
generated
19
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "simpl-liste",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "simpl-liste",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.2",
|
||||
"dependencies": {
|
||||
"@expo-google-fonts/inter": "^0.4.2",
|
||||
"@expo/ngrok": "^4.1.3",
|
||||
|
|
@ -43,6 +43,7 @@
|
|||
"react-native-android-widget": "^0.20.1",
|
||||
"react-native-draggable-flatlist": "^4.0.3",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-keyboard-controller": "1.18.5",
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
|
|
@ -10395,6 +10396,20 @@
|
|||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-keyboard-controller": {
|
||||
"version": "1.18.5",
|
||||
"resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.18.5.tgz",
|
||||
"integrity": "sha512-wbYN6Tcu3G5a05dhRYBgjgd74KqoYWuUmroLpigRg9cXy5uYo7prTMIvMgvLtARQtUF7BOtFggUnzgoBOgk0TQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-native-is-edge-to-edge": "^1.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-reanimated": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-reanimated": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz",
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
"react-native-android-widget": "^0.20.1",
|
||||
"react-native-draggable-flatlist": "^4.0.3",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-keyboard-controller": "1.18.5",
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
|
|
|
|||
Loading…
Reference in a new issue