mobile: let users dismiss the keyboard from a task card

Focusing a task field opened the keyboard with no way out — it covered the
card and the stream's tap-to-advance zones. Now:

- A "Done" pill appears at the card's top-right while editing (reusing the
  existing `editing` flag) and calls Keyboard.dismiss(); the title row
  reserves space so the pill never overlaps a long title.
- The card ScrollView gains keyboardDismissMode (interactive on iOS, on-drag
  on Android) so dragging the card also dismisses the keyboard.

Dismissing blurs the active field, which flips `editing` off and resumes the
dwell timer and tap navigation automatically.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01LqGPzXQ1AA9CqYHgCgmbtV
This commit is contained in:
Claude
2026-06-21 16:35:33 +00:00
parent c161d96534
commit 4a73ccad0b
@@ -1,5 +1,13 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { Pressable, ScrollView, Text, TextInput, View } from 'react-native'; import {
Keyboard,
Platform,
Pressable,
ScrollView,
Text,
TextInput,
View,
} from 'react-native';
import { deleteField } from 'firebase/firestore'; import { deleteField } from 'firebase/firestore';
import { Check, Plus, X } from 'lucide-react-native'; import { Check, Plus, X } from 'lucide-react-native';
import type { ChecklistItem, Particle } from '@/api/types'; import type { ChecklistItem, Particle } from '@/api/types';
@@ -158,15 +166,23 @@ export function TaskParticleView({
className="flex-1 items-center justify-center px-6" className="flex-1 items-center justify-center px-6"
style={{ paddingTop: safe.top + 16, paddingBottom: safe.bottom + 16 }} style={{ paddingTop: safe.top + 16, paddingBottom: safe.bottom + 16 }}
> >
<View className="w-full max-w-xl" style={{ maxHeight: '100%' }}>
<ScrollView <ScrollView
className="max-h-full w-full max-w-xl rounded-2xl bg-white/10" className="max-h-full rounded-2xl bg-white/10"
contentContainerClassName="px-5 py-5 gap-5" contentContainerClassName="px-5 py-5 gap-5"
showsVerticalScrollIndicator showsVerticalScrollIndicator
indicatorStyle="white" indicatorStyle="white"
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
// Dragging the card dismisses the keyboard (interactive follow on
// iOS; on-drag on Android, which lacks the interactive variant).
keyboardDismissMode={
Platform.OS === 'ios' ? 'interactive' : 'on-drag'
}
> >
{/* Title + done */} {/* Title + done */}
<View className="flex-row items-start gap-3"> <View
className={cn('flex-row items-start gap-3', editing && 'pr-12')}
>
<Pressable <Pressable
onPress={handleToggleDone} onPress={handleToggleDone}
hitSlop={8} hitSlop={8}
@@ -210,7 +226,9 @@ export function TaskParticleView({
onEndEditing={(e) => { onEndEditing={(e) => {
const value = e.nativeEvent.text; const value = e.nativeEvent.text;
if (value !== (notes ?? '')) { if (value !== (notes ?? '')) {
void updateParticleProperties<'task'>(docPath, { notes: value }); void updateParticleProperties<'task'>(docPath, {
notes: value,
});
} }
}} }}
placeholder="Add notes…" placeholder="Add notes…"
@@ -271,6 +289,23 @@ export function TaskParticleView({
</ScrollView> </ScrollView>
</View> </View>
</ScrollView> </ScrollView>
{/* While a field is focused the keyboard hides the stream's tap-zones,
so offer an explicit way out. Dismissing blurs the active field,
which flips `editing` off and resumes the dwell + tap navigation. */}
{editing ? (
<View className="absolute right-2 top-2">
<Pressable
onPress={() => Keyboard.dismiss()}
hitSlop={8}
accessibilityLabel="Done editing"
className="rounded-full bg-white/15 px-3 py-1.5"
>
<Text className="text-white text-sm font-medium">Done</Text>
</Pressable>
</View>
) : null}
</View>
</View> </View>
); );
} }