critical priority low complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

PersistentBackButton is a StatelessWidget in lib/widgets/persistent_back_button.dart
On non-root screens, widget renders an Icons.arrow_back icon with a visible 'Back' tooltip
Tap calls context.pop() via GoRouter (not Navigator.pop()) to preserve GoRouter's route state
Widget returns SizedBox.shrink() (invisible, zero-size) when the current route is a shell root (Home, Contacts, Add, Work, Notifications tab roots)
Root detection uses the GoRouter canPop() method on the current GoRouterState to determine visibility — no hardcoded path strings in the widget itself
Touch area is at least 44×44 logical pixels
Semantics wrapper has label: 'Go back' and hint: 'Navigates to the previous screen'
Widget is placed in the leading slot of the AppBar / page header component so it appears consistently top-left on all non-root screens
Widget test on a nested route confirms the back button is rendered and tappable
Widget test on a shell root route confirms SizedBox.shrink() is returned (no back button rendered)
All colors sourced from design token constants — no hardcoded values
Tooltip text reads 'Back' and is visible to sighted users on long-press (standard Flutter tooltip behavior)

Technical Requirements

frameworks
Flutter
go_router
flutter_test
performance requirements
Visibility check (canPop) must be O(1) — reading from GoRouterState, not traversing the route stack
security requirements
context.pop() must not allow back-navigation to screens the user no longer has permission to view — route guards on the destination screen are the enforcement layer, but this widget should not suppress pop() if the guard will handle it
ui components
PersistentBackButton
IconButton
Tooltip
Semantics

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Implement by reading GoRouterState from context: final canPop = GoRouter.of(context).canPop(). If false, return const SizedBox.shrink(). Otherwise render: Semantics(label: 'Go back', hint: 'Navigates to the previous screen', button: true, child: Tooltip(message: 'Back', child: IconButton(iconSize: 24, style: IconButton.styleFrom(minimumSize: const Size(44, 44)), icon: const Icon(Icons.arrow_back), onPressed: () => context.pop()))). The canPop() call requires the widget to be a descendant of a GoRouter — this is always true in the app but must be guaranteed in tests by wrapping with MaterialApp.router(routerConfig: goRouter).

The documentation from the workshops strongly emphasizes 'Tilbakeknapp fremfor sidelengs-sveip' (back button over swipe gestures) and 'Vertikal scroll er normen' — this widget is the direct implementation of that requirement. Ensure that on iOS, the native back swipe gesture is not disabled; PersistentBackButton is an additional affordance, not a replacement. Place this widget in the shared page header component so all screens inherit it automatically rather than adding it manually per screen.

Testing Requirements

Widget tests using flutter_test and go_router test helpers. Test 1: Push a nested route (e.g., /contacts/123) and pump PersistentBackButton — confirm Icons.arrow_back is rendered. Test 2: On the same nested route, tap the button, pumpAndSettle — confirm the route stack has popped back to /contacts. Test 3: Render PersistentBackButton at the /contacts root (shell route) — confirm tester.findByType(IconButton) returns nothing (SizedBox.shrink rendered).

Test 4: Confirm Semantics node has label='Go back' and hint='Navigates to the previous screen'. Test 5: Confirm touch target Rect is ≥44×44dp. Test 6: Confirm tooltip 'Back' appears on long-press via triggerLongPress() and confirm the tooltip widget is in the tree. Use AccessibilityAuditRunner from task-003 as a convenience check once it is available.

Component
Persistent Back 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.