fix: focus traversal in checklist popover (#4843)

* fix: focus traversal in checklist popover

* fix: dont trim input

* chore: remove redundant state var

* chore: remove late from controller
This commit is contained in:
Mathias Mogensen 2024-03-07 14:04:10 +01:00 committed by GitHub
parent 677617dcf2
commit cd245b5f0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 48 additions and 26 deletions

View File

@ -1,11 +1,12 @@
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; 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_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../editable_cell_skeleton/checklist.dart'; import '../editable_cell_skeleton/checklist.dart';
@ -25,6 +26,7 @@ class DesktopGridChecklistCellSkin extends IEditableChecklistCellSkin {
constraints: BoxConstraints.loose(const Size(360, 400)), constraints: BoxConstraints.loose(const Size(360, 400)),
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
skipTraversal: true,
popupBuilder: (BuildContext popoverContext) { popupBuilder: (BuildContext popoverContext) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
cellContainerNotifier.isFocus = true; cellContainerNotifier.isFocus = true;

View File

@ -1,6 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
@ -11,11 +14,10 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.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/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../application/cell/bloc/checklist_cell_bloc.dart'; import '../../application/cell/bloc/checklist_cell_bloc.dart';
import 'checklist_progress_bar.dart'; import 'checklist_progress_bar.dart';
class ChecklistCellEditor extends StatefulWidget { class ChecklistCellEditor extends StatefulWidget {
@ -345,12 +347,11 @@ class NewTaskItem extends StatefulWidget {
} }
class _NewTaskItemState extends State<NewTaskItem> { class _NewTaskItemState extends State<NewTaskItem> {
late final TextEditingController _textEditingController; final _textEditingController = TextEditingController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_textEditingController = TextEditingController();
if (widget.focusNode.canRequestFocus) { if (widget.focusNode.canRequestFocus) {
widget.focusNode.requestFocus(); widget.focusNode.requestFocus();
} }
@ -385,15 +386,13 @@ class _NewTaskItemState extends State<NewTaskItem> {
hintText: LocaleKeys.grid_checklist_addNew.tr(), hintText: LocaleKeys.grid_checklist_addNew.tr(),
), ),
onSubmitted: (taskDescription) { onSubmitted: (taskDescription) {
if (taskDescription.trim().isNotEmpty) { if (taskDescription.isNotEmpty) {
context.read<ChecklistCellBloc>().add( context
ChecklistCellEvent.createNewTask( .read<ChecklistCellBloc>()
taskDescription.trim(), .add(ChecklistCellEvent.createNewTask(taskDescription));
), _textEditingController.clear();
);
} }
widget.focusNode.requestFocus(); widget.focusNode.requestFocus();
_textEditingController.clear();
}, },
onChanged: (value) => setState(() {}), onChanged: (value) => setState(() {}),
), ),
@ -409,16 +408,17 @@ class _NewTaskItemState extends State<NewTaskItem> {
: Theme.of(context).colorScheme.primaryContainer, : Theme.of(context).colorScheme.primaryContainer,
fontColor: Theme.of(context).colorScheme.onPrimary, fontColor: Theme.of(context).colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
onPressed: () { onPressed: _textEditingController.text.isEmpty
final text = _textEditingController.text.trim(); ? null
if (text.isNotEmpty) { : () {
context.read<ChecklistCellBloc>().add( context.read<ChecklistCellBloc>().add(
ChecklistCellEvent.createNewTask(text), ChecklistCellEvent.createNewTask(
); _textEditingController.text,
} ),
widget.focusNode.requestFocus(); );
_textEditingController.clear(); widget.focusNode.requestFocus();
}, _textEditingController.clear();
},
), ),
], ],
), ),

View File

