critical priority low complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

AccessibleTouchTargetWrapper is a StatelessWidget that accepts: required Widget child, required VoidCallback onTap, optional VoidCallback? onLongPress, optional String? semanticsLabel, optional double minSize (default 44.0)
The tappable hit area is always at least minSize Γ— minSize logical pixels, regardless of the child's rendered size
GestureDetector uses HitTestBehavior.opaque so the transparent padding area also receives touch events
Child widget is visually centered within the touch target area β€” no layout shift compared to rendering the child without the wrapper
In debug mode, an assert verifies the wrapper's constraints allow at least minSize in both dimensions; provides a helpful error message if a parent constrains it smaller
Semantics node is correctly labeled: uses semanticsLabel if provided, otherwise inherits from child's existing semantics
onLongPress is forwarded to GestureDetector when provided; no callback is registered when null (avoids interfering with scroll gesture disambiguation)
Widget renders correctly in both LTR and RTL layouts
Widget test confirms hit test succeeds at coordinates (2, 2) when child is a 10Γ—10 Icon centered in the 44Γ—44 target
Widget test confirms no overflow when child is larger than minSize

Technical Requirements

frameworks
Flutter
Dart (latest)
flutter_test
apis
GestureDetector (HitTestBehavior.opaque)
ConstrainedBox / SizedBox
Semantics widget
Center widget
performance requirements
Widget tree depth increase must be minimal β€” maximum 3 additional nodes (ConstrainedBox β†’ GestureDetector β†’ Center β†’ child)
No additional renders or repaints introduced beyond what GestureDetector already triggers
security requirements
onTap callback must not be invoked when widget is inside an AbsorbPointer(absorbing: true) β€” relies on Flutter's standard pointer event system, no custom bypass needed
ui components
AccessibleTouchTargetWrapper (the widget itself)
Optional: AccessibleIconButton convenience wrapper that combines AccessibleTouchTargetWrapper + Icon for the common icon button pattern

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Place in lib/core/widgets/accessible_touch_target_wrapper.dart. The implementation is straightforward but has one subtle point: use ConstrainedBox(constraints: BoxConstraints(minWidth: minSize, minHeight: minSize)) as the outermost wrapper, then GestureDetector with HitTestBehavior.opaque, then Center, then child. The Center widget is important β€” without it, a small child (e.g., a 16px icon) would align to the top-left of the 44Γ—44 area, causing a visual regression. Do NOT use SizedBox with fixed dimensions because that would cap the size at 44Γ—44 even if the child is legitimately larger (e.g., a wide text button).

ConstrainedBox with minWidth/minHeight correctly allows the widget to grow beyond 44Γ—44 when the child demands it. For the debug assert, use LayoutBuilder to check the incoming constraints: assert(constraints.maxWidth >= minSize && constraints.maxHeight >= minSize, 'AccessibleTouchTargetWrapper is constrained below $minSizeΓ—$minSize pt. Parent: $constraints'). This widget directly addresses the NHF requirement for cognitive accessibility and motor impairment support β€” document this in the class dartdoc.

Testing Requirements

Use testWidgets in flutter_test. Test file: test/widgets/accessible_touch_target_wrapper_test.dart. Required test cases: (1) widget renders without error with a small Icon child, (2) tap at center of 44Γ—44 area triggers onTap when child is 10Γ—10, (3) tap at corner (2,2) of 44Γ—44 area triggers onTap (verifies HitTestBehavior.opaque), (4) tap at center does NOT trigger onTap when wrapped in AbsorbPointer, (5) semanticsLabel is reflected in Semantics tree (use find.bySemanticsLabel), (6) long press triggers onLongPress callback when provided, (7) widget with a 60Γ—60 child does not overflow or clip the child, (8) minSize parameter of 48 produces a 48Γ—48 hit area. Use WidgetTester.tap(find.byType(AccessibleTouchTargetWrapper)) and WidgetTester.tapAt(Offset(2, 2)) for gesture testing.

Epic Risks (4)
medium impact high prob integration

Flutter's textScaleFactor behaviour differs between iOS and Android, and third-party widgets used across the app (date pickers, bottom sheets, chips) may not respect the per-role scale caps applied by the dynamic-type-scale-service, causing overflow in screens this epic cannot directly control.

Mitigation & Contingency

Mitigation: Enumerate all third-party widget usages that render text. For each, verify whether they honour the inherited DefaultTextStyle and MediaQuery.textScaleFactor or use hardcoded sizes. File issues with upstream packages and wrap non-compliant widgets in MediaQuery overrides scoped to the safe cap for that role.

Contingency: If upstream packages cannot be patched within the sprint, implement a global MediaQuery wrapper at the app root that clamps textScaleFactor to the highest per-role safe value (typically 1.6–2.0), accepting that users at extreme OS scales see a safe cap rather than full scaling for those widgets.

high impact medium prob dependency

The CI accessibility lint runner depends on the Dart CLI toolchain and potentially custom_lint or a bespoke Dart script. CI environments differ from local dev environments in Dart SDK version, pub cache configuration, and platform availability, risking intermittent CI failures that block all pull requests.

Mitigation & Contingency

Mitigation: Pin the Dart SDK version in the CI workflow configuration. Package the lint runner as a self-contained Dart script with all dependencies vendored or declared in a dedicated pubspec.yaml. Add a CI smoke test that runs the runner against a known-compliant fixture and a known-violating fixture to verify the exit codes are correct.

Contingency: If the custom runner proves too fragile, fall back to running dart analyze with the flutter-accessibility-lint-config rules as the sole CI gate, and schedule the custom manifest validation as a separate non-blocking advisory check until the runner is stabilised.

medium impact medium prob technical

Wrapping all interactive widgets with a 44 pt minimum hit area via HitTestBehavior.opaque may cause unintended tap interception in widgets where interactive elements are closely stacked, particularly in the expense type selector, bulk confirmation screen, and notification filter bar.

Mitigation & Contingency

Mitigation: Conduct integration testing of the touch target wrapper specifically in dense layout scenarios (expense selector, filter bars, bottom sheets with multiple buttons). Use the Flutter Inspector to visualise hit areas and confirm no overlaps. Pair with the interactive-control-spacing-system to ensure minimum 8 dp gaps between expanded hit areas.

Contingency: If overlapping hit areas cause mis-tap regressions in specific screens, allow the touch target wrapper to accept an explicit hitAreaSize parameter that can be reduced below 44 pt only in contexts where the interactive-control-spacing-system guarantees sufficient gap, with a mandatory code review flag for any such override.

high impact medium prob scope

The contrast-safe-color-palette must guarantee WCAG AA ratios for both light and dark mode token sets. Dark mode color derivation is non-trivial β€” simply inverting a light palette often produces pairs that pass in one mode but fail in the other, and the token manifest must encode both sets explicitly.

Mitigation & Contingency

Mitigation: Define both light and dark token sets explicitly in the accessibility-token-manifest rather than deriving one from the other programmatically. Run the contrast-ratio-validator against both sets as part of the token manifest generation process and include both in the CI lint runner's validation scope.

Contingency: If time pressure forces a dark mode deferral, ship with light mode only and add a prominent in-app notice. Gate dark mode colour tokens behind a feature flag until the full dual-palette validation is complete.