fix: background color issues (#530)

This commit is contained in:
Lucas.Xu 2023-10-11 19:41:15 +08:00 • committed by GitHub
commit 31403c9b42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 157 additions and 91 deletions

View file

@ -142,7 +142,7 @@ class _MobileEditorState extends State<MobileEditor> {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
); );
map[ParagraphBlockKeys.type] = TextBlockComponentBuilder( map[ParagraphBlockKeys.type] = ParagraphBlockComponentBuilder(
configuration: BlockComponentConfiguration( configuration: BlockComponentConfiguration(
placeholderText: (node) => 'Type something...', placeholderText: (node) => 'Type something...',
), ),

View file

@ -50,6 +50,7 @@ class BlockSelectionContainer extends StatelessWidget {
? AlignmentDirectional.topStart ? AlignmentDirectional.topStart
: AlignmentDirectional.topEnd, : AlignmentDirectional.topEnd,
children: [ children: [
// block selection or selection area
BlockSelectionArea( BlockSelectionArea(
node: node, node: node,
delegate: delegate, delegate: delegate,
@ -57,9 +58,24 @@ class BlockSelectionContainer extends StatelessWidget {
cursorColor: cursorColor, cursorColor: cursorColor,
selectionColor: selectionColor, selectionColor: selectionColor,
blockColor: blockColor, blockColor: blockColor,
supportTypes: supportTypes, supportTypes: supportTypes
.where(
(element) => element != BlockSelectionType.cursor,
)
.toList(),
), ),
child, child,
// cursor
if (supportTypes.contains(BlockSelectionType.cursor))
BlockSelectionArea(
node: node,
delegate: delegate,
listenable: listenable,
cursorColor: cursorColor,
selectionColor: selectionColor,
blockColor: blockColor,
supportTypes: const [BlockSelectionType.cursor],
),
], ],
); );
} }

View file

@ -34,6 +34,7 @@ export 'image_block_component/resizable_image.dart';
// numbered list // numbered list
export 'numbered_list_block_component/numbered_list_block_component.dart'; export 'numbered_list_block_component/numbered_list_block_component.dart';
export 'numbered_list_block_component/numbered_list_character_shortcut.dart'; export 'numbered_list_block_component/numbered_list_character_shortcut.dart';
export 'paragraph_block_component/paragraph_block_component.dart';
// quote // quote
export 'quote_block_component/quote_block_component.dart'; export 'quote_block_component/quote_block_component.dart';
export 'quote_block_component/quote_character_shortcut.dart'; export 'quote_block_component/quote_character_shortcut.dart';
@ -47,7 +48,6 @@ export 'table_block_component/table_action.dart';
export 'table_block_component/table_block_component.dart'; export 'table_block_component/table_block_component.dart';
export 'table_block_component/table_cell_block_component.dart'; export 'table_block_component/table_cell_block_component.dart';
export 'table_block_component/table_commands.dart'; export 'table_block_component/table_commands.dart';
export 'text_block_component/text_block_component.dart';
// to-do list // to-do list
export 'todo_list_block_component/todo_list_block_component.dart'; export 'todo_list_block_component/todo_list_block_component.dart';
export 'todo_list_block_component/todo_list_character_shortcut.dart'; export 'todo_list_block_component/todo_list_character_shortcut.dart';

View file

