From 5fc4fd5cbd372251c657fe07e773e8ee4ce92aa9 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Sun, 4 Oct 2026 10:21:41 -0400 Subject: [PATCH] fix(web): stop filter controls crowding out search bars on phones The instrument showcase laid its search bar and three filter dropdowns out in one unwrapped row, so on a 390px phone the search bar shrank to 55px. It now stacks the search bar on its own full-width row, with the dropdowns sharing the row beneath, until the lg breakpoint. md is too early here: the sidebar appears at md and takes 19rem, which left the search bar 97px wide at 768px. The data hub search bar already gets its own row from libui's DataTableControls, but its Subject Lookup, Filters and Export controls did not wrap, which pushed Export off the right edge of a phone screen. They now wrap below md, and the md and wider layout is unchanged. Closes #1583 Co-Authored-By: Claude Opus 5.5 --- .../__tests__/datahub-table-controls.test.tsx | 40 +++++++++++++++++++ .../InstrumentShowcase/InstrumentShowcase.tsx | 12 +++--- .../__tests__/InstrumentShowcase.test.tsx | 9 +++++ apps/web/src/routes/_app/datahub/index.tsx | 6 +-- testing/src/pages/_app/datahub/index.page.ts | 6 ++- .../accessible-instruments.page.ts | 6 ++- .../src/specs/accessible-instruments.spec.ts | 19 +++++++++ testing/src/specs/datahub.spec.ts | 19 ++++++++- testing/src/support/constants.ts | 3 ++ 9 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/__tests__/datahub-table-controls.test.tsx diff --git a/apps/web/src/__tests__/datahub-table-controls.test.tsx b/apps/web/src/__tests__/datahub-table-controls.test.tsx new file mode 100644 index 000000000..5f96647d4 --- /dev/null +++ b/apps/web/src/__tests__/datahub-table-controls.test.tsx @@ -0,0 +1,40 @@ +import { cleanup, render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { Route } from '@/routes/_app/datahub/index'; + +import '@/services/i18n'; + +vi.mock('@tanstack/react-router', async (importOriginal) => ({ + ...(await importOriginal()), + useNavigate: () => vi.fn() +})); +vi.mock('@/hooks/useSubjectsQuery', () => ({ + subjectsQueryOptions: vi.fn(), + useSubjectsQuery: () => ({ data: [] }) +})); +vi.mock('@/store', () => ({ + useAppStore: (selector: (store: { currentGroup: null; currentUser: null }) => unknown) => + selector({ currentGroup: null, currentUser: null }) +})); + +describe('data hub table controls', () => { + beforeEach(() => { + // The toggles call `Route.useNavigate()`, which warns outside a RouterProvider; no navigation happens here. + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + }); + + afterEach(() => { + cleanup(); + vi.restoreAllMocks(); + }); + + // happy-dom computes no layout, so the wrapping is asserted through its utility classes; + // testing/src/specs/datahub.spec.ts measures the rendered result at phone width. + it('should let the controls wrap below the md breakpoint, so a phone never pushes Export off screen', () => { + const Component = Route.options.component!; + render(); + const controls = screen.getByTestId('subject-lookup-search-button').parentElement!; + expect([...controls.classList]).toEqual(expect.arrayContaining(['flex-wrap', 'md:flex-nowrap'])); + }); +}); diff --git a/apps/web/src/components/InstrumentShowcase/InstrumentShowcase.tsx b/apps/web/src/components/InstrumentShowcase/InstrumentShowcase.tsx index 5e24adc2a..df5ce225b 100644 --- a/apps/web/src/components/InstrumentShowcase/InstrumentShowcase.tsx +++ b/apps/web/src/components/InstrumentShowcase/InstrumentShowcase.tsx @@ -99,18 +99,18 @@ export const InstrumentShowcase: React.FC<{ tabIndex={0} onKeyDown={handleKeyDown} > -
+
-
-
+
+
-
+
-
+
diff --git a/apps/web/src/components/InstrumentShowcase/__tests__/InstrumentShowcase.test.tsx b/apps/web/src/components/InstrumentShowcase/__tests__/InstrumentShowcase.test.tsx index 794169635..7d3a9d759 100644 --- a/apps/web/src/components/InstrumentShowcase/__tests__/InstrumentShowcase.test.tsx +++ b/apps/web/src/components/InstrumentShowcase/__tests__/InstrumentShowcase.test.tsx @@ -17,6 +17,15 @@ describe('InstrumentShowcase', () => { expect(fireEvent.keyDown(searchBar, { key: 'Enter' })).toBe(false); }); + // happy-dom computes no layout, so the responsive stacking is asserted through its utility classes; + // testing/src/specs/accessible-instruments.spec.ts measures the rendered result at phone width. + it('should stack the search bar above the filters below the lg breakpoint, so a phone gives it the full width', () => { + render(); + const searchBar = screen.getByTestId('instrument-search-bar'); + expect([...searchBar.classList]).toContain('w-full'); + expect([...searchBar.parentElement!.classList]).toEqual(expect.arrayContaining(['flex-col', 'lg:flex-row'])); + }); + it('should not select anything when Enter is pressed with no matching instruments', () => { const onSelect = vi.fn(); render(); diff --git a/apps/web/src/routes/_app/datahub/index.tsx b/apps/web/src/routes/_app/datahub/index.tsx index 4b2b785c1..186af5d93 100644 --- a/apps/web/src/routes/_app/datahub/index.tsx +++ b/apps/web/src/routes/_app/datahub/index.tsx @@ -61,7 +61,7 @@ const Filters: React.FC<{