Browse Source

Merge pull request #1085 from AppFlowy-IO/fix/popover_bugs

Fix/popover bugs
Nathan.fooo 2 years ago
parent
commit
39b0fe69b5
20 changed files with 404 additions and 380 deletions
  1. 2 2
      frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart
  2. 0 1
      frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart
  3. 56 35
      frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart
  4. 61 42
      frontend/app_flowy/lib/plugins/board/presentation/card/card.dart
  5. 28 34
      frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart
  6. 53 55
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart
  7. 3 45
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart
  8. 3 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart
  9. 30 30
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart
  10. 108 30
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart
  11. 0 56
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_name_input.dart
  12. 0 4
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_list.dart
  13. 2 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart
  14. 3 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart
  15. 10 22
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart
  16. 1 7
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart
  17. 2 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart
  18. 4 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart
  19. 0 3
      frontend/app_flowy/lib/workspace/presentation/widgets/dialogs.dart
  20. 38 7
      frontend/app_flowy/packages/appflowy_popover/lib/popover.dart

+ 2 - 2
frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart

@@ -23,7 +23,7 @@ class EditableRowNotifier {
   EditableRowNotifier({required bool isEditing})
       : isEditing = ValueNotifier(isEditing);
 
-  void insertCell(
+  void bindCell(
     GridCellIdentifier cellIdentifier,
     EditableCellNotifier notifier,
   ) {
@@ -59,7 +59,7 @@ class EditableRowNotifier {
     _cells.values.first.isCellEditing.value = false;
   }
 
-  void clear() {
+  void unbind() {
     for (final notifier in _cells.values) {
       notifier.dispose();
     }

+ 0 - 1
frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart

@@ -3,7 +3,6 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_servic
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-
 import 'define.dart';
 
 class BoardNumberCell extends StatefulWidget {

+ 56 - 35
frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart

@@ -2,6 +2,8 @@ import 'package:app_flowy/plugins/board/application/card/board_select_option_cel
 import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
 import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart';
 import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart';
+import 'package:appflowy_popover/popover.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
@@ -26,9 +28,11 @@ class BoardSelectOptionCell extends StatefulWidget with EditableCell {
 
 class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
   late BoardSelectOptionCellBloc _cellBloc;
+  late PopoverController _popover;
 
   @override
   void initState() {
+    _popover = PopoverController();
     final cellController =
         widget.cellControllerBuilder.build() as GridSelectOptionCellController;
     _cellBloc = BoardSelectOptionCellBloc(cellController: cellController)
@@ -41,43 +45,60 @@ class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
     return BlocProvider.value(
       value: _cellBloc,
       child: BlocBuilder<BoardSelectOptionCellBloc, BoardSelectOptionCellState>(
-        buildWhen: (previous, current) {
-          return previous.selectedOptions != current.selectedOptions;
-        },
-        builder: (context, state) {
-          if (state.selectedOptions
-                  .where((element) => element.id == widget.groupId)
-                  .isNotEmpty ||
-              state.selectedOptions.isEmpty) {
-            return const SizedBox();
-          } else {
-            final children = state.selectedOptions
-                .map(
-                  (option) => SelectOptionTag.fromOption(
-                    context: context,
-                    option: option,
-                    onSelected: () {
-                      SelectOptionCellEditor.show(
-                        context: context,
-                        cellController: widget.cellControllerBuilder.build()
-                            as GridSelectOptionCellController,
-                      );
-                    },
-                  ),
-                )
-                .toList();
+          buildWhen: (previous, current) {
+        return previous.selectedOptions != current.selectedOptions;
+      }, builder: (context, state) {
+        // Returns SizedBox if the content of the cell is empty
+        if (_isEmpty(state)) return const SizedBox();
 
-            return IntrinsicHeight(
-              child: Padding(
-                padding: const EdgeInsets.symmetric(vertical: 6),
-                child: SizedBox.expand(
-                  child: Wrap(spacing: 4, runSpacing: 2, children: children),
-                ),
-              ),
+        final children = state.selectedOptions.map(
+          (option) {
+            final tag = SelectOptionTag.fromOption(
+              context: context,
+              option: option,
+              onSelected: () => _popover.show(),
             );
-          }
-        },
-      ),
+            return _wrapPopover(tag);
+          },
+        ).toList();
+
+        return IntrinsicHeight(
+          child: Padding(
+            padding: const EdgeInsets.symmetric(vertical: 6),
+            child: SizedBox.expand(
+              child: Wrap(spacing: 4, runSpacing: 2, children: children),
+            ),
+          ),
+        );
+      }),
+    );
+  }
+
+  bool _isEmpty(BoardSelectOptionCellState state) {
+    // The cell should hide if the option id is equal to the groupId.
+    final isInGroup = state.selectedOptions
+        .where((element) => element.id == widget.groupId)
+        .isNotEmpty;
+    return isInGroup || state.selectedOptions.isEmpty;
+  }
+
+  Widget _wrapPopover(Widget child) {
+    final constraints = BoxConstraints.loose(Size(
+      SelectOptionCellEditor.editorPanelWidth,
+      300,
+    ));
+    return AppFlowyStylePopover(
+      controller: _popover,
+      constraints: constraints,
+      direction: PopoverDirection.bottomWithLeftAligned,
+      popupBuilder: (BuildContext context) {
+        return SelectOptionCellEditor(
+          cellController: widget.cellControllerBuilder.build()
+              as GridSelectOptionCellController,
+        );
+      },
+      onClose: () {},
+      child: child,
     );
   }
 

+ 61 - 42
frontend/app_flowy/lib/plugins/board/presentation/card/card.dart

@@ -1,6 +1,5 @@
 import 'package:app_flowy/plugins/board/application/card/card_bloc.dart';
 import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart';
-import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
 import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_action_sheet.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
@@ -64,10 +63,16 @@ class _BoardCardState extends State<BoardCard> {
       value: _cardBloc,
       child: BlocBuilder<BoardCardBloc, BoardCardState>(
         buildWhen: (previous, current) {
+          // Rebuild when:
+          // 1.If the lenght of the cells is not the same
+          // 2.isEditing changed
           if (previous.cells.length != current.cells.length ||
               previous.isEditing != current.isEditing) {
             return true;
           }
+
+          // 3.Compare the content of the cells. The cells consisits of
+          // list of [BoardCellEquatable] that extends the [Equatable].
           return !listEquals(previous.cells, current.cells);
         },
         builder: (context, state) {
@@ -75,21 +80,16 @@ class _BoardCardState extends State<BoardCard> {
             buildAccessoryWhen: () => state.isEditing == false,
             accessoryBuilder: (context) {
               return [
-                _CardEditOption(
-                  startEditing: () => rowNotifier.becomeFirstResponder(),
-                ),
+                _CardEditOption(rowNotifier: rowNotifier),
                 const _CardMoreOption(),
               ];
             },
-            onTap: (context) {
-              widget.openCard(context);
-            },
-            child: Column(
-              mainAxisSize: MainAxisSize.min,
-              children: _makeCells(
-                context,
-                state.cells.map((cell) => cell.identifier).toList(),
-              ),
+            onTap: (context) => widget.openCard(context),
+            child: _CellColumn(
+              groupId: widget.groupId,
+              rowNotifier: rowNotifier,
+              cellBuilder: widget.cellBuilder,
+              cells: state.cells,
             ),
           );
         },
@@ -97,36 +97,62 @@ class _BoardCardState extends State<BoardCard> {
     );
   }
 
+  @override
+  Future<void> dispose() async {
+    rowNotifier.dispose();
+    _cardBloc.close();
+    super.dispose();
+  }
+}
+
+class _CellColumn extends StatelessWidget {
+  final String groupId;
+  final BoardCellBuilder cellBuilder;
+  final EditableRowNotifier rowNotifier;
+  final List<BoardCellEquatable> cells;
+  const _CellColumn({
+    required this.groupId,
+    required this.rowNotifier,
+    required this.cellBuilder,
+    required this.cells,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      children: _makeCells(context, cells),
+    );
+  }
+
   List<Widget> _makeCells(
     BuildContext context,
-    List<GridCellIdentifier> cells,
+    List<BoardCellEquatable> cells,
   ) {
     final List<Widget> children = [];
-    rowNotifier.clear();
+    // Remove all the cell listeners.
+    rowNotifier.unbind();
+
     cells.asMap().forEach(
-      (int index, GridCellIdentifier cellId) {
-        EditableCellNotifier cellNotifier;
+      (int index, BoardCellEquatable cell) {
+        final isEditing = index == 0 ? rowNotifier.isEditing.value : false;
+        final cellNotifier = EditableCellNotifier(isEditing: isEditing);
+
         if (index == 0) {
           // Only use the first cell to receive user's input when click the edit
           // button
-          cellNotifier = EditableCellNotifier(
-            isEditing: rowNotifier.isEditing.value,
-          );
-          rowNotifier.insertCell(cellId, cellNotifier);
-        } else {
-          cellNotifier = EditableCellNotifier();
+          rowNotifier.bindCell(cell.identifier, cellNotifier);
         }
 
-        Widget child = widget.cellBuilder.buildCell(
-          widget.groupId,
-          cellId,
-          cellNotifier,
-        );
-
-        child = Padding(
-          key: cellId.key(),
+        final child = Padding(
+          key: cell.identifier.key(),
           padding: const EdgeInsets.only(left: 4, right: 4),
-          child: child,
+          child: cellBuilder.buildCell(
+            groupId,
+            cell.identifier,
+            cellNotifier,
+          ),
         );
 
         children.add(child);
@@ -134,13 +160,6 @@ class _BoardCardState extends State<BoardCard> {
     );
     return children;
   }
-
-  @override
-  Future<void> dispose() async {
-    rowNotifier.dispose();
-    _cardBloc.close();
-    super.dispose();
-  }
 }
 
 class _CardMoreOption extends StatelessWidget with CardAccessory {
@@ -164,9 +183,9 @@ class _CardMoreOption extends StatelessWidget with CardAccessory {
 }
 
 class _CardEditOption extends StatelessWidget with CardAccessory {
-  final VoidCallback startEditing;
+  final EditableRowNotifier rowNotifier;
   const _CardEditOption({
-    required this.startEditing,
+    required this.rowNotifier,
     Key? key,
   }) : super(key: key);
 
@@ -183,6 +202,6 @@ class _CardEditOption extends StatelessWidget with CardAccessory {
 
   @override
   void onTap(BuildContext context) {
-    startEditing();
+    rowNotifier.becomeFirstResponder();
   }
 }

+ 28 - 34
frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart

@@ -72,52 +72,46 @@ class CardAccessoryContainer extends StatelessWidget {
   Widget build(BuildContext context) {
     final theme = context.read<AppTheme>();
     final children = accessories.map((accessory) {
-      final hover = FlowyHover(
-        style: HoverStyle(
-          hoverColor: theme.hover,
-          backgroundColor: theme.surface,
-          borderRadius: BorderRadius.zero,
-        ),
-        builder: (_, onHover) => SizedBox(
-          width: 24,
-          height: 24,
-          child: accessory,
-        ),
-      );
       return GestureDetector(
         behavior: HitTestBehavior.opaque,
         onTap: () => accessory.onTap(context),
-        child: hover,
+        child: _wrapHover(theme, accessory),
       );
     }).toList();
+    return _wrapDecoration(context, Row(children: children));
+  }
 
+  FlowyHover _wrapHover(AppTheme theme, CardAccessory accessory) {
+    return FlowyHover(
+      style: HoverStyle(
+        hoverColor: theme.hover,
+        backgroundColor: theme.surface,
+        borderRadius: BorderRadius.zero,
+      ),
+      builder: (_, onHover) => SizedBox(
+        width: 24,
+        height: 24,
+        child: accessory,
+      ),
+    );
+  }
+
+  Widget _wrapDecoration(BuildContext context, Widget child) {
+    final theme = context.read<AppTheme>();
+    final borderSide = BorderSide(color: theme.shader6, width: 1.0);
+    final decoration = BoxDecoration(
+      color: Colors.transparent,
+      border: Border.fromBorderSide(borderSide),
+      borderRadius: const BorderRadius.all(Radius.circular(4)),
+    );
     return Container(
       clipBehavior: Clip.hardEdge,
-      decoration: _makeBoxDecoration(context),
-      child: Row(children: children),
+      decoration: decoration,
+      child: child,
     );
   }
 }
 
-BoxDecoration _makeBoxDecoration(BuildContext context) {
-  final theme = context.read<AppTheme>();
-  final borderSide = BorderSide(color: theme.shader6, width: 1.0);
-  return BoxDecoration(
-    color: Colors.transparent,
-    border: Border.fromBorderSide(borderSide),
-    // boxShadow: const [
-    //   BoxShadow(
-    //     color: Colors.transparent,
-    //     spreadRadius: 0,
-    //     blurRadius: 5,
-    //     offset: Offset.zero,
-    //   )
-    // ],
-
-    borderRadius: const BorderRadius.all(Radius.circular(4)),
-  );
-}
-
 class _CardEnterRegion extends StatelessWidget {
   final Widget child;
   final List<CardAccessory> accessories;

+ 53 - 55
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart

@@ -164,67 +164,65 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    final Widget child;
-    if (widget.selectOptions.isEmpty && widget.cellStyle != null) {
-      child = Align(
-        alignment: Alignment.centerLeft,
-        child: FlowyText.medium(
-          widget.cellStyle!.placeholder,
-          fontSize: 14,
-          color: theme.shader3,
-        ),
-      );
-    } else {
-      child = Align(
-        alignment: Alignment.centerLeft,
-        child: Wrap(
-          spacing: 4,
-          runSpacing: 2,
-          children: widget.selectOptions
-              .map((option) => SelectOptionTag.fromOption(
-                    context: context,
-                    option: option,
-                  ))
-              .toList(),
-        ),
-      );
-    }
+    Widget child = _buildOptions(theme, context);
 
     return Stack(
       alignment: AlignmentDirectional.center,
       fit: StackFit.expand,
       children: [
-        AppFlowyStylePopover(
-          controller: _popover,
-          constraints: BoxConstraints.loose(
-              Size(SelectOptionCellEditor.editorPanelWidth, 300)),
-          offset: const Offset(0, 20),
-          direction: PopoverDirection.bottomWithLeftAligned,
-          // triggerActions: PopoverTriggerActionFlags.c,
-          popupBuilder: (BuildContext context) {
-            WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
-              widget.onFocus?.call(true);
-            });
-            return SizedBox(
-              width: SelectOptionCellEditor.editorPanelWidth,
-              child: SelectOptionCellEditor(
-                cellController: widget.cellControllerBuilder.build()
-                    as GridSelectOptionCellController,
-                onDismissed: () {
-                  widget.onFocus?.call(false);
-                },
-              ),
-            );
-          },
-          onClose: () {
-            widget.onFocus?.call(false);
-          },
-          child: child,
-        ),
-        InkWell(onTap: () {
-          _popover.show();
-        }),
+        _wrapPopover(child),
+        InkWell(onTap: () => _popover.show()),
       ],
     );
   }
+
+  Widget _wrapPopover(Widget child) {
+    final constraints = BoxConstraints.loose(Size(
+      SelectOptionCellEditor.editorPanelWidth,
+      300,
+    ));
+    return AppFlowyStylePopover(
+      controller: _popover,
+      constraints: constraints,
+      direction: PopoverDirection.bottomWithLeftAligned,
+      popupBuilder: (BuildContext context) {
+        WidgetsBinding.instance.addPostFrameCallback((_) {
+          widget.onFocus?.call(true);
+        });
+        return SelectOptionCellEditor(
+          cellController: widget.cellControllerBuilder.build()
+              as GridSelectOptionCellController,
+        );
+      },
+      onClose: () => widget.onFocus?.call(false),
+      child: child,
+    );
+  }
+
+  Widget _buildOptions(AppTheme theme, BuildContext context) {
+    final Widget child;
+    if (widget.selectOptions.isEmpty && widget.cellStyle != null) {
+      child = FlowyText.medium(
+        widget.cellStyle!.placeholder,
+        fontSize: 14,
+        color: theme.shader3,
+      );
+    } else {
+      final children = widget.selectOptions.map(
+        (option) {
+          return SelectOptionTag.fromOption(
+            context: context,
+            option: option,
+          );
+        },
+      ).toList();
+
+      child = Wrap(
+        spacing: 4,
+        runSpacing: 2,
+        children: children,
+      );
+    }
+    return Align(alignment: Alignment.centerLeft, child: child);
+  }
 }

+ 3 - 45
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart

@@ -25,17 +25,13 @@ import 'text_field.dart';
 
 const double _editorPanelWidth = 300;
 
-class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate {
+class SelectOptionCellEditor extends StatelessWidget {
   final GridSelectOptionCellController cellController;
-  final VoidCallback? onDismissed;
 
   static double editorPanelWidth = 300;
 
-  const SelectOptionCellEditor({
-    required this.cellController,
-    this.onDismissed,
-    Key? key,
-  }) : super(key: key);
+  const SelectOptionCellEditor({required this.cellController, Key? key})
+      : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -60,44 +56,6 @@ class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate {
       ),
     );
   }
-
-  static void show({
-    required BuildContext context,
-    required GridSelectOptionCellController cellController,
-    VoidCallback? onDismissed,
-  }) {
-    SelectOptionCellEditor.remove(context);
-    final editor = SelectOptionCellEditor(
-      cellController: cellController,
-      onDismissed: onDismissed,
-    );
-
-    //
-    FlowyOverlay.of(context).insertWithAnchor(
-      widget: OverlayContainer(
-        constraints: BoxConstraints.loose(const Size(_editorPanelWidth, 300)),
-        child: SizedBox(width: _editorPanelWidth, child: editor),
-      ),
-      identifier: SelectOptionCellEditor.identifier(),
-      anchorContext: context,
-      anchorDirection: AnchorDirection.bottomWithCenterAligned,
-      delegate: editor,
-    );
-  }
-
-  static void remove(BuildContext context) {
-    FlowyOverlay.of(context).remove(identifier());
-  }
-
-  static String identifier() {
-    return (SelectOptionCellEditor).toString();
-  }
-
-  @override
-  bool asBarrier() => true;
-
-  @override
-  void didRemove() => onDismissed?.call();
 }
 
 class _OptionList extends StatelessWidget {

+ 3 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart

@@ -3,6 +3,7 @@ import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
 import 'package:appflowy_popover/popover.dart';
 import 'package:flowy_infra/image.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/hover.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
@@ -29,7 +30,8 @@ class GridFieldCell extends StatelessWidget {
       },
       child: BlocBuilder<FieldCellBloc, FieldCellState>(
         builder: (context, state) {
-          final button = Popover(
+          final button = AppFlowyStylePopover(
+            constraints: BoxConstraints.loose(const Size(240, 840)),
             direction: PopoverDirection.bottomWithLeftAligned,
             triggerActions: PopoverTriggerActionFlags.click,
             offset: const Offset(0, 10),

+ 30 - 30
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart

@@ -3,9 +3,9 @@ import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_editor.
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/plugins/grid/application/prelude.dart';
 import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
+import 'package:appflowy_popover/popover.dart';
 import 'package:flowy_infra/image.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';
@@ -32,38 +32,32 @@ class _GridFieldCellActionSheetState extends State<GridFieldCellActionSheet> {
   Widget build(BuildContext context) {
     if (_showFieldEditor) {
       final field = widget.cellContext.field;
-      return OverlayContainer(
-        constraints: BoxConstraints.loose(const Size(240, 200)),
-        child: FieldEditor(
+      return FieldEditor(
+        gridId: widget.cellContext.gridId,
+        fieldName: field.name,
+        typeOptionLoader: FieldTypeOptionLoader(
           gridId: widget.cellContext.gridId,
-          fieldName: field.name,
-          typeOptionLoader: FieldTypeOptionLoader(
-            gridId: widget.cellContext.gridId,
-            field: field,
-          ),
+          field: field,
         ),
       );
     }
     return BlocProvider(
       create: (context) =>
           getIt<FieldActionSheetBloc>(param1: widget.cellContext),
-      child: OverlayContainer(
-        constraints: BoxConstraints.loose(const Size(240, 200)),
-        child: SingleChildScrollView(
-          child: Column(
-            children: [
-              _EditFieldButton(
-                cellContext: widget.cellContext,
-                onTap: () {
-                  setState(() {
-                    _showFieldEditor = true;
-                  });
-                },
-              ),
-              const VSpace(6),
-              _FieldOperationList(widget.cellContext, () {}),
-            ],
-          ),
+      child: SingleChildScrollView(
+        child: Column(
+          children: [
+            _EditFieldButton(
+              cellContext: widget.cellContext,
+              onTap: () {
+                setState(() {
+                  _showFieldEditor = true;
+                });
+              },
+            ),
+            const VSpace(6),
+            _FieldOperationList(widget.cellContext, () {}),
+          ],
         ),
       ),
     );
@@ -159,8 +153,11 @@ class FieldActionCell extends StatelessWidget {
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
     return FlowyButton(
-      text: FlowyText.medium(action.title(),
-          fontSize: 12, color: enable ? null : theme.shader4),
+      text: FlowyText.medium(
+        action.title(),
+        fontSize: 12,
+        color: enable ? null : theme.shader4,
+      ),
       hoverColor: theme.hover,
       onTap: () {
         if (enable) {
@@ -168,8 +165,10 @@ class FieldActionCell extends StatelessWidget {
           onTap();
         }
       },
-      leftIcon: svgWidget(action.iconName(),
-          color: enable ? theme.iconColor : theme.disableIconColor),
+      leftIcon: svgWidget(
+        action.iconName(),
+        color: enable ? theme.iconColor : theme.disableIconColor,
+      ),
     );
   }
 }
@@ -216,6 +215,7 @@ extension _FieldActionExtension on FieldAction {
             .add(const FieldActionSheetEvent.duplicateField());
         break;
       case FieldAction.delete:
+        PopoverContainer.of(context).close();
         NavigatorAlertDialog(
           title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
           confirm: () {

+ 108 - 30
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart

@@ -7,12 +7,12 @@ 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/rounded_input_field.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
-import 'field_name_input.dart';
 import 'field_type_option_editor.dart';
 
 class FieldEditor extends StatefulWidget {
@@ -44,6 +44,12 @@ class _FieldEditorState extends State<FieldEditor> {
     super.initState();
   }
 
+  @override
+  void dispose() {
+    popoverMutex.dispose();
+    super.dispose();
+  }
+
   @override
   Widget build(BuildContext context) {
     return BlocProvider(
@@ -58,21 +64,14 @@ class _FieldEditorState extends State<FieldEditor> {
           return ListView(
             shrinkWrap: true,
             children: [
-              FlowyText.medium(LocaleKeys.grid_field_editProperty.tr(),
-                  fontSize: 12),
-              const VSpace(10),
-              const _FieldNameCell(),
-              const VSpace(10),
-              _DeleteFieldButton(
-                popoverMutex: popoverMutex,
-                onDeleted: () {
-                  state.field.fold(
-                    () => Log.error('Can not delete the field'),
-                    (field) => widget.onDeleted?.call(field.id),
-                  );
-                },
+              FlowyText.medium(
+                LocaleKeys.grid_field_editProperty.tr(),
+                fontSize: 12,
               ),
               const VSpace(10),
+              _FieldNameTextField(popoverMutex: popoverMutex),
+              const VSpace(10),
+              ..._addDeleteFieldButton(state),
               _FieldTypeOptionCell(popoverMutex: popoverMutex),
             ],
           );
@@ -80,6 +79,23 @@ class _FieldEditorState extends State<FieldEditor> {
       ),
     );
   }
+
+  List<Widget> _addDeleteFieldButton(FieldEditorState state) {
+    if (widget.onDeleted == null) {
+      return [];
+    }
+    return [
+      _DeleteFieldButton(
+        popoverMutex: popoverMutex,
+        onDeleted: () {
+          state.field.fold(
+            () => Log.error('Can not delete the field'),
+            (field) => widget.onDeleted?.call(field.id),
+          );
+        },
+      ),
+    ];
+  }
 }
 
 class _FieldTypeOptionCell extends StatelessWidget {
@@ -111,25 +127,89 @@ class _FieldTypeOptionCell extends StatelessWidget {
   }
 }
 
-class _FieldNameCell extends StatelessWidget {
-  const _FieldNameCell({Key? key}) : super(key: key);
+class _FieldNameTextField extends StatefulWidget {
+  final PopoverMutex popoverMutex;
+  const _FieldNameTextField({
+    required this.popoverMutex,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<_FieldNameTextField> createState() => _FieldNameTextFieldState();
+}
+
+class _FieldNameTextFieldState extends State<_FieldNameTextField> {
+  late String name;
+  FocusNode focusNode = FocusNode();
+  VoidCallback? _popoverCallback;
+  TextEditingController controller = TextEditingController();
+
+  @override
+  void initState() {
+    focusNode.addListener(() {
+      if (focusNode.hasFocus) {
+        widget.popoverMutex.close();
+      }
+    });
+
+    super.initState();
+  }
 
   @override
   Widget build(BuildContext context) {
-    return BlocBuilder<FieldEditorBloc, FieldEditorState>(
-      builder: (context, state) {
-        return FieldNameTextField(
-          name: state.name,
-          errorText: context.read<FieldEditorBloc>().state.errorText,
-          onNameChanged: (newName) {
-            context
-                .read<FieldEditorBloc>()
-                .add(FieldEditorEvent.updateName(newName));
-          },
-        );
+    final theme = context.watch<AppTheme>();
+
+    controller.text = context.read<FieldEditorBloc>().state.name;
+    return BlocListener<FieldEditorBloc, FieldEditorState>(
+      listenWhen: (previous, current) => previous.name != current.name,
+      listener: (context, state) {
+        controller.text = state.name;
       },
+      child: BlocBuilder<FieldEditorBloc, FieldEditorState>(
+        builder: (context, state) {
+          listenOnPopoverChhanged(context);
+
+          return RoundedInputField(
+            height: 36,
+            autoFocus: true,
+            focusNode: focusNode,
+            style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
+            controller: controller,
+            normalBorderColor: theme.shader4,
+            errorBorderColor: theme.red,
+            focusBorderColor: theme.main1,
+            cursorColor: theme.main1,
+            errorText: context.read<FieldEditorBloc>().state.errorText,
+            onChanged: (newName) {
+              context
+                  .read<FieldEditorBloc>()
+                  .add(FieldEditorEvent.updateName(newName));
+            },
+          );
+        },
+      ),
     );
   }
+
+  void listenOnPopoverChhanged(BuildContext context) {
+    if (_popoverCallback != null) {
+      widget.popoverMutex.removePopoverStateListener(_popoverCallback!);
+    }
+    _popoverCallback = widget.popoverMutex.listenOnPopoverStateChanged(() {
+      if (focusNode.hasFocus) {
+        final node = FocusScope.of(context);
+        node.unfocus();
+      }
+    });
+  }
+
+  @override
+  void didUpdateWidget(covariant _FieldNameTextField oldWidget) {
+    controller.selection = TextSelection.fromPosition(
+        TextPosition(offset: controller.text.length));
+
+    super.didUpdateWidget(oldWidget);
+  }
 }
 
 class _DeleteFieldButton extends StatelessWidget {
@@ -171,12 +251,10 @@ class _DeleteFieldButton extends StatelessWidget {
       popupBuilder: (popupContext) {
         return PopoverAlertView(
           title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
-          cancel: () => popoverMutex.state?.close(),
+          cancel: () {},
           confirm: () {
             onDeleted?.call();
-            popoverMutex.state?.close();
           },
-          popoverMutex: popoverMutex,
         );
       },
       child: widget,

+ 0 - 56
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_name_input.dart

@@ -1,56 +0,0 @@
-import 'package:flowy_infra/theme.dart';
-import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-
-class FieldNameTextField extends StatefulWidget {
-  final void Function(String) onNameChanged;
-  final String name;
-  final String errorText;
-  const FieldNameTextField({
-    required this.name,
-    required this.errorText,
-    required this.onNameChanged,
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  State<FieldNameTextField> createState() => _FieldNameTextFieldState();
-}
-
-class _FieldNameTextFieldState extends State<FieldNameTextField> {
-  late String name;
-  TextEditingController controller = TextEditingController();
-
-  @override
-  void initState() {
-    controller.text = widget.name;
-    super.initState();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    final theme = context.watch<AppTheme>();
-    return RoundedInputField(
-      height: 36,
-      autoFocus: true,
-      style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
-      controller: controller,
-      normalBorderColor: theme.shader4,
-      errorBorderColor: theme.red,
-      focusBorderColor: theme.main1,
-      cursorColor: theme.main1,
-      errorText: widget.errorText,
-      onChanged: widget.onNameChanged,
-    );
-  }
-
-  @override
-  void didUpdateWidget(covariant FieldNameTextField oldWidget) {
-    controller.text = widget.name;
-    controller.selection = TextSelection.fromPosition(
-        TextPosition(offset: controller.text.length));
-
-    super.didUpdateWidget(oldWidget);
-  }
-}

+ 0 - 4
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_list.dart

@@ -47,10 +47,6 @@ class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {
       ),
     );
   }
-
-  static String identifier() {
-    return (FieldTypeList).toString();
-  }
 }
 
 class FieldTypeCell extends StatelessWidget {

+ 2 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart

@@ -66,7 +66,8 @@ class FieldTypeOptionEditor extends StatelessWidget {
       height: GridSize.typeOptionItemHeight,
       child: AppFlowyStylePopover(
         constraints: BoxConstraints.loose(const Size(460, 440)),
-        triggerActions: PopoverTriggerActionFlags.click,
+        triggerActions:
+            PopoverTriggerActionFlags.click | PopoverTriggerActionFlags.hover,
         mutex: popoverMutex,
         offset: const Offset(20, 0),
         popupBuilder: (context) {

+ 3 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart

@@ -50,7 +50,9 @@ Widget? makeTypeOptionWidget({
   required PopoverMutex popoverMutex,
 }) {
   final builder = makeTypeOptionWidgetBuilder(
-      dataController: dataController, popoverMutex: popoverMutex);
+    dataController: dataController,
+    popoverMutex: popoverMutex,
+  );
   return builder.build(context);
 }
 

+ 10 - 22
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart

@@ -75,7 +75,7 @@ class DateTypeOptionWidget extends TypeOptionWidget {
             context
                 .read<DateTypeOptionBloc>()
                 .add(DateTypeOptionEvent.didSelectDateFormat(format));
-            PopoverContainer.of(popoverContext).closeAll();
+            PopoverContainer.of(popoverContext).close();
           },
         );
       },
@@ -97,7 +97,7 @@ class DateTypeOptionWidget extends TypeOptionWidget {
             context
                 .read<DateTypeOptionBloc>()
                 .add(DateTypeOptionEvent.didSelectTimeFormat(format));
-            PopoverContainer.of(popoverContext).closeAll();
+            PopoverContainer.of(popoverContext).close();
           },
         );
       },
@@ -201,12 +201,10 @@ class DateFormatList extends StatelessWidget {
   Widget build(BuildContext context) {
     final cells = DateFormat.values.map((format) {
       return DateFormatCell(
-          dateFormat: format,
-          onSelected: (format) {
-            onSelected(format);
-            FlowyOverlay.of(context).remove(DateFormatList.identifier());
-          },
-          isSelected: selectedFormat == format);
+        dateFormat: format,
+        onSelected: onSelected,
+        isSelected: selectedFormat == format,
+      );
     }).toList();
 
     return SizedBox(
@@ -224,10 +222,6 @@ class DateFormatList extends StatelessWidget {
       ),
     );
   }
-
-  static String identifier() {
-    return (DateFormatList).toString();
-  }
 }
 
 class DateFormatCell extends StatelessWidget {
@@ -291,12 +285,10 @@ class TimeFormatList extends StatelessWidget {
   Widget build(BuildContext context) {
     final cells = TimeFormat.values.map((format) {
       return TimeFormatCell(
-          isSelected: format == selectedFormat,
-          timeFormat: format,
-          onSelected: (format) {
-            onSelected(format);
-            FlowyOverlay.of(context).remove(TimeFormatList.identifier());
-          });
+        isSelected: format == selectedFormat,
+        timeFormat: format,
+        onSelected: onSelected,
+      );
     }).toList();
 
     return SizedBox(
@@ -314,10 +306,6 @@ class TimeFormatList extends StatelessWidget {
       ),
     );
   }
-
-  static String identifier() {
-    return (TimeFormatList).toString();
-  }
 }
 
 class TimeFormatCell extends StatelessWidget {

+ 1 - 7
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart

@@ -82,7 +82,7 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
                     context
                         .read<NumberTypeOptionBloc>()
                         .add(NumberTypeOptionEvent.didSelectFormat(format));
-                    PopoverContainer.of(popoverContext).closeAll();
+                    PopoverContainer.of(popoverContext).close();
                   },
                   selectedFormat: state.typeOption.format,
                 );
@@ -123,8 +123,6 @@ class NumberFormatList extends StatelessWidget {
                       format: format,
                       onSelected: (format) {
                         onSelected(format);
-                        FlowyOverlay.of(context)
-                            .remove(NumberFormatList.identifier());
                       });
                 }).toList();
 
@@ -147,10 +145,6 @@ class NumberFormatList extends StatelessWidget {
       ),
     );
   }
-
-  static String identifier() {
-    return (NumberFormatList).toString();
-  }
 }
 
 class NumberFormatCell extends StatelessWidget {

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart

@@ -207,13 +207,13 @@ class _OptionCellState extends State<_OptionCell> {
             context
                 .read<SelectOptionTypeOptionBloc>()
                 .add(SelectOptionTypeOptionEvent.deleteOption(widget.option));
-            PopoverContainer.of(popoverContext).closeAll();
+            PopoverContainer.of(popoverContext).close();
           },
           onUpdated: (updatedOption) {
             context
                 .read<SelectOptionTypeOptionBloc>()
                 .add(SelectOptionTypeOptionEvent.updateOption(updatedOption));
-            PopoverContainer.of(popoverContext).closeAll();
+            PopoverContainer.of(popoverContext).close();
           },
           key: ValueKey(widget.option.id),
         );

+ 4 - 2
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart

@@ -131,8 +131,10 @@ class _GridPropertyCell extends StatelessWidget {
         return FieldEditor(
           gridId: gridId,
           fieldName: fieldContext.name,
-          typeOptionLoader:
-              FieldTypeOptionLoader(gridId: gridId, field: fieldContext.field),
+          typeOptionLoader: FieldTypeOptionLoader(
+            gridId: gridId,
+            field: fieldContext.field,
+          ),
         );
       },
     );

+ 0 - 3
frontend/app_flowy/lib/workspace/presentation/widgets/dialogs.dart

@@ -1,4 +1,3 @@
-import 'package:appflowy_popover/popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/text_style.dart';
 import 'package:flowy_infra/theme.dart';
@@ -88,13 +87,11 @@ class _CreateTextFieldDialog extends State<NavigatorTextFieldDialog> {
 }
 
 class PopoverAlertView extends StatelessWidget {
-  final PopoverMutex popoverMutex;
   final String title;
   final void Function()? cancel;
   final void Function()? confirm;
 
   const PopoverAlertView({
-    required this.popoverMutex,
     required this.title,
     this.confirm,
     this.cancel,

+ 38 - 7
frontend/app_flowy/packages/appflowy_popover/lib/popover.dart

@@ -6,7 +6,42 @@ import 'package:flutter/services.dart';
 /// If multiple popovers are exclusive,
 /// pass the same mutex to them.
 class PopoverMutex {
-  PopoverState? state;
+  final ValueNotifier<PopoverState?> _stateNofitier = ValueNotifier(null);
+  PopoverMutex();
+
+  void removePopoverStateListener(VoidCallback listener) {
+    _stateNofitier.removeListener(listener);
+  }
+
+  VoidCallback listenOnPopoverStateChanged(VoidCallback callback) {
+    listenerCallback() {
+      callback();
+    }
+
+    _stateNofitier.addListener(listenerCallback);
+    return listenerCallback;
+  }
+
+  void close() {
+    _stateNofitier.value?.close();
+  }
+
+  PopoverState? get state => _stateNofitier.value;
+
+  set state(PopoverState? newState) {
+    if (_stateNofitier.value != null && _stateNofitier.value != newState) {
+      _stateNofitier.value?.close();
+    }
+    _stateNofitier.value = newState;
+  }
+
+  void _removeState() {
+    _stateNofitier.value = null;
+  }
+
+  void dispose() {
+    _stateNofitier.dispose();
+  }
 }
 
 class PopoverController {
@@ -109,11 +144,7 @@ class PopoverState extends State<Popover> {
     close();
 
     if (widget.mutex != null) {
-      if (widget.mutex!.state != null && widget.mutex!.state != this) {
-        widget.mutex!.state!.close();
-      }
-
-      widget.mutex!.state = this;
+      widget.mutex?.state = this;
     }
 
     if (_popoverWithMask == null) {
@@ -163,7 +194,7 @@ class PopoverState extends State<Popover> {
     }
 
     if (widget.mutex?.state == this) {
-      widget.mutex!.state = null;
+      widget.mutex?._removeState();
     }
   }