mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +00:00
fix: keep bottom sheet buttons above the tablet taskbar
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
// With edge-to-edge on, every RN Modal on Android reaches under the
|
||||||
|
// navigation bar (a tablet's taskbar, a phone's gesture bar). Bottom sheets
|
||||||
|
// that ended at the screen edge without padding for it put their last row
|
||||||
|
// under the taskbar on a Pixel Tablet: New folder's Save and Cancel, the
|
||||||
|
// Files item actions' Delete and Delete permanently. A tap there opened an
|
||||||
|
// app from the taskbar instead. Every sheet surface inside a plain Modal pads
|
||||||
|
// its bottom by the inset (useSheetBottomInset, or insets.bottom).
|
||||||
|
|
||||||
|
const SRC = path.resolve(__dirname, '../..');
|
||||||
|
|
||||||
|
// Sheets in files other agents own; their fix is reported, not made here.
|
||||||
|
const OWNED_ELSEWHERE = new Set([
|
||||||
|
'components/SidebarDrawer.tsx',
|
||||||
|
'components/calendar/CalendarEditSheet.tsx',
|
||||||
|
'components/sharing/ShareCollectionSheet.tsx',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function sourceFiles(dir: string): string[] {
|
||||||
|
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
|
||||||
|
const p = path.join(dir, e.name);
|
||||||
|
if (e.isDirectory()) return e.name === '__tests__' ? [] : sourceFiles(p);
|
||||||
|
return e.name.endsWith('.tsx') ? [p] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What is between each plain <Modal ...> and its </Modal>. */
|
||||||
|
function modalBodies(source: string): string[] {
|
||||||
|
const out: string[] = [];
|
||||||
|
const re = /<Modal[\s>]/g;
|
||||||
|
let m: RegExpExecArray | null;
|
||||||
|
while ((m = re.exec(source))) {
|
||||||
|
const end = source.indexOf('</Modal>', m.index);
|
||||||
|
out.push(source.slice(m.index, end < 0 ? undefined : end));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A body that renders a bottom sheet surface (styles.sheet / styles.modalSheet). */
|
||||||
|
function rendersSheet(body: string): boolean {
|
||||||
|
return /styles\.(sheet|modalSheet)\b/.test(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
function padsBottom(body: string): boolean {
|
||||||
|
return /bottomInset|insets\.bottom/.test(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('bottom sheets in a Modal', () => {
|
||||||
|
it('pad their content above the navigation bar or taskbar', () => {
|
||||||
|
const offenders = sourceFiles(SRC).flatMap((file) => {
|
||||||
|
const rel = path.relative(SRC, file).split(path.sep).join('/');
|
||||||
|
if (OWNED_ELSEWHERE.has(rel)) return [];
|
||||||
|
const source = fs.readFileSync(file, 'utf8');
|
||||||
|
return modalBodies(source)
|
||||||
|
.filter((body) => rendersSheet(body) && !padsBottom(body))
|
||||||
|
.map(() => rel);
|
||||||
|
});
|
||||||
|
expect(offenders).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('covers the sheets the tablet walkthrough found', () => {
|
||||||
|
for (const rel of ['components/settings/FolderSettings.tsx', 'screens/FilesScreen.tsx', 'components/files/ShareSheet.tsx']) {
|
||||||
|
const bodies = modalBodies(fs.readFileSync(path.join(SRC, rel), 'utf8')).filter(rendersSheet);
|
||||||
|
expect(bodies.length, rel).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds the sheet in a Modal body', () => {
|
||||||
|
const src = '<Modal visible transparent>\n<View style={[styles.sheet, { paddingBottom: bottomInset }]} />\n</Modal>';
|
||||||
|
const bodies = modalBodies(src);
|
||||||
|
expect(bodies).toHaveLength(1);
|
||||||
|
expect(rendersSheet(bodies[0])).toBe(true);
|
||||||
|
expect(padsBottom(bodies[0])).toBe(true);
|
||||||
|
expect(padsBottom('<Modal><View style={styles.sheet} /></Modal>')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -22,6 +22,7 @@ import { useColors } from '../../theme/colors';
|
|||||||
import { useLocaleStore } from '../../stores/locale-store';
|
import { useLocaleStore } from '../../stores/locale-store';
|
||||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||||
import { useSheetInsets } from '../../lib/use-layout';
|
import { useSheetInsets } from '../../lib/use-layout';
|
||||||
|
import { useSheetBottomInset } from '../../lib/sheet-bottom-inset';
|
||||||
|
|
||||||
type RolePreset = 'read' | 'readWrite' | 'manager';
|
type RolePreset = 'read' | 'readWrite' | 'manager';
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ interface ShareSheetProps {
|
|||||||
export default function ShareSheet({ node, onClose, onChanged }: ShareSheetProps) {
|
export default function ShareSheet({ node, onClose, onChanged }: ShareSheetProps) {
|
||||||
// Centred on a wide window, clear of side insets in landscape.
|
// Centred on a wide window, clear of side insets in landscape.
|
||||||
const sheetInsets = useSheetInsets();
|
const sheetInsets = useSheetInsets();
|
||||||
|
const bottomInset = useSheetBottomInset();
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||||
const t = useLocaleStore((s) => s.t);
|
const t = useLocaleStore((s) => s.t);
|
||||||
@@ -189,7 +191,7 @@ export default function ShareSheet({ node, onClose, onChanged }: ShareSheetProps
|
|||||||
<TouchableWithoutFeedback accessible={false} onPress={onClose}>
|
<TouchableWithoutFeedback accessible={false} onPress={onClose}>
|
||||||
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||||
<TouchableWithoutFeedback accessible={false}>
|
<TouchableWithoutFeedback accessible={false}>
|
||||||
<View style={styles.sheet}>
|
<View style={[styles.sheet, { paddingBottom: spacing.xxl + bottomInset }]}>
|
||||||
<View style={styles.titleRow}>
|
<View style={styles.titleRow}>
|
||||||
<Icon size={18} color={c.textMuted} />
|
<Icon size={18} color={c.textMuted} />
|
||||||
<Text style={styles.title} numberOfLines={1}>
|
<Text style={styles.title} numberOfLines={1}>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { jmapClient } from '../../api/jmap-client';
|
|||||||
import type { Mailbox } from '../../api/types';
|
import type { Mailbox } from '../../api/types';
|
||||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||||
import { useSheetInsets } from '../../lib/use-layout';
|
import { useSheetInsets } from '../../lib/use-layout';
|
||||||
|
import { useSheetBottomInset } from '../../lib/sheet-bottom-inset';
|
||||||
import Badge from '../Badge';
|
import Badge from '../Badge';
|
||||||
|
|
||||||
const ROLE_ICON: Record<string, any> = {
|
const ROLE_ICON: Record<string, any> = {
|
||||||
@@ -68,6 +69,7 @@ function pathOf(all: Mailbox[], mb: Mailbox, t: (k: string, f?: string) => strin
|
|||||||
export function FolderSettings() {
|
export function FolderSettings() {
|
||||||
// Centred on a wide window, clear of side insets in landscape.
|
// Centred on a wide window, clear of side insets in landscape.
|
||||||
const sheetInsets = useSheetInsets();
|
const sheetInsets = useSheetInsets();
|
||||||
|
const bottomInset = useSheetBottomInset();
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||||
const t = useLocaleStore((s) => s.t);
|
const t = useLocaleStore((s) => s.t);
|
||||||
@@ -300,7 +302,7 @@ export function FolderSettings() {
|
|||||||
|
|
||||||
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible={!!editor} animationType="slide" transparent onRequestClose={closeEditor}>
|
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible={!!editor} animationType="slide" transparent onRequestClose={closeEditor}>
|
||||||
<View style={[styles.modalOverlay, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
<View style={[styles.modalOverlay, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||||
<View style={styles.modalSheet}>
|
<View style={[styles.modalSheet, { paddingBottom: bottomInset }]}>
|
||||||
<View style={styles.modalHeader}>
|
<View style={styles.modalHeader}>
|
||||||
<Text style={styles.modalTitle}>
|
<Text style={styles.modalTitle}>
|
||||||
{editor?.kind === 'edit'
|
{editor?.kind === 'edit'
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { sanitizeDisplayName } from '../../lib/rfc5322-mailbox';
|
|||||||
import { sanitizeSignatureHtml } from '../../lib/signature-utils';
|
import { sanitizeSignatureHtml } from '../../lib/signature-utils';
|
||||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||||
import { useSheetInsets } from '../../lib/use-layout';
|
import { useSheetInsets } from '../../lib/use-layout';
|
||||||
|
import { useSheetBottomInset } from '../../lib/sheet-bottom-inset';
|
||||||
import Badge from '../Badge';
|
import Badge from '../Badge';
|
||||||
import { identityDraftChanged } from './identity-draft';
|
import { identityDraftChanged } from './identity-draft';
|
||||||
|
|
||||||
@@ -98,6 +99,7 @@ function parseAddressList(value: string): { list: EmailAddress[]; invalid: strin
|
|||||||
export function IdentitySettings() {
|
export function IdentitySettings() {
|
||||||
// Centred on a wide window, clear of side insets in landscape.
|
// Centred on a wide window, clear of side insets in landscape.
|
||||||
const sheetInsets = useSheetInsets();
|
const sheetInsets = useSheetInsets();
|
||||||
|
const bottomInset = useSheetBottomInset();
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||||
const identities = useSettingsStore((s) => s.identities);
|
const identities = useSettingsStore((s) => s.identities);
|
||||||
@@ -330,7 +332,7 @@ export function IdentitySettings() {
|
|||||||
|
|
||||||
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible={!!editing} animationType="slide" transparent onRequestClose={requestClose}>
|
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible={!!editing} animationType="slide" transparent onRequestClose={requestClose}>
|
||||||
<View style={[styles.modalOverlay, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
<View style={[styles.modalOverlay, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||||
<View style={styles.modalSheet}>
|
<View style={[styles.modalSheet, { paddingBottom: bottomInset }]}>
|
||||||
<View style={styles.modalHeader}>
|
<View style={styles.modalHeader}>
|
||||||
<Text style={styles.modalTitle}>
|
<Text style={styles.modalTitle}>
|
||||||
{editing?.id ? t('settings.identities.edit', "Edit identity") : t('settings.identities.create', "New identity")}
|
{editing?.id ? t('settings.identities.edit', "Edit identity") : t('settings.identities.create', "New identity")}
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ import { useTemplatesStore, type EmailTemplate } from '../../stores/templates-st
|
|||||||
import { useLocaleStore } from '../../stores/locale-store';
|
import { useLocaleStore } from '../../stores/locale-store';
|
||||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||||
import { useSheetInsets } from '../../lib/use-layout';
|
import { useSheetInsets } from '../../lib/use-layout';
|
||||||
|
import { useSheetBottomInset } from '../../lib/sheet-bottom-inset';
|
||||||
|
|
||||||
export function TemplateSettings() {
|
export function TemplateSettings() {
|
||||||
// Centred on a wide window, clear of side insets in landscape.
|
// Centred on a wide window, clear of side insets in landscape.
|
||||||
const sheetInsets = useSheetInsets();
|
const sheetInsets = useSheetInsets();
|
||||||
|
const bottomInset = useSheetBottomInset();
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||||
const tr = useLocaleStore((s) => s.t);
|
const tr = useLocaleStore((s) => s.t);
|
||||||
@@ -213,7 +215,7 @@ export function TemplateSettings() {
|
|||||||
{/* Edit/Create modal */}
|
{/* Edit/Create modal */}
|
||||||
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible={!!editing} animationType="slide" transparent onRequestClose={closeEditor}>
|
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible={!!editing} animationType="slide" transparent onRequestClose={closeEditor}>
|
||||||
<View style={[styles.modalOverlay, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
<View style={[styles.modalOverlay, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||||
<View style={styles.modalSheet}>
|
<View style={[styles.modalSheet, { paddingBottom: bottomInset }]}>
|
||||||
<View style={styles.modalHeader}>
|
<View style={styles.modalHeader}>
|
||||||
<Text style={styles.modalTitle}>
|
<Text style={styles.modalTitle}>
|
||||||
{editing?.id ? tr('settings.templates.edit', 'Edit Template') : tr('settings.templates.add', 'New Template')}
|
{editing?.id ? tr('settings.templates.edit', 'Edit Template') : tr('settings.templates.add', 'New Template')}
|
||||||
@@ -287,7 +289,7 @@ export function TemplateSettings() {
|
|||||||
{/* Import modal */}
|
{/* Import modal */}
|
||||||
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible={importVisible} animationType="slide" transparent onRequestClose={() => setImportVisible(false)}>
|
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible={importVisible} animationType="slide" transparent onRequestClose={() => setImportVisible(false)}>
|
||||||
<View style={[styles.modalOverlay, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
<View style={[styles.modalOverlay, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||||
<View style={styles.modalSheet}>
|
<View style={[styles.modalSheet, { paddingBottom: bottomInset }]}>
|
||||||
<View style={styles.modalHeader}>
|
<View style={styles.modalHeader}>
|
||||||
<Text style={styles.modalTitle}>{tr('settings.templates.import', 'Import')}</Text>
|
<Text style={styles.modalTitle}>{tr('settings.templates.import', 'Import')}</Text>
|
||||||
<Pressable onPress={() => setImportVisible(false)} hitSlop={8} accessibilityRole="button" accessibilityLabel={tr('common.close', 'Close')}>
|
<Pressable onPress={() => setImportVisible(false)} hitSlop={8} accessibilityRole="button" accessibilityLabel={tr('common.close', 'Close')}>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How far the content of a bottom sheet or dialog has to stay above the
|
||||||
|
* screen bottom.
|
||||||
|
*
|
||||||
|
* Every RN Modal on Android is edge to edge here: with edge-to-edge on (Expo
|
||||||
|
* SDK 54) the Modal forces statusBarTranslucent and navigationBarTranslucent,
|
||||||
|
* so its window reaches under the navigation bar, a phone's gesture bar or a
|
||||||
|
* tablet's taskbar. A sheet that ends at `bottom: 0` (or a backdrop that
|
||||||
|
* aligns it with `justifyContent: 'flex-end'`) puts its last row under that
|
||||||
|
* bar, where a tap goes to the taskbar instead (an app icon opened). The sheet
|
||||||
|
* keeps its surface down to the screen edge and pads its content by this.
|
||||||
|
*
|
||||||
|
* Inside a plain Modal the insets come from the app's provider (React context
|
||||||
|
* reaches into the Modal); inside a SafeAreaModal from the Modal's own.
|
||||||
|
*/
|
||||||
|
export function useSheetBottomInset(): number {
|
||||||
|
return useSafeAreaInsets().bottom;
|
||||||
|
}
|
||||||
@@ -69,6 +69,7 @@ import { filesGridLayout } from '../lib/files-grid';
|
|||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { formatListDate } from '../lib/date-format';
|
import { formatListDate } from '../lib/date-format';
|
||||||
import { useLayout, useSheetInsets } from '../lib/use-layout';
|
import { useLayout, useSheetInsets } from '../lib/use-layout';
|
||||||
|
import { useSheetBottomInset } from '../lib/sheet-bottom-inset';
|
||||||
import { useBackWhileFocused } from '../lib/use-back-while-focused';
|
import { useBackWhileFocused } from '../lib/use-back-while-focused';
|
||||||
import { filesBackStep } from '../lib/files-back';
|
import { filesBackStep } from '../lib/files-back';
|
||||||
import { driveChange, type DriveScope } from '../lib/files-drive';
|
import { driveChange, type DriveScope } from '../lib/files-drive';
|
||||||
@@ -1807,6 +1808,7 @@ function ActionsSheet({
|
|||||||
}: ActionsSheetProps) {
|
}: ActionsSheetProps) {
|
||||||
// Centred on a wide window, clear of side insets in landscape.
|
// Centred on a wide window, clear of side insets in landscape.
|
||||||
const sheetInsets = useSheetInsets();
|
const sheetInsets = useSheetInsets();
|
||||||
|
const bottomInset = useSheetBottomInset();
|
||||||
const formatModified = useFormatModified();
|
const formatModified = useFormatModified();
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeSheetStyles(c), [c]);
|
const styles = React.useMemo(() => makeSheetStyles(c), [c]);
|
||||||
@@ -1828,7 +1830,7 @@ function ActionsSheet({
|
|||||||
<TouchableWithoutFeedback accessible={false} onPress={onClose}>
|
<TouchableWithoutFeedback accessible={false} onPress={onClose}>
|
||||||
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||||
<TouchableWithoutFeedback accessible={false}>
|
<TouchableWithoutFeedback accessible={false}>
|
||||||
<View style={styles.sheet}>
|
<View style={[styles.sheet, { paddingBottom: spacing.xxl + bottomInset }]}>
|
||||||
<Text style={styles.sheetTitle} numberOfLines={1}>
|
<Text style={styles.sheetTitle} numberOfLines={1}>
|
||||||
{target.displayName}
|
{target.displayName}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -1871,7 +1873,7 @@ function ActionsSheet({
|
|||||||
<TouchableWithoutFeedback accessible={false} onPress={onClose}>
|
<TouchableWithoutFeedback accessible={false} onPress={onClose}>
|
||||||
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||||
<TouchableWithoutFeedback accessible={false}>
|
<TouchableWithoutFeedback accessible={false}>
|
||||||
<View style={styles.sheet}>
|
<View style={[styles.sheet, { paddingBottom: spacing.xxl + bottomInset }]}>
|
||||||
<Text style={styles.sheetTitle} numberOfLines={1}>
|
<Text style={styles.sheetTitle} numberOfLines={1}>
|
||||||
{target.displayName}
|
{target.displayName}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -1961,6 +1963,7 @@ interface FolderPickerSheetProps {
|
|||||||
function FolderPickerSheet({ target, allNodes, rootLabel, t, onClose, onPick }: FolderPickerSheetProps) {
|
function FolderPickerSheet({ target, allNodes, rootLabel, t, onClose, onPick }: FolderPickerSheetProps) {
|
||||||
// Centred on a wide window, clear of side insets in landscape.
|
// Centred on a wide window, clear of side insets in landscape.
|
||||||
const sheetInsets = useSheetInsets();
|
const sheetInsets = useSheetInsets();
|
||||||
|
const bottomInset = useSheetBottomInset();
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeSheetStyles(c), [c]);
|
const styles = React.useMemo(() => makeSheetStyles(c), [c]);
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
@@ -2003,7 +2006,7 @@ function FolderPickerSheet({ target, allNodes, rootLabel, t, onClose, onPick }:
|
|||||||
<TouchableWithoutFeedback accessible={false} onPress={onClose}>
|
<TouchableWithoutFeedback accessible={false} onPress={onClose}>
|
||||||
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||||
<TouchableWithoutFeedback accessible={false}>
|
<TouchableWithoutFeedback accessible={false}>
|
||||||
<View style={styles.sheet}>
|
<View style={[styles.sheet, { paddingBottom: spacing.xxl + bottomInset }]}>
|
||||||
<Text style={styles.sheetTitle} numberOfLines={1}>
|
<Text style={styles.sheetTitle} numberOfLines={1}>
|
||||||
{t('files.move_to', 'Move to…')} · {target.displayName}
|
{t('files.move_to', 'Move to…')} · {target.displayName}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -2051,6 +2054,7 @@ interface SortSheetProps {
|
|||||||
function SortSheet({ visible, sortKey, sortDir, t, onClose, onChangeKey, onChangeDir }: SortSheetProps) {
|
function SortSheet({ visible, sortKey, sortDir, t, onClose, onChangeKey, onChangeDir }: SortSheetProps) {
|
||||||
// Centred on a wide window, clear of side insets in landscape.
|
// Centred on a wide window, clear of side insets in landscape.
|
||||||
const sheetInsets = useSheetInsets();
|
const sheetInsets = useSheetInsets();
|
||||||
|
const bottomInset = useSheetBottomInset();
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeSheetStyles(c), [c]);
|
const styles = React.useMemo(() => makeSheetStyles(c), [c]);
|
||||||
if (!visible) return null;
|
if (!visible) return null;
|
||||||
@@ -2060,7 +2064,7 @@ function SortSheet({ visible, sortKey, sortDir, t, onClose, onChangeKey, onChang
|
|||||||
<TouchableWithoutFeedback accessible={false} onPress={onClose}>
|
<TouchableWithoutFeedback accessible={false} onPress={onClose}>
|
||||||
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||||
<TouchableWithoutFeedback accessible={false}>
|
<TouchableWithoutFeedback accessible={false}>
|
||||||
<View style={styles.sheet}>
|
<View style={[styles.sheet, { paddingBottom: spacing.xxl + bottomInset }]}>
|
||||||
<Text style={styles.sheetTitle}>{t('files.settings_default_sort', 'Sort')}</Text>
|
<Text style={styles.sheetTitle}>{t('files.settings_default_sort', 'Sort')}</Text>
|
||||||
{keys.map((k) => (
|
{keys.map((k) => (
|
||||||
<Pressable key={k} style={styles.action} onPress={() => { onChangeKey(k); onClose(); }}>
|
<Pressable key={k} style={styles.action} onPress={() => { onChangeKey(k); onClose(); }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user