From 2a6d480428c404e4dc917fd1e1cf4c57452bbe03 Mon Sep 17 00:00:00 2001 From: rzuasti Date: Thu, 4 Jun 2026 09:57:20 -0400 Subject: [PATCH] Collapse notification cards on tap anywhere Tapping a notification's body already toggled expand/collapse, but the action area below it wasn't tappable. Wrap the whole card in an InkWell so tapping anywhere (body or action row) toggles, while the buttons still handle their own taps. Co-Authored-By: Claude Opus 4.8 --- frontend/lib/home/notification_card.dart | 60 ++++++++-------- .../test/widget/notification_card_test.dart | 70 +++++++++++++++++++ 2 files changed, 101 insertions(+), 29 deletions(-) create mode 100644 frontend/test/widget/notification_card_test.dart diff --git a/frontend/lib/home/notification_card.dart b/frontend/lib/home/notification_card.dart index 04ce47f..3dca903 100644 --- a/frontend/lib/home/notification_card.dart +++ b/frontend/lib/home/notification_card.dart @@ -71,37 +71,39 @@ class _NotificationCardState extends State { padding: const EdgeInsets.only(right: Insets.lg), child: const Icon(Icons.done), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ListTile( - leading: Icon( - widget.item.notificationType.icon, - color: widget.item.isNew ? theme.colorScheme.primary : null, + child: InkWell( + onTap: () => setState(() => _expanded = !_expanded), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ListTile( + leading: Icon( + widget.item.notificationType.icon, + color: widget.item.isNew ? theme.colorScheme.primary : null, + ), + title: Text( + '${widget.formatter.format(widget.item.createdOn)} - ${widget.item.title}', + style: widget.item.isNew + ? const TextStyle(fontWeight: FontWeight.bold) + : null, + ), + subtitle: Text( + widget.item.body, + maxLines: _expanded ? null : 2, + overflow: _expanded + ? TextOverflow.visible + : TextOverflow.ellipsis, + ), ), - title: Text( - '${widget.formatter.format(widget.item.createdOn)} - ${widget.item.title}', - style: widget.item.isNew - ? const TextStyle(fontWeight: FontWeight.bold) - : null, + AnimatedSize( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + child: _expanded + ? _buildActions(context) + : const SizedBox.shrink(), ), - subtitle: Text( - widget.item.body, - maxLines: _expanded ? null : 2, - overflow: _expanded - ? TextOverflow.visible - : TextOverflow.ellipsis, - ), - onTap: () => setState(() => _expanded = !_expanded), - ), - AnimatedSize( - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - child: _expanded - ? _buildActions(context) - : const SizedBox.shrink(), - ), - ], + ], + ), ), ), ); diff --git a/frontend/test/widget/notification_card_test.dart b/frontend/test/widget/notification_card_test.dart new file mode 100644 index 0000000..4ec7fbc --- /dev/null +++ b/frontend/test/widget/notification_card_test.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:frontend/home/notification_card.dart'; +import 'package:frontend/model/notification.dart' as oott_model; +import 'package:frontend/model/notification_type.dart'; +import 'package:frontend/utils/friendly_date_formatter.dart'; + +oott_model.Notification _sampleNotification() => oott_model.Notification( + id: 1, + title: 'New device found', + body: 'A long body that wraps across multiple lines when expanded.', + notificationType: NotificationType.newDeviceFound, + createdOn: DateTime(2026, 6, 4, 12), + isNew: true, + macAddress: 'AA:BB:CC:DD:EE:FF', +); + +Future _pumpCard(WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: NotificationCard( + item: _sampleNotification(), + formatter: FriendlyDateFormatter(), + onSetRead: (_) async => true, + ), + ), + ), + ); +} + +void main() { + testWidgets('tapping the body expands and then collapses the card', ( + tester, + ) async { + await _pumpCard(tester); + + // Initially collapsed: no action buttons. + expect(find.text('Mark as read'), findsNothing); + + // Tap the body to expand. + await tester.tap(find.byType(ListTile)); + await tester.pumpAndSettle(); + expect(find.text('Mark as read'), findsOneWidget); + + // Tap directly on the body text to collapse. + await tester.tap( + find.text('A long body that wraps across multiple lines when expanded.'), + ); + await tester.pumpAndSettle(); + expect(find.text('Mark as read'), findsNothing); + }); + + testWidgets('tapping the action area (not a button) collapses the card', ( + tester, + ) async { + await _pumpCard(tester); + + await tester.tap(find.byType(ListTile)); + await tester.pumpAndSettle(); + expect(find.text('Mark as read'), findsOneWidget); + + // Tap the action row outside any button: its left edge is empty space + // because the buttons are aligned to the end. + final actions = tester.getRect(find.byType(OverflowBar)); + await tester.tapAt(Offset(actions.left + 1, actions.center.dy)); + await tester.pumpAndSettle(); + expect(find.text('Mark as read'), findsNothing); + }); +}