Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(UX-1342): Added popup menu to ZdsComment #52

Merged
merged 1 commit into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions example/lib/pages/components/comment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,29 @@ class _CommentDemoState extends State<CommentDemo> {
),
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');
Expand Down
158 changes: 95 additions & 63 deletions lib/src/components/molecules/comment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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<ZdsPopupMenuItem<int>>? 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<int>? onMenuItemSelected;

@override
Widget build(BuildContext context) {
final colors = Zeta.of(context).colors;
Expand Down Expand Up @@ -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<int>(
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);
},
),
);
},
Expand All @@ -206,7 +237,8 @@ class ZdsComment extends StatelessWidget {
..add(DiagnosticsProperty<ZdsChatAttachment?>('attachment', attachment))
..add(ObjectFlagProperty<VoidCallback?>.has('downloadCallback', downloadCallback))
..add(StringProperty('deleteSemanticLabel', deleteSemanticLabel))
..add(StringProperty('replySemanticLabel', replySemanticLabel));
..add(StringProperty('replySemanticLabel', replySemanticLabel))
..add(ObjectFlagProperty<ValueChanged<int>?>.has('onMenuItemSelected', onMenuItemSelected));
}
}

Expand Down
52 changes: 50 additions & 2 deletions lib/src/components/molecules/menu.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<T> 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.
Expand All @@ -60,6 +79,15 @@ class ZdsPopupMenu<T> extends StatefulWidget {
/// A function called whenever an item is selected.
final PopupMenuItemSelected<T>? 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;

Expand All @@ -71,7 +99,10 @@ class ZdsPopupMenu<T> extends StatefulWidget {
properties
..add(ObjectFlagProperty<Widget Function(BuildContext p1, VoidCallback p2)>.has('builder', builder))
..add(ObjectFlagProperty<PopupMenuItemSelected<T>?>.has('onSelected', onSelected))
..add(ObjectFlagProperty<PopupMenuCanceled?>.has('onCanceled', onCanceled));
..add(ObjectFlagProperty<PopupMenuCanceled?>.has('onCanceled', onCanceled))
..add(EnumProperty<ZdsPopupMenuPosition>('menuPosition', menuPosition))
..add(DoubleProperty('verticalOffset', verticalOffset))
..add(DoubleProperty('horizontalOffset', horizontalOffset));
}
}

Expand All @@ -89,9 +120,26 @@ class ZdsPopupMenuState<T> extends State<ZdsPopupMenu<T>> {
}
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,
Expand Down
Loading