diff --git a/components/global-search/__tests__/search-tab-body.test.tsx b/components/global-search/__tests__/search-tab-body.test.tsx index 68807fe40..fea01fdcf 100644 --- a/components/global-search/__tests__/search-tab-body.test.tsx +++ b/components/global-search/__tests__/search-tab-body.test.tsx @@ -60,6 +60,8 @@ vi.mock('@/components/pro/pro-email-tab-body', () => ({ })); import { SearchTabBody } from '../search-tab-body'; +import { openHit } from '@/lib/global-search/open-hit'; +import { PaneSizeContext } from '@/hooks/use-pane-size'; /** Renders the tab the way the Pro page does: data flows from the tab store. */ function Harness({ tabId }: { tabId: string }) { @@ -123,3 +125,29 @@ describe('SearchTabBody scope switching', () => { } }); }); + +describe('SearchTabBody in a narrow pane', () => { + beforeEach(() => { + resetTabs(); + vi.mocked(openHit).mockClear(); + }); + + const renderInPane = async (width: number) => { + const tabId = useProTabStore.getState().openSearchTab({ query: 'Ann', scope: 'all', accountId: null, title: 'Ann' }); + render(); + await waitFor(() => expect(rowCount('mail')).toBe(1), { timeout: 5000 }); + return document.querySelector('[data-hit-kind="mail"]')!; + }; + + it('opens a hit on click where no preview fits', async () => { + const row = await renderInPane(500); + fireEvent.click(row); + expect(openHit).toHaveBeenCalledTimes(1); + }); + + it('only selects it where the preview shows', async () => { + const row = await renderInPane(1100); + fireEvent.click(row); + expect(openHit).not.toHaveBeenCalled(); + }); +}); diff --git a/components/global-search/search-tab-body.tsx b/components/global-search/search-tab-body.tsx index c6cc36ea2..e3ad4601c 100644 --- a/components/global-search/search-tab-body.tsx +++ b/components/global-search/search-tab-body.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react"; import { Loader2, Search } from "@/components/icons"; import { useTranslations } from "next-intl"; import { useGlobalSearch } from "@/hooks/use-global-search"; +import { usePaneSize } from "@/hooks/use-pane-size"; import { openHit } from "@/lib/global-search/open-hit"; import type { SearchScope } from "@/lib/global-search/query-parser"; import { hitKey, SEARCH_KINDS, type GlobalSearchHit, type SearchKind } from "@/lib/global-search/types"; @@ -13,9 +14,14 @@ import { useSearchHistoryStore } from "@/stores/search-history-store"; import { SearchFacets } from "@/components/global-search/search-facets"; import { SearchPreview } from "@/components/global-search/search-preview"; import { SearchResultList } from "@/components/global-search/search-result-list"; +import { cn } from "@/lib/utils"; export const SEARCH_TAB_REMOTE_LIMIT = 100; export const SEARCH_TAB_LOCAL_LIMIT = 50; +/** Facets, a 384 px list and a preview need a pane this wide (Tailwind's lg). */ +const PREVIEW_MIN_PANE = 1024; +/** Below this (a split pane) the facets narrow so the list keeps room for its rows. */ +const WIDE_FACETS_MIN_PANE = 768; export interface SearchTabBodyProps { tabId: string; @@ -89,9 +95,15 @@ export function SearchTabBody({ tabId, data }: SearchTabBodyProps) { const setAccountId = (accountId: string | null) => updateSearchTab(tabId, { accountId }); const handleOpen = (hit: GlobalSearchHit) => { void openHit(hit, { proShell: true }); }; + // The tab lives in a pane, which a split can make far narrower than the + // window, so the pane's width decides and not a viewport breakpoint. + const paneWidth = usePaneSize(); + const showPreview = paneWidth === null || paneWidth >= PREVIEW_MIN_PANE; + const wideFacets = paneWidth === null || paneWidth >= WIDE_FACETS_MIN_PANE; + return (
-
); }