chore: optimize context menu logic (#1161)
* chore: optimize context menu logic * chore: disable keyboard service and shortcut service
This commit is contained in:
parent
15289103a2
commit
cf0907738c
2 changed files with 414 additions and 67 deletions
|
|
@ -61,6 +61,8 @@ class _DesktopSelectionServiceWidgetState
|
||||||
listen: false,
|
listen: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_ContextMenuKeyboardInterceptor? _keyboardInterceptor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -157,9 +159,34 @@ class _DesktopSelectionServiceWidgetState
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearContextMenu() {
|
void _clearContextMenu() {
|
||||||
|
if (_contextMenuAreas.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_contextMenuAreas
|
_contextMenuAreas
|
||||||
..forEach((overlay) => overlay.remove())
|
..forEach((overlay) => overlay.remove())
|
||||||
..clear();
|
..clear();
|
||||||
|
|
||||||
|
if (_keyboardInterceptor != null) {
|
||||||
|
editorState.service.keyboardService
|
||||||
|
?.unregisterInterceptor(_keyboardInterceptor!);
|
||||||
|
_keyboardInterceptor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
editorState.service.keyboardService?.enableShortcuts();
|
||||||
|
editorState.service.keyboardService?.enable();
|
||||||
|
|
||||||
|
final selection = editorState.selectionNotifier.value;
|
||||||
|
if (selection != null) {
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
null,
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
selection,
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -288,35 +315,37 @@ class _DesktopSelectionServiceWidgetState
|
||||||
void _onSecondaryTapDown(TapDownDetails details) {
|
void _onSecondaryTapDown(TapDownDetails details) {
|
||||||
final offset = details.globalPosition;
|
final offset = details.globalPosition;
|
||||||
final selection = editorState.selectionNotifier.value;
|
final selection = editorState.selectionNotifier.value;
|
||||||
final selectable = getNodeInOffset(offset)?.selectable;
|
final node = getNodeInOffset(offset);
|
||||||
|
final selectable = node?.selectable;
|
||||||
|
|
||||||
if (selectable == null) {
|
if (selectable == null) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final wordBoundary = selectable.getWordBoundaryInOffset(offset);
|
|
||||||
|
|
||||||
|
final position = selectable.getPositionInOffset(offset);
|
||||||
final Selection? newSelection;
|
final Selection? newSelection;
|
||||||
|
|
||||||
final isSelectionCollapsed = selection == null || selection.isCollapsed;
|
// cases
|
||||||
|
// 1. if the selection is null, then select the current position as a collapsed selection
|
||||||
|
// 2. if the selection is collapsed, then keep it without changes
|
||||||
|
// 3. if the selection is not collapsed, then check if tap is within a selected node
|
||||||
|
// 4. if tap is within the selected nodes, then keep current selection
|
||||||
|
// 5. if tap is outside the selected nodes, then create a collapsed selection at tap point
|
||||||
|
|
||||||
final isWordBoundaryWithinSelection = !isSelectionCollapsed &&
|
if (selection == null) {
|
||||||
wordBoundary != null &&
|
newSelection = Selection.collapsed(position);
|
||||||
(wordBoundary.start.path > selection.start.path ||
|
} else if (selection.isCollapsed) {
|
||||||
wordBoundary.start.path == selection.start.path &&
|
|
||||||
wordBoundary.start.offset >= selection.start.offset) &&
|
|
||||||
(wordBoundary.end.path < selection.end.path ||
|
|
||||||
wordBoundary.end.path == selection.end.path &&
|
|
||||||
wordBoundary.end.offset <= selection.end.offset);
|
|
||||||
|
|
||||||
if (isWordBoundaryWithinSelection) {
|
|
||||||
newSelection = selection;
|
newSelection = selection;
|
||||||
} else if (selectable.cursorStyle == CursorStyle.verticalLine) {
|
|
||||||
newSelection = wordBoundary;
|
|
||||||
} else {
|
} else {
|
||||||
newSelection = Selection(
|
final selectedNodes = editorState.getNodesInSelection(selection);
|
||||||
start: selectable.start(),
|
final isTapInSelectedNode = selectedNodes.any((n) => n == node);
|
||||||
end: selectable.end(),
|
|
||||||
);
|
if (isTapInSelectedNode) {
|
||||||
|
newSelection = selection;
|
||||||
|
} else {
|
||||||
|
newSelection = Selection.collapsed(position);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
editorState.updateSelectionWithReason(
|
editorState.updateSelectionWithReason(
|
||||||
|
|
@ -498,6 +527,13 @@ class _DesktopSelectionServiceWidgetState
|
||||||
|
|
||||||
_contextMenuAreas.add(contextMenu);
|
_contextMenuAreas.add(contextMenu);
|
||||||
Overlay.of(context, rootOverlay: true).insert(contextMenu);
|
Overlay.of(context, rootOverlay: true).insert(contextMenu);
|
||||||
|
|
||||||
|
_keyboardInterceptor = _ContextMenuKeyboardInterceptor();
|
||||||
|
editorState.service.keyboardService
|
||||||
|
?.registerInterceptor(_keyboardInterceptor!);
|
||||||
|
|
||||||
|
editorState.service.keyboardService?.disableShortcuts();
|
||||||
|
editorState.service.keyboardService?.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -639,3 +675,49 @@ class _DesktopSelectionServiceWidgetState
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ContextMenuKeyboardInterceptor
|
||||||
|
extends AppFlowyKeyboardServiceInterceptor {
|
||||||
|
@override
|
||||||
|
Future<bool> interceptInsert(
|
||||||
|
TextEditingDeltaInsertion insertion,
|
||||||
|
EditorState editorState,
|
||||||
|
List<CharacterShortcutEvent> characterShortcutEvents,
|
||||||
|
) async {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> interceptDelete(
|
||||||
|
TextEditingDeltaDeletion deletion,
|
||||||
|
EditorState editorState,
|
||||||
|
) async {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> interceptReplace(
|
||||||
|
TextEditingDeltaReplacement replacement,
|
||||||
|
EditorState editorState,
|
||||||
|
List<CharacterShortcutEvent> characterShortcutEvents,
|
||||||
|
) async {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> interceptNonTextUpdate(
|
||||||
|
TextEditingDeltaNonTextUpdate nonTextUpdate,
|
||||||
|
EditorState editorState,
|
||||||
|
List<CharacterShortcutEvent> characterShortcutEvents,
|
||||||
|
) async {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> interceptPerformAction(
|
||||||
|
TextInputAction action,
|
||||||
|
EditorState editorState,
|
||||||
|
) async {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:appflowy_editor/src/editor/block_component/table_block_component/util.dart';
|
import 'package:appflowy_editor/src/editor/block_component/table_block_component/util.dart';
|
||||||
import '../new/infra/testable_editor.dart';
|
import '../new/infra/testable_editor.dart';
|
||||||
|
|
@ -79,54 +81,317 @@ void main() async {
|
||||||
await editor.dispose();
|
await editor.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: lucas.xu support context menu
|
testWidgets(
|
||||||
// testWidgets('Test secondary tap', (tester) async {
|
'Test secondary tap (right-click) - no selection creates collapsed selection',
|
||||||
// const text = 'Welcome to Appflowy 😁';
|
(tester) async {
|
||||||
// final editor = tester.editor..addParagraphs(3, initialText: text);
|
const text = 'Welcome to Appflowy 😁';
|
||||||
// await editor.startTesting();
|
final editor = tester.editor..addParagraphs(3, initialText: text);
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
// final secondNode = editor.nodeAtPath([1]) as Node;
|
final secondNode = editor.nodeAtPath([1]);
|
||||||
// final finder = find.byKey(secondNode.key);
|
final finder = find.byKey(secondNode!.key);
|
||||||
|
|
||||||
// final rect = tester.getRect(finder);
|
final rect = tester.getRect(finder);
|
||||||
// // secondary tap
|
expect(editor.selection, isNull);
|
||||||
// await tester.tapAt(
|
|
||||||
// rect.centerLeft + const Offset(10.0, 0.0),
|
|
||||||
// buttons: kSecondaryButton,
|
|
||||||
// );
|
|
||||||
// await tester.pump();
|
|
||||||
|
|
||||||
// const welcome = 'Welcome';
|
await tester.tapAt(
|
||||||
// expect(
|
rect.centerLeft + const Offset(50.0, 0.0),
|
||||||
// editor.selection,
|
buttons: kSecondaryButton,
|
||||||
// Selection.single(
|
);
|
||||||
// path: [1],
|
await tester.pump();
|
||||||
// startOffset: 0,
|
|
||||||
// endOffset: welcome.length,
|
|
||||||
// ), // Welcome
|
|
||||||
// );
|
|
||||||
|
|
||||||
// final contextMenu = find.byType(ContextMenu);
|
expect(editor.selection, isNotNull);
|
||||||
// expect(contextMenu, findsOneWidget);
|
expect(editor.selection!.isCollapsed, isTrue);
|
||||||
|
expect(editor.selection!.start.path, [1]);
|
||||||
|
|
||||||
// // test built in context menu items
|
final contextMenu = find.byType(ContextMenu);
|
||||||
|
expect(contextMenu, findsOneWidget);
|
||||||
|
|
||||||
// // Skip the Windows platform because the rich_clipboard package doesn't support it perfectly.
|
await editor.dispose();
|
||||||
// if (Platform.isWindows) {
|
},
|
||||||
// return;
|
);
|
||||||
// }
|
|
||||||
|
|
||||||
// // cut
|
testWidgets(
|
||||||
// await tester.tap(find.text('Cut'));
|
'Test secondary tap - collapsed selection remains unchanged',
|
||||||
// await tester.pump();
|
(tester) async {
|
||||||
// expect(
|
const text = 'Welcome to Appflowy 😁';
|
||||||
// secondNode.delta!.toPlainText(),
|
final editor = tester.editor..addParagraphs(3, initialText: text);
|
||||||
// text.replaceAll(welcome, ''),
|
await editor.startTesting();
|
||||||
// );
|
|
||||||
//
|
final secondNode = editor.nodeAtPath([1]);
|
||||||
// await editor.dispose();
|
final finder = find.byKey(secondNode!.key);
|
||||||
// // TODO: the copy and paste test is not working during test env.
|
|
||||||
// });
|
final rect = tester.getRect(finder);
|
||||||
|
|
||||||
|
await tester.tapAt(rect.centerLeft + const Offset(30.0, 0.0));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final originalSelection = editor.selection;
|
||||||
|
expect(originalSelection, isNotNull);
|
||||||
|
expect(originalSelection!.isCollapsed, isTrue);
|
||||||
|
|
||||||
|
await tester.tapAt(
|
||||||
|
rect.centerLeft + const Offset(80.0, 0.0),
|
||||||
|
buttons: kSecondaryButton,
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(editor.selection, equals(originalSelection));
|
||||||
|
|
||||||
|
final contextMenu = find.byType(ContextMenu);
|
||||||
|
expect(contextMenu, findsOneWidget);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Test secondary tap - non-collapsed selection remains when tapping within selected node',
|
||||||
|
(tester) async {
|
||||||
|
const text = 'Welcome to Appflowy 😁';
|
||||||
|
final editor = tester.editor..addParagraphs(3, initialText: text);
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
|
final secondNode = editor.nodeAtPath([1]);
|
||||||
|
final finder = find.byKey(secondNode!.key);
|
||||||
|
|
||||||
|
final rect = tester.getRect(finder);
|
||||||
|
|
||||||
|
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
|
||||||
|
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final originalSelection = editor.selection;
|
||||||
|
expect(originalSelection, isNotNull);
|
||||||
|
expect(originalSelection!.isCollapsed, isFalse);
|
||||||
|
expect(originalSelection.start.path, [1]);
|
||||||
|
expect(originalSelection.end.path, [1]);
|
||||||
|
|
||||||
|
await tester.tapAt(
|
||||||
|
rect.centerLeft + const Offset(20.0, 0.0),
|
||||||
|
buttons: kSecondaryButton,
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(editor.selection, equals(originalSelection));
|
||||||
|
|
||||||
|
final contextMenu = find.byType(ContextMenu);
|
||||||
|
expect(contextMenu, findsOneWidget);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Test secondary tap - multi-node selection remains when tapping within selected nodes',
|
||||||
|
(tester) async {
|
||||||
|
const text = 'Welcome to Appflowy 😁';
|
||||||
|
final editor = tester.editor..addParagraphs(3, initialText: text);
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
|
final firstNode = editor.nodeAtPath([0]);
|
||||||
|
final secondNode = editor.nodeAtPath([1]);
|
||||||
|
final firstFinder = find.byKey(firstNode!.key);
|
||||||
|
final secondFinder = find.byKey(secondNode!.key);
|
||||||
|
|
||||||
|
final firstRect = tester.getRect(firstFinder);
|
||||||
|
final secondRect = tester.getRect(secondFinder);
|
||||||
|
|
||||||
|
await tester.timedDragFrom(
|
||||||
|
firstRect.centerLeft + const Offset(10.0, 0.0),
|
||||||
|
secondRect.center - firstRect.centerLeft - const Offset(10.0, 0.0),
|
||||||
|
const Duration(milliseconds: 500),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final originalSelection = editor.selection;
|
||||||
|
expect(originalSelection, isNotNull);
|
||||||
|
expect(originalSelection!.isCollapsed, isFalse);
|
||||||
|
|
||||||
|
await tester.tapAt(
|
||||||
|
secondRect.centerLeft + const Offset(30.0, 0.0),
|
||||||
|
buttons: kSecondaryButton,
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(editor.selection, equals(originalSelection));
|
||||||
|
|
||||||
|
final contextMenu = find.byType(ContextMenu);
|
||||||
|
expect(contextMenu, findsOneWidget);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Test secondary tap - selection changes when tapping outside selected nodes',
|
||||||
|
(tester) async {
|
||||||
|
const text = 'Welcome to Appflowy 😁';
|
||||||
|
final editor = tester.editor..addParagraphs(3, initialText: text);
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
|
final firstNode = editor.nodeAtPath([0]);
|
||||||
|
final secondNode = editor.nodeAtPath([1]);
|
||||||
|
final thirdNode = editor.nodeAtPath([2]);
|
||||||
|
final firstFinder = find.byKey(firstNode!.key);
|
||||||
|
final secondFinder = find.byKey(secondNode!.key);
|
||||||
|
final thirdFinder = find.byKey(thirdNode!.key);
|
||||||
|
|
||||||
|
final firstRect = tester.getRect(firstFinder);
|
||||||
|
final secondRect = tester.getRect(secondFinder);
|
||||||
|
final thirdRect = tester.getRect(thirdFinder);
|
||||||
|
|
||||||
|
await tester.timedDragFrom(
|
||||||
|
firstRect.centerLeft + const Offset(10.0, 0.0),
|
||||||
|
secondRect.center - firstRect.centerLeft - const Offset(10.0, 0.0),
|
||||||
|
const Duration(milliseconds: 500),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final originalSelection = editor.selection;
|
||||||
|
expect(originalSelection, isNotNull);
|
||||||
|
expect(originalSelection!.isCollapsed, isFalse);
|
||||||
|
|
||||||
|
await tester.tapAt(
|
||||||
|
thirdRect.centerLeft + const Offset(30.0, 0.0),
|
||||||
|
buttons: kSecondaryButton,
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(editor.selection, isNotNull);
|
||||||
|
expect(editor.selection!.isCollapsed, isTrue);
|
||||||
|
expect(editor.selection!.start.path, [2]);
|
||||||
|
expect(editor.selection, isNot(equals(originalSelection)));
|
||||||
|
|
||||||
|
final contextMenu = find.byType(ContextMenu);
|
||||||
|
expect(contextMenu, findsOneWidget);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Test secondary tap - toolbar re-enabled when context menu is dismissed by clicking mask',
|
||||||
|
(tester) async {
|
||||||
|
const text = 'Welcome to Appflowy 😁';
|
||||||
|
final editor = tester.editor..addParagraphs(3, initialText: text);
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
|
final secondNode = editor.nodeAtPath([1]);
|
||||||
|
final finder = find.byKey(secondNode!.key);
|
||||||
|
|
||||||
|
final rect = tester.getRect(finder);
|
||||||
|
|
||||||
|
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
|
||||||
|
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final originalSelection = editor.selection;
|
||||||
|
expect(originalSelection, isNotNull);
|
||||||
|
expect(originalSelection!.isCollapsed, isFalse);
|
||||||
|
|
||||||
|
await tester.tapAt(
|
||||||
|
rect.centerLeft + const Offset(20.0, 0.0),
|
||||||
|
buttons: kSecondaryButton,
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final contextMenu = find.byType(ContextMenu);
|
||||||
|
expect(contextMenu, findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tapAt(const Offset(10.0, 10.0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(ContextMenu), findsNothing);
|
||||||
|
|
||||||
|
expect(editor.selection, equals(originalSelection));
|
||||||
|
expect(editor.selection, isNotNull);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Test secondary tap - context menu and toolbar behavior',
|
||||||
|
(tester) async {
|
||||||
|
const text = 'Welcome to Appflowy 😁';
|
||||||
|
final editor = tester.editor..addParagraphs(3, initialText: text);
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
|
final secondNode = editor.nodeAtPath([1]);
|
||||||
|
final finder = find.byKey(secondNode!.key);
|
||||||
|
|
||||||
|
final rect = tester.getRect(finder);
|
||||||
|
|
||||||
|
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
|
||||||
|
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final originalSelection = editor.selection;
|
||||||
|
expect(originalSelection, isNotNull);
|
||||||
|
expect(originalSelection!.isCollapsed, isFalse);
|
||||||
|
|
||||||
|
await tester.tapAt(
|
||||||
|
rect.centerLeft + const Offset(20.0, 0.0),
|
||||||
|
buttons: kSecondaryButton,
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final contextMenu = find.byType(ContextMenu);
|
||||||
|
expect(contextMenu, findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tapAt(const Offset(10.0, 10.0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(ContextMenu), findsNothing);
|
||||||
|
expect(editor.selection, isNotNull);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Test secondary tap - shortcuts blocked when context menu shows',
|
||||||
|
(tester) async {
|
||||||
|
const text = 'Welcome to Appflowy 😁';
|
||||||
|
final editor = tester.editor..addParagraphs(3, initialText: text);
|
||||||
|
await editor.startTesting();
|
||||||
|
|
||||||
|
final secondNode = editor.nodeAtPath([1]);
|
||||||
|
final finder = find.byKey(secondNode!.key);
|
||||||
|
|
||||||
|
final rect = tester.getRect(finder);
|
||||||
|
|
||||||
|
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
|
||||||
|
await tester.tapAt(rect.centerLeft + const Offset(10.0, 0.0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final originalSelection = editor.selection;
|
||||||
|
expect(originalSelection, isNotNull);
|
||||||
|
expect(originalSelection!.isCollapsed, isFalse);
|
||||||
|
|
||||||
|
await tester.tapAt(
|
||||||
|
rect.centerLeft + const Offset(20.0, 0.0),
|
||||||
|
buttons: kSecondaryButton,
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final contextMenu = find.byType(ContextMenu);
|
||||||
|
expect(contextMenu, findsOneWidget);
|
||||||
|
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(editor.selection, equals(originalSelection));
|
||||||
|
|
||||||
|
await tester.tapAt(const Offset(10.0, 10.0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(ContextMenu), findsNothing);
|
||||||
|
|
||||||
|
await editor.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('single tap with horizontal nodes', (tester) async {
|
testWidgets('single tap with horizontal nodes', (tester) async {
|
||||||
var tableNode = TableNode.fromList([
|
var tableNode = TableNode.fromList([
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue