From 057defd5a945382826ae8746416473943304da32 Mon Sep 17 00:00:00 2001 From: Daniel Eshkeri Date: Thu, 5 Sep 2024 16:26:00 +0100 Subject: [PATCH] feat(UX-1073): List Item notification (#172) feat: updated notification list item to match latest design feat: added swipe-able actions to notification list item fix: _getSlidableExtend() now won't return over 1.0 --- .../components/notification_list_example.dart | 251 +++++++++++++++-- .../notification_list_item_widgetbook.dart | 72 +++-- .../list_item/notification_list_item.dart | 257 +++++++++++++----- 3 files changed, 440 insertions(+), 140 deletions(-) diff --git a/example/lib/pages/components/notification_list_example.dart b/example/lib/pages/components/notification_list_example.dart index 5d259f41..a13ae1ad 100644 --- a/example/lib/pages/components/notification_list_example.dart +++ b/example/lib/pages/components/notification_list_example.dart @@ -8,37 +8,230 @@ class NotificationListItemExample extends StatelessWidget { @override Widget build(BuildContext context) { + final Image image = Image.network( + "https://i.ytimg.com/vi/KItsWUzFUOs/maxresdefault.jpg", + ); + return ExampleScaffold( name: name, - child: Column( - children: [ - ZetaNotificationListItem( - body: Text( - "New urgent" * 300, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - title: 'Urgent Message', - leading: ZetaNotificationBadge.icon(icon: ZetaIcons.check_circle), - notificationTime: "Just now", - action: ZetaButton.negative( - label: "Remove", - onPressed: () {}, - ), - ), - ZetaNotificationListItem( - body: Text( - "New urgent", - ), - title: 'Urgent Message', - leading: ZetaNotificationBadge.icon(icon: ZetaIcons.check_circle), - notificationTime: "Just now", - action: ZetaButton.negative( - label: "Remove", - onPressed: () {}, - ), - ), - ].gap(Zeta.of(context).spacing.xl_4), + child: SingleChildScrollView( + child: Column( + children: [ + Text( + 'ZetaNotificationListItem with avatar', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ZetaNotificationListItem( + body: Text( + "New urgent message from John Smith that spans multiple lines but line count caps at two", + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + title: 'Urgent Message', + leading: ZetaNotificationBadge.avatar( + avatar: ZetaAvatar.initials( + initials: "JS", + lowerBadge: ZetaAvatarBadge.icon( + color: ZetaColors().surfacePositive, + icon: Icons.check, + ), + )), + notificationTime: "Just now", + attachment: Text("Spring-Donation-Drive.pdf"), + showBellIcon: true, + action: ZetaButton.outline( + label: "User Action", + onPressed: () {}, + size: ZetaWidgetSize.small, + ), + slidableActions: [ + ZetaSlidableAction.menuMore(onPressed: () {}), + ZetaSlidableAction.call(onPressed: () {}), + ZetaSlidableAction.ptt(onPressed: () {}), + ZetaSlidableAction.delete(onPressed: () {}), + ], + paleButtonColors: true, + ), + ZetaNotificationListItem( + body: Text( + "New urgent message from John Smith that spans multiple lines but line count caps at two", + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + title: 'Urgent Message', + leading: ZetaNotificationBadge.avatar( + avatar: ZetaAvatar.initials( + initials: "JS", + lowerBadge: ZetaAvatarBadge.icon( + color: ZetaColors().surfacePositive, + icon: Icons.check, + ), + )), + showDivider: true, + notificationTime: "10:32 AM", + showBellIcon: true, + attachment: Text("Spring-Donation-Drive.pdf"), + slidableActions: [ + ZetaSlidableAction.menuMore(onPressed: () {}), + ZetaSlidableAction.delete(onPressed: () {}), + ], + ), + ZetaNotificationListItem( + body: Text( + "New urgent message from John Smith that spans multiple lines but line count caps at two", + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + title: 'Urgent Message', + leading: ZetaNotificationBadge.avatar( + avatar: ZetaAvatar.initials( + initials: "JS", + lowerBadge: ZetaAvatarBadge.icon( + color: ZetaColors().surfacePositive, + icon: Icons.check, + ), + ), + ), + notificationRead: true, + notificationTime: "03/09/2024", + slidableActions: [ + ZetaSlidableAction.call(onPressed: () {}), + ZetaSlidableAction.ptt(onPressed: () {}), + ], + paleButtonColors: true, + ), + const SizedBox(height: 20), + Text( + 'ZetaNotificationListItem with image', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ZetaNotificationListItem( + body: Text( + "New urgent message from John Smith that spans multiple lines but line count caps at two", + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + title: 'Urgent Message', + leading: ZetaNotificationBadge.image( + image: image, + ), + notificationTime: "Just now", + showBellIcon: true, + action: ZetaButton.outline( + label: "User Action", + onPressed: () {}, + size: ZetaWidgetSize.small, + ), + attachment: Text("Spring-Donation-Drive.pdf"), + slidableActions: [ + ZetaSlidableAction.menuMore(onPressed: () {}), + ZetaSlidableAction.call(onPressed: () {}), + ZetaSlidableAction.ptt(onPressed: () {}), + ZetaSlidableAction.delete(onPressed: () {}), + ], + paleButtonColors: true, + ), + ZetaNotificationListItem( + body: Text( + "New urgent message from John Smith that spans multiple lines but line count caps at two", + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + title: 'Urgent Message', + leading: ZetaNotificationBadge.image( + image: image, + ), + showDivider: true, + notificationTime: "10:32 AM", + showBellIcon: true, + attachment: Text("Spring-Donation-Drive.pdf"), + slidableActions: [ + ZetaSlidableAction.menuMore(onPressed: () {}), + ZetaSlidableAction.delete(onPressed: () {}), + ], + ), + ZetaNotificationListItem( + body: Text( + "New urgent message from John Smith that spans multiple lines but line count caps at two", + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + title: 'Urgent Message', + leading: ZetaNotificationBadge.image( + image: image, + ), + notificationRead: true, + notificationTime: "03/09/2024", + slidableActions: [ + ZetaSlidableAction.call(onPressed: () {}), + ZetaSlidableAction.ptt(onPressed: () {}), + ], + paleButtonColors: true, + ), + const SizedBox(height: 20), + Text( + 'ZetaNotificationListItem with icon', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ZetaNotificationListItem( + body: Text( + "New urgent message from John Smith that spans multiple lines but line count caps at two", + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + title: 'Urgent Message', + leading: ZetaNotificationBadge.icon(icon: ZetaIcons.check_circle), + notificationTime: "Just now", + showBellIcon: true, + action: ZetaButton.outline( + label: "User Action", + onPressed: () {}, + size: ZetaWidgetSize.small, + ), + attachment: Text("Spring-Donation-Drive.pdf"), + slidableActions: [ + ZetaSlidableAction.menuMore(onPressed: () {}), + ZetaSlidableAction.call(onPressed: () {}), + ZetaSlidableAction.ptt(onPressed: () {}), + ZetaSlidableAction.delete(onPressed: () {}), + ], + paleButtonColors: true, + ), + ZetaNotificationListItem( + body: Text( + "New urgent message from John Smith that spans multiple lines but line count caps at two", + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + title: 'Urgent Message', + leading: ZetaNotificationBadge.icon(icon: ZetaIcons.check_circle), + showDivider: true, + notificationTime: "10:32 AM", + showBellIcon: true, + attachment: Text("Spring-Donation-Drive.pdf"), + slidableActions: [ + ZetaSlidableAction.menuMore(onPressed: () {}), + ZetaSlidableAction.delete(onPressed: () {}), + ], + ), + ZetaNotificationListItem( + body: Text( + "New urgent message from John Smith that spans multiple lines but line count caps at two", + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + title: 'Urgent Message', + leading: ZetaNotificationBadge.icon(icon: ZetaIcons.check_circle), + notificationRead: true, + notificationTime: "03/09/2024", + slidableActions: [ + ZetaSlidableAction.call(onPressed: () {}), + ZetaSlidableAction.ptt(onPressed: () {}), + ], + paleButtonColors: true, + ), + ].gap(Zeta.of(context).spacing.xl_4), + ), ), ); } diff --git a/example/widgetbook/pages/components/notification_list_item_widgetbook.dart b/example/widgetbook/pages/components/notification_list_item_widgetbook.dart index 66c6175e..028f42b6 100644 --- a/example/widgetbook/pages/components/notification_list_item_widgetbook.dart +++ b/example/widgetbook/pages/components/notification_list_item_widgetbook.dart @@ -6,32 +6,30 @@ import '../../utils/scaffold.dart'; Widget notificationListItemUseCase(BuildContext context) => WidgetbookScaffold( builder: (context, _) => Padding( - padding: EdgeInsets.symmetric(horizontal: context.knobs.list(label: "Size", options: [100, 200, 400])), + padding: EdgeInsets.symmetric( + horizontal: + context.knobs.list(label: "Size", options: [100, 200, 400])), child: ZetaNotificationListItem( - body: context.knobs.boolean(label: "Include Link") - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "New urgent" * 300, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ZetaButton.text(label: "label") - ], - ) - : Text( - "New urgent" * 300, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - title: context.knobs.string(label: "Title", initialValue: "Urgent Notification"), - notificationTime: context.knobs.stringOrNull(label: "Notification Time", initialValue: "Just Now"), - notificationRead: context.knobs.boolean(label: "Notification Read", initialValue: false), + body: Text( + "New urgent message " * 300, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + attachment: + context.knobs.boolean(label: "Attachment", initialValue: false) + ? Text("Spring-Donation-Drive.pdf") + : null, + title: context.knobs + .string(label: "Title", initialValue: "Urgent Notification"), + notificationTime: context.knobs.stringOrNull( + label: "Notification Time", initialValue: "Just Now"), + notificationRead: context.knobs + .boolean(label: "Notification Read", initialValue: false), leading: context.knobs.list( label: 'Badge', options: [ - ZetaNotificationBadge.avatar(avatar: ZetaAvatar.initials(initials: "AO")), + ZetaNotificationBadge.avatar( + avatar: ZetaAvatar.initials(initials: "JS")), ZetaNotificationBadge.icon(icon: ZetaIcons.check_circle), ZetaNotificationBadge.image( image: Image.network( @@ -43,23 +41,19 @@ Widget notificationListItemUseCase(BuildContext context) => WidgetbookScaffold( ? "Icon" : "Image", ), - action: context.knobs.list( - label: "Action Buttons", - options: [ - ZetaButton.negative(label: "Remove"), - ZetaButton.positive(label: "Add"), - ZetaButton.outline(label: "Action"), - ], - labelBuilder: (value) { - final button = (value as ZetaButton); - return button.label == "Remove" - ? "Negative" - : button.label == "Add" - ? "Positive" - : "Netutral"; - }, - ), - showDivider: context.knobs.booleanOrNull(label: "Has More"), + action: + context.knobs.boolean(label: "Show action", initialValue: true) + ? ZetaButton.outline( + label: "User Action", + onPressed: () {}, + size: ZetaWidgetSize.small, + borderType: ZetaWidgetBorder.rounded, + ) + : null, + showDivider: + context.knobs.boolean(label: "Show Divider", initialValue: false), + showBellIcon: context.knobs + .boolean(label: "Show Bell Icon", initialValue: true), ), ), ); diff --git a/lib/src/components/list_item/notification_list_item.dart b/lib/src/components/list_item/notification_list_item.dart index a1559e8e..a82b3948 100644 --- a/lib/src/components/list_item/notification_list_item.dart +++ b/lib/src/components/list_item/notification_list_item.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; import '../../../zeta_flutter.dart'; @@ -15,9 +16,13 @@ class ZetaNotificationListItem extends ZetaStatelessWidget { required this.title, this.notificationRead = false, this.notificationTime, - required this.action, + this.action, this.showDivider = false, this.semanticLabel, + this.attachment, + this.showBellIcon = false, + this.slidableActions = const [], + this.paleButtonColors, }); /// Notification Badge to indicate type of notification or who it's coming from @@ -39,13 +44,43 @@ class ZetaNotificationListItem extends ZetaStatelessWidget { final bool? showDivider; /// Pass in a action widget to handle action functionality. - final Widget action; + final Widget? action; /// Semantic label for the notification list item. /// /// {@macro zeta-widget-semantic-label} final String? semanticLabel; + /// Pass in a widget to display an attached file. + final Widget? attachment; + + /// Show bell icon if notification is recent/important. + final bool? showBellIcon; + + /// List of slidable actions. + /// + /// The actions are displayed in the order they are provided; from left to right. + final List slidableActions; + + /// Whether to apply pale color to action buttons. + /// + /// Pale buttons was the default behavior before 0.15.2, but now buttons have darker colors by default. + final bool? paleButtonColors; + + double _getSlidableExtend({ + required int slidableActionsCount, + required double maxScreenWidth, + required BuildContext context, + }) { + final actionWith = slidableActionsCount * Zeta.of(context).spacing.xl_10; + final maxButtonWidth = actionWith / maxScreenWidth; + final extend = actionWith / maxScreenWidth; + if (extend.clamp(0, maxButtonWidth).toDouble() > 1) { + return 1; + } + return extend.clamp(0, maxButtonWidth).toDouble(); + } + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -54,83 +89,154 @@ class ZetaNotificationListItem extends ZetaStatelessWidget { ..add(StringProperty('title', title)) ..add(DiagnosticsProperty('notificationRead', notificationRead)) ..add(DiagnosticsProperty('showDivider', showDivider)) - ..add(StringProperty('semanticLabel', semanticLabel)); + ..add(StringProperty('semanticLabel', semanticLabel)) + ..add(DiagnosticsProperty('attachment', attachment)) + ..add(DiagnosticsProperty('showBellIcon', showBellIcon)) + ..add(DiagnosticsProperty('paleButtonColors', paleButtonColors)); } @override Widget build(BuildContext context) { final colors = Zeta.of(context).colors; - return ZetaRoundedScope( - rounded: context.rounded, - child: Semantics( - explicitChildNodes: true, - label: semanticLabel, - button: true, - child: DecoratedBox( - decoration: _getStyle(context), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - leading, - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MergeSemantics( - child: Row( - children: [ - if (!notificationRead) - ZetaIndicator( - color: colors.blue, - size: ZetaWidgetSize.small, - ), - Text( - title, - style: ZetaTextStyles.labelLarge, + + final actions = [...slidableActions]; + + return Padding( + padding: EdgeInsets.all(Zeta.of(context).spacing.small), + child: ZetaRoundedScope( + rounded: context.rounded, + child: Semantics( + explicitChildNodes: true, + label: semanticLabel, + button: true, + child: LayoutBuilder( + builder: (context, constraints) { + return Slidable( + enabled: actions.isNotEmpty, + endActionPane: actions.isEmpty + ? null + : ActionPane( + extentRatio: _getSlidableExtend( + slidableActionsCount: actions.length, + maxScreenWidth: constraints.maxWidth, + context: context, + ), + motion: const ScrollMotion(), + children: paleButtonColors != null + ? actions + .map( + (action) => action.copyWith( + paleColor: paleButtonColors, ), - ], - ), - ), - Row( + ) + .toList() + : actions, + ), + child: DecoratedBox( + decoration: _getStyle(context), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + leading, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (notificationTime != null) - Text( - notificationTime!, - style: ZetaTextStyles.bodySmall.apply(color: colors.textDisabled), - ), - Container( - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: colors.surfaceNegative, - borderRadius: Zeta.of(context).radius.full, - ), - child: ZetaIcon( - ZetaIcons.important_notification, - color: colors.white, - size: Zeta.of(context).spacing.medium, - ), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + MergeSemantics( + child: Row( + children: [ + if (!notificationRead) + ZetaIndicator.icon( + color: ZetaColors().primary, + size: ZetaWidgetSize.small, + ), + SizedBox( + width: Zeta.of(context) + .spacing + .minimum, + ), + Text( + title, + style: ZetaTextStyles.labelLarge, + ), + ], + ), + ), + Row( + children: [ + if (notificationTime != null) + Text( + notificationTime!, + style: + ZetaTextStyles.bodySmall.apply( + color: colors.textDisabled, + ), + ), + if (showBellIcon ?? false) + Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: colors.surfaceNegative, + borderRadius: + Zeta.of(context).radius.full, + ), + child: ZetaIcon( + ZetaIcons.important_notification, + color: colors.white, + size: Zeta.of(context) + .spacing + .large, + ), + ), + ].gap(Zeta.of(context).spacing.minimum), + ), + ], ), + body, + if (attachment != null) + Container( + padding: EdgeInsets.symmetric( + vertical: + Zeta.of(context).spacing.minimum, + ), + child: Row( + children: [ + ZetaIcon( + ZetaIcons.attachment, + size: Zeta.of(context).spacing.medium, + color: colors.primary, + ), + DefaultTextStyle( + style: ZetaTextStyles.bodyXSmall + .apply(color: colors.primary), + child: attachment!, + ), + ], + ), + ), ].gap(Zeta.of(context).spacing.minimum), ), - ], - ), - body, - ].gap(Zeta.of(context).spacing.minimum), - ), - ), - ].gap(Zeta.of(context).spacing.small), - ), - Container(alignment: Alignment.centerRight, child: action), - ], - ).paddingAll(Zeta.of(context).spacing.small), + ), + ].gap(Zeta.of(context).spacing.small), + ), + if (action != null) + Container( + alignment: Alignment.bottomRight, child: action), + ], + ).paddingAll(Zeta.of(context).spacing.small), + ), + ); + }, + ), ), ), ); @@ -142,8 +248,13 @@ class ZetaNotificationListItem extends ZetaStatelessWidget { return BoxDecoration( color: notificationRead ? colors.surfacePrimary : colors.surfaceSelected, borderRadius: Zeta.of(context).radius.rounded, - border: (showDivider ?? false) - ? Border(bottom: BorderSide(width: Zeta.of(context).spacing.minimum, color: colors.blue)) + boxShadow: (showDivider ?? false) + ? [ + BoxShadow( + color: colors.primary, + offset: Offset(0, Zeta.of(context).spacing.minimum), + ), + ] : null, ); } @@ -213,7 +324,9 @@ class ZetaNotificationBadge extends StatelessWidget { : ClipRRect( borderRadius: Zeta.of(context).radius.rounded, child: SizedBox.fromSize( - size: Size.square(Zeta.of(context).spacing.xl_8), // Image radius + size: Size.square( + Zeta.of(context).spacing.xl_8, + ), // Image radius child: image!.copyWith(fit: BoxFit.cover), ), );