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' }))