Переглянути джерело

chore: replace overlay with popover (#1250)

Nathan.fooo 2 роки тому
батько
коміт
ca8be6ab10

+ 0 - 1
frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart

@@ -68,7 +68,6 @@ class _SettingButtonState extends State<_SettingButton> {
       child: FlowyIconButton(
         hoverColor: theme.hover,
         width: 22,
-        onPressed: () {},
         icon: Padding(
           padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 3.0),
           child: svgWidget("grid/setting/setting", color: theme.iconColor),

+ 28 - 53
frontend/app_flowy/lib/plugins/doc/document.dart

@@ -11,12 +11,11 @@ import 'package:app_flowy/workspace/presentation/home/toast.dart';
 import 'package:app_flowy/workspace/presentation/widgets/left_bar_item.dart';
 import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:clipboard/clipboard.dart';
-import 'package:dartz/dartz.dart' as dartz;
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra/theme.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/widget/rounded_button.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
@@ -130,7 +129,6 @@ class DocumentShareButton extends StatelessWidget {
         },
         child: BlocBuilder<DocShareBloc, DocShareState>(
           builder: (context, state) {
-            final theme = context.watch<AppTheme>();
             return ChangeNotifierProvider.value(
               value: Provider.of<AppearanceSetting>(context, listen: true),
               child: Selector<AppearanceSetting, Locale>(
@@ -140,14 +138,7 @@ class DocumentShareButton extends StatelessWidget {
                     height: 30,
                     width: 100,
                   ),
-                  child: RoundedTextButton(
-                    title: LocaleKeys.shareAction_buttonText.tr(),
-                    fontSize: 12,
-                    borderRadius: Corners.s6Border,
-                    color: theme.main1,
-                    onPressed: () =>
-                        _showActionList(context, const Offset(0, 10)),
-                  ),
+                  child: const ShareActionList(),
                 ),
               ),
             );
@@ -171,11 +162,30 @@ class DocumentShareButton extends StatelessWidget {
   }
 
   void _handleExportError(FlowyError error) {}
+}
+
+class ShareActionList extends StatelessWidget {
+  const ShareActionList({Key? key}) : super(key: key);
 
-  void _showActionList(BuildContext context, Offset offset) {
-    final actionList = ShareActions(onSelected: (result) {
-      result.fold(() {}, (action) {
-        switch (action) {
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return PopoverActionList<ShareActionWrapper>(
+      direction: PopoverDirection.bottomWithCenterAligned,
+      actions: ShareAction.values
+          .map((action) => ShareActionWrapper(action))
+          .toList(),
+      withChild: (controller) {
+        return RoundedTextButton(
+          title: LocaleKeys.shareAction_buttonText.tr(),
+          fontSize: 12,
+          borderRadius: Corners.s6Border,
+          color: theme.main1,
+          onPressed: () => controller.show(),
+        );
+      },
+      onSelected: (action, controller) {
+        switch (action.inner) {
           case ShareAction.markdown:
             context
                 .read<DocShareBloc>()
@@ -189,53 +199,18 @@ class DocumentShareButton extends StatelessWidget {
                 .show(context);
             break;
         }
-      });
-    });
-    actionList.show(
-      context,
-      anchorDirection: AnchorDirection.bottomWithRightAligned,
-      anchorOffset: offset,
+        controller.close();
+      },
     );
   }
 }
 
-class ShareActions with ActionList<ShareActionWrapper>, FlowyOverlayDelegate {
-  final Function(dartz.Option<ShareAction>) onSelected;
-  final _items =
-      ShareAction.values.map((action) => ShareActionWrapper(action)).toList();
-
-  ShareActions({required this.onSelected});
-
-  @override
-  double get itemHeight => 22;
-
-  @override
-  List<ShareActionWrapper> get items => _items;
-
-  @override
-  void Function(dartz.Option<ShareActionWrapper> p1) get selectCallback =>
-      (result) {
-        result.fold(
-          () => onSelected(dartz.none()),
-          (wrapper) => onSelected(
-            dartz.some(wrapper.inner),
-          ),
-        );
-      };
-
-  @override
-  FlowyOverlayDelegate? get delegate => this;
-
-  @override
-  void didRemove() => onSelected(dartz.none());
-}
-
 enum ShareAction {
   markdown,
   copyLink,
 }
 
-class ShareActionWrapper extends ActionItem {
+class ShareActionWrapper extends ActionCell {
   final ShareAction inner;
 
   ShareActionWrapper(this.inner);

+ 73 - 47
frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart

@@ -1,22 +1,21 @@
 import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
+import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:expandable/expandable.dart';
 import 'package:flowy_infra/icon_data.dart';
 import 'package:flowy_infra/theme.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:app_flowy/workspace/application/app/app_bloc.dart';
 import 'package:styled_widget/styled_widget.dart';
-import 'package:dartz/dartz.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:flowy_infra/image.dart';
 
 import '../menu_app.dart';
 import 'add_button.dart';
-import 'right_click_action.dart';
 
 class MenuAppHeader extends StatelessWidget {
   final AppPB app;
@@ -79,30 +78,23 @@ class MenuAppHeader extends StatelessWidget {
             expandableController.toggle();
           }
         },
-        child: GestureDetector(
-          behavior: HitTestBehavior.opaque,
-          onTap: () => ExpandableController.of(context,
-                  rebuildOnChange: false, required: true)
-              ?.toggle(),
-          onSecondaryTap: () {
-            final actionList = AppDisclosureActionSheet(
-              onSelected: (action) => _handleAction(context, action),
-            );
-            actionList.show(
-              context,
-              anchorDirection: AnchorDirection.bottomWithCenterAligned,
-            );
-          },
-          child: BlocSelector<AppBloc, AppState, AppPB>(
-            selector: (state) => state.app,
-            builder: (context, app) => FlowyText.medium(
-              app.name,
-              fontSize: 12,
-              color: theme.textColor,
-              overflow: TextOverflow.ellipsis,
-            ),
-          ),
-        ),
+        child: AppActionList(onSelected: (action) {
+          switch (action) {
+            case AppDisclosureAction.rename:
+              NavigatorTextFieldDialog(
+                title: LocaleKeys.menuAppHeader_renameDialog.tr(),
+                value: context.read<AppBloc>().state.app.name,
+                confirm: (newValue) {
+                  context.read<AppBloc>().add(AppEvent.rename(newValue));
+                },
+              ).show(context);
+
+              break;
+            case AppDisclosureAction.delete:
+              context.read<AppBloc>().add(const AppEvent.delete());
+              break;
+          }
+        }),
       ),
     );
   }
@@ -123,26 +115,6 @@ class MenuAppHeader extends StatelessWidget {
       ).padding(right: MenuAppSizes.headerPadding),
     );
   }
-
-  void _handleAction(BuildContext context, Option<AppDisclosureAction> action) {
-    action.fold(() {}, (action) {
-      switch (action) {
-        case AppDisclosureAction.rename:
-          NavigatorTextFieldDialog(
-            title: LocaleKeys.menuAppHeader_renameDialog.tr(),
-            value: context.read<AppBloc>().state.app.name,
-            confirm: (newValue) {
-              context.read<AppBloc>().add(AppEvent.rename(newValue));
-            },
-          ).show(context);
-
-          break;
-        case AppDisclosureAction.delete:
-          context.read<AppBloc>().add(const AppEvent.delete());
-          break;
-      }
-    });
-  }
 }
 
 enum AppDisclosureAction {
@@ -169,3 +141,57 @@ extension AppDisclosureExtension on AppDisclosureAction {
     }
   }
 }
+
+class AppActionList extends StatelessWidget {
+  final Function(AppDisclosureAction) onSelected;
+  const AppActionList({
+    required this.onSelected,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.read<AppTheme>();
+    return PopoverActionList<DisclosureActionWrapper>(
+      direction: PopoverDirection.bottomWithCenterAligned,
+      actions: AppDisclosureAction.values
+          .map((action) => DisclosureActionWrapper(action))
+          .toList(),
+      withChild: (controller) {
+        return GestureDetector(
+          behavior: HitTestBehavior.opaque,
+          onTap: () => ExpandableController.of(context,
+                  rebuildOnChange: false, required: true)
+              ?.toggle(),
+          onSecondaryTap: () {
+            controller.show();
+          },
+          child: BlocSelector<AppBloc, AppState, AppPB>(
+            selector: (state) => state.app,
+            builder: (context, app) => FlowyText.medium(
+              app.name,
+              fontSize: 12,
+              color: theme.textColor,
+              overflow: TextOverflow.ellipsis,
+            ),
+          ),
+        );
+      },
+      onSelected: (action, controller) {
+        onSelected(action.inner);
+        controller.close();
+      },
+    );
+  }
+}
+
+class DisclosureActionWrapper extends ActionCell {
+  final AppDisclosureAction inner;
+
+  DisclosureActionWrapper(this.inner);
+  @override
+  Widget? icon(Color iconColor) => inner.icon(iconColor);
+
+  @override
+  String get name => inner.name;
+}

+ 0 - 51
frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/right_click_action.dart

@@ -1,51 +0,0 @@
-import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
-import 'package:dartz/dartz.dart' as dartz;
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flutter/material.dart';
-
-import 'header.dart';
-
-class AppDisclosureActionSheet
-    with ActionList<DisclosureActionWrapper>, FlowyOverlayDelegate {
-  final Function(dartz.Option<AppDisclosureAction>) onSelected;
-  final _items = AppDisclosureAction.values
-      .map((action) => DisclosureActionWrapper(action))
-      .toList();
-
-  AppDisclosureActionSheet({
-    required this.onSelected,
-  });
-
-  @override
-  List<DisclosureActionWrapper> get items => _items;
-
-  @override
-  void Function(dartz.Option<DisclosureActionWrapper> p1) get selectCallback =>
-      (result) {
-        result.fold(
-          () => onSelected(dartz.none()),
-          (wrapper) => onSelected(
-            dartz.some(wrapper.inner),
-          ),
-        );
-      };
-
-  @override
-  FlowyOverlayDelegate? get delegate => this;
-
-  @override
-  void didRemove() {
-    onSelected(dartz.none());
-  }
-}
-
-class DisclosureActionWrapper extends ActionItem {
-  final AppDisclosureAction inner;
-
-  DisclosureActionWrapper(this.inner);
-  @override
-  Widget? icon(Color iconColor) => inner.icon(iconColor);
-
-  @override
-  String get name => inner.name;
-}

+ 0 - 130
frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart

@@ -1,130 +0,0 @@
-import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
-import 'package:dartz/dartz.dart' as dartz;
-import 'package:flowy_infra/image.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flowy_infra_ui/style_widget/icon_button.dart';
-import 'package:flutter/gestures.dart';
-import 'package:flutter/material.dart';
-import 'package:flowy_infra/theme.dart';
-import 'package:provider/provider.dart';
-
-import 'item.dart';
-
-// [[Widget: LifeCycle]]
-// https://flutterbyexample.com/lesson/stateful-widget-lifecycle
-class ViewDisclosureButton extends StatelessWidget
-    with ActionList<ViewDisclosureActionWrapper>, FlowyOverlayDelegate {
-  final Function() onTap;
-  final Function(dartz.Option<ViewDisclosureAction>) onSelected;
-  final _items = ViewDisclosureAction.values
-      .map((action) => ViewDisclosureActionWrapper(action))
-      .toList();
-
-  ViewDisclosureButton({
-    Key? key,
-    required this.onTap,
-    required this.onSelected,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    final theme = context.watch<AppTheme>();
-    return FlowyIconButton(
-      iconPadding: const EdgeInsets.all(5),
-      width: 26,
-      onPressed: () {
-        onTap();
-        show(context);
-      },
-      icon: svgWidget("editor/details", color: theme.iconColor),
-    );
-  }
-
-  @override
-  List<ViewDisclosureActionWrapper> get items => _items;
-
-  @override
-  void Function(dartz.Option<ViewDisclosureActionWrapper> p1)
-      get selectCallback => (result) {
-            result.fold(
-              () => onSelected(dartz.none()),
-              (wrapper) => onSelected(dartz.some(wrapper.inner)),
-            );
-          };
-
-  @override
-  FlowyOverlayDelegate? get delegate => this;
-
-  @override
-  void didRemove() {
-    onSelected(dartz.none());
-  }
-}
-
-class ViewDisclosureRegion extends StatelessWidget
-    with ActionList<ViewDisclosureActionWrapper>, FlowyOverlayDelegate {
-  final Widget child;
-  final Function() onTap;
-  final Function(dartz.Option<ViewDisclosureAction>) onSelected;
-  final _items = ViewDisclosureAction.values
-      .map((action) => ViewDisclosureActionWrapper(action))
-      .toList();
-
-  ViewDisclosureRegion(
-      {Key? key,
-      required this.onSelected,
-      required this.onTap,
-      required this.child})
-      : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return Listener(
-      onPointerDown: (event) => _handleClick(event, context),
-      child: child,
-    );
-  }
-
-  @override
-  FlowyOverlayDelegate? get delegate => this;
-
-  @override
-  List<ViewDisclosureActionWrapper> get items => _items;
-
-  @override
-  void Function(dartz.Option<ViewDisclosureActionWrapper> p1)
-      get selectCallback => (result) {
-            result.fold(
-              () => onSelected(dartz.none()),
-              (wrapper) => onSelected(dartz.some(wrapper.inner)),
-            );
-          };
-
-  @override
-  void didRemove() {
-    onSelected(dartz.none());
-  }
-
-  void _handleClick(PointerDownEvent event, BuildContext context) {
-    if (event.kind == PointerDeviceKind.mouse &&
-        event.buttons == kSecondaryMouseButton) {
-      RenderBox box = context.findRenderObject() as RenderBox;
-      Offset position = box.localToGlobal(Offset.zero);
-      double x = event.position.dx - position.dx - box.size.width;
-      double y = event.position.dy - position.dy - box.size.height;
-      onTap();
-      show(context, anchorOffset: Offset(x, y));
-    }
-  }
-}
-
-class ViewDisclosureActionWrapper extends ActionItem {
-  final ViewDisclosureAction inner;
-
-  ViewDisclosureActionWrapper(this.inner);
-  @override
-  Widget? icon(Color iconColor) => inner.icon(iconColor);
-
-  @override
-  String get name => inner.name;
-}

+ 100 - 56
frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart

@@ -3,7 +3,6 @@ import 'package:app_flowy/workspace/application/view/view_bloc.dart';
 import 'package:app_flowy/workspace/application/view/view_ext.dart';
 import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
 import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
-import 'package:dartz/dartz.dart' as dartz;
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/style_widget/hover.dart';
@@ -16,7 +15,9 @@ import 'package:styled_widget/styled_widget.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:flowy_infra/image.dart';
 
-import 'disclosure_action.dart';
+import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 
 // ignore: must_be_immutable
 class ViewSectionItem extends StatelessWidget {
@@ -37,40 +38,41 @@ class ViewSectionItem extends StatelessWidget {
     return MultiBlocProvider(
       providers: [
         BlocProvider(
-            create: (ctx) =>
-                getIt<ViewBloc>(param1: view)..add(const ViewEvent.initial())),
+            create: (ctx) => getIt<ViewBloc>(param1: view)
+              ..add(
+                const ViewEvent.initial(),
+              )),
       ],
       child: BlocBuilder<ViewBloc, ViewState>(
-        builder: (context, state) {
-          return ViewDisclosureRegion(
-              onTap: () => context
-                  .read<ViewBloc>()
-                  .add(const ViewEvent.setIsEditing(true)),
-              onSelected: (action) {
-                context
-                    .read<ViewBloc>()
-                    .add(const ViewEvent.setIsEditing(false));
-                _handleAction(context, action);
-              },
-              child: Padding(
-                padding: const EdgeInsets.symmetric(vertical: 2),
-                child: InkWell(
-                  onTap: () => onSelected(context.read<ViewBloc>().state.view),
-                  child: FlowyHover(
-                    style: HoverStyle(hoverColor: theme.bg3),
-                    builder: (_, onHover) =>
-                        _render(context, onHover, state, theme.iconColor),
-                    setSelected: () => state.isEditing || isSelected,
-                  ),
+        builder: (blocContext, state) {
+          return Padding(
+            padding: const EdgeInsets.symmetric(vertical: 2),
+            child: InkWell(
+              onTap: () => onSelected(blocContext.read<ViewBloc>().state.view),
+              child: FlowyHover(
+                style: HoverStyle(hoverColor: theme.bg3),
+                buildWhen: () => !state.isEditing,
+                builder: (_, onHover) => _render(
+                  blocContext,
+                  onHover,
+                  state,
+                  theme.iconColor,
                 ),
-              ));
+                isSelected: () => state.isEditing || isSelected,
+              ),
+            ),
+          );
         },
       ),
     );
   }
 
   Widget _render(
-      BuildContext context, bool onHover, ViewState state, Color iconColor) {
+    BuildContext blocContext,
+    bool onHover,
+    ViewState state,
+    Color iconColor,
+  ) {
     List<Widget> children = [
       SizedBox(
         width: 16,
@@ -90,11 +92,29 @@ class ViewSectionItem extends StatelessWidget {
     if (onHover || state.isEditing) {
       children.add(
         ViewDisclosureButton(
-          onTap: () =>
-              context.read<ViewBloc>().add(const ViewEvent.setIsEditing(true)),
-          onSelected: (action) {
-            context.read<ViewBloc>().add(const ViewEvent.setIsEditing(false));
-            _handleAction(context, action);
+          onEdit: (isEdit) =>
+              blocContext.read<ViewBloc>().add(ViewEvent.setIsEditing(isEdit)),
+          onAction: (action) {
+            switch (action) {
+              case ViewDisclosureAction.rename:
+                NavigatorTextFieldDialog(
+                  title: LocaleKeys.disclosureAction_rename.tr(),
+                  value: blocContext.read<ViewBloc>().state.view.name,
+                  confirm: (newValue) {
+                    blocContext
+                        .read<ViewBloc>()
+                        .add(ViewEvent.rename(newValue));
+                  },
+                ).show(blocContext);
+
+                break;
+              case ViewDisclosureAction.delete:
+                blocContext.read<ViewBloc>().add(const ViewEvent.delete());
+                break;
+              case ViewDisclosureAction.duplicate:
+                blocContext.read<ViewBloc>().add(const ViewEvent.duplicate());
+                break;
+            }
           },
         ),
       );
@@ -108,30 +128,6 @@ class ViewSectionItem extends StatelessWidget {
       ),
     );
   }
-
-  void _handleAction(
-      BuildContext context, dartz.Option<ViewDisclosureAction> action) {
-    action.foldRight({}, (action, previous) {
-      switch (action) {
-        case ViewDisclosureAction.rename:
-          NavigatorTextFieldDialog(
-            title: LocaleKeys.disclosureAction_rename.tr(),
-            value: context.read<ViewBloc>().state.view.name,
-            confirm: (newValue) {
-              context.read<ViewBloc>().add(ViewEvent.rename(newValue));
-            },
-          ).show(context);
-
-          break;
-        case ViewDisclosureAction.delete:
-          context.read<ViewBloc>().add(const ViewEvent.delete());
-          break;
-        case ViewDisclosureAction.duplicate:
-          context.read<ViewBloc>().add(const ViewEvent.duplicate());
-          break;
-      }
-    });
-  }
 }
 
 enum ViewDisclosureAction {
@@ -163,3 +159,51 @@ extension ViewDisclosureExtension on ViewDisclosureAction {
     }
   }
 }
+
+class ViewDisclosureButton extends StatelessWidget {
+  final Function(bool) onEdit;
+  final Function(ViewDisclosureAction) onAction;
+  const ViewDisclosureButton({
+    required this.onEdit,
+    required this.onAction,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return PopoverActionList<ViewDisclosureActionWrapper>(
+      direction: PopoverDirection.bottomWithCenterAligned,
+      actions: ViewDisclosureAction.values
+          .map((action) => ViewDisclosureActionWrapper(action))
+          .toList(),
+      withChild: (controller) {
+        return FlowyIconButton(
+          iconPadding: const EdgeInsets.all(5),
+          width: 26,
+          icon: svgWidget("editor/details", color: theme.iconColor),
+          onPressed: () {
+            onEdit(true);
+            controller.show();
+          },
+        );
+      },
+      onSelected: (action, controller) {
+        onEdit(false);
+        onAction(action.inner);
+        controller.close();
+      },
+    );
+  }
+}
+
+class ViewDisclosureActionWrapper extends ActionCell {
+  final ViewDisclosureAction inner;
+
+  ViewDisclosureActionWrapper(this.inner);
+  @override
+  Widget? icon(Color iconColor) => inner.icon(iconColor);
+
+  @override
+  String get name => inner.name;
+}

+ 73 - 98
frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart

@@ -1,16 +1,15 @@
 import 'package:app_flowy/startup/tasks/rust_sdk.dart';
 import 'package:app_flowy/workspace/presentation/home/toast.dart';
 import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/theme.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:provider/provider.dart';
-import 'package:dartz/dartz.dart' as dartz;
 import 'package:styled_widget/styled_widget.dart';
 import 'package:package_info_plus/package_info_plus.dart';
 import 'package:url_launcher/url_launcher.dart';
@@ -22,41 +21,59 @@ class QuestionBubble extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    final theme = context.watch<AppTheme>();
-    return SizedBox(
+    return const SizedBox(
       width: 30,
       height: 30,
-      child: FlowyTextButton(
-        '?',
-        tooltip: LocaleKeys.questionBubble_help.tr(),
-        fontSize: 12,
-        fontWeight: FontWeight.w600,
-        fillColor: theme.selector,
-        mainAxisAlignment: MainAxisAlignment.center,
-        radius: BorderRadius.circular(10),
-        onPressed: () {
-          final actionList = QuestionBubbleActionSheet(onSelected: (result) {
-            result.fold(() {}, (action) {
-              switch (action) {
-                case BubbleAction.whatsNews:
-                  _launchURL("https://www.appflowy.io/whatsnew");
-                  break;
-                case BubbleAction.help:
-                  _launchURL("https://discord.gg/9Q2xaN37tV");
-                  break;
-                case BubbleAction.debug:
-                  _DebugToast().show();
-                  break;
-              }
-            });
-          });
-          actionList.show(
-            context,
-            anchorDirection: AnchorDirection.topWithRightAligned,
-            anchorOffset: const Offset(0, -10),
-          );
-        },
-      ),
+      child: BubbleActionList(),
+    );
+  }
+}
+
+class BubbleActionList extends StatelessWidget {
+  const BubbleActionList({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+
+    final List<PopoverAction> actions = [];
+    actions.addAll(
+      BubbleAction.values.map((action) => BubbleActionWrapper(action)),
+    );
+    actions.add(FlowyVersionDescription());
+
+    return PopoverActionList<PopoverAction>(
+      direction: PopoverDirection.topWithRightAligned,
+      actions: actions,
+      withChild: (controller) {
+        return FlowyTextButton(
+          '?',
+          tooltip: LocaleKeys.questionBubble_help.tr(),
+          fontSize: 12,
+          fontWeight: FontWeight.w600,
+          fillColor: theme.selector,
+          mainAxisAlignment: MainAxisAlignment.center,
+          radius: BorderRadius.circular(10),
+          onPressed: () => controller.show(),
+        );
+      },
+      onSelected: (action, controller) {
+        if (action is BubbleActionWrapper) {
+          switch (action.inner) {
+            case BubbleAction.whatsNews:
+              _launchURL("https://www.appflowy.io/whatsnew");
+              break;
+            case BubbleAction.help:
+              _launchURL("https://discord.gg/9Q2xaN37tV");
+              break;
+            case BubbleAction.debug:
+              _DebugToast().show();
+              break;
+          }
+        }
+
+        controller.close();
+      },
     );
   }
 
@@ -101,54 +118,9 @@ class _DebugToast {
   }
 }
 
-class QuestionBubbleActionSheet
-    with ActionList<BubbleActionWrapper>, FlowyOverlayDelegate {
-  final Function(dartz.Option<BubbleAction>) onSelected;
-  final _items =
-      BubbleAction.values.map((action) => BubbleActionWrapper(action)).toList();
-
-  QuestionBubbleActionSheet({
-    required this.onSelected,
-  });
-
-  @override
-  double get itemHeight => 22;
-
-  @override
-  List<BubbleActionWrapper> get items => _items;
-
-  @override
-  void Function(dartz.Option<BubbleActionWrapper> p1) get selectCallback =>
-      (result) {
-        result.fold(
-          () => onSelected(dartz.none()),
-          (wrapper) => onSelected(
-            dartz.some(wrapper.inner),
-          ),
-        );
-      };
-
-  @override
-  FlowyOverlayDelegate? get delegate => this;
-
+class FlowyVersionDescription extends CustomActionCell {
   @override
-  void didRemove() {
-    onSelected(dartz.none());
-  }
-
-  @override
-  ListOverlayFooter? get footer => ListOverlayFooter(
-        widget: const FlowyVersionDescription(),
-        height: 40,
-        padding: const EdgeInsets.only(top: 6),
-      );
-}
-
-class FlowyVersionDescription extends StatelessWidget {
-  const FlowyVersionDescription({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
+  Widget buildWithContext(BuildContext context) {
     final theme = context.watch<AppTheme>();
 
     return FutureBuilder(
@@ -165,23 +137,26 @@ class FlowyVersionDescription extends StatelessWidget {
           String version = packageInfo.version;
           String buildNumber = packageInfo.buildNumber;
 
-          return Column(
-            mainAxisAlignment: MainAxisAlignment.start,
-            crossAxisAlignment: CrossAxisAlignment.start,
-            children: [
-              Divider(height: 1, color: theme.shader6, thickness: 1.0),
-              const VSpace(6),
-              FlowyText(
-                "$appName $version.$buildNumber",
-                fontSize: 12,
-                color: theme.shader4,
-              ),
-            ],
-          ).padding(
-            horizontal: ActionListSizes.itemHPadding + ActionListSizes.hPadding,
+          return SizedBox(
+            height: 30,
+            child: Column(
+              mainAxisAlignment: MainAxisAlignment.start,
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Divider(height: 1, color: theme.shader6, thickness: 1.0),
+                const VSpace(6),
+                FlowyText(
+                  "$appName $version.$buildNumber",
+                  fontSize: 12,
+                  color: theme.shader4,
+                ),
+              ],
+            ).padding(
+              horizontal: ActionListSizes.itemHPadding,
+            ),
           );
         } else {
-          return const CircularProgressIndicator();
+          return const SizedBox(height: 30);
         }
       },
     );
@@ -190,7 +165,7 @@ class FlowyVersionDescription extends StatelessWidget {
 
 enum BubbleAction { whatsNews, help, debug }
 
-class BubbleActionWrapper extends ActionItem {
+class BubbleActionWrapper extends ActionCell {
   final BubbleAction inner;
 
   BubbleActionWrapper(this.inner);

+ 78 - 46
frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/hover.dart';
@@ -6,66 +7,90 @@ import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 import 'package:styled_widget/styled_widget.dart';
-import 'package:dartz/dartz.dart' as dartz;
 
-abstract class ActionList<T extends ActionItem> {
-  List<T> get items;
+class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
+  final List<T> actions;
+  final Function(T, PopoverController) onSelected;
+  final BoxConstraints constraints;
+  final PopoverDirection direction;
+  final Widget Function(PopoverController) withChild;
 
-  String get identifier => toString();
-
-  double get maxWidth => 300;
+  const PopoverActionList({
+    required this.actions,
+    required this.withChild,
+    required this.onSelected,
+    this.direction = PopoverDirection.rightWithTopAligned,
+    this.constraints = const BoxConstraints(
+      minWidth: 120,
+      maxWidth: 360,
+      maxHeight: 300,
+    ),
+    Key? key,
+  }) : super(key: key);
 
-  double get minWidth => 120;
+  @override
+  State<PopoverActionList<T>> createState() => _PopoverActionListState<T>();
+}
 
-  double get itemHeight => ActionListSizes.itemHeight;
+class _PopoverActionListState<T extends PopoverAction>
+    extends State<PopoverActionList<T>> {
+  late PopoverController popoverController;
 
-  ListOverlayFooter? get footer => null;
+  @override
+  void initState() {
+    popoverController = PopoverController();
+    super.initState();
+  }
 
-  void Function(dartz.Option<T>) get selectCallback;
+  @override
+  Widget build(BuildContext context) {
+    final child = widget.withChild(popoverController);
 
-  FlowyOverlayDelegate? get delegate;
+    return AppFlowyPopover(
+      controller: popoverController,
+      constraints: widget.constraints,
+      direction: widget.direction,
+      triggerActions: PopoverTriggerFlags.none,
+      popupBuilder: (BuildContext popoverContext) {
+        final List<Widget> children = widget.actions.map((action) {
+          if (action is ActionCell) {
+            return ActionCellWidget<T>(
+              action: action,
+              itemHeight: ActionListSizes.itemHeight,
+              onSelected: (action) {
+                widget.onSelected(action, popoverController);
+              },
+            );
+          } else {
+            final custom = action as CustomActionCell;
+            return custom.buildWithContext(context);
+          }
+        }).toList();
 
-  void show(
-    BuildContext buildContext, {
-    BuildContext? anchorContext,
-    AnchorDirection anchorDirection = AnchorDirection.bottomRight,
-    Offset? anchorOffset,
-  }) {
-    ListOverlay.showWithAnchor(
-      buildContext,
-      identifier: identifier,
-      itemCount: items.length,
-      itemBuilder: (context, index) {
-        final action = items[index];
-        return ActionCell<T>(
-          action: action,
-          itemHeight: itemHeight,
-          onSelected: (action) {
-            FlowyOverlay.of(buildContext).remove(identifier);
-            selectCallback(dartz.some(action));
-          },
+        return IntrinsicHeight(
+          child: IntrinsicWidth(
+            child: Column(
+              children: children,
+            ),
+          ),
         );
       },
-      anchorContext: anchorContext ?? buildContext,
-      anchorDirection: anchorDirection,
-      constraints: BoxConstraints(
-        minHeight: items.length * (itemHeight + ActionListSizes.vPadding * 2),
-        maxHeight: items.length * (itemHeight + ActionListSizes.vPadding * 2),
-        maxWidth: maxWidth,
-        minWidth: minWidth,
-      ),
-      delegate: delegate,
-      anchorOffset: anchorOffset,
-      footer: footer,
+      child: child,
     );
   }
 }
 
-abstract class ActionItem {
+abstract class ActionCell extends PopoverAction {
   Widget? icon(Color iconColor);
   String get name;
 }
 
+abstract class CustomActionCell extends PopoverAction {
+  Widget buildWithContext(BuildContext context);
+}
+
+abstract class PopoverAction {}
+
 class ActionListSizes {
   static double itemHPadding = 10;
   static double itemHeight = 20;
@@ -73,11 +98,11 @@ class ActionListSizes {
   static double hPadding = 10;
 }
 
-class ActionCell<T extends ActionItem> extends StatelessWidget {
+class ActionCellWidget<T extends PopoverAction> extends StatelessWidget {
   final T action;
   final Function(T) onSelected;
   final double itemHeight;
-  const ActionCell({
+  const ActionCellWidget({
     Key? key,
     required this.action,
     required this.onSelected,
@@ -86,8 +111,9 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    final actionCell = action as ActionCell;
     final theme = context.watch<AppTheme>();
-    final icon = action.icon(theme.iconColor);
+    final icon = actionCell.icon(theme.iconColor);
 
     return FlowyHover(
       style: HoverStyle(hoverColor: theme.hover),
@@ -99,7 +125,13 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
           child: Row(
             children: [
               if (icon != null) ...[icon, HSpace(ActionListSizes.itemHPadding)],
-              FlowyText.medium(action.name, fontSize: 12),
+              Expanded(
+                child: FlowyText.medium(
+                  actionCell.name,
+                  fontSize: 12,
+                  overflow: TextOverflow.visible,
+                ),
+              ),
             ],
           ),
         ).padding(

+ 12 - 3
frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart

@@ -11,7 +11,7 @@ class AppFlowyPopover extends StatelessWidget {
   final Widget Function(BuildContext context) popupBuilder;
   final PopoverDirection direction;
   final int triggerActions;
-  final BoxConstraints? constraints;
+  final BoxConstraints constraints;
   final void Function()? onClose;
   final PopoverMutex? mutex;
   final Offset? offset;
@@ -58,12 +58,12 @@ class AppFlowyPopover extends StatelessWidget {
 
 class _PopoverContainer extends StatelessWidget {
   final Widget child;
-  final BoxConstraints? constraints;
+  final BoxConstraints constraints;
   final EdgeInsets margin;
   const _PopoverContainer({
     required this.child,
     required this.margin,
-    this.constraints,
+    required this.constraints,
     Key? key,
   }) : super(key: key);
 
@@ -74,6 +74,7 @@ class _PopoverContainer extends StatelessWidget {
       theme.surface,
       theme.shadowColor.withOpacity(0.15),
     );
+
     return Material(
       type: MaterialType.transparency,
       child: Container(
@@ -81,6 +82,14 @@ class _PopoverContainer extends StatelessWidget {
         decoration: decoration,
         constraints: constraints,
         child: child,
+
+        // SingleChildScrollView(
+        //   scrollDirection: Axis.horizontal,
+        //   child: ConstrainedBox(
+        //     constraints: constraints,
+        //     child: child,
+        //   ),
+        // ),
       ),
     );
   }

+ 1 - 1
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart

@@ -37,7 +37,7 @@ class FlowyButton extends StatelessWidget {
           hoverColor: hoverColor,
         ),
         onHover: onHover,
-        setSelected: () => isSelected,
+        isSelected: () => isSelected,
         builder: (context, onHover) => _render(),
       ),
     );

+ 28 - 18
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/hover.dart

@@ -8,19 +8,21 @@ class FlowyHover extends StatefulWidget {
   final HoverStyle style;
   final HoverBuilder? builder;
   final Widget? child;
-  final bool Function()? setSelected;
+  final bool Function()? isSelected;
   final void Function(bool)? onHover;
   final MouseCursor? cursor;
+  final bool Function()? buildWhen;
 
-  const FlowyHover(
-      {Key? key,
-      this.builder,
-      this.child,
-      required this.style,
-      this.setSelected,
-      this.onHover,
-      this.cursor})
-      : super(key: key);
+  const FlowyHover({
+    Key? key,
+    this.builder,
+    this.child,
+    required this.style,
+    this.isSelected,
+    this.onHover,
+    this.cursor,
+    this.buildWhen,
+  }) : super(key: key);
 
   @override
   State<FlowyHover> createState() => _FlowyHoverState();
@@ -35,15 +37,23 @@ class _FlowyHoverState extends State<FlowyHover> {
       cursor: widget.cursor != null ? widget.cursor! : SystemMouseCursors.click,
       opaque: false,
       onEnter: (p) {
-        setState(() => _onHover = true);
-        if (widget.onHover != null) {
-          widget.onHover!(true);
+        if (_onHover) return;
+
+        if (widget.buildWhen?.call() ?? true) {
+          setState(() => _onHover = true);
+          if (widget.onHover != null) {
+            widget.onHover!(true);
+          }
         }
       },
       onExit: (p) {
-        setState(() => _onHover = false);
-        if (widget.onHover != null) {
-          widget.onHover!(false);
+        if (_onHover == false) return;
+
+        if (widget.buildWhen?.call() ?? true) {
+          setState(() => _onHover = false);
+          if (widget.onHover != null) {
+            widget.onHover!(false);
+          }
         }
       },
       child: renderWidget(),
@@ -52,8 +62,8 @@ class _FlowyHoverState extends State<FlowyHover> {
 
   Widget renderWidget() {
     var showHover = _onHover;
-    if (!showHover && widget.setSelected != null) {
-      showHover = widget.setSelected!();
+    if (!showHover && widget.isSelected != null) {
+      showHover = widget.isSelected!();
     }
 
     final child = widget.child ?? widget.builder!(context, _onHover);