On phones the results toolbar's filters were a row you had to swipe, led by More filters. At 390px the row was 804px wide: Phase showed only in part, School type and Clear not at all, and the panel More filters opened held neither Phase nor School type. Distance also showed twice, in the search summary and again as the first chip.
Change (phones, ≤640px only; desktop and tablet unchanged)
One Filters button beside the folded search summary, with a count of every applied filter.
A bottom sheet with every filter. Distance as five segments (0.25–5 mi), then Phase, School type and Local authority, plus Gender, Sixth form and Admissions when they apply. Changes apply at once, as on desktop, so Show N schools just closes the sheet with the real count. Footer has Clear all, which keeps the search.
Applied-filter chips (Primary ✕) on a second line that appears only when something is applied. Tap to remove.
The summary's visible text says B1 1BB · 0.5 mi, so the distance still shows beside the button at 360px (checked on staging with a DOM mockup: 360/390/430 fit, no overflow). Its accessible name keeps the long form.
Modal: role="dialog" + aria-modal + aria-labelledby, a footer that stays visible while the content scrolls, focus moves in and returns to the button that opened it, and z-index 1000 → 2100. The old value sat under the pinned results toolbar (1001), the floating List/Map button (1002) and the comparison toast (2000).
Gender / Sixth form / Admissions selects now have accessible names on desktop too.
Testing
Jest: new FilterSheet.test.tsx (15 cases) and updates to ResultsToolbar.test.tsx. 540/540 pass, typecheck clean, next build passes with DATABASE_URL unset.
E2E: new journey "a phone filters through one sheet, and sees what it applied as chips" at 390px. It covers opening the sheet, setting a phase (which applies straight away), 44px tap targets, the sheet clearing the tab bar, the chip and count, removing the chip, and no horizontal overflow. Run against current staging, it fails as expected at the first step (no Filters button). It can only pass once this is deployed, because the staging e2e gate runs after merge. The phone overflow journey now excludes the chips line rather than the old row.
## Problem
On phones the results toolbar's filters were a row you had to swipe, led by **More filters**. At 390px the row was 804px wide: Phase showed only in part, School type and Clear not at all, and the panel **More filters** opened held neither Phase nor School type. Distance also showed twice, in the search summary and again as the first chip.
## Change (phones, ≤640px only; desktop and tablet unchanged)
- **One Filters button** beside the folded search summary, with a count of *every* applied filter.
- **A bottom sheet with every filter.** Distance as five segments (0.25–5 mi), then Phase, School type and Local authority, plus Gender, Sixth form and Admissions when they apply. Changes apply at once, as on desktop, so **Show N schools** just closes the sheet with the real count. Footer has **Clear all**, which keeps the search.
- **Applied-filter chips** (`Primary ✕`) on a second line that appears only when something is applied. Tap to remove.
- The summary's visible text says `B1 1BB · 0.5 mi`, so the distance still shows beside the button at 360px (checked on staging with a DOM mockup: 360/390/430 fit, no overflow). Its accessible name keeps the long form.
- `Modal`: `role="dialog"` + `aria-modal` + `aria-labelledby`, a `footer` that stays visible while the content scrolls, focus moves in and returns to the button that opened it, and z-index 1000 → 2100. The old value sat *under* the pinned results toolbar (1001), the floating List/Map button (1002) and the comparison toast (2000).
- Gender / Sixth form / Admissions selects now have accessible names on desktop too.
## Testing
- Jest: new `FilterSheet.test.tsx` (15 cases) and updates to `ResultsToolbar.test.tsx`. 540/540 pass, typecheck clean, `next build` passes with `DATABASE_URL` unset.
- E2E: new journey **"a phone filters through one sheet, and sees what it applied as chips"** at 390px. It covers opening the sheet, setting a phase (which applies straight away), 44px tap targets, the sheet clearing the tab bar, the chip and count, removing the chip, and no horizontal overflow. Run against current staging, it fails as expected at the first step (no Filters button). It can only pass once this is deployed, because the staging e2e gate runs after merge. The phone overflow journey now excludes the chips line rather than the old row.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
On phones the results toolbar's filters were a sideways-scrolling row
led by "More filters", so phase showed only in part and school type not
at all, and the panel "More filters" opened held neither of them.
Phones now get a single Filters button beside the folded search summary,
counting every applied filter. It opens a bottom sheet with every filter:
distance as five segments, then phase, school type, local authority and
the secondary-only filters. Changes apply at once, as on desktop, so the
footer's "Show N schools" only closes the sheet. Applied filters show as
removable chips on a second line, which appears only when something is
applied. Desktop and tablet are unchanged.
Modal gains dialog semantics, a pinned footer and focus handling, and
moves above the pinned toolbar, the floating List/Map button and the
comparison toast, which its old z-index sat beneath.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a phone-only Filters button and bottom sheet (built on the shared Modal) holding every filter, plus removable applied-filter chips. It replaces the sideways-scrolling controls row on phones, and the desktop row is unchanged. The Modal also gains focus handling, dialog semantics, a footer slot and a higher z-index. The change looks sound, with tests covering the new behaviour and no production, data or security risks found.
🟡 Minor
nextjs-app/components/FilterSheet.tsx: Every sheet control is disabled while isPending, so a select or radio the user just changed loses keyboard focus when it becomes disabled. Screen-reader and keyboard users may be dropped back to the dialog container after each change. Consider aria-busy, or keeping the controls enabled.
nextjs-app/components/Modal.module.css: The z-index for every Modal rises from 1000 to 2100 and the base .modal now has outline:none. The z-index change is intended, but other modals now cover the comparison toast, so it is worth a quick visual check. The focus outline is removed only on the programmatically focused container, which is fine.
nextjs-app/components/FilterBar.tsx: The sheet is gated only by sheetOpen state, not by viewport. If it is open and the viewport grows past 640px (rotation or resize), the sheet's selects and the desktop row's selects with the same aria-labels are both visible. Minor and an edge case.
## 🤖 AI Code Review (Claude Code)
Adds a phone-only Filters button and bottom sheet (built on the shared Modal) holding every filter, plus removable applied-filter chips. It replaces the sideways-scrolling controls row on phones, and the desktop row is unchanged. The Modal also gains focus handling, dialog semantics, a footer slot and a higher z-index. The change looks sound, with tests covering the new behaviour and no production, data or security risks found.
### 🟡 Minor
- **nextjs-app/components/FilterSheet.tsx**: Every sheet control is disabled while isPending, so a select or radio the user just changed loses keyboard focus when it becomes disabled. Screen-reader and keyboard users may be dropped back to the dialog container after each change. Consider aria-busy, or keeping the controls enabled.
- **nextjs-app/components/Modal.module.css**: The z-index for every Modal rises from 1000 to 2100 and the base .modal now has outline:none. The z-index change is intended, but other modals now cover the comparison toast, so it is worth a quick visual check. The focus outline is removed only on the programmatically focused container, which is fine.
- **nextjs-app/components/FilterBar.tsx**: The sheet is gated only by sheetOpen state, not by viewport. If it is open and the viewport grows past 640px (rotation or resize), the sheet's selects and the desktop row's selects with the same aria-labels are both visible. Minor and an edge case.
The sheet's controls were disabled while a filter change navigated, and
a control disabled under focus drops it to <body>, out of the dialog.
They now stay enabled, with aria-busy on the sheet instead.
The disabling had also been covering a race: updateURL built from
useSearchParams, which only catches up once a navigation lands, so a
second change made before then undid the first. It now builds on the
URL the navigation in flight is heading to.
The sheet also closes if the screen widens past phone width while it is
open, so its selects and the desktop row's are never both showing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the phone's sideways-scrolling filter row with a Filters button, a bottom-sheet modal holding every filter, and removable applied-filter chips. It also extends Modal with dialog semantics, focus management, a footer slot and a higher z-index. The change is well tested and I found no production-breaking issues.
🟡 Minor
nextjs-app/components/FilterBar.tsx: pendingQueryRef is cleared only by an effect keyed on isPending. If a push does not make the transition pending (for example, navigating to an identical URL), the ref keeps its stale value. The next updateURL would then build on that value instead of the real searchParams, for example after back/forward navigation. Clearing the ref when searchParams changes as well would be more robust.
nextjs-app/components/Modal.tsx: The new global z-index of 2100 and the added role="dialog" and aria-modal attributes apply to every existing Modal user. Those other modals should be checked for stacking against toasts and for duplicate dialog roles. The focus-restore cleanup also runs on unmount, so it may focus an element that has since been removed.
## 🤖 AI Code Review (Claude Code)
Replaces the phone's sideways-scrolling filter row with a Filters button, a bottom-sheet modal holding every filter, and removable applied-filter chips. It also extends Modal with dialog semantics, focus management, a footer slot and a higher z-index. The change is well tested and I found no production-breaking issues.
### 🟡 Minor
- **nextjs-app/components/FilterBar.tsx**: pendingQueryRef is cleared only by an effect keyed on isPending. If a push does not make the transition pending (for example, navigating to an identical URL), the ref keeps its stale value. The next updateURL would then build on that value instead of the real searchParams, for example after back/forward navigation. Clearing the ref when searchParams changes as well would be more robust.
- **nextjs-app/components/Modal.tsx**: The new global z-index of 2100 and the added role="dialog" and aria-modal attributes apply to every existing Modal user. Those other modals should be checked for stacking against toasts and for duplicate dialog roles. The focus-restore cleanup also runs on unmount, so it may focus an element that has since been removed.
tudor
merged commit eb13ab0b5e into main2026-10-02 08:51:17 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
On phones the results toolbar's filters were a row you had to swipe, led by More filters. At 390px the row was 804px wide: Phase showed only in part, School type and Clear not at all, and the panel More filters opened held neither Phase nor School type. Distance also showed twice, in the search summary and again as the first chip.
Change (phones, ≤640px only; desktop and tablet unchanged)
Primary ✕) on a second line that appears only when something is applied. Tap to remove.B1 1BB · 0.5 mi, so the distance still shows beside the button at 360px (checked on staging with a DOM mockup: 360/390/430 fit, no overflow). Its accessible name keeps the long form.Modal:role="dialog"+aria-modal+aria-labelledby, afooterthat stays visible while the content scrolls, focus moves in and returns to the button that opened it, and z-index 1000 → 2100. The old value sat under the pinned results toolbar (1001), the floating List/Map button (1002) and the comparison toast (2000).Testing
FilterSheet.test.tsx(15 cases) and updates toResultsToolbar.test.tsx. 540/540 pass, typecheck clean,next buildpasses withDATABASE_URLunset.🤖 Generated with Claude Code
🤖 AI Code Review (Claude Code)
Adds a phone-only Filters button and bottom sheet (built on the shared Modal) holding every filter, plus removable applied-filter chips. It replaces the sideways-scrolling controls row on phones, and the desktop row is unchanged. The Modal also gains focus handling, dialog semantics, a footer slot and a higher z-index. The change looks sound, with tests covering the new behaviour and no production, data or security risks found.
🟡 Minor
🤖 AI Code Review (Claude Code)
Replaces the phone's sideways-scrolling filter row with a Filters button, a bottom-sheet modal holding every filter, and removable applied-filter chips. It also extends Modal with dialog semantics, focus management, a footer slot and a higher z-index. The change is well tested and I found no production-breaking issues.
🟡 Minor