critical priority medium complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

MentorListFallback renders a scrollable list of all mentors matching the current BLoC filter state
Each list item displays mentor display name, availability status badge (available/busy), and a tappable affordance navigating to the peer mentor detail screen
VoiceOver (iOS) and TalkBack (Android) correctly read each list item as a single semantic unit including name and availability status without the user needing to navigate into sub-elements
Focus order is top-to-bottom, item by item; no interactive elements inside a list item are skipped
When filter results change, a live region announces 'Showing [N] mentors' within 500ms of the state update (using SemanticsService.announce or equivalent)
Empty state renders a labelled message 'No mentors match your current filters' readable by screen readers
All list item touch targets meet the 48dp minimum height requirement per WCAG 2.2 AA
List items are visually distinguishable with sufficient colour contrast (≥ 4.5:1 for normal text, ≥ 3:1 for large text) using design tokens only
The widget correctly handles lists of 0, 1, and 100+ mentors without layout overflow or performance degradation
Tapping a list item navigates to the peer mentor detail screen (same behaviour as popup deep-link in task-008)

Technical Requirements

frameworks
Flutter
BLoC
apis
Supabase REST API (filtered mentor list)
data models
PeerMentor
AvailabilityStatus
FilterState
performance requirements
List must render 100 items without jank using ListView.builder (not ListView with fixed children)
Filter result announcement must fire within 500ms of BLoC state emission
Initial list build must complete within 200ms for up to 50 mentors
security requirements
Only display mentor data the current user is authorised to see per Supabase row-level security policies
No PII beyond display name and availability shown in list view
ui components
MentorListFallback (244-mentor-list-fallback)
MentorListItem (new sub-widget)
AvailabilityBadge
EmptyStateWidget

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use BlocBuilder to rebuild the list from MapBLoC (or equivalent FilterBloc) state. Use ListView.builder for performance. Each list item should use MergeSemantics to combine child semantics into a single readable unit for screen readers. For the live region, call SemanticsService.announce('Showing $count mentors', TextDirection.ltr) inside the BlocListener's state change handler.

Use design tokens from the project's token system for all colours and spacing — do not hard-code values. The availability badge colour must use design tokens with sufficient contrast against the card background. Wrap each item in a Semantics widget with onTapHint: 'View profile' to give screen reader users action context.

Testing Requirements

Write widget tests for MentorListFallback covering: correct item count from BLoC state, empty state rendering, navigation on item tap, and live region text. Use tester.getSemantics() to assert each item's merged semantics label includes name and availability. Write a BLoC unit test confirming the widget reacts to FilterChanged events. Run accessibility audit using flutter_test's SemanticsHandle.

Test with a list of 0, 1, 10, and 100 items. Verify 48dp touch targets with tester.getSize().

Component
Mentor List Fallback View
ui low
Epic Risks (3)
high impact high prob technical

Flutter's map canvas (flutter_map) does not natively support semantic focus traversal for screen readers, meaning map markers may be entirely invisible to VoiceOver/TalkBack users. If the accessible list fallback is not treated as a first-class view, screen reader users will have no access to the feature.

Mitigation & Contingency

Mitigation: From sprint 1, treat mentor-list-fallback as a fully featured primary view, not an afterthought. Implement and test it in parallel with the map canvas. Make the view-toggle-button keyboard focusable and announced on every screen state. Conduct VoiceOver testing on device before submitting each PR touching UI components.

Contingency: If map canvas accessibility cannot be achieved for marker focus traversal, make the view-toggle-button the default focus target on screen load for VoiceOver users (detected via screen-reader-detection-service) so they are immediately directed to the list fallback without needing to discover the toggle.

medium impact medium prob technical

The mentor-info-popup must occupy no more than 40% of visible map area on small screens. On devices with screen heights under 667px (iPhone SE), overlapping with the filter panel or obscuring most of the map could severely degrade usability.

Mitigation & Contingency

Mitigation: Implement the popup as a bottom sheet capped at 40% of screen height with a ScrollView for overflow content. Test on iPhone SE (375x667pt) and the smallest commonly used Android form factor in the device lab. Define max-height as a percentage constant in location-privacy-config or design tokens.

Contingency: If the popup cannot fit all required fields within 40% height on smallest targets, truncate assigned contact count and certification badge to icons-only in the compact view, with a 'View Profile' button always visible at the bottom of the popup regardless of scroll position.

medium impact medium prob integration

Filter state must remain perfectly synchronised between the map view and the list fallback. If the filter panel emits state that is not consumed identically by both views, coordinators switching between views will see inconsistent mentor sets, eroding trust in the feature.

Mitigation & Contingency

Mitigation: Store active filter criteria in a single shared Riverpod provider owned by the map-view-screen and consumed by both map-marker-widget (via mentor-location-service) and mentor-list-fallback. Write integration tests that apply a filter, switch views, and assert identical mentor counts in both views.

Contingency: If filter sync proves brittle, simplify to a single filter state object passed explicitly as a constructor argument to both views on each rebuild, eliminating indirect state sharing.