From d20532af9156d7331ee89d8746a2d7c8718504fe Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:18:15 +0100 Subject: [PATCH] fix: keep search everything reachable once mail search results load --- .../__tests__/search-everything-entry.test.ts | 34 ++++++++++++++++++ src/lib/search-everything-entry.ts | 17 +++++++++ src/screens/EmailListScreen.tsx | 36 +++++++++++++++---- 3 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 src/lib/__tests__/search-everything-entry.test.ts create mode 100644 src/lib/search-everything-entry.ts diff --git a/src/lib/__tests__/search-everything-entry.test.ts b/src/lib/__tests__/search-everything-entry.test.ts new file mode 100644 index 00000000..5c46fac7 --- /dev/null +++ b/src/lib/__tests__/search-everything-entry.test.ts @@ -0,0 +1,34 @@ +// The mail search's "Search everything" row was drawn over by the search and +// folder chips once results loaded, and was gone once the field lost focus. +import { describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { searchEverythingPlacement } from '../search-everything-entry'; + +const src = fs.readFileSync(path.resolve(__dirname, '../../screens/EmailListScreen.tsx'), 'utf8'); + +describe('Search everything in the mail search', () => { + it('sits in the panel while the field has focus', () => { + expect(searchEverythingPlacement({ searchFocused: true, hasActiveSearchOrFilter: true })).toBe('panel'); + expect(searchEverythingPlacement({ searchFocused: true, hasActiveSearchOrFilter: false })).toBe('panel'); + }); + + it('stays reachable as a chip once the field loses focus with a search on', () => { + expect(searchEverythingPlacement({ searchFocused: false, hasActiveSearchOrFilter: true })).toBe('chips'); + expect(searchEverythingPlacement({ searchFocused: false, hasActiveSearchOrFilter: false })).toBeNull(); + }); + + it('has a row that keeps its height, not flex: 1 in the panel column', () => { + const row = / { + expect(src).toMatch(/searchEverythingAt === 'panel' && \(/); + expect(src).toMatch(/searchEverythingAt === 'chips' \? \(\s* { + setSearchFocused(false); + searchInputRef.current?.blur(); + setGlobalSearchOpen(true); + }; React.useEffect(() => { // Keep local input in sync when the store is cleared externally // (e.g. clearing the search from the chips row). @@ -1143,6 +1149,8 @@ export default function EmailListScreen({ (filters.keyword ? 1 : 0) + (filters.folder && filters.folder !== 'current' ? 1 : 0); const hasActiveSearchOrFilter = Boolean(storeSearchQuery) || activeFilterCount > 0; + // In the panel under the focused field, else a chip while a search is on. + const searchEverythingAt = searchEverythingPlacement({ searchFocused, hasActiveSearchOrFilter }); // 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). @@ -1567,15 +1575,13 @@ export default function EmailListScreen({ {/* Search bar (always visible; inside the header in a short window) */} {!shortHeight && renderSearchBar(false)} - {searchFocused && ( + {searchEverythingAt === 'panel' && ( { - setSearchFocused(false); - searchInputRef.current?.blur(); - setGlobalSearchOpen(true); - }} + // Not recentSearchMain: flex 1 in this column left the row's + // height out, and the chips below were drawn over it. + style={styles.searchEverythingRow} + onPress={openSearchEverything} accessibilityRole="button" testID="search-everything" > @@ -1629,6 +1635,13 @@ export default function EmailListScreen({ {hasActiveSearchOrFilter && ( + {searchEverythingAt === 'chips' ? ( + } + label={t('global_search.title', 'Search everything')} + onPress={openSearchEverything} + /> + ) : null} {storeSearchQuery ? ( } @@ -2489,6 +2502,7 @@ function makeStyles(c: ThemePalette) { justifyContent: 'center', }, recentSearches: { + flexShrink: 0, marginHorizontal: spacing.lg, marginBottom: spacing.sm, backgroundColor: c.popover, @@ -2511,6 +2525,14 @@ function makeStyles(c: ThemePalette) { gap: spacing.sm, }, recentSearchMain: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: spacing.sm }, + searchEverythingRow: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + paddingHorizontal: spacing.md, + minHeight: 44, + flexShrink: 0, + }, recentSearchText: { ...typography.body, color: c.text, flexShrink: 1 }, emptyFolderBanner: { flexDirection: 'row',