TF-4195 Show submenu when hover Label As item
This commit is contained in:
@@ -3,16 +3,23 @@ import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef OnPopupMenuActionClick = void Function(PopupMenuItemAction action);
|
||||
typedef OnHoverShowSubmenu = void Function(GlobalKey key);
|
||||
|
||||
abstract class PopupMenuItemAction<T> with EquatableMixin {
|
||||
final T action;
|
||||
final String? key;
|
||||
final int category;
|
||||
final Widget? submenu;
|
||||
|
||||
PopupMenuItemAction(this.action, {this.key, this.category = -1});
|
||||
PopupMenuItemAction(
|
||||
this.action, {
|
||||
this.key,
|
||||
this.category = -1,
|
||||
this.submenu,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [action, key, category];
|
||||
List<Object?> get props => [action, key, category, submenu];
|
||||
|
||||
String get actionName;
|
||||
|
||||
@@ -40,9 +47,22 @@ mixin OptionalPopupSelectedIcon<T> {
|
||||
double get selectedIconSize => 16.0;
|
||||
}
|
||||
|
||||
mixin OptionalPopupHoverIcon {
|
||||
String get hoverIcon;
|
||||
|
||||
Color get hoverIconColor => AppColor.steelGrayA540;
|
||||
|
||||
double get hoverIconSize => 16.0;
|
||||
}
|
||||
|
||||
abstract class PopupMenuItemActionRequiredIcon<T> extends PopupMenuItemAction<T>
|
||||
with OptionalPopupIcon {
|
||||
PopupMenuItemActionRequiredIcon(super.action, {super.key, super.category});
|
||||
with OptionalPopupIcon, OptionalPopupHoverIcon {
|
||||
PopupMenuItemActionRequiredIcon(
|
||||
super.action, {
|
||||
super.key,
|
||||
super.category,
|
||||
super.submenu,
|
||||
});
|
||||
}
|
||||
|
||||
abstract class PopupMenuItemActionRequiredSelectedIcon<T>
|
||||
@@ -54,6 +74,7 @@ abstract class PopupMenuItemActionRequiredSelectedIcon<T>
|
||||
this.selectedAction, {
|
||||
super.key,
|
||||
super.category,
|
||||
super.submenu,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,6 +87,7 @@ abstract class PopupMenuItemActionRequiredFull<T> extends PopupMenuItemAction<T>
|
||||
this.selectedAction, {
|
||||
super.key,
|
||||
super.category,
|
||||
super.submenu,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,5 +101,6 @@ abstract class PopupMenuItemActionRequiredIconWithMultipleSelected<T>
|
||||
this.selectedActions, {
|
||||
super.key,
|
||||
super.category,
|
||||
super.submenu,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:tmail_ui_user/features/base/extensions/popup_menu_action_list_ex
|
||||
import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/base/model/popup_menu_item_action.dart';
|
||||
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart';
|
||||
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_submenu_controller.dart';
|
||||
|
||||
typedef OnPopupMenuActionSelected = void Function(PopupMenuItemAction action);
|
||||
|
||||
@@ -11,11 +12,13 @@ class PopupMenuActionGroupWidget with PopupContextMenuActionMixin {
|
||||
final List<PopupMenuItemAction> actions;
|
||||
final OnPopupMenuActionSelected onActionSelected;
|
||||
final double dividerOpacity;
|
||||
final PopupSubmenuController? submenuController;
|
||||
|
||||
const PopupMenuActionGroupWidget({
|
||||
PopupMenuActionGroupWidget({
|
||||
required this.actions,
|
||||
required this.onActionSelected,
|
||||
this.dividerOpacity = 0.12,
|
||||
this.submenuController,
|
||||
});
|
||||
|
||||
Future<void> show(
|
||||
@@ -37,6 +40,15 @@ class PopupMenuActionGroupWidget with PopupContextMenuActionMixin {
|
||||
Navigator.pop(context);
|
||||
onActionSelected(menuAction);
|
||||
},
|
||||
onHoverShowSubmenu: submenuController != null && menuAction.submenu != null
|
||||
? (itemKey) => _showPopupSubmenu(
|
||||
context: context,
|
||||
itemKey: itemKey,
|
||||
submenuController: submenuController!,
|
||||
submenu: menuAction.submenu!,
|
||||
)
|
||||
: null,
|
||||
onHoverOtherItem: submenuController?.hide,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -50,4 +62,23 @@ class PopupMenuActionGroupWidget with PopupContextMenuActionMixin {
|
||||
|
||||
return openPopupMenuAction(context, position, popupMenuItems);
|
||||
}
|
||||
|
||||
void _showPopupSubmenu({
|
||||
required BuildContext context,
|
||||
required GlobalKey itemKey,
|
||||
required PopupSubmenuController submenuController,
|
||||
required Widget submenu,
|
||||
}) {
|
||||
final renderBox = itemKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (renderBox == null) return;
|
||||
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
final rect = offset & renderBox.size;
|
||||
|
||||
submenuController.show(
|
||||
context: context,
|
||||
anchor: rect,
|
||||
submenu: submenu,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,27 +2,52 @@ import 'package:core/presentation/extensions/color_extension.dart';
|
||||
import 'package:core/presentation/utils/theme_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:model/email/email_action_type.dart';
|
||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
||||
import 'package:tmail_ui_user/features/base/model/popup_menu_item_action.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/model/popup_menu_item_email_action.dart';
|
||||
|
||||
class PopupMenuItemActionWidget extends StatelessWidget {
|
||||
class PopupMenuItemActionWidget extends StatefulWidget {
|
||||
final PopupMenuItemAction menuAction;
|
||||
final OnPopupMenuActionClick menuActionClick;
|
||||
final OnHoverShowSubmenu? onHoverShowSubmenu;
|
||||
final VoidCallback? onHoverOtherItem;
|
||||
|
||||
const PopupMenuItemActionWidget({
|
||||
super.key,
|
||||
required this.menuAction,
|
||||
required this.menuActionClick,
|
||||
this.onHoverShowSubmenu,
|
||||
this.onHoverOtherItem,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PopupMenuItemActionWidget> createState() =>
|
||||
_PopupMenuItemActionWidgetState();
|
||||
}
|
||||
|
||||
class _PopupMenuItemActionWidgetState extends State<PopupMenuItemActionWidget> {
|
||||
GlobalKey? _itemKey;
|
||||
ValueNotifier<bool>? _hoverItemNotifier;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.menuAction is PopupMenuItemEmailAction) {
|
||||
_itemKey = GlobalKey();
|
||||
_hoverItemNotifier = ValueNotifier(false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget? iconWidget;
|
||||
Widget? selectedIconWidget;
|
||||
bool isSelected = false;
|
||||
|
||||
if (menuAction is PopupMenuItemActionRequiredIcon) {
|
||||
final specificMenuAction = menuAction as PopupMenuItemActionRequiredIcon;
|
||||
if (widget.menuAction is PopupMenuItemActionRequiredIcon) {
|
||||
final specificMenuAction =
|
||||
widget.menuAction as PopupMenuItemActionRequiredIcon;
|
||||
iconWidget = Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||
child: SvgPicture.asset(
|
||||
@@ -33,9 +58,9 @@ class PopupMenuItemActionWidget extends StatelessWidget {
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
);
|
||||
} else if (menuAction is PopupMenuItemActionRequiredSelectedIcon) {
|
||||
} else if (widget.menuAction is PopupMenuItemActionRequiredSelectedIcon) {
|
||||
final specificMenuAction =
|
||||
menuAction as PopupMenuItemActionRequiredSelectedIcon;
|
||||
widget.menuAction as PopupMenuItemActionRequiredSelectedIcon;
|
||||
selectedIconWidget = Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 16),
|
||||
child: SvgPicture.asset(
|
||||
@@ -46,34 +71,11 @@ class PopupMenuItemActionWidget extends StatelessWidget {
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
);
|
||||
isSelected = specificMenuAction.selectedAction == menuAction.action;
|
||||
} else if (menuAction is PopupMenuItemActionRequiredFull) {
|
||||
final specificMenuAction = menuAction as PopupMenuItemActionRequiredFull;
|
||||
iconWidget = Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||
child: SvgPicture.asset(
|
||||
specificMenuAction.actionIcon,
|
||||
width: specificMenuAction.actionIconSize,
|
||||
height: specificMenuAction.actionIconSize,
|
||||
colorFilter: specificMenuAction.actionIconColor.asFilter(),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
);
|
||||
selectedIconWidget = Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 16),
|
||||
child: SvgPicture.asset(
|
||||
specificMenuAction.selectedIcon,
|
||||
width: specificMenuAction.selectedIconSize,
|
||||
height: specificMenuAction.selectedIconSize,
|
||||
colorFilter: specificMenuAction.selectedIconColor.asFilter(),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
);
|
||||
isSelected = specificMenuAction.selectedAction == menuAction.action;
|
||||
} else if (menuAction
|
||||
is PopupMenuItemActionRequiredIconWithMultipleSelected) {
|
||||
isSelected =
|
||||
specificMenuAction.selectedAction == widget.menuAction.action;
|
||||
} else if (widget.menuAction is PopupMenuItemActionRequiredFull) {
|
||||
final specificMenuAction =
|
||||
menuAction as PopupMenuItemActionRequiredIconWithMultipleSelected;
|
||||
widget.menuAction as PopupMenuItemActionRequiredFull;
|
||||
iconWidget = Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||
child: SvgPicture.asset(
|
||||
@@ -95,27 +97,137 @@ class PopupMenuItemActionWidget extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
isSelected =
|
||||
specificMenuAction.selectedActions.contains(menuAction.action);
|
||||
specificMenuAction.selectedAction == widget.menuAction.action;
|
||||
} else if (widget.menuAction
|
||||
is PopupMenuItemActionRequiredIconWithMultipleSelected) {
|
||||
final specificMenuAction = widget.menuAction
|
||||
as PopupMenuItemActionRequiredIconWithMultipleSelected;
|
||||
iconWidget = Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||
child: SvgPicture.asset(
|
||||
specificMenuAction.actionIcon,
|
||||
width: specificMenuAction.actionIconSize,
|
||||
height: specificMenuAction.actionIconSize,
|
||||
colorFilter: specificMenuAction.actionIconColor.asFilter(),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
);
|
||||
selectedIconWidget = Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 16),
|
||||
child: SvgPicture.asset(
|
||||
specificMenuAction.selectedIcon,
|
||||
width: specificMenuAction.selectedIconSize,
|
||||
height: specificMenuAction.selectedIconSize,
|
||||
colorFilter: specificMenuAction.selectedIconColor.asFilter(),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
);
|
||||
isSelected =
|
||||
specificMenuAction.selectedActions.contains(widget.menuAction.action);
|
||||
}
|
||||
|
||||
if (widget.menuAction is PopupMenuItemEmailAction &&
|
||||
widget.menuAction.action == EmailActionType.labelAs) {
|
||||
final specificMenuAction = widget.menuAction as PopupMenuItemEmailAction;
|
||||
|
||||
return PointerInterceptor(
|
||||
child: MouseRegion(
|
||||
onEnter: (_) {
|
||||
if (_hoverItemNotifier != null) {
|
||||
_hoverItemNotifier?.value = true;
|
||||
}
|
||||
if (_itemKey != null) {
|
||||
widget.onHoverShowSubmenu?.call(_itemKey!);
|
||||
} else {
|
||||
widget.onHoverOtherItem?.call();
|
||||
}
|
||||
},
|
||||
onExit: (_) {
|
||||
if (_hoverItemNotifier != null) {
|
||||
_hoverItemNotifier?.value = false;
|
||||
}
|
||||
},
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () => specificMenuAction.onClick(widget.menuActionClick),
|
||||
hoverColor: AppColor.popupMenuItemHovered,
|
||||
child: Container(
|
||||
key: _itemKey,
|
||||
height: 48,
|
||||
width: double.infinity,
|
||||
padding: specificMenuAction.itemPadding,
|
||||
child: Row(
|
||||
children: [
|
||||
if (iconWidget != null) iconWidget,
|
||||
Expanded(
|
||||
child: Text(
|
||||
specificMenuAction.actionName,
|
||||
style: ThemeUtils.textStyleBodyBody3(
|
||||
color: specificMenuAction.actionNameColor,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (isSelected && selectedIconWidget != null)
|
||||
selectedIconWidget,
|
||||
if (_hoverItemNotifier != null)
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _hoverItemNotifier!,
|
||||
builder: (_, value, __) {
|
||||
if (value) {
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsetsDirectional.only(start: 16),
|
||||
child: SvgPicture.asset(
|
||||
specificMenuAction.hoverIcon,
|
||||
width: specificMenuAction.hoverIconSize,
|
||||
height: specificMenuAction.hoverIconSize,
|
||||
colorFilter: specificMenuAction.hoverIconColor
|
||||
.asFilter(),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return PointerInterceptor(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () => menuAction.onClick(menuActionClick),
|
||||
onTap: () => widget.menuAction.onClick(widget.menuActionClick),
|
||||
hoverColor: AppColor.popupMenuItemHovered,
|
||||
onHover: (_) {
|
||||
if (_hoverItemNotifier != null) {
|
||||
_hoverItemNotifier?.value = false;
|
||||
}
|
||||
widget.onHoverOtherItem?.call();
|
||||
},
|
||||
child: Container(
|
||||
key: _itemKey,
|
||||
height: 48,
|
||||
width: double.infinity,
|
||||
padding: menuAction.itemPadding,
|
||||
padding: widget.menuAction.itemPadding,
|
||||
child: Row(
|
||||
children: [
|
||||
if (iconWidget != null) iconWidget,
|
||||
Expanded(
|
||||
child: Text(
|
||||
menuAction.actionName,
|
||||
widget.menuAction.actionName,
|
||||
style: ThemeUtils.textStyleBodyBody3(
|
||||
color: menuAction.actionNameColor,
|
||||
color: widget.menuAction.actionNameColor,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -130,4 +242,13 @@ class PopupMenuItemActionWidget extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_hoverItemNotifier != null) {
|
||||
_hoverItemNotifier?.dispose();
|
||||
_hoverItemNotifier = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum SubmenuDirection { left, right, auto }
|
||||
|
||||
class PopupSubmenuController {
|
||||
OverlayEntry? _submenuEntry;
|
||||
|
||||
bool get isShowing => _submenuEntry != null;
|
||||
|
||||
void show({
|
||||
required BuildContext context,
|
||||
required Rect anchor,
|
||||
required Widget submenu,
|
||||
SubmenuDirection direction = SubmenuDirection.auto,
|
||||
double submenuWidth = 249,
|
||||
double submenuMaxHeight = 400,
|
||||
double offset = 0,
|
||||
}) {
|
||||
hide();
|
||||
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
final rightPosition = anchor.right + offset;
|
||||
final leftPosition = anchor.left - submenuWidth - offset;
|
||||
|
||||
bool canShowRight = rightPosition + submenuWidth <= screenWidth;
|
||||
bool canShowLeft = leftPosition >= 0;
|
||||
|
||||
double? finalLeft;
|
||||
|
||||
if (direction == SubmenuDirection.right) {
|
||||
finalLeft = rightPosition;
|
||||
} else if (direction == SubmenuDirection.left) {
|
||||
finalLeft = leftPosition;
|
||||
} else {
|
||||
if (canShowRight) {
|
||||
finalLeft = rightPosition;
|
||||
} else if (canShowLeft) {
|
||||
finalLeft = leftPosition;
|
||||
} else {
|
||||
finalLeft = rightPosition;
|
||||
}
|
||||
}
|
||||
|
||||
_submenuEntry = OverlayEntry(
|
||||
builder: (_) {
|
||||
return Positioned(
|
||||
left: finalLeft,
|
||||
top: anchor.top,
|
||||
child: MouseRegion(
|
||||
onExit: (_) => hide(),
|
||||
child: Material(
|
||||
elevation: 8,
|
||||
color: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: submenuWidth,
|
||||
height: submenuMaxHeight,
|
||||
child: submenu,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_submenuEntry!);
|
||||
}
|
||||
|
||||
void hide() {
|
||||
_submenuEntry?.remove();
|
||||
_submenuEntry = null;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user