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