chore: improve grid focus and hover event handling (#1735)

* chore: improve primary cell accessory behavior

* fix: focus border disappearing

* chore: port to GridCellState

* chore: fix typo

* chore: connect popover controller

* chore: final
This commit is contained in:
Richard Shiue
2023-01-27 10:35:17 +08:00
committed by GitHub
parent 347245aaa1
commit 243a781b6c
8 changed files with 109 additions and 122 deletions

View File

@ -1,6 +1,6 @@
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/application/cell/checklist_cell_bloc.dart'; import 'package:app_flowy/plugins/grid/application/cell/checklist_cell_bloc.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/checklist_cell/checklist_cell.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/cell/checklist_cell/checklist_progress_bar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -29,7 +29,10 @@ class _BoardChecklistCellState extends State<BoardChecklistCell> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider.value( return BlocProvider.value(
value: _cellBloc, value: _cellBloc,
child: const ChecklistProgressBar(), child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) =>
ChecklistProgressBar(percent: state.percent),
),
); );
} }
} }

View File

@ -67,9 +67,6 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
with GridCellAccessoryState { with GridCellAccessoryState {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.isCellEditing) {
return const SizedBox();
} else {
return Tooltip( return Tooltip(
message: LocaleKeys.tooltip_openAsPage.tr(), message: LocaleKeys.tooltip_openAsPage.tr(),
textStyle: AFThemeExtension.of(context).caption.textColor(Colors.white), textStyle: AFThemeExtension.of(context).caption.textColor(Colors.white),
@ -79,7 +76,6 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
), ),
); );
} }
}
@override @override
void onTap() => widget.onTapCallback(); void onTap() => widget.onTapCallback();

View File

