diff --git a/components/search/__tests__/filter-panel-host.test.tsx b/components/search/__tests__/filter-panel-host.test.tsx index 4ccc6ad1c..375ce474d 100644 --- a/components/search/__tests__/filter-panel-host.test.tsx +++ b/components/search/__tests__/filter-panel-host.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import { describe, it, expect } from 'vitest'; -import { FilterPanelHost } from '../filter-panel-host'; +import { FilterPanelHost, filterPanelBox } from '../filter-panel-host'; // Mountain View moves the filter button into the global top bar. The panel // follows it there, given an anchor; without one it stays in the flow of the @@ -36,6 +36,17 @@ describe('FilterPanelHost', () => { anchor.remove(); }); + it('stays wide enough for its fields when the search field is narrow', () => { + // The field is 286px wide at 1024: a panel that size cut the date fields to "dd". + expect(filterPanelBox({ left: 165, right: 451, bottom: 56, width: 286 }, 1024, false)).toEqual({ top: 60, left: 165, width: 480 }); + }); + + it('grows towards the trailing side in RTL and stays on screen', () => { + expect(filterPanelBox({ left: 573, right: 859, bottom: 56, width: 286 }, 1024, true)).toEqual({ top: 60, left: 379, width: 480 }); + expect(filterPanelBox({ left: 700, right: 986, bottom: 56, width: 286 }, 1024, false).left).toBe(536); + expect(filterPanelBox({ left: 20, right: 300, bottom: 56, width: 280 }, 400, false)).toEqual({ top: 60, left: 8, width: 384 }); + }); + it('draws it at the end of , above the sidebar column and not inside the top bar', () => { const anchor = document.createElement('div'); document.body.appendChild(anchor); diff --git a/components/search/filter-panel-host.tsx b/components/search/filter-panel-host.tsx index 10dbb137e..e4feeb2fd 100644 --- a/components/search/filter-panel-host.tsx +++ b/components/search/filter-panel-host.tsx @@ -52,6 +52,26 @@ export function FilterPanelHost({ ); } +/** Below this the form's labelled rows leave its date and size fields a few characters wide. */ +const MIN_PANEL_WIDTH = 480; +const VIEWPORT_MARGIN = 8; + +/** + * Where the panel goes for a given search field: under it, sharing its leading + * edge and width. The field shrinks with the window, so a narrow one gets a + * panel wider than itself, growing towards the trailing side and kept on screen. + */ +export function filterPanelBox( + rect: { left: number; right: number; bottom: number; width: number }, + viewportWidth: number, + rtl: boolean, +): CSSProperties { + const width = Math.max(rect.width, Math.min(MIN_PANEL_WIDTH, viewportWidth - VIEWPORT_MARGIN * 2)); + const preferred = rtl ? rect.right - width : rect.left; + const left = Math.max(VIEWPORT_MARGIN, Math.min(preferred, viewportWidth - width - VIEWPORT_MARGIN)); + return { top: rect.bottom + 4, left, width }; +} + /** The anchor's leading edge, width and bottom, kept current as the bar resizes. */ function useAnchorBox(anchor: HTMLElement | null): CSSProperties | null { const [box, setBox] = useState(null); @@ -62,8 +82,7 @@ function useAnchorBox(anchor: HTMLElement | null): CSSProperties | null { return; } const measure = () => { - const rect = anchor.getBoundingClientRect(); - setBox({ top: rect.bottom + 4, left: rect.left, width: rect.width }); + setBox(filterPanelBox(anchor.getBoundingClientRect(), window.innerWidth, getComputedStyle(anchor).direction === "rtl")); }; measure();