fix: keep sheet and dialog fields and buttons above the keyboard

This commit is contained in:
Linus Rath
2026-10-31 13:19:56 +01:00
parent 426f740359
commit 980243469c
13 changed files with 298 additions and 18 deletions
@@ -14,6 +14,7 @@ import { isTrustedSendersSyncOn } from '../../lib/trusted-senders';
import { removeTrustedSenderWithUndo } from '../../lib/settings-undo';
import { useLocaleStore } from '../../stores/locale-store';
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
import { useSheetBottomInset } from '../../lib/sheet-bottom-inset';
import { ToastHost } from '../ToastHost';
interface TrustedRow {
@@ -48,6 +49,8 @@ export function ContentSendersSettings() {
const removeFromTrustedSendersBook = useContactsStore((s) => s.removeFromTrustedSendersBook);
const [modalOpen, setModalOpen] = useState(false);
// The card centres above the keyboard while an address is typed in.
const bottomInset = useSheetBottomInset(modalOpen);
const [newSender, setNewSender] = useState('');
const [busy, setBusy] = useState<string | null>(null);
@@ -197,7 +200,7 @@ export function ContentSendersSettings() {
animationType="fade"
onRequestClose={() => setModalOpen(false)}
>
<Pressable accessible={false} style={styles.modalBackdrop} onPress={() => setModalOpen(false)}>
<Pressable accessible={false} style={[styles.modalBackdrop, { paddingBottom: bottomInset }]} onPress={() => setModalOpen(false)}>
<Pressable accessible={false} style={styles.modalCard} onPress={() => {}}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>{t('settings.email_behavior.trusted_senders.modal_title', 'Trusted Senders')}</Text>
+4 -1
View File
@@ -19,6 +19,7 @@ import { spacing, radius, typography, type ThemePalette } from '../../theme/toke
import { useColors } from '../../theme/colors';
import { useLocaleStore } from '../../stores/locale-store';
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
import { useModalKeyboardPadding } from '../../lib/sheet-bottom-inset';
import { findPrincipalByEmail } from '../../api/delegation';
import type { Principal } from '../../api/types';
import type { PrincipalListing } from '../../api/files';
@@ -74,6 +75,8 @@ export function DelegateDialog({
const c = useColors();
const styles = useMemo(() => makeStyles(c), [c]);
const t = useLocaleStore((s) => s.t);
// The Android dialog doesn't shrink for the keyboard; the body ends above it.
const keyboardPadding = useModalKeyboardPadding();
const changing = !!initial;
const [principal, setPrincipal] = useState<Principal | null>(initial?.principal ?? null);
@@ -154,7 +157,7 @@ export function DelegateDialog({
return (
<SafeAreaModal supportedOrientations={MODAL_ORIENTATIONS} visible animationType="slide" onRequestClose={close}>
<SafeAreaView style={styles.screen} edges={['top', 'bottom', 'left', 'right']}>
<KeyboardAvoidingView style={styles.screen} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
<KeyboardAvoidingView style={[styles.screen, { paddingBottom: keyboardPadding }]} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
<View style={styles.header}>
<Text style={styles.title} numberOfLines={1} accessibilityRole="header">
{changing && principal
+3 -1
View File
@@ -69,7 +69,6 @@ function pathOf(all: Mailbox[], mb: Mailbox, t: (k: string, f?: string) => strin
export function FolderSettings() {
// Centred on a wide window, clear of side insets in landscape.
const sheetInsets = useSheetInsets();
const bottomInset = useSheetBottomInset();
const c = useColors();
const styles = React.useMemo(() => makeStyles(c), [c]);
const t = useLocaleStore((s) => s.t);
@@ -78,6 +77,9 @@ export function FolderSettings() {
const fetchMailboxes = useEmailStore((s) => s.fetchMailboxes);
const [editor, setEditor] = useState<Editor | null>(null);
// Save and Cancel above the taskbar, and above the keyboard while the
// name is typed in.
const bottomInset = useSheetBottomInset(!!editor);
const [draftName, setDraftName] = useState('');
const [draftParent, setDraftParent] = useState<string>(NO_PARENT);
const [draftAccount, setDraftAccount] = useState<string>(OWN_ACCOUNT);
+2 -1
View File
@@ -99,7 +99,6 @@ function parseAddressList(value: string): { list: EmailAddress[]; invalid: strin
export function IdentitySettings() {
// Centred on a wide window, clear of side insets in landscape.
const sheetInsets = useSheetInsets();
const bottomInset = useSheetBottomInset();
const c = useColors();
const styles = React.useMemo(() => makeStyles(c), [c]);
const identities = useSettingsStore((s) => s.identities);
@@ -113,6 +112,8 @@ export function IdentitySettings() {
const preferredId = accountId ? preferredIdentityIds[accountId] : undefined;
const [editing, setEditing] = useState<DraftIdentity | null>(null);
// Save above the taskbar, and above the keyboard while a field is typed in.
const bottomInset = useSheetBottomInset(!!editing);
const [saving, setSaving] = useState(false);
const [deletingId, setDeletingId] = useState<string | null>(null);
+2 -1
View File
@@ -24,7 +24,6 @@ import { useSheetBottomInset } from '../../lib/sheet-bottom-inset';
export function TemplateSettings() {
// Centred on a wide window, clear of side insets in landscape.
const sheetInsets = useSheetInsets();
const bottomInset = useSheetBottomInset();
const c = useColors();
const styles = React.useMemo(() => makeStyles(c), [c]);
const tr = useLocaleStore((s) => s.t);
@@ -41,6 +40,8 @@ export function TemplateSettings() {
const [editing, setEditing] = useState<EmailTemplate | null>(null);
const [importVisible, setImportVisible] = useState(false);
// Save above the taskbar, and above the keyboard while a field is typed in.
const bottomInset = useSheetBottomInset(!!editing || importVisible);
const [importText, setImportText] = useState('');
const [draftName, setDraftName] = useState('');
const [draftSubject, setDraftSubject] = useState('');