@ -116,7 +116,6 @@ class _BulletedListBlockComponentWidgetState
); );
Widget child = Container( Widget child = Container(
color: withBackgroundColor ? backgroundColor : null,
width: double.infinity, width: double.infinity,
alignment: alignment, alignment: alignment,
child: Row( child: Row(
@ -155,10 +154,13 @@ class _BulletedListBlockComponentWidgetState
), ),
); );
child = Padding( child = Container(
key: blockComponentKey, color: withBackgroundColor ? backgroundColor : null,
padding: padding, child: Padding(
child: child, key: blockComponentKey,
padding: padding,
child: child,
),
); );
child = BlockSelectionContainer( child = BlockSelectionContainer(
@ -215,17 +217,14 @@ class _BulletedListIcon extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return Container(
width: 22, constraints: const BoxConstraints(minWidth: 26, minHeight: 22),
height: 22, padding: const EdgeInsets.only(right: 4.0),
child: Padding( child: Center(
padding: const EdgeInsets.only(right: 5.0), child: Text(
child: Center( icon,
child: Text( style: textStyle,
icon, textScaleFactor: 0.5,
style: textStyle,
textScaleFactor: 0.5,
),
), ),
), ),
); );

View file

@ -126,7 +126,6 @@ class _HeadingBlockComponentWidgetState
); );
Widget child = Container( Widget child = Container(
color: backgroundColor,
width: double.infinity, width: double.infinity,
alignment: alignment, alignment: alignment,
// Related issue: https://github.com/AppFlowy-IO/AppFlowy/issues/3175 // Related issue: https://github.com/AppFlowy-IO/AppFlowy/issues/3175
@ -170,10 +169,13 @@ class _HeadingBlockComponentWidgetState
), ),
); );
child = Padding( child = Container(
key: blockComponentKey, color: backgroundColor,
padding: padding, child: Padding(
child: child, key: blockComponentKey,
padding: padding,
child: child,
),
); );
child = BlockSelectionContainer( child = BlockSelectionContainer(

View file

@ -38,16 +38,16 @@ void showImageMenu(
} }
menuService.dismiss(); menuService.dismiss();
imageMenuEntry.remove(); imageMenuEntry.remove();
keepEditorFocusNotifier.value -= 1; keepEditorFocusNotifier.decrease();
} }
keepEditorFocusNotifier.value += 1; keepEditorFocusNotifier.increase();
imageMenuEntry = FullScreenOverlayEntry( imageMenuEntry = FullScreenOverlayEntry(
left: left, left: left,
right: right, right: right,
top: top, top: top,
bottom: bottom, bottom: bottom,
dismissCallback: () => keepEditorFocusNotifier.value -= 1, dismissCallback: () => keepEditorFocusNotifier.decrease(),
builder: (context) => UploadImageMenu( builder: (context) => UploadImageMenu(
backgroundColor: menuService.style.selectionMenuBackgroundColor, backgroundColor: menuService.style.selectionMenuBackgroundColor,
headerColor: menuService.style.selectionMenuItemTextColor, headerColor: menuService.style.selectionMenuItemTextColor,

View file

@ -121,7 +121,6 @@ class _NumberedListBlockComponentWidgetState
); );
Widget child = Container( Widget child = Container(
color: withBackgroundColor ? backgroundColor : null,
width: double.infinity, width: double.infinity,
alignment: alignment, alignment: alignment,
child: Row( child: Row(
@ -161,10 +160,13 @@ class _NumberedListBlockComponentWidgetState
), ),
); );
child = Padding( child = Container(
key: blockComponentKey, color: withBackgroundColor ? backgroundColor : null,
padding: padding, child: Padding(
child: child, key: blockComponentKey,
padding: padding,
child: child,
),
); );
child = BlockSelectionContainer( child = BlockSelectionContainer(
@ -206,15 +208,18 @@ class _NumberedListIcon extends StatelessWidget {
final editorState = context.read<EditorState>(); final editorState = context.read<EditorState>();
final text = editorState.editorStyle.textStyleConfiguration.text; final text = editorState.editorStyle.textStyleConfiguration.text;
final level = _NumberedListIconBuilder(node: node).level; final level = _NumberedListIconBuilder(node: node).level;
return Padding( return Container(
padding: const EdgeInsets.only(right: 5.0), constraints: const BoxConstraints(minWidth: 26, minHeight: 22),
child: Text.rich( padding: const EdgeInsets.only(right: 4.0),
textHeightBehavior: const TextHeightBehavior( child: Center(
applyHeightToFirstAscent: false, child: Text.rich(
applyHeightToLastDescent: false, textHeightBehavior: const TextHeightBehavior(
applyHeightToFirstAscent: false,
applyHeightToLastDescent: false,
),
TextSpan(text: '$level.', style: text.combine(textStyle)),
textDirection: direction,
), ),
TextSpan(text: '$level.', style: text.combine(textStyle)),
textDirection: direction,
), ),
); );
} }

View file

@ -0,0 +1,194 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
class ParagraphBlockKeys {
ParagraphBlockKeys._();
static const String type = 'paragraph';
static const String delta = blockComponentDelta;
static const String backgroundColor = blockComponentBackgroundColor;
static const String textDirection = blockComponentTextDirection;
}
Node paragraphNode({
String? text,
Delta? delta,
String? textDirection,
Attributes? attributes,
Iterable<Node> children = const [],
}) {
return Node(
type: ParagraphBlockKeys.type,
attributes: {
ParagraphBlockKeys.delta:
(delta ?? (Delta()..insert(text ?? ''))).toJson(),
if (attributes != null) ...attributes,
if (textDirection != null)
ParagraphBlockKeys.textDirection: textDirection,
},
children: children,
);
}
class ParagraphBlockComponentBuilder extends BlockComponentBuilder {
ParagraphBlockComponentBuilder({
super.configuration,
});
@override
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
final node = blockComponentContext.node;
return ParagraphBlockComponentWidget(
node: node,
key: node.key,
configuration: configuration,
showActions: showActions(node),
actionBuilder: (context, state) => actionBuilder(
blockComponentContext,
state,
),
);
}
@override
bool validate(Node node) {
return node.delta != null;
}
}
class ParagraphBlockComponentWidget extends BlockComponentStatefulWidget {
const ParagraphBlockComponentWidget({
super.key,
required super.node,
super.showActions,
super.actionBuilder,
super.configuration = const BlockComponentConfiguration(),
});
@override
State<ParagraphBlockComponentWidget> createState() =>
_ParagraphBlockComponentWidgetState();
}
class _ParagraphBlockComponentWidgetState
extends State<ParagraphBlockComponentWidget>
with
SelectableMixin,
DefaultSelectableMixin,
BlockComponentConfigurable,
BlockComponentBackgroundColorMixin,
NestedBlockComponentStatefulWidgetMixin,
BlockComponentTextDirectionMixin,
BlockComponentAlignMixin {
@override
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
@override
GlobalKey<State<StatefulWidget>> get containerKey => widget.node.key;
@override
GlobalKey<State<StatefulWidget>> blockComponentKey = GlobalKey(
debugLabel: ParagraphBlockKeys.type,
);
@override
BlockComponentConfiguration get configuration => widget.configuration;
@override
Node get node => widget.node;
bool _showPlaceholder = false;
@override
void initState() {
super.initState();
editorState.selectionNotifier.addListener(_onSelectionChange);
_onSelectionChange();
}
@override
void dispose() {
editorState.selectionNotifier.removeListener(_onSelectionChange);
super.dispose();
}
void _onSelectionChange() {
final selection = editorState.selection;
final showPlaceholder = selection != null &&
(selection.isSingle && selection.start.path.equals(node.path));
if (showPlaceholder != _showPlaceholder) {
setState(() => _showPlaceholder = showPlaceholder);
}
}
@override
Widget buildComponent(
BuildContext context, {
bool withBackgroundColor = true,
}) {
final textDirection = calculateTextDirection(
layoutDirection: Directionality.maybeOf(context),
);
Widget child = Container(
width: double.infinity,
alignment: alignment,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
textDirection: textDirection,
children: [
AppFlowyRichText(
key: forwardKey,
delegate: this,
node: widget.node,
editorState: editorState,
textAlign: alignment?.toTextAlign,
placeholderText: _showPlaceholder ? placeholderText : ' ',
textSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(textStyle),
placeholderTextSpanDecorator: (textSpan) =>
textSpan.updateTextStyle(placeholderTextStyle),
textDirection: textDirection,
cursorColor: editorState.editorStyle.cursorColor,
selectionColor: editorState.editorStyle.selectionColor,
),
],
),
);
child = Container(
color: withBackgroundColor ? backgroundColor : null,
child: Padding(
key: blockComponentKey,
padding: padding,
child: child,
),
);
child = BlockSelectionContainer(
node: node,
delegate: this,
listenable: editorState.selectionNotifier,
blockColor: editorState.editorStyle.selectionColor,
supportTypes: const [
BlockSelectionType.block,
],
child: child,
);
if (widget.showActions && widget.actionBuilder != null) {
child = BlockComponentActionWrapper(
node: node,
actionBuilder: widget.actionBuilder!,
child: child,
);
}
return child;
}
}

View file

@ -112,7 +112,6 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
); );
Widget child = Container( Widget child = Container(
color: backgroundColor,
width: double.infinity, width: double.infinity,
alignment: alignment, alignment: alignment,
child: IntrinsicHeight( child: IntrinsicHeight(
@ -150,10 +149,13 @@ class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
), ),
); );
child = Padding( child = Container(
key: blockComponentKey, color: backgroundColor,
padding: padding, child: Padding(
child: child, key: blockComponentKey,
padding: padding,
child: child,
),
); );
child = BlockSelectionContainer( child = BlockSelectionContainer(
@ -184,11 +186,17 @@ class _QuoteIcon extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const EditorSvg( return Container(
width: 20, constraints: const BoxConstraints(minWidth: 26, minHeight: 22),
height: 20, padding: const EdgeInsets.only(right: 4.0),
padding: EdgeInsets.only(right: 5.0), child: const Center(
name: 'quote', child: EditorSvg(
width: 20,
height: 20,
padding: EdgeInsets.only(right: 4.0),
name: 'quote',
),
),
); );
} }
} }

