From fcfae58a70253a2cae4e2c5e9344eb16aad548ce Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Tue, 3 Nov 2026 14:47:08 +0100
Subject: [PATCH] fix: keep the open message when / focuses Mountain View's
search field in the top bar
---
.../mail/__tests__/mail-app-mv-search.test.tsx | 13 ++++++++++++-
.../mail/__tests__/mail-app-return-to-list.test.tsx | 13 ++++++++++++-
components/mail/mail-app.tsx | 13 +++++++++----
3 files changed, 33 insertions(+), 6 deletions(-)
diff --git a/components/mail/__tests__/mail-app-mv-search.test.tsx b/components/mail/__tests__/mail-app-mv-search.test.tsx
index 1fdc6b175..f9597d7ed 100644
--- a/components/mail/__tests__/mail-app-mv-search.test.tsx
+++ b/components/mail/__tests__/mail-app-mv-search.test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Email, Mailbox } from '@/lib/jmap/types';
import { useAuthStore } from '@/stores/auth-store';
@@ -22,6 +22,7 @@ vi.mock('@/components/layout/mv-top-bar', () => ({
<>
+
>
),
}));
@@ -129,4 +130,14 @@ describe('searching in Mountain View while a message is open', () => {
await waitFor(() => expect(advancedSearch).toHaveBeenCalled());
expect(selectedId()).toBeNull();
});
+
+ it('leaves the message open for the / shortcut, whose field is in the top bar', async () => {
+ render();
+ await screen.findByText('message viewer');
+ act(() => { fireEvent.keyDown(window, { key: '/' }); });
+ await waitFor(() => expect(document.activeElement).toBe(screen.getByLabelText('stub search field')));
+ // Long enough for a go-back that runs after the field is focused.
+ await act(async () => { await new Promise((resolve) => setTimeout(resolve, 50)); });
+ expect(selectedId()).toBe('m1');
+ });
});
diff --git a/components/mail/__tests__/mail-app-return-to-list.test.tsx b/components/mail/__tests__/mail-app-return-to-list.test.tsx
index b9b3ec364..6f64a44b3 100644
--- a/components/mail/__tests__/mail-app-return-to-list.test.tsx
+++ b/components/mail/__tests__/mail-app-return-to-list.test.tsx
@@ -47,7 +47,7 @@ vi.mock('@/components/layout/mobile-header', () => ({ MobileHeader: () => null }
vi.mock('@/components/layout/mv-top-bar', () => ({ MvTopBar: () => null }));
vi.mock('@/components/layout/mv-side-panel', () => ({ MvSidePanel: () => null }));
vi.mock('@/components/email/message-list-tabs', () => ({ useMessageListPanelProps: (id: string) => ({ id }), MessageListTabs: () => null }));
-vi.mock('@/components/search/search-box', () => ({ SearchBox: () => null }));
+vi.mock('@/components/search/search-box', () => ({ SearchBox: () => }));
vi.mock('@/components/tour/tour-provider', () => ({ useTour: () => ({ activeTour: null, registerStep: vi.fn() }) }));
vi.mock('@/components/plugins/plugin-slot', () => ({ PluginSlot: () => null }));
vi.mock('@/hooks/use-plugin-slot-offers', () => ({ usePluginSlotOffers: () => [] }));
@@ -288,6 +288,17 @@ describe('A list collapsed under an open message (focused layout)', () => {
act(() => { fireEvent.keyDown(window, { key: '/' }); });
await waitFor(() => expect(selectedId()).toBeNull());
await waitFor(() => expect(list()).not.toHaveAttribute('inert'));
+ await waitFor(() => expect(document.activeElement).toBe(screen.getByLabelText('stub search field')));
+ });
+
+ it('keeps the selection for the / shortcut while a linked message is still arriving', async () => {
+ // The mailboxes never arrive, so the link stays unapplied and the list collapsed for it.
+ useEmailStore.setState({ fetchMailboxes: vi.fn(() => new Promise(() => {})) } as never);
+ render();
+ expect(list()).toHaveAttribute('inert');
+ act(() => { fireEvent.keyDown(window, { key: '/' }); });
+ await act(async () => { await new Promise((resolve) => setTimeout(resolve, 50)); });
+ expect(selectedId()).toBe('m1');
});
});
diff --git a/components/mail/mail-app.tsx b/components/mail/mail-app.tsx
index 5299ac7fa..50cd06e14 100644
--- a/components/mail/mail-app.tsx
+++ b/components/mail/mail-app.tsx
@@ -1125,15 +1125,20 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
},
onFocusSearch: () => {
if (isScheduledView) return;
+ const findSearchInput = () => document.querySelector('[data-search-input]');
const focusSearch = () => {
- const searchInput = document.querySelector('[data-search-input]') as HTMLInputElement;
+ const searchInput = findSearchInput();
if (searchInput) {
searchInput.focus();
searchInput.select();
}
};
- // The field is in the list, which an open message can have collapsed.
- if (shouldCollapseListPane) {
+ // The field can be in the list, which an open message has collapsed, and
+ // that message has to close first. Mountain View's is in the top bar.
+ if (findSearchInput()?.closest('[inert]')) {
+ // A linked message still arriving holds the list collapsed: nothing to
+ // go back to yet.
+ if (deepLinkPane !== null) return;
handleMobileBack();
requestAnimationFrame(() => requestAnimationFrame(focusSearch));
} else {
@@ -1220,7 +1225,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
- }), [activeEmails, selectedEmail, client, selectedMailbox, isMobile, isTablet, selectedEmailKeys, mailboxes, isScheduledView, mvShell, cursorOnly, showCrossStarred, isUnifiedView, unifiedRole, archiveActionEnabled]);
+ }), [activeEmails, selectedEmail, client, selectedMailbox, isMobile, isTablet, selectedEmailKeys, mailboxes, isScheduledView, mvShell, cursorOnly, showCrossStarred, isUnifiedView, unifiedRole, archiveActionEnabled, deepLinkPane]);
// Initialize keyboard shortcuts
// Mountain View keeps the ticked rows while a message is read (#1231). The