diff --git a/example/lib/pages/components/comment.dart b/example/lib/pages/components/comment.dart index 3c74281..8a6a3f6 100644 --- a/example/lib/pages/components/comment.dart +++ b/example/lib/pages/components/comment.dart @@ -22,6 +22,29 @@ class _CommentDemoState extends State { ), author: 'John Doe', downloadCallback: () {}, + onMenuItemSelected: (val) { + print(val); + }, + menuItems: [ + ZdsPopupMenuItem( + value: 1, + child: Row( + children: [ + Icon(ZdsIcons.delete), + Text('Delete'), + ], + ), + ), + ZdsPopupMenuItem( + value: 2, + child: Row( + children: [ + Icon(ZetaIcons.reply), + Text('Reply'), + ], + ), + ), + ], comment: 'This is a comment', onReply: () { print('reply'); diff --git a/lib/src/components/molecules/comment.dart b/lib/src/components/molecules/comment.dart index aca998c..ce0eba9 100644 --- a/lib/src/components/molecules/comment.dart +++ b/lib/src/components/molecules/comment.dart @@ -21,6 +21,8 @@ class ZdsComment extends StatelessWidget { this.deleteSemanticLabel, this.replySemanticLabel, this.attachmentThumbnail, + this.menuItems, + this.onMenuItemSelected, }) : assert( onReply != null && replySemanticLabel != null || onReply == null && replySemanticLabel == null, 'replySemanticLabel must be not null if onReply is defined', @@ -72,6 +74,14 @@ class ZdsComment extends StatelessWidget { /// The custom thumbnail to display for the attachment. final Widget? attachmentThumbnail; + /// The menu items to display in the popup menu. + /// If defined, the pouup menu will be shown when the user taps on the comment. + final List>? menuItems; + + /// The callback to be called when a menu item is selected. + /// Menu items must be given a value for the callback to trigger. + final ValueChanged? onMenuItemSelected; + @override Widget build(BuildContext context) { final colors = Zeta.of(context).colors; @@ -117,73 +127,94 @@ class ZdsComment extends StatelessWidget { foregroundColor: colors.error, ), ], - child: Container( - decoration: BoxDecoration( - color: colors.surfaceDefault, - border: Border( - bottom: BorderSide( - color: colors.borderSubtle, + child: Builder( + builder: (context) { + final child = Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: colors.borderSubtle, + ), + ), ), - ), - ), - padding: EdgeInsets.symmetric( - vertical: spacing.large, - horizontal: spacing.medium, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: spacing.minimum), - child: Row( - children: [ - if (avatar != null) - Padding( - padding: EdgeInsets.only(right: spacing.small), - child: avatar, + padding: EdgeInsets.symmetric( + vertical: spacing.large, + horizontal: spacing.medium, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: spacing.minimum), + child: Row( + children: [ + if (avatar != null) + Padding( + padding: EdgeInsets.only(right: spacing.small), + child: avatar, + ), + if (author != null) + Text( + author!, + style: ZetaTextStyles.labelLarge.copyWith( + fontWeight: FontWeight.w500, + ), + ), + const Spacer(), + if (timeStamp != null) + Padding( + padding: EdgeInsets.only(left: spacing.small), + child: Text( + timeStamp!, + style: ZetaTextStyles.bodyXSmall.copyWith(color: colors.textSubtle), + ), + ), + ], + ), + ), + if (comment != null) + Padding( + padding: EdgeInsets.only( + top: spacing.small, + left: spacing.minimum, + right: spacing.minimum, ), - if (author != null) - Text( - author!, - style: ZetaTextStyles.labelLarge.copyWith( - fontWeight: FontWeight.w500, - ), + child: Text( + comment!, + style: Theme.of(context).textTheme.bodyMedium, ), - const Spacer(), - if (timeStamp != null) - Padding( - padding: EdgeInsets.only(left: spacing.small), - child: Text( - timeStamp!, - style: ZetaTextStyles.bodyXSmall.copyWith(color: colors.textSubtle), - ), + ), + if (attachment != null) + Padding( + padding: EdgeInsets.only(top: spacing.medium), + child: _AttachmentRow( + attachment: attachment!, + downloadCallback: downloadCallback, + customThumbnail: attachmentThumbnail, ), - ], - ), + ), + ], ), - if (comment != null) - Padding( - padding: EdgeInsets.only( - top: spacing.small, - left: spacing.minimum, - right: spacing.minimum, - ), - child: Text( - comment!, - style: Theme.of(context).textTheme.bodyMedium, - ), - ), - if (attachment != null) - Padding( - padding: EdgeInsets.only(top: spacing.medium), - child: _AttachmentRow( - attachment: attachment!, - downloadCallback: downloadCallback, - customThumbnail: attachmentThumbnail, - ), - ), - ], - ), + ); + if (menuItems != null) { + return ZdsPopupMenu( + menuPosition: ZdsPopupMenuPosition.topRight, + verticalOffset: spacing.small, + items: menuItems ?? [], + onSelected: onMenuItemSelected, + builder: (context, open) { + return Material( + color: colors.surfaceDefault, + child: InkWell( + onTap: open, + child: child, + ), + ); + }, + ); + } + return ColoredBox(color: colors.surfaceDefault, child: child); + }, ), ); }, @@ -206,7 +237,8 @@ class ZdsComment extends StatelessWidget { ..add(DiagnosticsProperty('attachment', attachment)) ..add(ObjectFlagProperty.has('downloadCallback', downloadCallback)) ..add(StringProperty('deleteSemanticLabel', deleteSemanticLabel)) - ..add(StringProperty('replySemanticLabel', replySemanticLabel)); + ..add(StringProperty('replySemanticLabel', replySemanticLabel)) + ..add(ObjectFlagProperty?>.has('onMenuItemSelected', onMenuItemSelected)); } } diff --git a/lib/src/components/molecules/menu.dart b/lib/src/components/molecules/menu.dart index 4b8457f..e446570 100644 --- a/lib/src/components/molecules/menu.dart +++ b/lib/src/components/molecules/menu.dart @@ -4,6 +4,21 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../../../../zds_flutter.dart'; +/// Defines the position of a [ZdsPopupMenu]. +enum ZdsPopupMenuPosition { + /// The menu will appear at the top left of the button. + topLeft, + + /// The menu will appear at the top right of the button. + topRight, + + /// The menu will appear at the bottom left below the button. + bottomLeft, + + /// The menu will appear at the bottom right below the button. + bottomRight, +} + /// Creates a popup menu. /// /// This component is typically used to display more options that do not fit in a [ZdsAppBar], or to show more @@ -34,15 +49,19 @@ import '../../../../zds_flutter.dart'; /// See also: /// /// * [ZdsPopupMenuItem], used to create the options that appear in this menu. +/// * [ZdsPopupMenuPosition], defines the position of a menu. /// * [ZdsAppBar], where this component is used to show more actions that do not typically fit. class ZdsPopupMenu extends StatefulWidget { /// Creates a pop up menu. const ZdsPopupMenu({ required this.builder, required this.items, + this.menuPosition = ZdsPopupMenuPosition.bottomLeft, super.key, this.onCanceled, this.onSelected, + this.verticalOffset = 0, + this.horizontalOffset = 0, }) : assert(items.length > 0, 'Must have at least 1 item'); /// Defines how this component will appear on screen. @@ -60,6 +79,15 @@ class ZdsPopupMenu extends StatefulWidget { /// A function called whenever an item is selected. final PopupMenuItemSelected? onSelected; + /// The position of the menu. + final ZdsPopupMenuPosition menuPosition; + + /// The vertical offset of the menu. + final double verticalOffset; + + /// The horizontal offset of the menu. + final double horizontalOffset; + /// A function called whenever the user doesn't select an item and instead closes the menu. final PopupMenuCanceled? onCanceled; @@ -71,7 +99,10 @@ class ZdsPopupMenu extends StatefulWidget { properties ..add(ObjectFlagProperty.has('builder', builder)) ..add(ObjectFlagProperty?>.has('onSelected', onSelected)) - ..add(ObjectFlagProperty.has('onCanceled', onCanceled)); + ..add(ObjectFlagProperty.has('onCanceled', onCanceled)) + ..add(EnumProperty('menuPosition', menuPosition)) + ..add(DoubleProperty('verticalOffset', verticalOffset)) + ..add(DoubleProperty('horizontalOffset', horizontalOffset)); } } @@ -89,9 +120,26 @@ class ZdsPopupMenuState extends State> { } final RenderBox button = _key.currentContext!.findRenderObject()! as RenderBox; final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox; + + final double verticalPosition = switch (widget.menuPosition) { + ZdsPopupMenuPosition.topLeft || ZdsPopupMenuPosition.topRight => 0, + ZdsPopupMenuPosition.bottomLeft || ZdsPopupMenuPosition.bottomRight => button.size.height, + }; + + final double horizontalPosition = switch (widget.menuPosition) { + ZdsPopupMenuPosition.topLeft || ZdsPopupMenuPosition.bottomLeft => 0, + ZdsPopupMenuPosition.topRight || ZdsPopupMenuPosition.bottomRight => button.size.width, + }; + final RelativeRect position = RelativeRect.fromRect( Rect.fromPoints( - button.localToGlobal(Offset(0, button.size.height), ancestor: overlay), + button.localToGlobal( + Offset( + horizontalPosition + widget.horizontalOffset, + verticalPosition + widget.verticalOffset, + ), + ancestor: overlay, + ), button.localToGlobal( button.size.bottomRight(Offset.zero) + Offset(0, button.size.height), ancestor: overlay,