Implement BufdirReportSectionWidget collapsible card
epic-bufdir-report-preview-ui-components-task-005 — Build BufdirReportSectionWidget as a collapsible card (ExpansionTile or custom AnimatedContainer) that renders a semantic heading (h2/h3 via ExcludeSemantics and Semantics header flag), a section-level validation badge showing error/warning counts, and a scrollable list of BufdirFieldRowWidget children. Collapsed state shows only the heading and badge. Apply WCAG design tokens for all color choices.
Acceptance Criteria
Technical Requirements
Execution Context
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.
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.
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.