EPAC-1940 Calendar scrolls to Jan 2001 on Parliament tab re-tap; add prominent Today action and de-clutter toolbar
Context / background
On the Parliament tab's sitting calendar (SittingCalendarView), tapping the Parliament tab item while it is already the active tab scrolls the calendar all the way up to January 2001 - the start of the configured visible date range. There is no parliamentary data before 2016 (the year picker's lower bound is hard-coded to 2016 at SittingCalendarView.swift:204), so the user lands in an empty pre-history with no obvious way back.
There is a hidden "tap to scroll to today" affordance on the legend dot at SittingCalendarView.swift:127-133, but it reads as a legend swatch, not as a control. The accessibility label "Today - tap to scroll to current month" confirms it is meant as a button, but visually it is indistinguishable from the "Upcoming" legend swatch next to it.
Adjacent concern flagged by user: the Parliament toolbar and the Members toolbar are getting crowded - this issue should also consolidate.
Root cause analysis
Confirmed root cause: standard iOS UIScrollView.scrollsToTop behavior, applied to a HorizonCalendar CalendarScrollView whose content range begins at 2001-01-01.
Evidence:
- HorizonCalendar's scroll view is a
UIScrollViewwith default settings.CalendarScrollView: UIScrollViewatios/.build/SourcePackages/checkouts/HorizonCalendar/Sources/Internal/CalendarScrollView.swift:24. It does not overridescrollsToTop, so it inherits the defaulttrue. - iOS tab re-tap triggers
scrollsToTop. When the user taps aTabView's currently-active tab item, UIKit walks the active tab's view hierarchy and asks the first eligible scroll view withscrollsToTop == trueto scroll itscontentOffsetto.zero. This is the same mechanism as the legacy status-bar tap. contentOffset.zerofor this calendar is January 2001.visibleDatesis configured from2001-01-01through2026-12-31at SittingCalendarView.swift:30 withmonthsLayout: .vertical. So "top" of the scroll view is January 2001 - the earliest month in the range.- Year picker bound differs from calendar bound. The chevron year picker is clamped to 2016+ (SittingCalendarView.swift:194-204), so even when the user lands in 2001 the picker cannot take them back through chevrons. The only existing escape is the unobvious "Today" legend dot.
- Tab re-tap intent is correct; the calendar's response is wrong. Double-tapping a tab bar item to scroll to top is standard iOS behavior we should not fight system-wide - but for this scroll view it produces a non-actionable state. We do not gain anything from preserving "scroll to start of 25-year range," and we lose discoverability.
Rejected candidates (with reasons):
- NavigationStack popping to root - the calendar is the root of
parliamentStack(ContentView.swift:464-481); there is no parent to pop to. The popping happens, and is correct, on subsequent taps, but it is not what scrolls the calendar to 2001. task(id: viewModel.currentYear)re-firing - the initial-load task at SittingCalendarView.swift:153-160 does scroll to today, but onlyif viewModel.dates.isEmpty. On subsequent tab visits the dates are cached, so the task body is a no-op. Not the cause.- HorizonCalendar's own scroll restoration -
CalendarViewProxydoes not register an automatic scroll-to-top hook with the tab bar. The behavior comes from UIKit's traversal, not HorizonCalendar.
Confidence: high. The mechanism is well-documented UIKit behavior, the source file confirms CalendarScrollView does not opt out, and the date-range arithmetic explains why "top" is January 2001 specifically.
Acceptance criteria
- [ ] Given the user is on the Parliament tab with the calendar visible at any month, when they tap the Parliament tab bar item (re-selecting the active tab), then the calendar does not scroll to January 2001.
- [ ] Given the user is anywhere in the Parliament calendar's date range, when they tap a prominent "Today" affordance, then the calendar scrolls/animates to the month containing today and centers today's date if possible.
- [ ] The "Today" affordance must be visible from the calendar's default state without requiring the user to scroll, open a menu, or recognize the legend dot as a button. Pick one placement and remove the legend-dot tap target so there is only one canonical Today action: prefer a toolbar item (label
"Today", system imagecalendar.circleor similar), OR a sticky bottom-bar pill above the safe-area inset. Author decides; both meet AC. - [ ] The "Today" control is hidden or disabled when today's date is already inside the currently visible month range (avoid noise when there is nothing to do).
- [ ] Parliament toolbar is de-cluttered. Today's top-bar contains: year-picker (principal), refresh, calendar-export menu, Order Paper link, Committees link, Ontario Debates link - 6 distinct items plus a 3-item principal cluster. Consolidate by either (a) grouping Order Paper / Committees / Ontario Debates into a single
Menu(ellipsis orline.3.horizontal) with one toolbar slot, or (b) moving Committees + Ontario Debates into theAccountability/Membershubs they thematically belong to (see ADR-001 inCLAUDE.md). Author decides; document the choice in the PR description. - [ ] Members toolbar (
MembersView.swift:45-118) is also de-cluttered. Five sequential filter menus + a clear button in oneToolbarItemGroupis too dense. Consolidate by either (a) combiningParty,Province,Status,Cabinetinto a single "Filters" menu that expands into pickers, with the active-filter count rendered as a badge on the filter icon, or (b) moving filters into a sheet/bottom-bar surfaced by one toolbar button. Author decides. - [ ] The fix does not change scroll-to-top behavior on tabs whose primary content is conventionally oriented (Home, Members list, Accountability, Search). Specifically: do not blanket-disable
scrollsToTopacross the app; scope the change to the calendar scroll view only. - [ ] On tab re-tap of Parliament, if the calendar is not currently showing today, the system can optionally scroll to today (a reasonable user expectation given "tab re-tap = go to a sensible home"). If implemented, document the choice in the PR description; if not, do nothing and rely on the explicit Today button. Either is acceptable.
- [ ] Existing snapshot / unit tests in
SittingCalendarViewModelTestscontinue to pass; new test added:SittingCalendarViewdoes not opt the underlying scroll view intoscrollsToTop. - [ ] Manual verification steps below pass on iPhone simulator (iOS 17 baseline, iOS 18+ acceptable) and on iPad.
Implementation approach (non-prescriptive)
Two viable mechanisms - pick the one with fewer side effects after spike-checking both:
Option A - opt the calendar scroll view out of scrollsToTop. HorizonCalendar does not currently expose a public modifier to set scrollsToTop. Either:
- Submit a small upstream PR to
airbnb/HorizonCalendaradding.scrollsToTop(_:)onCalendarViewRepresentable(preferred long-term), or - Walk the view hierarchy from a
UIViewRepresentableshim inserted alongside the calendar to find theCalendarScrollViewand setscrollsToTop = false. This is fragile but localized - keep it in one helper file with a comment pointing at this issue.
Option B - intercept the tab re-tap. Add a UITabBarControllerDelegate (via UITabBarController.appearance() is not enough; need a real introspection layer) that, when the user re-selects the Parliament tab, runs calendarViewProxy.scrollToMonth(containing: .now, ...) instead of letting the system scroll to .zero. iOS 18 ships a tabViewSelection API that makes this cleaner.
Recommended: Option A, scoped to the calendar view only. It is the smaller change and does not reach across the app. If Option A is infeasible without an upstream change, Option B is the fallback.
External validation gates
- [ ] None - this is a pure iOS UI fix verifiable from repo changes, simulator, and a real device. No App Store Connect, legal, or product sign-off needed.
Out of scope
- Changing the calendar's date range lower bound (the data backstop is 2016, but HorizonCalendar's range stays 2001+ as a buffer). Not touching this here.
- Removing the legend "Sitting days / Today / Upcoming" row entirely. The colors-legend stays; only its dual-purpose tap behavior is removed once the explicit Today control replaces it.
- Reworking the iPad sidebar /
parliamentStacknavigation flow. - Backend or data pipeline work.
- Any change to
HomeFeedView,AccountabilityHubView, orSearchView.
Inputs / dependencies
- iOS source:
ios/epac/Views/Calendar/SittingCalendarView.swift,ios/epac/Views/ContentView.swift,ios/epac/Views/Members/MembersView.swift. - HorizonCalendar SDK source (read-only):
ios/.build/SourcePackages/checkouts/HorizonCalendar/Sources/. Pinned version comes fromios/epac.xcodeprojSPM resolution. - Existing
CalendarViewProxyAPI:scrollToMonth(containing:scrollPosition:animated:)already used at SittingCalendarView.swift:131 and:157. - Tab structure:
ContentView.phoneLayoutandiPadLayoutinios/epac/Views/ContentView.swift.
Risks / notes for implementer
- Do not blanket-disable
scrollsToTopapp-wide. Status-bar / tab re-tap scroll-to-top is correct behavior on every other scroll view in the app. Scope strictly to the calendar. CalendarScrollViewis internal to HorizonCalendar. If walking the view hierarchy viaUIViewRepresentableintrospection, do it in a single helper file and add a// swiftlint:disable:next ... - reasoncomment if needed. Add a test that fails loudly if HorizonCalendar's internal class name changes on a future package update.- iPad sidebar selection is not tab re-tap. iPad uses
NavigationSplitViewwithrouter.selectedTabbindings (ContentView.swift:195-228). The scroll-to-top bug only manifests on phone (compact). Verify iPad is not accidentally regressed. - Mac Catalyst. The
MacCommandCenteralready callsviewModel.onSelectedDateChangedon refresh (ContentView.swift:256-265). Nothing to change here, but verify on Catalyst. - Year picker chevrons independently scroll to the target year's January via
calendarViewProxy.scrollToMonth(containing: jan, ...)(SittingCalendarView.swift:196-198). Do not break this when restructuring the toolbar.
Definition of Done
- Re-tapping the Parliament tab item no longer scrolls the calendar to January 2001 on iPhone and iPad simulators.
- A single, visually obvious "Today" affordance returns the calendar to today's month from any point in the range.
- Parliament and Members toolbars are visibly less crowded (count of top-bar items reduced; document the count delta in the PR description).
- New scroll-behavior test added; existing tests pass; SwiftLint
--strictpasses;make buildandmake simulatorsucceed. - PR includes before/after screenshots of:
- Parliament tab re-tap behavior (recorded as a short GIF or two screenshots).
- Parliament toolbar before and after.
- Members toolbar before and after.
Release-Note:line added (e.g.Release-Note: Added a Today button to the Parliament calendar and stopped tab re-tap from scrolling to 2001).
Architecture Impact
- Affected layer: presentation only (Views/Calendar, Views/Members, possibly a tiny UIKit-bridge helper).
- Dependency direction unchanged: View ->
CalendarViewProxy(already in place). - No new ports, no schema/migration impact, no backend change.
- Test strategy: ViewModel layer unchanged (no new ViewModel tests). Add one View-layer integration check that asserts the calendar's scroll view has
scrollsToTop == falseafter first appearance.
Mergeability / change ownership
- Single reason to change: fix calendar tab-re-tap scroll + reduce Parliament/Members toolbar density.
- Primary code owner:
ios/epac/Views/Calendar/SittingCalendarView.swift(and ViewModels owner per CODEOWNERS). - Hot files:
SittingCalendarView.swift,MembersView.swift, possibly a newUtil/CalendarScrollBehavior.swifthelper. - Sibling issues likely to touch the same area: none currently. Confirm at pickup by scanning open EPAC PRs touching
Views/Calendar/orViews/Members/. - Conflict risk: low - narrow surface, no model or migration changes.
- Sequencing lane:
parallel.
Clean Architecture Shape
N/A - UI affordance + scroll-view configuration change. No new use case, no port/adapter boundary, no domain entity touched. Catalog update: not required.
Manual reproduction steps
cd ios && SIM_UUID=<booted-sim-uuid> make simulator(or run from Xcode against an iPhone simulator).- App opens at Home.
- Tap Parliament tab. Calendar should show current month.
- Tap the Parliament tab item again.
- Observe: calendar instantly jumps to January 2001 with no green sitting cells, no obvious affordance to return. (Year picker chevron-left is disabled at 2016; chevron-right works but takes many taps.)
- The only escape before this fix is the tiny red "Today" legend dot near the bottom - tap it to return.
Expected after fix: re-tapping Parliament does not move the calendar (or, if implementing the optional scroll-to-today on re-tap, scrolls to today's month). A prominent toolbar or bottom-bar "Today" action returns the user to today from anywhere.
Resolves the user-reported bug filed against the Parliament calendar screen on 2026-05-18.