critical priority medium complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

BufdirReportSectionWidget is a StatefulWidget accepting a BufdirReportSectionData parameter and an optional bool initiallyExpanded (default false)
Collapsed state shows: section title text, validation badge (error count + warning count), and a chevron icon indicating collapsed state
Expanded state shows the collapsed content plus a list of BufdirFieldRowWidget for each field in BufdirReportSectionData.fields
Tap on the header row toggles expanded/collapsed with an animated height transition (minimum 200ms, uses Flutter AnimationController or ExpansionTile animation)
Section title has a Semantics widget with header: true and the correct heading level announced (e.g., 'Section: {title}, heading level 2')
Validation badge shows error count in anomaly color and warning count in warning color (sourced from BufdirAccessibilityUtils); badge is hidden if both counts are zero
The field list inside the expanded area is NOT itself scrollable — it relies on the parent scroll view (use shrinkWrap: true, physics: NeverScrollableScrollPhysics())
All color choices pass WCAG 2.2 AA contrast ratio checks
Golden tests for: (1) collapsed with zero validation issues; (2) collapsed with 2 errors + 1 warning; (3) expanded state with 3 field rows
Widget test: toggling expansion shows/hides BufdirFieldRowWidget children
Widget test: Semantics tree includes header annotation on the title

Technical Requirements

frameworks
Flutter
BufdirAccessibilityUtils (internal)
Flutter Semantics API
Flutter Animation APIs
data models
BufdirReportSectionData
BufdirValidationSummary
BufdirFieldRowData
performance requirements
Field list uses ListView.builder if section has more than 10 fields to avoid building all rows at once
Animation must maintain 60fps — no heavy computation in AnimationController listener
security requirements
Section title rendered as plain Text — no HTML parsing
Validation counts are integers from the model — no user-supplied strings in badge
ui components
ExpansionTile or AnimatedContainer
ListView.builder
BufdirFieldRowWidget
Semantics
AnimatedRotation (chevron)
Row
Text
Container (badge)

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Prefer a custom implementation over raw ExpansionTile to have full control over semantics and animation. Use AnimationController with CurvedAnimation(curve: Curves.easeInOut, duration: Duration(milliseconds: 250)). The header Row should contain: [Expanded(child: semanticTitle), if (hasIssues) ValidationBadgeWidget(...), AnimatedRotation(chevron)]. For the semantic heading: wrap the title Text in Semantics(header: true, label: 'Section: ${data.title}').

The heading level (h2/h3) cannot be directly expressed in Flutter semantics as an ARIA heading level — use the label prefix 'Section:' as the accessible convention for this app. For the validation badge, create a small private _ValidationBadge StatelessWidget with two colored pill containers side by side for errors and warnings — keep it reusable for future use in BufdirPreviewScreen. Use shrinkWrap: true and NeverScrollableScrollPhysics on the inner ListView to prevent nested scroll conflicts. Test in a ConstrainedBox(maxHeight: 600) to simulate being inside a parent scroll view.

Testing Requirements

Write widget tests in test/features/bufdir/widgets/bufdir_report_section_widget_test.dart. Cover: (1) in collapsed state, BufdirFieldRowWidget children are not present in widget tree (find.byType(BufdirFieldRowWidget) returns zero); (2) after tap on header, children appear (findsNWidgets(data.fields.length)); (3) second tap collapses again; (4) validation badge is absent when errorCount=0 and warningCount=0; (5) validation badge shows correct counts from BufdirValidationSummary; (6) Semantics header flag is set on title widget (use tester.getSemantics and check SemanticsFlag.isHeader); (7) no RenderFlex overflow with 20 field rows. Golden tests for all three specified states. Run flutter test --update-goldens to capture baselines.

Component
Bufdir Report Section Widget
ui medium
Epic Risks (2)
high impact medium prob technical

Implementing the tap-to-scroll-and-focus behavior from the validation banner to a specific field row in a long scrollable list is complex in Flutter. If focus management is incorrectly implemented, VoiceOver users who navigate to the banner and select an issue will not be moved to the relevant field row, breaking the accessibility workflow and violating WCAG 2.4.3 (Focus Order).

Mitigation & Contingency

Mitigation: Use BufdirAccessibilityUtils focus management utilities (built in the foundation epic) with explicit GlobalKey-based scroll anchors on each field row. Test with a real iOS device running VoiceOver during widget development, not only in the Flutter accessibility inspector.

Contingency: If programmatic scroll-to-focus cannot be reliably achieved before the TestFlight deadline, fall back to a navigation approach where tapping a banner issue opens a modal detail sheet for that field row rather than scrolling in place, and file a follow-up ticket for the inline scroll implementation.

medium impact low prob technical

The validation summary banner must reactively update its issue count as underlying aggregated data changes (e.g., if the coordinator has navigated away and data was refreshed). If the banner's Riverpod provider is not correctly scoped, it may display stale issue counts or fail to disappear when all issues are resolved, eroding coordinator trust in the validation system.

Mitigation & Contingency

Mitigation: Drive the banner exclusively from the same Riverpod provider that powers the full preview model — do not maintain a separate local state for issue counts. Write a widget test that simulates a data refresh mid-review and asserts the banner updates within one frame.

Contingency: If stale state reaches production, add a manual refresh button to the banner as a short-term workaround while the provider scoping is corrected in the next release cycle.