Implement mentor info popup card widget
epic-geographic-peer-mentor-map-ui-accessibility-task-007 — Build the MentorInfoPopup widget that renders when a map marker is tapped. The popup must load instantly from already-fetched BLoC state with no additional network call. Display mentor name, availability, specialisations, and a deep-link button to the peer mentor detail screen. Implement as an accessible modal overlay with focus trap, close button with semantic label, and VoiceOver/TalkBack announcement on open.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 4 - 323 tasks
Can start after Tier 3 completes
Implementation Notes
Do not use showModalBottomSheet for this popup — it pushes a new route onto the Navigator stack, which would require the user to navigate back rather than tap outside to dismiss, and complicates focus management. Instead, render the popup as an AnimatedPositioned child inside the MapViewScreen Stack, toggled by the BLoC's selectedMentorId field. Use SemanticsService.announce(text, TextDirection.ltr) in didUpdateWidget or in a post-frame callback when the popup becomes visible — this is the correct way to trigger a VoiceOver/TalkBack announcement without relying on AutomaticKeepAlive or focus changes. For the focus trap, use a FocusTraversalGroup with OrderedTraversalPolicy containing exactly two FocusNodes: closeButton and viewProfileButton.
The Back button / swipe-down dismiss can be handled with a WillPopScope (Flutter <3.0) or PopScope (Flutter ≥3.0) that emits a PopupDismissed event to the BLoC instead of popping the route. Specialisation overflow: show the first 4 chips and a '+N more' text label for counts above 4 — no expandable section needed in this popup.
Testing Requirements
Unit tests: verify no Supabase or BLoC query is triggered on popup open; verify 'View profile' button constructs the correct GoRouter route with mentorId. Widget tests: golden tests for popup with 1, 3, and 6 specialisations (overflow case); verify Semantics announcement on open via SemanticsController; verify focus trap — simulate Tab key presses and assert focus does not escape the popup; verify close button Semantics label; verify popup dismisses on outside tap and focus returns to expected target. Integration tests: open popup, tap 'View profile', assert navigation to correct peer mentor detail route. Accessibility test: verify announcement text matches expected format.
Coverage target: 100% of popup open/close and navigation logic.
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.
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.
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.