feat(search): school type groups and a faith filter #170
No files matched your search
@@ -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(<FilterBar filters={filters} />);
|
||||
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(<FilterBar filters={filters} />);
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(<HomeView initialSchools={results()} filters={filters} />);
|
||||
const chips = container.querySelector('[class*="activeFilters"]')!;
|
||||
expect(chips).toHaveTextContent('State school: council-run');
|
||||
expect(chips).not.toHaveTextContent(/\bcouncil\b(?!-run)/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<svg
|
||||
@@ -444,7 +465,7 @@ export function FilterBar({
|
||||
const labelled = { school_type: typeOptions, faith: faithOptions }[
|
||||
key as "school_type" | "faith"
|
||||
];
|
||||
if (labelled) return labelled.find((o) => 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<Record<FilterKey, string[]>> = {
|
||||
@@ -490,16 +511,22 @@ export function FilterBar({
|
||||
</SelectShell>
|
||||
);
|
||||
|
||||
const typeSelected = selectedOf(typeOptions, currentType);
|
||||
const faithSelected = selectedOf(faithOptions, currentFaith);
|
||||
|
||||
const typeSelect = (look: Look) => (
|
||||
<SelectShell wide={look !== "pill"}>
|
||||
<select
|
||||
value={currentType}
|
||||
value={typeSelected.value}
|
||||
onChange={(e) => handleFilterChange("school_type", e.target.value)}
|
||||
className={selectClass(look, currentType)}
|
||||
aria-label="School type"
|
||||
disabled={isPending && look !== "sheet"}
|
||||
>
|
||||
<option value="">Any school type</option>
|
||||
{typeSelected.unlisted && (
|
||||
<option value={typeSelected.unlisted}>{typeSelected.unlisted}</option>
|
||||
)}
|
||||
{typeOptions.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
@@ -531,13 +558,16 @@ export function FilterBar({
|
||||
const faithSelect = (look: Look) => (
|
||||
<SelectShell wide>
|
||||
<select
|
||||
value={currentFaith}
|
||||
value={faithSelected.value}
|
||||
onChange={(e) => handleFilterChange("faith", e.target.value)}
|
||||
className={selectClass(look, currentFaith)}
|
||||
aria-label="Faith"
|
||||
disabled={isPending && look !== "sheet"}
|
||||
>
|
||||
<option value="">Any faith or none</option>
|
||||
{faithSelected.unlisted && (
|
||||
<option value={faithSelected.unlisted}>{faithSelected.unlisted}</option>
|
||||
)}
|
||||
{faithOptions.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
|
||||
@@ -292,6 +292,11 @@ export function HomeView({ initialSchools, filters, totalSchools, howItWorks, ed
|
||||
const hasSearch = searchParams.get('search') || searchParams.get('postcode');
|
||||
const isLocationSearch = !!searchParams.get('postcode');
|
||||
const isSearchActive = !!(hasSearch || searchParams.get('local_authority') || searchParams.get('school_type'));
|
||||
// The URL carries a type group's key; the chip reads its label. An old link's
|
||||
// raw GIAS type reads as itself.
|
||||
const schoolTypeParam = searchParams.get('school_type');
|
||||
const schoolTypeChip = (schoolTypeParam && filters.school_type_groups?.find(
|
||||
(o) => o.value.toLowerCase() === schoolTypeParam.toLowerCase())?.label) || schoolTypeParam;
|
||||
const currentPhase = searchParams.get('phase') || '';
|
||||
const secondaryCount = allSchools.filter(s => s.attainment_8_score != null).length;
|
||||
const primaryCount = allSchools.filter(s => s.rwm_expected_pct != null).length;
|
||||
@@ -811,7 +816,7 @@ export function HomeView({ initialSchools, filters, totalSchools, howItWorks, ed
|
||||
<div className={styles.activeFilters}>
|
||||
{searchParams.get('search') && <span className={styles.filterChip}>Search: {searchParams.get('search')}<a href="/" className={styles.chipRemove} onClick={e => { e.preventDefault(); }}>×</a></span>}
|
||||
{searchParams.get('local_authority') && <span className={styles.filterChip}>{searchParams.get('local_authority')}</span>}
|
||||
{searchParams.get('school_type') && <span className={styles.filterChip}>{searchParams.get('school_type')}</span>}
|
||||
{schoolTypeParam && <span className={styles.filterChip}>{schoolTypeChip}</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user