medium priority low complexity frontend pending frontend specialist Tier 13

Acceptance Criteria

Each participant row includes a visible remove icon button (trailing position) with a minimum touch target size of 48×48 dp, meeting WCAG 2.2 AA Success Criterion 2.5.8
Tapping the remove button invokes the `onRemove` callback with the corresponding BulkParticipant as the argument
After onRemove is called, the row animates out (slide + fade or equivalent) before disappearing — no instant jump
Optionally, swipe-to-dismiss (Dismissible widget) is also supported as an alternative gesture — if implemented, the dismiss direction is end-to-start (right-to-left) only, with a red background and delete icon revealed during swipe
If swipe-to-dismiss is implemented, it also calls onRemove and is not the only removal mechanism (icon button must still be present for accessibility)
The remove icon button has a Semantics label: 'Remove [participant name]' so screen readers announce the specific participant being removed
Remove action is not reversible within the widget — undo/undo toast (if needed) is the responsibility of the parent screen, not this widget
After removal, the list reflows smoothly with no layout jump or overflow errors
Widget tests confirm: (1) remove button is present for every row, (2) tapping it fires onRemove with the correct participant, (3) touch target size is at least 48dp (verified via widget bounds)

Technical Requirements

frameworks
Flutter
BLoC
data models
BulkParticipant
performance requirements
Row removal animation must complete within 300ms to feel responsive
AnimatedList should be used instead of rebuilding the full ListView to avoid re-rendering all rows on removal
security requirements
The remove action must be idempotent — calling onRemove twice for the same participant (e.g., via rapid double-tap) must not cause errors; debounce the button or disable it after first tap until state updates
ui components
RemoveParticipantButton (StatelessWidget or IconButton wrapper)
AnimatedList (replaces ListView.builder from task-001 if animation is added)
Dismissible (optional swipe gesture wrapper)

Execution Context

Execution Tier
Tier 13

Tier 13 - 6 tasks

Can start after Tier 12 completes

Dependencies (21)
epic-proxy-activity-registration-orchestration-task-001 component Cross-Epic Component bulk-participant-list depends on proxy-registration-service
epic-proxy-activity-registration-orchestration-task-002 component Cross-Epic Component bulk-participant-list depends on proxy-registration-service
epic-proxy-activity-registration-orchestration-task-003 component Cross-Epic Component bulk-participant-list depends on proxy-registration-service
epic-proxy-activity-registration-orchestration-task-004 component Cross-Epic Component bulk-participant-list depends on proxy-registration-service
epic-proxy-activity-registration-orchestration-task-005 component Cross-Epic Component bulk-participant-list depends on proxy-registration-service
epic-proxy-activity-registration-orchestration-task-010 component Cross-Epic Component bulk-participant-list depends on proxy-registration-service
epic-proxy-activity-registration-orchestration-task-006 component Cross-Epic Component bulk-participant-list depends on bulk-registration-service
epic-proxy-activity-registration-orchestration-task-007 component Cross-Epic Component bulk-participant-list depends on bulk-registration-service
epic-proxy-activity-registration-orchestration-task-008 component Cross-Epic Component bulk-participant-list depends on bulk-registration-service
epic-proxy-activity-registration-orchestration-task-009 component Cross-Epic Component bulk-participant-list depends on bulk-registration-service
epic-proxy-activity-registration-orchestration-task-011 component Cross-Epic Component bulk-participant-list depends on bulk-registration-service
epic-proxy-activity-registration-core-services-task-001 component Cross-Epic Component bulk-participant-list depends on proxy-duplicate-detection-service
epic-proxy-activity-registration-core-services-task-002 component Cross-Epic Component bulk-participant-list depends on proxy-duplicate-detection-service
epic-proxy-activity-registration-core-services-task-003 component Cross-Epic Component bulk-participant-list depends on proxy-duplicate-detection-service
epic-proxy-activity-registration-core-services-task-008 component Cross-Epic Component bulk-participant-list depends on proxy-duplicate-detection-service
epic-proxy-activity-registration-core-services-task-004 component Cross-Epic Component bulk-participant-list depends on activity-attribution-service
epic-proxy-activity-registration-core-services-task-005 component Cross-Epic Component bulk-participant-list depends on activity-attribution-service
epic-proxy-activity-registration-core-services-task-006 component Cross-Epic Component bulk-participant-list depends on activity-attribution-service
epic-proxy-activity-registration-core-services-task-007 component Cross-Epic Component bulk-participant-list depends on activity-attribution-service
epic-proxy-activity-registration-core-services-task-009 component Cross-Epic Component bulk-participant-list depends on activity-attribution-service
epic-proxy-activity-registration-core-services-task-010 component Cross-Epic Component bulk-participant-list depends on activity-attribution-service