@ -1,7 +1,8 @@
import 'package:appflowy_popover/src/layout.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:appflowy_popover/src/layout.dart';
import 'mask.dart'; import 'mask.dart';
import 'mutex.dart'; import 'mutex.dart';
@ -90,6 +91,8 @@ class Popover extends StatefulWidget {
/// the conflict won't be resolve by using Listener, we want these two gestures exclusive. /// the conflict won't be resolve by using Listener, we want these two gestures exclusive.
final PopoverClickHandler clickHandler; final PopoverClickHandler clickHandler;
final bool skipTraversal;
/// The content area of the popover. /// The content area of the popover.
final Widget child; final Widget child;
@ -110,6 +113,7 @@ class Popover extends StatefulWidget {
this.canClose, this.canClose,
this.asBarrier = false, this.asBarrier = false,
this.clickHandler = PopoverClickHandler.listener, this.clickHandler = PopoverClickHandler.listener,
this.skipTraversal = false,
}); });
@override @override
@ -158,6 +162,7 @@ class PopoverState extends State<Popover> {
popupBuilder: widget.popupBuilder, popupBuilder: widget.popupBuilder,
onClose: () => close(), onClose: () => close(),
onCloseAll: () => _removeRootOverlay(), onCloseAll: () => _removeRootOverlay(),
skipTraversal: widget.skipTraversal,
), ),
); );
@ -263,6 +268,7 @@ class PopoverContainer extends StatefulWidget {
final EdgeInsets windowPadding; final EdgeInsets windowPadding;
final void Function() onClose; final void Function() onClose;
final void Function() onCloseAll; final void Function() onCloseAll;
final bool skipTraversal;
const PopoverContainer({ const PopoverContainer({
super.key, super.key,
@ -273,6 +279,7 @@ class PopoverContainer extends StatefulWidget {
required this.windowPadding, required this.windowPadding,
required this.onClose, required this.onClose,
required this.onCloseAll, required this.onCloseAll,
required this.skipTraversal,
}); });
@override @override
@ -293,6 +300,7 @@ class PopoverContainerState extends State<PopoverContainer> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Focus( return Focus(
autofocus: true, autofocus: true,
skipTraversal: widget.skipTraversal,
child: CustomSingleChildLayout( child: CustomSingleChildLayout(
delegate: PopoverLayoutDelegate( delegate: PopoverLayoutDelegate(
direction: widget.direction, direction: widget.direction,

View File

@ -26,6 +26,10 @@ class AppFlowyPopover extends StatelessWidget {
/// the conflict won't be resolve by using Listener, we want these two gestures exclusive. /// the conflict won't be resolve by using Listener, we want these two gestures exclusive.
final PopoverClickHandler clickHandler; final PopoverClickHandler clickHandler;
/// If true the popover will not participate in focus traversal.
///
final bool skipTraversal;
const AppFlowyPopover({ const AppFlowyPopover({
super.key, super.key,
required this.child, required this.child,
@ -43,6 +47,7 @@ class AppFlowyPopover extends StatelessWidget {
this.windowPadding = const EdgeInsets.all(8.0), this.windowPadding = const EdgeInsets.all(8.0),
this.decoration, this.decoration,
this.clickHandler = PopoverClickHandler.listener, this.clickHandler = PopoverClickHandler.listener,
this.skipTraversal = false,
}); });
@override @override
@ -58,6 +63,7 @@ class AppFlowyPopover extends StatelessWidget {
windowPadding: windowPadding, windowPadding: windowPadding,
offset: offset, offset: offset,
clickHandler: clickHandler, clickHandler: clickHandler,
skipTraversal: skipTraversal,
popupBuilder: (context) { popupBuilder: (context) {
return _PopoverContainer( return _PopoverContainer(
constraints: constraints, constraints: constraints,

View File

@ -1,12 +1,13 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
class FlowyButton extends StatelessWidget { class FlowyButton extends StatelessWidget {
final Widget text; final Widget text;
@ -213,6 +214,7 @@ class FlowyTextButton extends StatelessWidget {
); );
child = RawMaterialButton( child = RawMaterialButton(
focusNode: FocusNode(skipTraversal: onPressed == null),
hoverElevation: 0, hoverElevation: 0,
highlightElevation: 0, highlightElevation: 0,
shape: RoundedRectangleBorder(borderRadius: radius ?? Corners.s6Border), shape: RoundedRectangleBorder(borderRadius: radius ?? Corners.s6Border),
@ -237,6 +239,10 @@ class FlowyTextButton extends StatelessWidget {
); );
} }
if (onPressed == null) {
child = ExcludeFocus(child: child);
}
return child; return child;
} }
} }