From a913345b26d64db917f2c9c18506f827f149eaf9 Mon Sep 17 00:00:00 2001 From: Piotr Garlej Date: Thu, 30 Apr 2026 12:50:30 +0200 Subject: [PATCH 1/3] fix keybord slide for list --- app/add-list.tsx | 69 +++------------------ app/edit-list/[id].tsx | 92 ++++++++-------------------- app/edit-note/[id].tsx | 18 ++---- components/AddContentDropdown.tsx | 4 ++ components/ListForm.tsx | 99 +++++++++++++++++++++++++++++++ components/NoteForm.tsx | 81 +++++++++---------------- hooks/useKeyboardOffset.ts | 27 +++++++++ package.json | 6 +- 8 files changed, 197 insertions(+), 199 deletions(-) create mode 100644 components/ListForm.tsx create mode 100644 hooks/useKeyboardOffset.ts diff --git a/app/add-list.tsx b/app/add-list.tsx index 4367e66..7911e40 100644 --- a/app/add-list.tsx +++ b/app/add-list.tsx @@ -1,45 +1,22 @@ import { HeaderBackButton } from '@/components/navigation/HeaderBackButton'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Text } from '@/components/ui/text'; import { useHardwareBackHandler } from '@/hooks/useHardwareBackHandler'; import { addList, type ListItem } from '@/lib/dataStorage'; import { useSQLiteContext } from 'expo-sqlite'; import { Stack, useRouter } from 'expo-router'; -import { useCallback, useState } from 'react'; -import { KeyboardAvoidingView, Platform, ScrollView } from 'react-native'; +import { useCallback } from 'react'; +import { ListForm } from '@/components/ListForm'; export default function AddListScreen() { const db = useSQLiteContext(); const router = useRouter(); - const [title, setTitle] = useState(''); - const [items, setItems] = useState([]); - const [saving, setSaving] = useState(false); - const handleAddRow = useCallback(() => { - setItems((current) => [...current, { checked: false, text: '' }]); - }, []); - - const handleUpdateRow = useCallback((index: number, text: string) => { - setItems((current) => - current.map((item, currentIndex) => (currentIndex === index ? { ...item, text } : item)) - ); - }, []); - - const handleSave = useCallback(async () => { - const trimmedTitle = title.trim(); - if (!trimmedTitle || saving) return; - const sanitizedItems = items - .map((item) => ({ ...item, text: item.text.trim() })) - .filter((item) => item.text.length > 0); - setSaving(true); - try { - await addList(db, { title: trimmedTitle, items: sanitizedItems }); + const handleSave = useCallback( + async (title: string, items: ListItem[]) => { + await addList(db, { title, items }); router.replace('/'); - } finally { - setSaving(false); - } - }, [db, items, router, saving, title]); + }, + [db, router] + ); useHardwareBackHandler(() => { router.replace('/'); @@ -59,35 +36,7 @@ export default function AddListScreen() { ), }} /> - - - - {items.map((item, index) => ( - handleUpdateRow(index, text)} - editable={!saving} - /> - ))} - - - - + ); } diff --git a/app/edit-list/[id].tsx b/app/edit-list/[id].tsx index 8f343fc..a8314c4 100644 --- a/app/edit-list/[id].tsx +++ b/app/edit-list/[id].tsx @@ -1,9 +1,6 @@ import { HeaderBackButton } from '@/components/navigation/HeaderBackButton'; import { ScreenLoadingState } from '@/components/state/ScreenLoadingState'; import { ScreenNotFoundState } from '@/components/state/ScreenNotFoundState'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Text } from '@/components/ui/text'; import { useHardwareBackHandler } from '@/hooks/useHardwareBackHandler'; import { useParsedNumericRouteParam } from '@/hooks/useParsedNumericRouteParam'; import { @@ -16,7 +13,7 @@ import { import { useSQLiteContext } from 'expo-sqlite'; import { Stack, useRouter } from 'expo-router'; import { useCallback, useEffect, useState } from 'react'; -import { KeyboardAvoidingView, Platform, ScrollView } from 'react-native'; +import { ListForm } from '@/components/ListForm'; type ListEditState = | 'loading' @@ -31,8 +28,7 @@ export default function EditListScreen() { const router = useRouter(); const { rawValue: id, value: listId, isValid: isValidId } = useParsedNumericRouteParam('id'); const [list, setList] = useState('loading'); - const [title, setTitle] = useState(''); - const [items, setItems] = useState([]); + const [saving, setSaving] = useState(false); const backTarget = isValidId ? `/list/${id}` : '/'; @@ -50,8 +46,6 @@ export default function EditListScreen() { } const loadedItems = (await getListItemsById(db, listId)) ?? []; - setTitle(content.title); - setItems(loadedItems); setList({ title: content.title, items: loadedItems }); }, [db, isValidId, listId]); @@ -63,33 +57,26 @@ export default function EditListScreen() { router.replace(backTarget as never); }, [backTarget, router]); - const handleAddRow = useCallback(() => { - setItems((current) => [...current, { checked: false, text: '' }]); - }, []); - - const handleUpdateRow = useCallback((index: number, text: string) => { - setItems((current) => - current.map((item, currentIndex) => (currentIndex === index ? { ...item, text } : item)) - ); - }, []); - - const handleSave = useCallback(async () => { - if (!isValidId || saving) return; - const trimmedTitle = title.trim(); - if (!trimmedTitle) return; - - const sanitizedItems = items - .map((item) => ({ ...item, text: item.text.trim() })) - .filter((item) => item.text.length > 0); - - setSaving(true); - try { - await updateList(db, listId, { title: trimmedTitle, items: sanitizedItems }); - router.replace(`/list/${id}`); - } finally { - setSaving(false); - } - }, [db, id, isValidId, items, listId, router, saving, title]); + const handleSave = useCallback( + async (title: string, items: ListItem[]) => { + if (!isValidId || saving) return; + const trimmedTitle = title.trim(); + if (!trimmedTitle) return; + + const sanitizedItems = items + .map((item) => ({ ...item, text: item.text.trim() })) + .filter((item) => item.text.length > 0); + + setSaving(true); + try { + await updateList(db, listId, { title: trimmedTitle, items: sanitizedItems }); + router.replace(`/list/${id}`); + } finally { + setSaving(false); + } + }, + [db, id, isValidId, listId, router, saving] + ); useHardwareBackHandler(handleBackToPreviousScreen); @@ -108,42 +95,11 @@ export default function EditListScreen() { title: 'Edit list', headerBackVisible: false, headerLeft: () => ( - + ), }} /> - - - - {items.map((item, index) => ( - handleUpdateRow(index, text)} - editable={!saving} - /> - ))} - - - - + ); } diff --git a/app/edit-note/[id].tsx b/app/edit-note/[id].tsx index 2fb092c..00fae00 100644 --- a/app/edit-note/[id].tsx +++ b/app/edit-note/[id].tsx @@ -4,7 +4,7 @@ import { ScreenLoadingState } from '@/components/state/ScreenLoadingState'; import { ScreenNotFoundState } from '@/components/state/ScreenNotFoundState'; import { useHardwareBackHandler } from '@/hooks/useHardwareBackHandler'; import { useParsedNumericRouteParam } from '@/hooks/useParsedNumericRouteParam'; -import { LIST_TYPE, getNoteById, updateNote, type Note } from '@/lib/dataStorage'; +import { NOTE_TYPE, getNoteById, updateNote, type Note } from '@/lib/dataStorage'; import { useSQLiteContext } from 'expo-sqlite'; import { Stack, useRouter } from 'expo-router'; import { useCallback, useEffect, useState } from 'react'; @@ -12,8 +12,7 @@ import { useCallback, useEffect, useState } from 'react'; export default function EditNoteScreen() { const db = useSQLiteContext(); const router = useRouter(); - const { rawValue: id, value: noteId, isValid: isValidId } = - useParsedNumericRouteParam('id'); + const { rawValue: id, value: noteId, isValid: isValidId } = useParsedNumericRouteParam('id'); const [note, setNote] = useState('loading'); const backTarget = isValidId ? `/note/${id}` : '/'; @@ -24,7 +23,7 @@ export default function EditNoteScreen() { return; } const found = await getNoteById(db, noteId); - if (found?.type === LIST_TYPE) { + if (found?.type !== NOTE_TYPE) { setNote(null); return; } @@ -65,18 +64,11 @@ export default function EditNoteScreen() { title: 'Edit note', headerBackVisible: false, headerLeft: () => ( - + ), }} /> - + ); } diff --git a/components/AddContentDropdown.tsx b/components/AddContentDropdown.tsx index fe7e650..3c5d271 100644 --- a/components/AddContentDropdown.tsx +++ b/components/AddContentDropdown.tsx @@ -1,6 +1,8 @@ import { Button } from '@/components/ui/button'; import { Text } from '@/components/ui/text'; import { Pressable, View } from 'react-native'; +import { Icon } from '@/components/ui/icon'; +import { Pen, SquareCheck } from 'lucide-react-native'; type AddContentDropdownProps = { visible: boolean; @@ -22,9 +24,11 @@ export function AddContentDropdown({ diff --git a/components/ListForm.tsx b/components/ListForm.tsx new file mode 100644 index 0000000..052d8db --- /dev/null +++ b/components/ListForm.tsx @@ -0,0 +1,99 @@ +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Text } from '@/components/ui/text'; +import { useKeyboardOffset } from '@/hooks/useKeyboardOffset'; +import { ListItem } from '@/lib/dataStorage'; +import { useCallback, useState } from 'react'; +import { KeyboardAvoidingView, Platform, ScrollView, View } from 'react-native'; + +type NoteFormProps = { + initialTitle?: string; + initialItems?: ListItem[]; + onSave: (title: string, items: ListItem[]) => Promise; + submitLabel?: string; +}; + +export function ListForm({ + initialTitle = '', + initialItems = [], + onSave, + submitLabel = 'Save', +}: NoteFormProps) { + const [title, setTitle] = useState(initialTitle); + const [items, setItems] = useState(initialItems); + const [saving, setSaving] = useState(false); + + const { paddingBottom } = useKeyboardOffset(); + + const handleAddRow = useCallback(() => { + setItems((current) => [...current, { checked: false, text: '' }]); + }, []); + + const handleUpdateRow = useCallback((index: number, text: string) => { + setItems((current) => + current.map((item, currentIndex) => (currentIndex === index ? { ...item, text } : item)) + ); + }, []); + + const handleSave = useCallback(async () => { + const trimmedTitle = title.trim(); + if (!trimmedTitle) return; + setSaving(true); + try { + await onSave( + trimmedTitle, + items.map((item) => ({ ...item, text: item.text.trim() })) + ); + } finally { + setSaving(false); + } + }, [title, items, onSave]); + + return ( + + + + + + + + {items.map((item, index) => ( + handleUpdateRow(index, text)} + editable={!saving} + /> + ))} + + + + + + + + + ); +} diff --git a/components/NoteForm.tsx b/components/NoteForm.tsx index 004861d..04fdb6d 100644 --- a/components/NoteForm.tsx +++ b/components/NoteForm.tsx @@ -2,18 +2,9 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Text } from '@/components/ui/text'; import { Textarea } from '@/components/ui/textarea'; +import { useKeyboardOffset } from '@/hooks/useKeyboardOffset'; import { useCallback, useEffect, useState } from 'react'; -import { - Keyboard, - KeyboardAvoidingView, - Platform, - ScrollView, - View, -} from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -/** Extra space above Android keyboard for the input accessory bar (emoji, mic, etc.). */ -const ANDROID_KEYBOARD_ACCESSORY_OFFSET = 52; +import { KeyboardAvoidingView, Platform, ScrollView, View } from 'react-native'; type NoteFormProps = { initialTitle?: string; @@ -28,30 +19,16 @@ export function NoteForm({ onSave, submitLabel = 'Save', }: NoteFormProps) { - const insets = useSafeAreaInsets(); const [title, setTitle] = useState(initialTitle); const [noteContent, setNoteContent] = useState(initialContent); const [saving, setSaving] = useState(false); - const [keyboardAccessoryPadding, setKeyboardAccessoryPadding] = useState(0); useEffect(() => { setTitle(initialTitle); setNoteContent(initialContent); }, [initialTitle, initialContent]); - useEffect(() => { - if (Platform.OS !== 'android') return; - const showSub = Keyboard.addListener('keyboardDidShow', () => { - setKeyboardAccessoryPadding(ANDROID_KEYBOARD_ACCESSORY_OFFSET); - }); - const hideSub = Keyboard.addListener('keyboardDidHide', () => { - setKeyboardAccessoryPadding(0); - }); - return () => { - showSub.remove(); - hideSub.remove(); - }; - }, []); + const { paddingBottom } = useKeyboardOffset(); const handleSave = useCallback(async () => { const trimmedTitle = title.trim(); @@ -68,42 +45,38 @@ export function NoteForm({ - - - -