Implementation Notes

Upgrade BulkParticipantList from ListView.builder to AnimatedList to support smooth row removal animation. AnimatedList requires a GlobalKey and explicit `removeItem()` calls — manage this inside BulkParticipantList's State class (convert to StatefulWidget for this reason only). On remove button tap: (1) call `_listKey.currentState!.removeItem(index, (context, animation) => SizeTransition(sizeFactor: animation, child: row))` to animate the row out, (2) call `widget.onRemove(participant)` after the animation starts (not after it completes) to keep the BLoC state in sync. Use `IconButton` with `constraints: BoxConstraints(minWidth: 48, minHeight: 48)` to meet the touch target requirement — do not rely on default IconButton sizing.

Add a `key: ValueKey(participant.id)` to each row widget so Flutter's diffing algorithm correctly identifies rows during list mutation. If Dismissible is added, nest it inside the AnimatedList item builder and configure `confirmDismiss` to call onRemove and return true.

Testing Requirements

Widget tests in `test/widgets/bulk_participant_list_test.dart`. Required test cases: (1) remove button renders for each row in a 3-item list, (2) tapping the first row's remove button fires onRemove with the first participant object, (3) tapping the remove button for the last remaining participant fires onRemove (no crash on empty list), (4) semantic label on the remove button includes the participant's displayName. Use `tester.tap(find.byIcon(Icons.remove_circle_outline).first)` or equivalent. Use a mock onRemove callback (via a captured variable) to assert it was called with the expected argument.

If Dismissible is implemented, add a drag test using `tester.drag()`.

Component
Bulk Participant List Widget
ui low
Epic Risks (4)
medium impact high prob scope

The bulk registration screen combines pre-filled defaults, a dynamic multi-select participant list, per-participant conflict badges, and a batch submission confirmation — making it one of the most complex screens in the application. Scope creep or underestimated interaction complexity could cause the epic to exceed its estimate significantly.

Mitigation & Contingency

Mitigation: Implement the bulk screen in two vertical slices: (1) participant selection and form submission without conflict handling, (2) conflict badge rendering and override flow. Validate slice 1 with coordinators before building slice 2.

Contingency: If the full conflict review UI cannot be completed within the epic, ship the bulk screen with a simplified 'skip all duplicates' fallback mode and defer per-participant override toggles to a follow-up sprint.

medium impact medium prob technical

The proxy-registration-bloc must manage state across two distinct flows (single proxy and bulk) with branching conflict paths, intermediate buffering of bulk participant selections, and reliable state reset. Incorrect state transitions could leave the UI in a loading or stale-conflict state after submission.

Mitigation & Contingency

Mitigation: Model the BLoC state as a sealed class hierarchy with exhaustive pattern matching in the UI. Write state-machine unit tests that exercise every valid transition and assert that invalid transitions are no-ops. Use flutter_bloc's BlocObserver in debug builds to log all transitions.

Contingency: If BLoC state bugs surface in QA, introduce an explicit ResetToIdle event triggered on screen disposal to guarantee clean state, and add a 'Start over' affordance visible to the coordinator at any conflict step.

high impact medium prob technical

The typeahead peer mentor selector with multi-select mode may be difficult to operate with VoiceOver/TalkBack, particularly the dynamic search results list and the selected-chip removal controls, risking WCAG 2.2 AA non-compliance for screen reader users.

Mitigation & Contingency

Mitigation: Wrap all search result items and selected chips with explicit Semantics widgets providing role, label, and selected-state announcements. Test the selector with VoiceOver on iOS and TalkBack on Android during development, not only at QA time.

Contingency: If the multi-select typeahead cannot be made fully accessible within the sprint, provide an alternative flat scrollable list with checkboxes as a fallback mode, toggled by an accessibility-settings flag.

high impact medium prob security

The peer mentor selector must apply RLS chapter-scope filtering to show only mentors the coordinator is responsible for. If the Supabase query for the selector does not correctly join against the coordinator's chapter assignments, coordinators may see mentors from other chapters, violating data isolation.

Mitigation & Contingency

Mitigation: Implement the selector's data query using the same RLS-aware Supabase client used by the contact list feature, which already handles chapter-scope filtering. Write an integration test with a multi-chapter coordinator fixture asserting cross-chapter mentors are not returned.

Contingency: If a data isolation breach is discovered, immediately add a client-side chapter_id filter as a defence-in-depth measure and audit selector query logs for any unauthorised cross-chapter results.