diff --git a/client/src/components/Planner/PlacesSidebar.test.tsx b/client/src/components/Planner/PlacesSidebar.test.tsx index e5d99f6b9..ae82a874f 100644 --- a/client/src/components/Planner/PlacesSidebar.test.tsx +++ b/client/src/components/Planner/PlacesSidebar.test.tsx @@ -1,5 +1,5 @@ // FE-COMP-PLACES-001 to FE-COMP-PLACES-015 + FE-PLANNER-SIDEBAR-016 to 043 -import { render, screen, fireEvent, waitFor, act } from '../../../tests/helpers/render'; +import { render, screen, fireEvent, waitFor, act, within } from '../../../tests/helpers/render'; import userEvent from '@testing-library/user-event'; import { http, HttpResponse } from 'msw'; import { useAuthStore } from '../../store/authStore'; @@ -50,6 +50,12 @@ beforeEach(() => { seedStore(useTripStore, { trip: buildTrip({ id: 1 }) }); }); +/** The filter is one select now: open it, then pick the option by its label. */ +async function pickFilter(user: ReturnType, label: string) { + await user.click(within(screen.getByTestId('places-filter')).getByRole('button')); + await user.click(await screen.findByRole('button', { name: new RegExp(`^${label}`) })); +} + describe('PlacesSidebar', () => { it('FE-COMP-PLACES-001: renders without crashing', () => { render(); @@ -251,7 +257,7 @@ describe('Filter tabs', () => { const unplanned = buildPlace({ name: 'Unplanned Place' }); const assignments = { '1': [buildAssignment({ place: planned, day_id: 1 })] }; render(); - await user.click(screen.getByRole('button', { name: /Unplanned/i })); + await pickFilter(user, 'Unplanned'); expect(screen.queryByText('Planned Place')).not.toBeInTheDocument(); expect(screen.getByText('Unplanned Place')).toBeInTheDocument(); }); @@ -262,8 +268,8 @@ describe('Filter tabs', () => { const unplanned = buildPlace({ name: 'Unplanned Place' }); const assignments = { '1': [buildAssignment({ place: planned, day_id: 1 })] }; render(); - await user.click(screen.getByRole('button', { name: /Unplanned/i })); - await user.click(screen.getByRole('button', { name: /^All/i })); + await pickFilter(user, 'Unplanned'); + await pickFilter(user, 'All'); expect(screen.getByText('Planned Place')).toBeInTheDocument(); expect(screen.getByText('Unplanned Place')).toBeInTheDocument(); }); @@ -273,7 +279,7 @@ describe('Filter tabs', () => { const place = buildPlace({ name: 'Assigned Place' }); const assignments = { '1': [buildAssignment({ place, day_id: 1 })] }; render(); - await user.click(screen.getByRole('button', { name: /Unplanned/i })); + await pickFilter(user, 'Unplanned'); expect(screen.getByText(/All places are planned/i)).toBeInTheDocument(); }); @@ -515,7 +521,7 @@ describe('Category filter dropdown', () => { const unplanned = buildPlace({ name: 'Unplanned Place' }); const assignments = { '1': [buildAssignment({ place: planned, day_id: 1 })] }; const { unmount } = render(); - await user.click(screen.getByRole('button', { name: /Unplanned/i })); + await pickFilter(user, 'Unplanned'); expect(screen.queryByText('Planned Place')).not.toBeInTheDocument(); unmount(); render(); diff --git a/client/src/components/Planner/PlacesSidebarHeader.tsx b/client/src/components/Planner/PlacesSidebarHeader.tsx index 72d03dd7e..9148bd174 100644 --- a/client/src/components/Planner/PlacesSidebarHeader.tsx +++ b/client/src/components/Planner/PlacesSidebarHeader.tsx @@ -1,6 +1,7 @@ import { Search, Plus, X, Upload, FileDown, ChevronDown, Check, MapPin, Star, CalendarPlus, CalendarDays } from 'lucide-react' import { getCategoryIcon } from '../shared/categoryIcons' import Tooltip from '../shared/Tooltip' +import CustomSelect from '../shared/CustomSelect' import { useElementSize } from '../../hooks/useElementSize' import type { SidebarState } from './usePlacesSidebar' @@ -177,40 +178,17 @@ export function PlacesHeader(S: SidebarState) { hasTracks ? { id: 'tracks', label: t('places.filterTracks') } : null, ] as const).filter(Boolean) as Array<{ id: 'all' | 'unplanned' | 'planned' | 'tracks'; label: string }> return ( - // The group always spans the rail and never wraps onto a second line. - // Each tab keeps its own text width and only the leftover space is - // shared out, so "Unplanned" stays wider than "All" instead of the four - // being forced to one size. -
- {tabs.map(f => { - const active = filter === f.id - return ( - - ) - })} + // One select where four pills used to fight for the rail's width: with the + // tracks tab the row had grown past what a narrow panel can hold without + // clipping a word. The count rides on the trigger the way it rode on the + // active pill, and every option carries its own in the menu. +
+ { setFilter(v as 'all' | 'unplanned' | 'planned' | 'tracks'); setSelectedIds(new Set()) }} + options={tabs.map(f => ({ value: f.id, label: f.label, badge: String(counts[f.id]) }))} + />
) })()}