@ -12,20 +12,23 @@ class CellContainer extends StatelessWidget {
final GridCellWidget child; final GridCellWidget child;
final AccessoryBuilder? accessoryBuilder; final AccessoryBuilder? accessoryBuilder;
final double width; final double width;
final RegionStateNotifier rowStateNotifier; final bool isPrimary;
final CellContainerNotifier cellContainerNotifier;
const CellContainer({ const CellContainer({
Key? key, Key? key,
required this.child, required this.child,
required this.width, required this.width,
required this.rowStateNotifier, required this.isPrimary,
required this.cellContainerNotifier,
this.accessoryBuilder, this.accessoryBuilder,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider<_CellContainerNotifier>( return ChangeNotifierProvider.value(
create: (_) => _CellContainerNotifier(child), value: cellContainerNotifier,
child: Selector<_CellContainerNotifier, bool>( child: Selector<CellContainerNotifier, bool>(
selector: (context, notifier) => notifier.isFocus, selector: (context, notifier) => notifier.isFocus,
builder: (privderContext, isFocus, _) { builder: (privderContext, isFocus, _) {
Widget container = Center(child: GridCellShortcuts(child: child)); Widget container = Center(child: GridCellShortcuts(child: child));
@ -41,6 +44,7 @@ class CellContainer extends StatelessWidget {
if (accessories.isNotEmpty) { if (accessories.isNotEmpty) {
container = _GridCellEnterRegion( container = _GridCellEnterRegion(
accessories: accessories, accessories: accessories,
isPrimary: isPrimary,
child: container, child: container,
); );
} }
@ -81,17 +85,23 @@ class CellContainer extends StatelessWidget {
class _GridCellEnterRegion extends StatelessWidget { class _GridCellEnterRegion extends StatelessWidget {
final Widget child; final Widget child;
final List<GridCellAccessoryBuilder> accessories; final List<GridCellAccessoryBuilder> accessories;
const _GridCellEnterRegion( final bool isPrimary;
{required this.child, required this.accessories, Key? key}) const _GridCellEnterRegion({
: super(key: key); required this.child,
required this.accessories,
required this.isPrimary,
Key? key,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector2<RegionStateNotifier, _CellContainerNotifier, bool>( return Selector2<RegionStateNotifier, CellContainerNotifier, bool>(
selector: (context, regionNotifier, cellNotifier) => !cellNotifier.isFocus && (cellNotifier.onEnter), selector: (context, regionNotifier, cellNotifier) =>
builder: (context, onEnter, _) { !cellNotifier.isFocus &&
(cellNotifier.onEnter || regionNotifier.onEnter && isPrimary),
builder: (context, showAccessory, _) {
List<Widget> children = [child]; List<Widget> children = [child];
if (onEnter) { if (showAccessory) {
children.add( children.add(
CellAccessoryContainer(accessories: accessories).positioned( CellAccessoryContainer(accessories: accessories).positioned(
right: GridSize.cellContentInsets.right, right: GridSize.cellContentInsets.right,
@ -102,10 +112,10 @@ class _GridCellEnterRegion extends StatelessWidget {
return MouseRegion( return MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
onEnter: (p) => onEnter: (p) =>
Provider.of<_CellContainerNotifier>(context, listen: false) Provider.of<CellContainerNotifier>(context, listen: false)
.onEnter = true, .onEnter = true,
onExit: (p) => onExit: (p) =>
Provider.of<_CellContainerNotifier>(context, listen: false) Provider.of<CellContainerNotifier>(context, listen: false)
.onEnter = false, .onEnter = false,
child: Stack( child: Stack(
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.center,
@ -118,13 +128,13 @@ class _GridCellEnterRegion extends StatelessWidget {
} }
} }
class _CellContainerNotifier extends ChangeNotifier { class CellContainerNotifier extends ChangeNotifier {
final CellEditable cellEditable; final CellEditable cellEditable;
VoidCallback? _onCellFocusListener; VoidCallback? _onCellFocusListener;
bool _isFocus = false; bool _isFocus = false;
bool _onEnter = false; bool _onEnter = false;
_CellContainerNotifier(this.cellEditable) { CellContainerNotifier(this.cellEditable) {
_onCellFocusListener = () => isFocus = cellEditable.onCellFocus.value; _onCellFocusListener = () => isFocus = cellEditable.onCellFocus.value;
cellEditable.onCellFocus.addListener(_onCellFocusListener!); cellEditable.onCellFocus.addListener(_onCellFocusListener!);
} }

View File

@ -5,9 +5,10 @@ import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../cell_builder.dart'; import '../cell_builder.dart';
import 'checklist_cell_editor.dart'; import 'checklist_cell_editor.dart';
import 'checklist_prograss_bar.dart'; import 'checklist_progress_bar.dart';
class GridChecklistCell extends GridCellWidget { class GridChecklistCell extends GridCellWidget {
final GridCellControllerBuilder cellControllerBuilder; final GridCellControllerBuilder cellControllerBuilder;
@ -15,12 +16,12 @@ class GridChecklistCell extends GridCellWidget {
: super(key: key); : super(key: key);
@override @override
GridChecklistCellState createState() => GridChecklistCellState(); GridCellState<GridChecklistCell> createState() => GridChecklistCellState();
} }
class GridChecklistCellState extends State<GridChecklistCell> { class GridChecklistCellState extends GridCellState<GridChecklistCell> {
late PopoverController _popover;
late ChecklistCellBloc _cellBloc; late ChecklistCellBloc _cellBloc;
late final PopoverController _popover;
@override @override
void initState() { void initState() {
@ -36,19 +37,7 @@ class GridChecklistCellState extends State<GridChecklistCell> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider.value( return BlocProvider.value(
value: _cellBloc, value: _cellBloc,
child: Stack( child: AppFlowyPopover(
alignment: AlignmentDirectional.center,
fit: StackFit.expand,
children: [
_wrapPopover(const ChecklistProgressBar()),
InkWell(onTap: () => _popover.show()),
],
),
);
}
Widget _wrapPopover(Widget child) {
return AppFlowyPopover(
controller: _popover, controller: _popover,
constraints: BoxConstraints.loose(const Size(260, 400)), constraints: BoxConstraints.loose(const Size(260, 400)),
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
@ -65,23 +54,15 @@ class GridChecklistCellState extends State<GridChecklistCell> {
onClose: () => widget.onCellEditing.value = false, onClose: () => widget.onCellEditing.value = false,
child: Padding( child: Padding(
padding: GridSize.cellContentInsets, padding: GridSize.cellContentInsets,
child: child, child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) =>
ChecklistProgressBar(percent: state.percent),
),
),
), ),
); );
} }
}
class ChecklistProgressBar extends StatelessWidget {
const ChecklistProgressBar({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { void requestBeginFocus() => _popover.show();
return BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
return ChecklistPrograssBar(
percent: state.percent,
);
},
);
}
} }

View File

@ -1,7 +1,7 @@
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/application/cell/checklist_cell_editor_bloc.dart'; import 'package:app_flowy/plugins/grid/application/cell/checklist_cell_editor_bloc.dart';
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart'; import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/checklist_cell/checklist_prograss_bar.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/cell/checklist_cell/checklist_progress_bar.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/header/type_option/select_option_editor.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/header/type_option/select_option_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
@ -48,7 +48,7 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
child: BlocBuilder<ChecklistCellEditorBloc, ChecklistCellEditorState>( child: BlocBuilder<ChecklistCellEditorBloc, ChecklistCellEditorState>(
builder: (context, state) { builder: (context, state) {
final List<Widget> slivers = [ final List<Widget> slivers = [
const SliverChecklistPrograssBar(), const SliverChecklistProgressBar(),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: GridSize.typeOptionContentInsets, padding: GridSize.typeOptionContentInsets,

View File

@ -8,9 +8,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:percent_indicator/percent_indicator.dart'; import 'package:percent_indicator/percent_indicator.dart';
class ChecklistPrograssBar extends StatelessWidget { class ChecklistProgressBar extends StatelessWidget {
final double percent; final double percent;
const ChecklistPrograssBar({required this.percent, Key? key}) const ChecklistProgressBar({required this.percent, Key? key})
: super(key: key); : super(key: key);
@override @override
@ -26,21 +26,21 @@ class ChecklistPrograssBar extends StatelessWidget {
} }
} }
class SliverChecklistPrograssBar extends StatelessWidget { class SliverChecklistProgressBar extends StatelessWidget {
const SliverChecklistPrograssBar({Key? key}) : super(key: key); const SliverChecklistProgressBar({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverPersistentHeader( return SliverPersistentHeader(
pinned: true, pinned: true,
delegate: _SliverChecklistPrograssBarDelegate(), delegate: _SliverChecklistProgressBarDelegate(),
); );
} }
} }
class _SliverChecklistPrograssBarDelegate class _SliverChecklistProgressBarDelegate
extends SliverPersistentHeaderDelegate { extends SliverPersistentHeaderDelegate {
_SliverChecklistPrograssBarDelegate(); _SliverChecklistProgressBarDelegate();
double fixHeight = 60; double fixHeight = 60;
@ -71,7 +71,7 @@ class _SliverChecklistPrograssBarDelegate
), ),
Padding( Padding(
padding: const EdgeInsets.only(top: 6.0), padding: const EdgeInsets.only(top: 6.0),
child: ChecklistPrograssBar(percent: state.percent), child: ChecklistProgressBar(percent: state.percent),
), ),
], ],
), ),

View File

@ -39,11 +39,12 @@ class GridSingleSelectCell extends GridCellWidget {
} }
@override @override
State<GridSingleSelectCell> createState() => _SingleSelectCellState(); GridCellState<GridSingleSelectCell> createState() => _SingleSelectCellState();
} }
class _SingleSelectCellState extends State<GridSingleSelectCell> { class _SingleSelectCellState extends GridCellState<GridSingleSelectCell> {
late SelectOptionCellBloc _cellBloc; late SelectOptionCellBloc _cellBloc;
late final PopoverController _popover;
@override @override
void initState() { void initState() {
@ -51,6 +52,7 @@ class _SingleSelectCellState extends State<GridSingleSelectCell> {
widget.cellControllerBuilder.build() as GridSelectOptionCellController; widget.cellControllerBuilder.build() as GridSelectOptionCellController;
_cellBloc = getIt<SelectOptionCellBloc>(param1: cellController) _cellBloc = getIt<SelectOptionCellBloc>(param1: cellController)
..add(const SelectOptionCellEvent.initial()); ..add(const SelectOptionCellEvent.initial());
_popover = PopoverController();
super.initState(); super.initState();
} }
@ -63,8 +65,10 @@ class _SingleSelectCellState extends State<GridSingleSelectCell> {
return SelectOptionWrap( return SelectOptionWrap(
selectOptions: state.selectedOptions, selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle, cellStyle: widget.cellStyle,
onFocus: (value) => widget.onCellEditing.value = value, onCellEditing: widget.onCellEditing,
cellControllerBuilder: widget.cellControllerBuilder); popoverController: _popover,
cellControllerBuilder: widget.cellControllerBuilder,
);
}, },
), ),
); );
@ -75,6 +79,9 @@ class _SingleSelectCellState extends State<GridSingleSelectCell> {
_cellBloc.close(); _cellBloc.close();
super.dispose(); super.dispose();
} }
@override
void requestBeginFocus() => _popover.show();
} }
//---------------------------------------------------------------- //----------------------------------------------------------------
@ -95,11 +102,12 @@ class GridMultiSelectCell extends GridCellWidget {
} }
@override @override
State<GridMultiSelectCell> createState() => _MultiSelectCellState(); GridCellState<GridMultiSelectCell> createState() => _MultiSelectCellState();
} }
class _MultiSelectCellState extends State<GridMultiSelectCell> { class _MultiSelectCellState extends GridCellState<GridMultiSelectCell> {
late SelectOptionCellBloc _cellBloc; late SelectOptionCellBloc _cellBloc;
late final PopoverController _popover;
@override @override
void initState() { void initState() {
@ -107,6 +115,7 @@ class _MultiSelectCellState extends State<GridMultiSelectCell> {
widget.cellControllerBuilder.build() as GridSelectOptionCellController; widget.cellControllerBuilder.build() as GridSelectOptionCellController;
_cellBloc = getIt<SelectOptionCellBloc>(param1: cellController) _cellBloc = getIt<SelectOptionCellBloc>(param1: cellController)
..add(const SelectOptionCellEvent.initial()); ..add(const SelectOptionCellEvent.initial());
_popover = PopoverController();
super.initState(); super.initState();
} }
@ -119,7 +128,8 @@ class _MultiSelectCellState extends State<GridMultiSelectCell> {
return SelectOptionWrap( return SelectOptionWrap(
selectOptions: state.selectedOptions, selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle, cellStyle: widget.cellStyle,
onFocus: (value) => widget.onCellEditing.value = value, onCellEditing: widget.onCellEditing,
popoverController: _popover,
cellControllerBuilder: widget.cellControllerBuilder, cellControllerBuilder: widget.cellControllerBuilder,
); );
}, },
@ -132,17 +142,23 @@ class _MultiSelectCellState extends State<GridMultiSelectCell> {
_cellBloc.close(); _cellBloc.close();
super.dispose(); super.dispose();
} }
@override
void requestBeginFocus() => _popover.show();
} }
class SelectOptionWrap extends StatefulWidget { class SelectOptionWrap extends StatefulWidget {
final List<SelectOptionPB> selectOptions; final List<SelectOptionPB> selectOptions;
final void Function(bool)? onFocus;
final SelectOptionCellStyle? cellStyle; final SelectOptionCellStyle? cellStyle;
final GridCellControllerBuilder cellControllerBuilder; final GridCellControllerBuilder cellControllerBuilder;
final PopoverController popoverController;
final ValueNotifier onCellEditing;
const SelectOptionWrap({ const SelectOptionWrap({
required this.selectOptions, required this.selectOptions,
required this.cellControllerBuilder, required this.cellControllerBuilder,
this.onFocus, required this.onCellEditing,
required this.popoverController,
this.cellStyle, this.cellStyle,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -152,48 +168,29 @@ class SelectOptionWrap extends StatefulWidget {
} }
class _SelectOptionWrapState extends State<SelectOptionWrap> { class _SelectOptionWrapState extends State<SelectOptionWrap> {
late PopoverController _popover;
@override
void initState() {
_popover = PopoverController();
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child = _buildOptions(context); Widget child = _buildOptions(context);
return Stack(
alignment: AlignmentDirectional.center,
fit: StackFit.expand,
children: [
_wrapPopover(child),
InkWell(onTap: () => _popover.show()),
],
);
}
Widget _wrapPopover(Widget child) {
final constraints = BoxConstraints.loose(Size( final constraints = BoxConstraints.loose(Size(
SelectOptionCellEditor.editorPanelWidth, SelectOptionCellEditor.editorPanelWidth,
300, 300,
)); ));
return AppFlowyPopover( return AppFlowyPopover(
controller: _popover, controller: widget.popoverController,
constraints: constraints, constraints: constraints,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onFocus?.call(true); widget.onCellEditing.value = true;
}); });
return SelectOptionCellEditor( return SelectOptionCellEditor(
cellController: widget.cellControllerBuilder.build() cellController: widget.cellControllerBuilder.build()
as GridSelectOptionCellController, as GridSelectOptionCellController,
); );
}, },
onClose: () => widget.onFocus?.call(false), onClose: () => widget.onCellEditing.value = false,
child: Padding( child: Padding(
padding: GridSize.cellContentInsets, padding: GridSize.cellContentInsets,
child: child, child: child,

View File

@ -225,8 +225,8 @@ class RowContent extends StatelessWidget {
return CellContainer( return CellContainer(
width: cellId.fieldInfo.width.toDouble(), width: cellId.fieldInfo.width.toDouble(),
rowStateNotifier: isPrimary: cellId.fieldInfo.isPrimary,
Provider.of<RegionStateNotifier>(context, listen: false), cellContainerNotifier: CellContainerNotifier(child),
accessoryBuilder: (buildContext) { accessoryBuilder: (buildContext) {
final builder = child.accessoryBuilder; final builder = child.accessoryBuilder;
List<GridCellAccessoryBuilder> accessories = []; List<GridCellAccessoryBuilder> accessories = [];