View file

@ -5,7 +5,7 @@ const standardBlockComponentConfiguration = BlockComponentConfiguration();
final Map<String, BlockComponentBuilder> standardBlockComponentBuilderMap = { final Map<String, BlockComponentBuilder> standardBlockComponentBuilderMap = {
PageBlockKeys.type: PageBlockComponentBuilder(), PageBlockKeys.type: PageBlockComponentBuilder(),
ParagraphBlockKeys.type: TextBlockComponentBuilder( ParagraphBlockKeys.type: ParagraphBlockComponentBuilder(
configuration: standardBlockComponentConfiguration.copyWith( configuration: standardBlockComponentConfiguration.copyWith(
placeholderText: (_) => PlatformExtension.isDesktopOrWeb placeholderText: (_) => PlatformExtension.isDesktopOrWeb
? AppFlowyEditorLocalizations.current.slashPlaceHolder ? AppFlowyEditorLocalizations.current.slashPlaceHolder

View file

@ -132,7 +132,6 @@ class _TodoListBlockComponentWidgetState
); );
Widget child = Container( Widget child = Container(
color: withBackgroundColor ? backgroundColor : null,
width: double.infinity, width: double.infinity,
alignment: alignment, alignment: alignment,
child: Row( child: Row(
@ -173,10 +172,13 @@ class _TodoListBlockComponentWidgetState
), ),
); );
child = Padding( child = Container(
key: blockComponentKey, color: withBackgroundColor ? backgroundColor : null,
padding: padding, child: Padding(
child: child, key: blockComponentKey,
padding: padding,
child: child,
),
); );
child = BlockSelectionContainer( child = BlockSelectionContainer(
@ -236,11 +238,14 @@ class _TodoListIcon extends StatelessWidget {
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: onTap, onTap: onTap,
child: EditorSvg( child: Container(
width: 22, constraints: const BoxConstraints(minWidth: 26, minHeight: 22),
height: 22, padding: const EdgeInsets.only(right: 4.0),
padding: const EdgeInsets.only(right: 5.0), child: EditorSvg(
name: checked ? 'check' : 'uncheck', width: 22,
height: 22,
name: checked ? 'check' : 'uncheck',
),
), ),
), ),
); );

View file

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/flutter/overlay.dart'; import 'package:appflowy_editor/src/flutter/overlay.dart';
import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart'; import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart';
@ -11,7 +13,7 @@ import 'package:provider/provider.dart';
// decrease the value when the popover is closed // decrease the value when the popover is closed
// only grab the focus when the value is 0 // only grab the focus when the value is 0
// the operation must be paired // the operation must be paired
ValueNotifier<int> keepEditorFocusNotifier = ValueNotifier(0); KeepEditorFocusNotifier keepEditorFocusNotifier = KeepEditorFocusNotifier();
class AppFlowyEditor extends StatefulWidget { class AppFlowyEditor extends StatefulWidget {
AppFlowyEditor({ AppFlowyEditor({
@ -267,3 +269,26 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
builders: {...widget.blockComponentBuilders}, builders: {...widget.blockComponentBuilders},
); );
} }
class KeepEditorFocusNotifier extends ValueNotifier<int> {
KeepEditorFocusNotifier() : super(0);
bool get shouldKeepFocus => value > 0;
@override
set value(int v) {
super.value = max(0, v);
}
void increase() {
value++;
}
void decrease() {
value--;
}
void reset() {
value = 0;
}
}

View file

@ -119,7 +119,7 @@ class NonDeltaTextInputService extends TextInputService with TextInputClient {
@override @override
void close() { void close() {
keepEditorFocusNotifier.value = 0; keepEditorFocusNotifier.reset();
currentTextEditingValue = null; currentTextEditingValue = null;
composingTextRange = null; composingTextRange = null;
_textInputConnection?.close(); _textInputConnection?.close();

View file

@ -265,7 +265,7 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
// clear the selection when the focus is lost. // clear the selection when the focus is lost.
if (!focusNode.hasFocus) { if (!focusNode.hasFocus) {
if (PlatformExtension.isDesktopOrWeb) { if (PlatformExtension.isDesktopOrWeb) {
if (keepEditorFocusNotifier.value > 0) { if (keepEditorFocusNotifier.shouldKeepFocus) {
return; return;
} }
} }
@ -283,7 +283,7 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
'keyboard service - on keep editor focus changed: ${keepEditorFocusNotifier.value}}', 'keyboard service - on keep editor focus changed: ${keepEditorFocusNotifier.value}}',
); );
if (keepEditorFocusNotifier.value == 0) { if (!keepEditorFocusNotifier.shouldKeepFocus) {
focusNode.requestFocus(); focusNode.requestFocus();
} }
} }
@ -302,7 +302,10 @@ class KeyboardServiceWidgetState extends State<KeyboardServiceWidget>
if (renderBox != null && selectable != null) { if (renderBox != null && selectable != null) {
final size = renderBox.size; final size = renderBox.size;
final transform = renderBox.getTransformTo(null); final transform = renderBox.getTransformTo(null);
final rect = selectable.getCursorRectInPosition(selection.end); final rect = selectable.getCursorRectInPosition(
selection.end,
shiftWithBaseOffset: true,
);
if (rect != null) { if (rect != null) {
textInputService.updateCaretPosition(size, transform, rect); textInputService.updateCaretPosition(size, transform, rect);
} }

View file

@ -81,10 +81,10 @@ Future<bool> _showSlashMenu(
if (shouldInsertSlash) { if (shouldInsertSlash) {
if (kIsWeb) { if (kIsWeb) {
// Have no idea why the focus will lose after inserting on web. // Have no idea why the focus will lose after inserting on web.
keepEditorFocusNotifier.value += 1; keepEditorFocusNotifier.increase();
await editorState.insertTextAtPosition('/', position: selection.start); await editorState.insertTextAtPosition('/', position: selection.start);
WidgetsBinding.instance.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
(timeStamp) => keepEditorFocusNotifier.value -= 1, (timeStamp) => keepEditorFocusNotifier.decrease(),
); );
} else { } else {
await editorState.insertTextAtPosition('/', position: selection.start); await editorState.insertTextAtPosition('/', position: selection.start);

View file

@ -268,7 +268,7 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
_showingItems = widget.items; _showingItems = widget.items;
keepEditorFocusNotifier.value += 1; keepEditorFocusNotifier.increase();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus(); _focusNode.requestFocus();
}); });
@ -277,7 +277,7 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
@override @override
void dispose() { void dispose() {
_focusNode.dispose(); _focusNode.dispose();
keepEditorFocusNotifier.value -= 1; keepEditorFocusNotifier.decrease();
super.dispose(); super.dispose();
} }

View file

@ -37,7 +37,7 @@ void showColorMenu(
overlay = null; overlay = null;
} }
keepEditorFocusNotifier.value += 1; keepEditorFocusNotifier.increase();
overlay = FullScreenOverlayEntry( overlay = FullScreenOverlayEntry(
top: top, top: top,
bottom: bottom, bottom: bottom,
@ -67,7 +67,7 @@ void showColorMenu(
withUpdateSelection: true, withUpdateSelection: true,
); );
dismissOverlay(); dismissOverlay();
keepEditorFocusNotifier.value -= 1; keepEditorFocusNotifier.decrease();
}, },
resetText: isTextColor resetText: isTextColor
? AppFlowyEditorLocalizations.current.resetToDefaultColor ? AppFlowyEditorLocalizations.current.resetToDefaultColor

View file

@ -63,18 +63,18 @@ void showLinkMenu(
OverlayEntry? overlay; OverlayEntry? overlay;
void dismissOverlay() { void dismissOverlay() {
keepEditorFocusNotifier.value -= 1; keepEditorFocusNotifier.decrease();
overlay?.remove(); overlay?.remove();
overlay = null; overlay = null;
} }
keepEditorFocusNotifier.value += 1; keepEditorFocusNotifier.increase();
overlay = FullScreenOverlayEntry( overlay = FullScreenOverlayEntry(
top: top, top: top,
bottom: bottom, bottom: bottom,
left: left, left: left,
right: right, right: right,
dismissCallback: () => keepEditorFocusNotifier.value -= 1, dismissCallback: () => keepEditorFocusNotifier.decrease(),
builder: (context) { builder: (context) {
return LinkMenu( return LinkMenu(
linkText: linkText, linkText: linkText,

View file

@ -21,7 +21,7 @@ void main() async {
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pump(); await tester.pump();
await gesture.moveTo( await gesture.moveTo(
tester.getCenter(find.byType(TextBlockComponentWidget)), tester.getCenter(find.byType(ParagraphBlockComponentWidget)),
); );
await tester.pumpAndSettle(const Duration(milliseconds: 500)); await tester.pumpAndSettle(const Duration(milliseconds: 500));
@ -60,7 +60,7 @@ class CustomActionBuilder extends StatelessWidget {
final editorState = EditorState(document: document); final editorState = EditorState(document: document);
final paragraphBuilder = TextBlockComponentBuilder( final paragraphBuilder = ParagraphBlockComponentBuilder(
configuration: standardBlockComponentConfiguration, configuration: standardBlockComponentConfiguration,
); );
paragraphBuilder.showActions = (_) => true; paragraphBuilder.showActions = (_) => true;

View file

@ -50,7 +50,7 @@ void main() async {
expect(editorState.selection, null); expect(editorState.selection, null);
await tester.tapAt( await tester.tapAt(
tester.getTopLeft(find.byType(TextBlockComponentWidget)), tester.getTopLeft(find.byType(ParagraphBlockComponentWidget)),
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(widget.focusNode.hasFocus, false); expect(widget.focusNode.hasFocus, false);