fix: keyboard scroll via Keyboard listener, bigger widget expand button (#6, #9)

- Replace unreliable setTimeout scroll with Keyboard.addListener('keyboardDidShow')
- Track subtask input focus state to scroll only when relevant
- Increase bottom spacer from h-32 to h-80 for more scroll room
- Enlarge expand/collapse button (32px with background) and arrow (fontSize 18)
- Ensure subtasks array is always initialized in widget handler

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-03-01 11:04:38 -05:00
parent 55e02e1b3a
commit a03085c768
4 changed files with 38 additions and 10 deletions

View file

@ -9,6 +9,7 @@ import {
Alert,
Platform,
KeyboardAvoidingView,
Keyboard,
} from 'react-native';
import { useRouter, useLocalSearchParams } from 'expo-router';
import {
@ -83,6 +84,16 @@ export default function TaskDetailScreen() {
const [saving, setSaving] = useState(false);
const scrollRef = useRef<ScrollView>(null);
const subtaskInputY = useRef(0);
const subtaskFocused = useRef(false);
useEffect(() => {
const sub = Keyboard.addListener('keyboardDidShow', () => {
if (subtaskFocused.current) {
scrollRef.current?.scrollTo({ y: subtaskInputY.current - 80, animated: true });
}
});
return () => sub.remove();
}, []);
useEffect(() => {
if (!isValidUUID(id)) {
@ -405,7 +416,8 @@ export default function TaskDetailScreen() {
value={newSubtask}
onChangeText={setNewSubtask}
onSubmitEditing={handleAddSubtask}
onFocus={() => setTimeout(() => scrollRef.current?.scrollTo({ y: subtaskInputY.current - 80, animated: true }), 300)}
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]'}`}
@ -413,7 +425,7 @@ export default function TaskDetailScreen() {
/>
</View>
<View className="h-32" />
<View className="h-80" />
</ScrollView>
</KeyboardAvoidingView>
</View>

View file

@ -8,6 +8,7 @@ import {
useColorScheme,
Platform,
KeyboardAvoidingView,
Keyboard,
} from 'react-native';
import { useRouter, useLocalSearchParams } from 'expo-router';
import {
@ -61,6 +62,16 @@ export default function NewTaskScreen() {
const [newSubtask, setNewSubtask] = useState('');
const scrollRef = useRef<ScrollView>(null);
const subtaskInputY = useRef(0);
const subtaskFocused = useRef(false);
useEffect(() => {
const sub = Keyboard.addListener('keyboardDidShow', () => {
if (subtaskFocused.current) {
scrollRef.current?.scrollTo({ y: subtaskInputY.current - 80, animated: true });
}
});
return () => sub.remove();
}, []);
useEffect(() => {
getAllLists().then(setLists);
@ -375,7 +386,8 @@ export default function NewTaskScreen() {
value={newSubtask}
onChangeText={setNewSubtask}
onSubmitEditing={handleAddPendingSubtask}
onFocus={() => setTimeout(() => scrollRef.current?.scrollTo({ y: subtaskInputY.current - 80, animated: true }), 300)}
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]'}`}
@ -383,7 +395,7 @@ export default function NewTaskScreen() {
/>
</View>
<View className="h-32" />
<View className="h-80" />
</ScrollView>
</KeyboardAvoidingView>
</View>

View file

@ -251,12 +251,13 @@ function TaskItemRow({
{hasSubtasks ? (
<FlexWidget
style={{
width: 28,
height: 28,
borderRadius: 14,
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: isDark ? '#2A2A2A' as ColorProp : '#F0E8DC' as ColorProp,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 4,
marginLeft: 6,
}}
clickAction="TOGGLE_EXPAND"
clickActionData={{ taskId: task.id }}
@ -264,7 +265,7 @@ function TaskItemRow({
<TextWidget
text={isExpanded ? '▾' : '▸'}
style={{
fontSize: 14,
fontSize: 18,
fontFamily: FONT_SEMIBOLD,
color: TODAY_COLOR,
}}

View file

@ -27,7 +27,10 @@ async function getWidgetTasks(): Promise<WidgetTask[]> {
if (!data) return [];
const parsed: unknown = JSON.parse(data);
if (!Array.isArray(parsed)) return [];
return parsed.filter(isWidgetTask);
return parsed.filter(isWidgetTask).map((t) => ({
...t,
subtasks: Array.isArray(t.subtasks) ? t.subtasks : [],
}));
} catch {
return [];
}