diff --git a/nextjs-app/__tests__/components/FilterBarTypeFaith.test.tsx b/nextjs-app/__tests__/components/FilterBarTypeFaith.test.tsx index f03d1b9..0a82705 100644 --- a/nextjs-app/__tests__/components/FilterBarTypeFaith.test.tsx +++ b/nextjs-app/__tests__/components/FilterBarTypeFaith.test.tsx @@ -112,3 +112,27 @@ describe('Faith', () => { expect(within(openSheet()).queryByRole('combobox', { name: 'Faith' })).not.toBeInTheDocument(); }); }); + +describe('a URL value the options do not spell the same way', () => { + const row = () => screen.getByRole('group', { name: 'Filters' }); + + it('shows an old raw-label type in the select, and lets "Any" clear it', () => { + params = new URLSearchParams('search=school&school_type=Community+school'); + render(); + const type = within(row()).getByRole('combobox', { name: 'School type' }); + expect(type).toHaveValue('Community school'); + expect(within(type).getByRole('option', { name: 'Community school' })).toBeInTheDocument(); + fireEvent.change(type, { target: { value: '' } }); + expect(pushedParams().get('school_type')).toBeNull(); + }); + + it('matches a key in another case, in the select and the chip', () => { + params = new URLSearchParams('postcode=SW196AR&radius=1&school_type=Special&faith=Roman_Catholic'); + render(); + expect(within(row()).getByRole('combobox', { name: 'School type' })).toHaveValue('special'); + expect(screen.getByRole('button', { name: 'Remove filter: Special school (SEND)' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Remove filter: Roman Catholic' })).toBeInTheDocument(); + expect(screen.getByRole('combobox', { name: 'Faith' })).toHaveValue('roman_catholic'); + }); +}); + diff --git a/nextjs-app/__tests__/components/ResultsToolbar.test.tsx b/nextjs-app/__tests__/components/ResultsToolbar.test.tsx index bc16678..5a9b92e 100644 --- a/nextjs-app/__tests__/components/ResultsToolbar.test.tsx +++ b/nextjs-app/__tests__/components/ResultsToolbar.test.tsx @@ -166,3 +166,14 @@ describe('the folded search', () => { expect(screen.queryByRole('button', { name: /Edit search/ })).not.toBeInTheDocument(); }); }); + +describe('the results list filter chips', () => { + it('name a school type group by its label, not its key', () => { + params = new URLSearchParams('search=southmead&school_type=council'); + const { container } = render(); + const chips = container.querySelector('[class*="activeFilters"]')!; + expect(chips).toHaveTextContent('State school: council-run'); + expect(chips).not.toHaveTextContent(/\bcouncil\b(?!-run)/); + }); +}); + diff --git a/nextjs-app/components/FilterBar.tsx b/nextjs-app/components/FilterBar.tsx index 6265ac7..0d5a831 100644 --- a/nextjs-app/components/FilterBar.tsx +++ b/nextjs-app/components/FilterBar.tsx @@ -9,7 +9,7 @@ import { useSchoolSuggest } from "@/hooks/useSchoolSuggest"; import { SuggestList, suggestOptionId } from "./SuggestList"; import { FilterSheet, SheetField, RADIUS_OPTIONS, radiusLabel as milesLabel } from "./FilterSheet"; import type { Suggestion } from "@/lib/suggest"; -import type { Filters, ResultFilters } from "@/lib/types"; +import type { FilterOption, Filters, ResultFilters } from "@/lib/types"; import styles from "./FilterBar.module.css"; interface FilterBarProps { @@ -84,6 +84,27 @@ function hasSecondaryFilters(phase: string): boolean { return !(p === "nursery" || (p.includes("primary") && !p.includes("secondary"))); } +/** + * The option a URL value names, ignoring case as the API does. An old link's + * raw GIAS type names none, and is shown as itself. + */ +function optionFor(options: FilterOption[], value: string) { + const v = value.toLowerCase(); + return options.find((o) => o.value.toLowerCase() === v); +} + +/** + * A select's value and, when the URL holds something no option spells, an + * extra option for it, so the select shows what is applied and "Any" can be + * chosen to clear it. Without one, React shows the first option, "Any", + * while the list is still filtered, and choosing it changes nothing. + */ +function selectedOf(options: FilterOption[], value: string) { + if (!value) return { value, unlisted: null }; + const match = optionFor(options, value); + return match ? { value: match.value, unlisted: null } : { value, unlisted: value }; +} + function SlidersIcon() { return ( o.value === value)?.label ?? value; + if (labelled) return optionFor(labelled, value)?.label ?? value; // Phase, gender and admissions values are lowercased option names; the // rest are the names themselves. const named: Partial> = { @@ -490,16 +511,22 @@ export function FilterBar({ ); + const typeSelected = selectedOf(typeOptions, currentType); + const faithSelected = selectedOf(faithOptions, currentFaith); + const typeSelect = (look: Look) => (
{searchParams.get('search') && Search: {searchParams.get('search')} { e.preventDefault(); }}>×} {searchParams.get('local_authority') && {searchParams.get('local_authority')}} - {searchParams.get('school_type') && {searchParams.get('school_type')}} + {schoolTypeParam && {schoolTypeChip}}
)}