fix: clear the Files selection on Android back instead of switching to Mail

This commit is contained in:
Linus Rath
2026-09-24 18:41:23 +02:00
parent caea3d0c60
commit 8d7fbfc99c
3 changed files with 86 additions and 0 deletions
@@ -0,0 +1,58 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
const handlers: Array<() => boolean> = [];
const remove = vi.fn();
vi.mock('react-native', () => ({
BackHandler: {
addEventListener: vi.fn((_event: string, handler: () => boolean) => {
handlers.push(handler);
return { remove };
}),
},
}));
// Run the focus effect as if the screen had just gained focus and keep its
// cleanup, which react-navigation calls on blur.
let blur: void | (() => void);
vi.mock('@react-navigation/native', () => ({
useFocusEffect: (effect: () => void | (() => void)) => {
blur = effect();
},
}));
vi.mock('react', () => ({ useCallback: <T,>(fn: T) => fn }));
import { BackHandler } from 'react-native';
import { useBackWhileFocused } from '../use-back-while-focused';
beforeEach(() => {
vi.clearAllMocks();
handlers.length = 0;
blur = undefined;
});
describe('useBackWhileFocused', () => {
it('runs onBack and consumes the back press while active', () => {
const onBack = vi.fn();
useBackWhileFocused(true, onBack);
expect(BackHandler.addEventListener).toHaveBeenCalledWith('hardwareBackPress', expect.any(Function));
expect(handlers[0]()).toBe(true);
expect(onBack).toHaveBeenCalledTimes(1);
});
it('leaves the back press to navigation while inactive', () => {
useBackWhileFocused(false, vi.fn());
expect(BackHandler.addEventListener).not.toHaveBeenCalled();
});
it('stops listening when the screen loses focus', () => {
useBackWhileFocused(true, vi.fn());
expect(typeof blur).toBe('function');
(blur as () => void)();
expect(remove).toHaveBeenCalledTimes(1);
});
});
+23
View File
@@ -0,0 +1,23 @@
import { useCallback } from 'react';
import { BackHandler } from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
/**
* While `active` and the screen is focused, Android's hardware back runs
* `onBack` instead of navigating (e.g. to leave a selection mode first).
* Tab screens stay mounted while another tab is shown, so the listener is
* tied to focus: a plain BackHandler listener would also swallow the back
* presses meant for the other tabs.
*/
export function useBackWhileFocused(active: boolean, onBack: () => void): void {
useFocusEffect(
useCallback(() => {
if (!active) return undefined;
const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
onBack();
return true;
});
return () => subscription.remove();
}, [active, onBack]),
);
}
+5
View File
@@ -52,6 +52,7 @@ import { jmapClient } from '../api/jmap-client';
import { downloadAttachment, shareAttachment } from '../lib/email-export';
import { secureFetch } from '../lib/client-cert';
import { getUniqueName } from '../lib/filenode-name';
import { useBackWhileFocused } from '../lib/use-back-while-focused';
import type { FileNode } from '../api/types';
import { spacing, radius, typography, type ThemePalette } from '../theme/tokens';
import { useColors } from '../theme/colors';
@@ -321,6 +322,10 @@ export default function FilesScreen() {
const clearSelection = useCallback(() => setSelection(new Set()), []);
// Android back leaves selection mode first, like the header's X, instead
// of switching to the Mail tab.
useBackWhileFocused(selectionMode, clearSelection);
// Shared-with-me rows stay out of multi-select: batch delete routes to our
// own account and would fail (or worse, mismatch) on namespaced ids.
const toggleSelect = useCallback((row: FileRow) => {