critical priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

Semantics label is 'Close' (not 'Close dialog' — that was the prior placeholder; this task sets the final production value matching VoiceOver/TalkBack conventions)
Semantics hint is 'Activates to dismiss this dialog'
Semantics onTapHint is 'Dismiss dialog' (short, action-oriented string for TalkBack's 'double-tap to [hint]' announcement)
After Navigator.pop() is called, SemanticsService.announce('Dialog dismissed', TextDirection.ltr) is invoked so screen reader users receive explicit confirmation
The announce() call fires after the pop completes — use a post-frame callback (WidgetsBinding.instance.addPostFrameCallback) to avoid announcing while the widget tree is being disposed
Widget test using tester.getSemantics() confirms label, hint, and onTapHint are all set correctly
Manual verification on a device with VoiceOver (iOS) confirms the announcement is spoken after dismiss
Navigator.pop() successfully dismisses ModalBottomSheet, AlertDialog, and showDialog routes in widget tests
The SemanticsService.announce() call is conditional: it only fires when SemanticsBinding.instance.accessibilityFeatures.accessibleNavigation is true, to avoid unnecessary overhead for sighted users
No regression in the touch target size or icon rendering from task-005

Technical Requirements

frameworks
Flutter
flutter_test
Flutter Semantics API (SemanticsService, SemanticsBinding)
performance requirements
SemanticsService.announce() must be called asynchronously post-frame to avoid blocking the dismiss animation
ui components
ModalCloseButton (enhanced)
Semantics
SemanticsService

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

This task modifies lib/widgets/modal_close_button.dart from task-005. Change the Semantics wrapper to: Semantics(label: 'Close', hint: 'Activates to dismiss this dialog', onTapHint: 'Dismiss dialog', button: true, child: ...). For the dismissal announcement, convert ModalCloseButton from StatelessWidget to StatefulWidget only if needed for context access — but prefer keeping it stateless by passing context through the onPressed closure. The dismiss action becomes: () { final nav = Navigator.of(context); nav.pop(); if (SemanticsBinding.instance.accessibilityFeatures.accessibleNavigation) { WidgetsBinding.instance.addPostFrameCallback((_) { SemanticsService.announce('Dialog dismissed', TextDirection.ltr); }); } }.

Important: SemanticsService.announce() interrupts the current screen reader focus chain, which is the desired behavior here (user needs to know the dialog is gone). Test on real iOS hardware with VoiceOver to validate timing — simulator semantics behavior can differ. Note that the Semantics label change from 'Close dialog' (task-005) to 'Close' here aligns with Apple and Google HIG: the label names the element, the hint describes the action. Confirm with the design/accessibility team that 'Close' is the approved final label before merging.

Testing Requirements

Widget tests using flutter_test. Test 1: Pump ModalCloseButton, call tester.getSemantics(find.byType(ModalCloseButton)), assert SemanticsData has label='Close', hint='Activates to dismiss this dialog', onTapHint='Dismiss dialog'. Test 2: Pump ModalCloseButton inside a showDialog route, tap button, pumpAndSettle — confirm the dialog route is no longer in the Navigator stack. Test 3: Pump inside a showModalBottomSheet — same pop assertion.

Test 4: Enable accessibility via tester.binding.defaultBinaryMessenger overrides (or mock SemanticsBinding) to confirm SemanticsService.announce is called after tap when accessibleNavigation is true. Test 5: With accessibleNavigation false, confirm announce() is NOT called. Use mockito or manual bool flag spy for the announce assertion.

Component
Modal Close Button
ui low
Epic Risks (3)
high impact high prob technical

Flutter's ModalBottomSheet and showDialog do not automatically confine VoiceOver or TalkBack focus to the modal's subtree on all platform versions. Background content may remain reachable by screen readers, confusing users and violating WCAG 2.2 criterion 1.3.1.

Mitigation & Contingency

Mitigation: Wrap modal content in an ExcludeSemantics or BlockSemantics widget for background content. Use a Semantics node with liveRegion on the modal container and manually request focus via FocusScope after the modal animation completes. Test on both iOS (VoiceOver) and Android (TalkBack) during widget development.

Contingency: If platform-level focus trapping is unreliable, implement a custom modal wrapper widget that uses a FocusTrap widget (available in Flutter's internal tooling) and an Overlay entry with semantics blocking on the dimmed background layer.

medium impact medium prob technical

On iOS, the system-level swipe-back gesture (UINavigationController) can bypass PopScope and GoRouter's gesture suppression, meaning users can still accidentally dismiss screens via swipe even after the component is implemented. This breaks the gesture-free contract for motor-impaired users.

Mitigation & Contingency

Mitigation: Set popGestureEnabled: false in GoRouter route configurations where swipe-back is suppressed. Test specifically against Flutter's CupertinoPageRoute, which respects this flag, and verify that GoRouter generates Cupertino routes on iOS rather than Material routes with gesture enabled.

Contingency: If go_router's popGestureEnabled flag does not propagate correctly, wrap affected routes in a WillPopScope replacement (PopScope with canPop: false) and file a bug with the go_router maintainers. Document the workaround in the navigation-route-config component for future maintainers.

medium impact medium prob scope

The feature description implies migrating all existing ModalBottomSheet and dialog call sites across the app to use the new accessible helpers, which is a cross-cutting change. Scope underestimation could mean the epic finishes the new components but leaves many call sites un-migrated, leaving the accessibility promise partially broken.

Mitigation & Contingency

Mitigation: Audit all existing modal call sites at the start of the epic (grep for showModalBottomSheet, showDialog, showCupertinoDialog) and add the count to the task list. Treat migration as explicit tasks, not an implied post-step.

Contingency: If migration scope grows beyond the epic's estimate, create a follow-up tech-debt epic scoped only to call-site migration, and gate the release on at minimum all flows used by the accessibility user-story acceptance criteria being migrated.