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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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/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_bloc/flutter_bloc.dart';
@ -29,7 +29,10 @@ class _BoardChecklistCellState extends State<BoardChecklistCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: const ChecklistProgressBar(),
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) =>
ChecklistProgressBar(percent: state.percent),
),
);
}
}

View File

@ -67,18 +67,14 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
with GridCellAccessoryState {
@override
Widget build(BuildContext context) {
if (widget.isCellEditing) {
return const SizedBox();
} else {
return Tooltip(
message: LocaleKeys.tooltip_openAsPage.tr(),
textStyle: AFThemeExtension.of(context).caption.textColor(Colors.white),
child: svgWidget(
"grid/expander",
color: Theme.of(context).colorScheme.primary,
),
);
}
return Tooltip(
message: LocaleKeys.tooltip_openAsPage.tr(),
textStyle: AFThemeExtension.of(context).caption.textColor(Colors.white),
child: svgWidget(
"grid/expander",
color: Theme.of(context).colorScheme.primary,
),
);
}
@override

View File

@ -12,20 +12,23 @@ class CellContainer extends StatelessWidget {
final GridCellWidget child;
final AccessoryBuilder? accessoryBuilder;
final double width;
final RegionStateNotifier rowStateNotifier;
final bool isPrimary;
final CellContainerNotifier cellContainerNotifier;
const CellContainer({
Key? key,
required this.child,
required this.width,
required this.rowStateNotifier,
required this.isPrimary,
required this.cellContainerNotifier,
this.accessoryBuilder,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<_CellContainerNotifier>(
create: (_) => _CellContainerNotifier(child),
child: Selector<_CellContainerNotifier, bool>(
return ChangeNotifierProvider.value(
value: cellContainerNotifier,
child: Selector<CellContainerNotifier, bool>(
selector: (context, notifier) => notifier.isFocus,
builder: (privderContext, isFocus, _) {
Widget container = Center(child: GridCellShortcuts(child: child));
@ -41,6 +44,7 @@ class CellContainer extends StatelessWidget {
if (accessories.isNotEmpty) {
container = _GridCellEnterRegion(
accessories: accessories,
isPrimary: isPrimary,
child: container,
);
}
@ -81,17 +85,23 @@ class CellContainer extends StatelessWidget {
class _GridCellEnterRegion extends StatelessWidget {
final Widget child;
final List<GridCellAccessoryBuilder> accessories;
const _GridCellEnterRegion(
{required this.child, required this.accessories, Key? key})
: super(key: key);
final bool isPrimary;
const _GridCellEnterRegion({
required this.child,
required this.accessories,
required this.isPrimary,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Selector2<RegionStateNotifier, _CellContainerNotifier, bool>(
selector: (context, regionNotifier, cellNotifier) => !cellNotifier.isFocus && (cellNotifier.onEnter),
builder: (context, onEnter, _) {
return Selector2<RegionStateNotifier, CellContainerNotifier, bool>(
selector: (context, regionNotifier, cellNotifier) =>
!cellNotifier.isFocus &&
(cellNotifier.onEnter || regionNotifier.onEnter && isPrimary),
builder: (context, showAccessory, _) {
List<Widget> children = [child];
if (onEnter) {
if (showAccessory) {
children.add(
CellAccessoryContainer(accessories: accessories).positioned(
right: GridSize.cellContentInsets.right,
@ -102,10 +112,10 @@ class _GridCellEnterRegion extends StatelessWidget {
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (p) =>
Provider.of<_CellContainerNotifier>(context, listen: false)
Provider.of<CellContainerNotifier>(context, listen: false)
.onEnter = true,
onExit: (p) =>
Provider.of<_CellContainerNotifier>(context, listen: false)
Provider.of<CellContainerNotifier>(context, listen: false)
.onEnter = false,
child: Stack(
alignment: AlignmentDirectional.center,
@ -118,13 +128,13 @@ class _GridCellEnterRegion extends StatelessWidget {
}
}
class _CellContainerNotifier extends ChangeNotifier {
class CellContainerNotifier extends ChangeNotifier {
final CellEditable cellEditable;
VoidCallback? _onCellFocusListener;
bool _isFocus = false;
bool _onEnter = false;
_CellContainerNotifier(this.cellEditable) {
CellContainerNotifier(this.cellEditable) {
_onCellFocusListener = () => isFocus = cellEditable.onCellFocus.value;
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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../cell_builder.dart';
import 'checklist_cell_editor.dart';
import 'checklist_prograss_bar.dart';
import 'checklist_progress_bar.dart';
class GridChecklistCell extends GridCellWidget {
final GridCellControllerBuilder cellControllerBuilder;
@ -15,12 +16,12 @@ class GridChecklistCell extends GridCellWidget {
: super(key: key);
@override
GridChecklistCellState createState() => GridChecklistCellState();
GridCellState<GridChecklistCell> createState() => GridChecklistCellState();
}
class GridChecklistCellState extends State<GridChecklistCell> {
late PopoverController _popover;
class GridChecklistCellState extends GridCellState<GridChecklistCell> {
late ChecklistCellBloc _cellBloc;
late final PopoverController _popover;
@override
void initState() {
@ -36,52 +37,32 @@ class GridChecklistCellState extends State<GridChecklistCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: Stack(
alignment: AlignmentDirectional.center,
fit: StackFit.expand,
children: [
_wrapPopover(const ChecklistProgressBar()),
InkWell(onTap: () => _popover.show()),
],
child: AppFlowyPopover(
controller: _popover,
constraints: BoxConstraints.loose(const Size(260, 400)),
direction: PopoverDirection.bottomWithLeftAligned,
triggerActions: PopoverTriggerFlags.none,
popupBuilder: (BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onCellEditing.value = true;
});
return GridChecklistCellEditor(
cellController: widget.cellControllerBuilder.build()
as GridChecklistCellController,
);
},
onClose: () => widget.onCellEditing.value = false,
child: Padding(
padding: GridSize.cellContentInsets,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) =>
ChecklistProgressBar(percent: state.percent),
),
),
),
);
}
Widget _wrapPopover(Widget child) {
return AppFlowyPopover(
controller: _popover,
constraints: BoxConstraints.loose(const Size(260, 400)),
direction: PopoverDirection.bottomWithLeftAligned,
triggerActions: PopoverTriggerFlags.none,
popupBuilder: (BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onCellEditing.value = true;
});
return GridChecklistCellEditor(
cellController: widget.cellControllerBuilder.build()
as GridChecklistCellController,
);
},
onClose: () => widget.onCellEditing.value = false,
child: Padding(
padding: GridSize.cellContentInsets,
child: child,
),
);
}
}
class ChecklistProgressBar extends StatelessWidget {
const ChecklistProgressBar({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
return ChecklistPrograssBar(
percent: state.percent,
);
},
);
}
void requestBeginFocus() => _popover.show();
}

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/checklist_cell_editor_bloc.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:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/image.dart';
@ -48,7 +48,7 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
child: BlocBuilder<ChecklistCellEditorBloc, ChecklistCellEditorState>(
builder: (context, state) {
final List<Widget> slivers = [
const SliverChecklistPrograssBar(),
const SliverChecklistProgressBar(),
SliverToBoxAdapter(
child: Padding(
padding: GridSize.typeOptionContentInsets,

View File

@ -8,9 +8,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:percent_indicator/percent_indicator.dart';
class ChecklistPrograssBar extends StatelessWidget {
class ChecklistProgressBar extends StatelessWidget {
final double percent;
const ChecklistPrograssBar({required this.percent, Key? key})
const ChecklistProgressBar({required this.percent, Key? key})
: super(key: key);
@override
@ -26,21 +26,21 @@ class ChecklistPrograssBar extends StatelessWidget {
}
}
class SliverChecklistPrograssBar extends StatelessWidget {
const SliverChecklistPrograssBar({Key? key}) : super(key: key);
class SliverChecklistProgressBar extends StatelessWidget {
const SliverChecklistProgressBar({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SliverPersistentHeader(
pinned: true,
delegate: _SliverChecklistPrograssBarDelegate(),
delegate: _SliverChecklistProgressBarDelegate(),
);
}
}
class _SliverChecklistPrograssBarDelegate
class _SliverChecklistProgressBarDelegate
extends SliverPersistentHeaderDelegate {
_SliverChecklistPrograssBarDelegate();
_SliverChecklistProgressBarDelegate();
double fixHeight = 60;
@ -71,7 +71,7 @@ class _SliverChecklistPrograssBarDelegate
),
Padding(
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
State<GridSingleSelectCell> createState() => _SingleSelectCellState();
GridCellState<GridSingleSelectCell> createState() => _SingleSelectCellState();
}
class _SingleSelectCellState extends State<GridSingleSelectCell> {
class _SingleSelectCellState extends GridCellState<GridSingleSelectCell> {
late SelectOptionCellBloc _cellBloc;
late final PopoverController _popover;
@override
void initState() {
@ -51,6 +52,7 @@ class _SingleSelectCellState extends State<GridSingleSelectCell> {
widget.cellControllerBuilder.build() as GridSelectOptionCellController;
_cellBloc = getIt<SelectOptionCellBloc>(param1: cellController)
..add(const SelectOptionCellEvent.initial());
_popover = PopoverController();
super.initState();
}
@ -61,10 +63,12 @@ class _SingleSelectCellState extends State<GridSingleSelectCell> {
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
builder: (context, state) {
return SelectOptionWrap(
selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle,
onFocus: (value) => widget.onCellEditing.value = value,
cellControllerBuilder: widget.cellControllerBuilder);
selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle,
onCellEditing: widget.onCellEditing,
popoverController: _popover,
cellControllerBuilder: widget.cellControllerBuilder,
);
},
),
);
@ -75,6 +79,9 @@ class _SingleSelectCellState extends State<GridSingleSelectCell> {
_cellBloc.close();
super.dispose();
}
@override
void requestBeginFocus() => _popover.show();
}
//----------------------------------------------------------------
@ -95,11 +102,12 @@ class GridMultiSelectCell extends GridCellWidget {
}
@override
State<GridMultiSelectCell> createState() => _MultiSelectCellState();
GridCellState<GridMultiSelectCell> createState() => _MultiSelectCellState();
}
class _MultiSelectCellState extends State<GridMultiSelectCell> {
class _MultiSelectCellState extends GridCellState<GridMultiSelectCell> {
late SelectOptionCellBloc _cellBloc;
late final PopoverController _popover;
@override
void initState() {
@ -107,6 +115,7 @@ class _MultiSelectCellState extends State<GridMultiSelectCell> {
widget.cellControllerBuilder.build() as GridSelectOptionCellController;
_cellBloc = getIt<SelectOptionCellBloc>(param1: cellController)
..add(const SelectOptionCellEvent.initial());
_popover = PopoverController();
super.initState();
}
@ -119,7 +128,8 @@ class _MultiSelectCellState extends State<GridMultiSelectCell> {
return SelectOptionWrap(
selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle,
onFocus: (value) => widget.onCellEditing.value = value,
onCellEditing: widget.onCellEditing,
popoverController: _popover,
cellControllerBuilder: widget.cellControllerBuilder,
);
},
@ -132,17 +142,23 @@ class _MultiSelectCellState extends State<GridMultiSelectCell> {
_cellBloc.close();
super.dispose();
}
@override
void requestBeginFocus() => _popover.show();
}
class SelectOptionWrap extends StatefulWidget {
final List<SelectOptionPB> selectOptions;
final void Function(bool)? onFocus;
final SelectOptionCellStyle? cellStyle;
final GridCellControllerBuilder cellControllerBuilder;
final PopoverController popoverController;
final ValueNotifier onCellEditing;
const SelectOptionWrap({
required this.selectOptions,
required this.cellControllerBuilder,
this.onFocus,
required this.onCellEditing,
required this.popoverController,
this.cellStyle,
Key? key,
}) : super(key: key);
@ -152,48 +168,29 @@ class SelectOptionWrap extends StatefulWidget {
}
class _SelectOptionWrapState extends State<SelectOptionWrap> {
late PopoverController _popover;
@override
void initState() {
_popover = PopoverController();
super.initState();
}
@override
Widget build(BuildContext 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(
SelectOptionCellEditor.editorPanelWidth,
300,
));
return AppFlowyPopover(
controller: _popover,
controller: widget.popoverController,
constraints: constraints,
margin: EdgeInsets.zero,
direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onFocus?.call(true);
widget.onCellEditing.value = true;
});
return SelectOptionCellEditor(
cellController: widget.cellControllerBuilder.build()
as GridSelectOptionCellController,
);
},
onClose: () => widget.onFocus?.call(false),
onClose: () => widget.onCellEditing.value = false,
child: Padding(
padding: GridSize.cellContentInsets,
child: child,

View File

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