From 4b263c8e4b82522a230589bab9bfb6e640214794 Mon Sep 17 00:00:00 2001
From: EvKoh <1309463+EvKoh@users.noreply.github.com>
Date: Sat, 3 Oct 2026 15:53:57 +0200
Subject: [PATCH] fix(mobile): align the places browser's filter panel with the
map control
The panel button is named "Filters" like the map control, its badge counts
kinds of filter through countActivePlacesFilters (the pool, shown as chips
above it, stays out), and picking a rating floor clears the selection as
picking a pool does.
---
.../screens/trip/places/MPlacesBrowser.tsx | 11 ++++--
.../trip/places/MPlacesFilterControls.tsx | 10 +++--
.../unit/mobile/trip/MPlacesBrowser.test.tsx | 37 ++++++++++++++++---
3 files changed, 45 insertions(+), 13 deletions(-)
diff --git a/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx b/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx
index fb9fe9252..19da6cdc4 100644
--- a/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx
+++ b/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx
@@ -23,6 +23,7 @@ import MPlacesBulkCategorySheet from './MPlacesBulkCategorySheet'
import MPlacesSaveToCollectionSheet from './MPlacesSaveToCollectionSheet'
import { filterPool, firstPlannedDayNumbers, plannedPlaceIds } from './placesBrowserModel'
import { MCategoryFilterList, MRatingFloorChips, SquareCheck } from './MPlacesFilterControls'
+import { countActivePlacesFilters } from '../../../../utils/placesFilter'
/**
* Fullscreen places pool (mode === 'browse'): All/Unplanned/Tracks filter
@@ -142,8 +143,10 @@ export default function MPlacesBrowser({ planner, shell }: MPlacesBrowserProps)
shell.openSheet('bract', { placeId: place.id, dayPicker: false })
}
- // The panel holds the categories and the rating floor; the badge counts both.
- const panelFilterCount = categoryFilters.size + (ratingFilter === 'all' ? 0 : 1)
+ // The panel holds the categories and the rating floor; its badge counts them the
+ // way the map's Filters badge does (one per kind of filter). The pool is left out:
+ // its chips sit in plain sight above the panel.
+ const panelFilterCount = countActivePlacesFilters({ filter: 'all', categoryFilters, ratingFilter })
return (
@@ -196,7 +199,7 @@ export default function MPlacesBrowser({ planner, shell }: MPlacesBrowserProps)
type="button"
onClick={() => setCatOpen(v => !v)}
aria-expanded={catOpen}
- aria-label={t('places.allCategories')}
+ aria-label={t('places.filters')}
className="relative flex w-[42px] flex-none items-center justify-center rounded-full border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] text-m-muted"
>
@@ -289,7 +292,7 @@ export default function MPlacesBrowser({ planner, shell }: MPlacesBrowserProps)
{catOpen && (
-
+ setSelectedIds(new Set())} />
diff --git a/client/src/mobile/screens/trip/places/MPlacesFilterControls.tsx b/client/src/mobile/screens/trip/places/MPlacesFilterControls.tsx
index e9835b2f4..320e4dc78 100644
--- a/client/src/mobile/screens/trip/places/MPlacesFilterControls.tsx
+++ b/client/src/mobile/screens/trip/places/MPlacesFilterControls.tsx
@@ -86,15 +86,19 @@ export function MCategoryFilterList({ categories, places }: { categories: Catego
)
}
-/** A minimum of stars, the same floors as the desktop list and the collections bar (#1435). */
-export function MRatingFloorChips() {
+/**
+ * A minimum of stars, the same floors as the desktop list and the collections bar
+ * (#1435). `onPick` runs after a floor is chosen: the places browser drops its
+ * selection then, as it does for a new pool.
+ */
+export function MRatingFloorChips({ onPick }: { onPick?: () => void } = {}) {
const { t } = useTranslation()
const ratingFilter = useTripStore(s => s.placesRatingFilter)
const setRatingFilter = useTripStore(s => s.setPlacesRatingFilter)
return (
{RATING_FLOORS.map(floor => (
- setRatingFilter(floor)}>
+ { setRatingFilter(floor); onPick?.() }}>
{floor === 'all' ? t('common.all') : (
<>
diff --git a/client/tests/unit/mobile/trip/MPlacesBrowser.test.tsx b/client/tests/unit/mobile/trip/MPlacesBrowser.test.tsx
index 55ff3ec67..d13024ada 100644
--- a/client/tests/unit/mobile/trip/MPlacesBrowser.test.tsx
+++ b/client/tests/unit/mobile/trip/MPlacesBrowser.test.tsx
@@ -10,7 +10,7 @@ import { resetAllStores, seedStore } from '../../../helpers/store'
import type { MTripShellApi, TripPlanner } from '../../../../src/mobile/screens/trip/MTripShell'
import type { AssignmentsMap, Category, Day, Place } from '../../../../src/types'
-// FE-MOB-PBROW-001 to FE-MOB-PBROW-034
+// FE-MOB-PBROW-001 to FE-MOB-PBROW-036
const CATEGORIES = [
{ id: 1, name: 'Sights', color: '#123456', icon: 'landmark' },
@@ -152,7 +152,7 @@ describe('MPlacesBrowser', () => {
it('FE-MOB-PBROW-010: the category panel toggles the shared filter set and badges its size', () => {
renderBrowser()
- const catBtn = screen.getByRole('button', { name: 'places.allCategories' })
+ const catBtn = screen.getByRole('button', { name: 'places.filters' })
expect(catBtn).toHaveAttribute('aria-expanded', 'false')
fireEvent.click(catBtn)
expect(catBtn).toHaveAttribute('aria-expanded', 'true')
@@ -173,7 +173,7 @@ describe('MPlacesBrowser', () => {
places: [{ ...LOUVRE, rating_avg: 4.5 }, { ...EIFFEL, rating_avg: 3 }, SEINE],
} as Partial)
renderBrowser(planner)
- const panelBtn = screen.getByRole('button', { name: 'places.allCategories' })
+ const panelBtn = screen.getByRole('button', { name: 'places.filters' })
fireEvent.click(panelBtn)
const floors = screen.getByRole('group', { name: 'Filter by rating' })
fireEvent.click(within(floors).getByRole('button', { name: '4+' }))
@@ -189,6 +189,31 @@ describe('MPlacesBrowser', () => {
expect(screen.getByText('Seine Track')).toBeInTheDocument()
})
+ it('FE-MOB-PBROW-035: the panel badge counts kinds of filter, like the map badge', () => {
+ renderBrowser()
+ const panelBtn = screen.getByRole('button', { name: 'places.filters' })
+ fireEvent.click(panelBtn)
+ fireEvent.click(screen.getByRole('checkbox', { name: 'Sights' }))
+ fireEvent.click(screen.getByRole('checkbox', { name: 'Food' }))
+ // Two categories are one kind of filter.
+ expect(panelBtn).toHaveTextContent('1')
+ fireEvent.click(within(screen.getByRole('group', { name: 'Filter by rating' })).getByRole('button', { name: '4+' }))
+ expect(panelBtn).toHaveTextContent('2')
+ // The pool has its own chips in plain sight, so the panel badge leaves it out.
+ fireEvent.click(screen.getByRole('button', { name: 'places.unplanned' }))
+ expect(panelBtn).toHaveTextContent('2')
+ })
+
+ it('FE-MOB-PBROW-036: a new rating floor starts a fresh selection, as a new pool does', () => {
+ renderBrowser()
+ fireEvent.click(screen.getByRole('button', { name: 'common.select' }))
+ fireEvent.click(row('Louvre'))
+ expect(screen.getByText('places.selectionCount:1')).toBeInTheDocument()
+ fireEvent.click(screen.getByRole('button', { name: 'places.filters' }))
+ fireEvent.click(within(screen.getByRole('group', { name: 'Filter by rating' })).getByRole('button', { name: '1+' }))
+ expect(screen.queryByText('places.selectionCount:1')).not.toBeInTheDocument()
+ })
+
it('FE-MOB-PBROW-033: a floor set from the map sheet already thins the list on arrival', () => {
seedStore(useTripStore, { placesRatingFilter: 5 })
renderBrowser(makePlanner({ places: [{ ...LOUVRE, rating_avg: 5 }, EIFFEL] } as Partial))
@@ -198,7 +223,7 @@ describe('MPlacesBrowser', () => {
it('FE-MOB-PBROW-011: the uncategorized row filters the places without a category', () => {
renderBrowser()
- fireEvent.click(screen.getByRole('button', { name: 'places.allCategories' }))
+ fireEvent.click(screen.getByRole('button', { name: 'places.filters' }))
fireEvent.click(screen.getByRole('checkbox', { name: 'No Category' }))
expect([...useTripStore.getState().placesCategoryFilter]).toEqual(['uncategorized'])
expect(screen.getByText('Eiffel Tower')).toBeInTheDocument()
@@ -208,7 +233,7 @@ describe('MPlacesBrowser', () => {
it('FE-MOB-PBROW-012: the uncategorized row is dropped when every place has a category', () => {
const planner = makePlanner({ places: [LOUVRE, SEINE] } as Partial)
renderBrowser(planner)
- fireEvent.click(screen.getByRole('button', { name: 'places.allCategories' }))
+ fireEvent.click(screen.getByRole('button', { name: 'places.filters' }))
expect(screen.queryByRole('checkbox', { name: 'No Category' })).not.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'Food' })).toBeInTheDocument()
})
@@ -433,7 +458,7 @@ describe('MPlacesBrowser', () => {
// Narrow to the two categorised places: two visible, two selected — but not
// the same two, so the toolbar must still offer select-all.
- fireEvent.click(screen.getByRole('button', { name: 'places.allCategories' }))
+ fireEvent.click(screen.getByRole('button', { name: 'places.filters' }))
fireEvent.click(screen.getByRole('checkbox', { name: 'Sights' }))
fireEvent.click(screen.getByRole('checkbox', { name: 'Food' }))