diff --git a/src/lib/__tests__/list-back.test.ts b/src/lib/__tests__/list-back.test.ts new file mode 100644 index 00000000..529d7ac7 --- /dev/null +++ b/src/lib/__tests__/list-back.test.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from 'vitest'; +import { listBackStep } from '../list-back'; + +describe('listBackStep', () => { + it('ends selection mode first', () => { + expect(listBackStep({ selecting: true, searching: true })).toBe('clear-selection'); + }); + + it('clears an open search instead of leaving the app', () => { + expect(listBackStep({ selecting: false, searching: true })).toBe('clear-search'); + }); + + it('leaves only from a plain list', () => { + expect(listBackStep({ selecting: false, searching: false })).toBe('leave'); + }); +}); diff --git a/src/lib/list-back.ts b/src/lib/list-back.ts new file mode 100644 index 00000000..cba9ef8b --- /dev/null +++ b/src/lib/list-back.ts @@ -0,0 +1,14 @@ +/** What Android back does on a list screen (mail, contacts). */ +export type ListBackStep = 'clear-selection' | 'clear-search' | 'leave'; + +/** + * Back works through the list's own layers first: selection mode ends, then + * an open search (and on mail its filters) clears. Only a plain list lets + * back `leave`: on the mail root that closes the app, on another tab it goes + * back to Mail. A search used to exit the app straight away. + */ +export function listBackStep(state: { selecting: boolean; searching: boolean }): ListBackStep { + if (state.selecting) return 'clear-selection'; + if (state.searching) return 'clear-search'; + return 'leave'; +} diff --git a/src/screens/EmailListScreen.tsx b/src/screens/EmailListScreen.tsx index 57cd0c28..ca9d22b2 100644 --- a/src/screens/EmailListScreen.tsx +++ b/src/screens/EmailListScreen.tsx @@ -31,6 +31,8 @@ import { } from '../stores/email-store'; import { useSettingsStore, type SwipeAction, type SwipeMode } from '../stores/settings-store'; import { useKeywordsStore, type KeywordDef } from '../stores/keywords-store'; +import { useBackWhileFocused } from '../lib/use-back-while-focused'; +import { listBackStep } from '../lib/list-back'; import { useLocaleStore } from '../stores/locale-store'; import { useSearchHistoryStore } from '../stores/search-history-store'; import { useContactsStore } from '../stores/contacts-store'; @@ -885,6 +887,22 @@ export default function EmailListScreen({ (filters.keyword ? 1 : 0) + (filters.folder && filters.folder !== 'current' ? 1 : 0); const hasActiveSearchOrFilter = Boolean(storeSearchQuery) || activeFilterCount > 0; + + // Back first leaves the selection, then the search and filters; only then + // does it leave the app (it used to exit with a search still open). + const backStep = listBackStep({ + selecting: selectionMode, + searching: hasActiveSearchOrFilter || searchInput.length > 0, + }); + const handleBack = React.useCallback(() => { + if (backStep === 'clear-selection') { + clearSelection(); + } else if (backStep === 'clear-search') { + setSearchInput(''); + clearSearchAndFilters(); + } + }, [backStep, clearSelection, clearSearchAndFilters]); + useBackWhileFocused(backStep !== 'leave', handleBack); const folderScope = effectiveFolderScope(storeSearchQuery, filters); // Accounts an "All folders" list could not reach (#1082). const unreachedAccounts = spansAccounts({ searchQuery: storeSearchQuery, filters })