Skip to content

Commit 25db622

Browse files
[SuperEditor][Android][Web] - Fix ENTER not correctly splitting list items and tasks (Resolves #2600) (#2603)
1 parent cae6336 commit 25db622

File tree

3 files changed

+222
-6
lines changed

3 files changed

+222
-6
lines changed

super_editor/lib/src/default_editor/document_ime/document_delta_editing.dart

+6-6
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,10 @@ class TextDeltasDocumentEditor {
124124
editorImeLog.fine('Old text: "${delta.oldText}"');
125125

126126
if (delta.textInserted == "\n") {
127-
// On iOS, newlines are reported here and also to performAction().
128-
// On Android, newlines are only reported here. So, on Android,
127+
// On iOS native and Android Web, newlines are reported here and also to performAction().
128+
// On Android native, newlines are only reported here. So, on Android native,
129129
// we forward the newline action to performAction.
130-
if (defaultTargetPlatform == TargetPlatform.android) {
130+
if (defaultTargetPlatform == TargetPlatform.android && !CurrentPlatform.isWeb) {
131131
editorImeLog.fine("Received a newline insertion on Android. Forwarding to newline input action.");
132132
onPerformAction(TextInputAction.newline);
133133
} else {
@@ -197,10 +197,10 @@ class TextDeltasDocumentEditor {
197197
editorImeLog.fine('Old text: "${delta.oldText}"');
198198

199199
if (delta.replacementText == "\n") {
200-
// On iOS, newlines are reported here and also to performAction().
201-
// On Android, newlines are only reported here. So, on Android,
200+
// On iOS native and Android Web, newlines are reported here and also to performAction().
201+
// On Android native, newlines are only reported here. So, on Android native,
202202
// we forward the newline action to performAction.
203-
if (defaultTargetPlatform == TargetPlatform.android) {
203+
if (defaultTargetPlatform == TargetPlatform.android && !CurrentPlatform.isWeb) {
204204
editorImeLog.fine("Received a newline replacement on Android. Forwarding to newline input action.");
205205
onPerformAction(TextInputAction.newline);
206206
} else {

super_editor/test/super_editor/components/list_items_test.dart

+153
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,54 @@ void main() {
443443
);
444444
});
445445

446+
testWidgetsOnWebAndroid("inserts new item upon new line insertion at end of existing item", (tester) async {
447+
final context = await tester //
448+
.createDocument()
449+
.fromMarkdown('* Item 1')
450+
.pump();
451+
452+
final document = context.findEditContext().document;
453+
454+
// Place the caret at the end of the list item.
455+
await tester.placeCaretInParagraph(document.first.id, 6);
456+
457+
// Type at the end of the list item to generate a composing region,
458+
// simulating the Samsung keyboard.
459+
await tester.typeImeText('2');
460+
await tester.ime.sendDeltas(const [
461+
TextEditingDeltaNonTextUpdate(
462+
oldText: '. Item 12',
463+
selection: TextSelection.collapsed(offset: 9),
464+
composing: TextRange.collapsed(9),
465+
),
466+
], getter: imeClientGetter);
467+
468+
// On Android Web, pressing ENTER generates both a "\n" insertion and a newline input action.
469+
await tester.pressEnterWithIme(getter: imeClientGetter);
470+
471+
// Ensure that a new, empty list item was created.
472+
expect(document.nodeCount, 2);
473+
474+
// Ensure the existing item remains the same.
475+
expect(document.first, isA<ListItemNode>());
476+
expect((document.first as ListItemNode).text.toPlainText(), "Item 12");
477+
478+
// Ensure the new item has the correct list item type and indentation.
479+
expect(document.last, isA<ListItemNode>());
480+
expect((document.last as ListItemNode).text.toPlainText(), "");
481+
expect((document.last as ListItemNode).type, ListItemType.unordered);
482+
expect((document.last as ListItemNode).indent, 0);
483+
expect(
484+
SuperEditorInspector.findDocumentSelection(),
485+
DocumentSelection.collapsed(
486+
position: DocumentPosition(
487+
nodeId: document.last.id,
488+
nodePosition: const TextNodePosition(offset: 0),
489+
),
490+
),
491+
);
492+
});
493+
446494
testWidgetsOnMobile("inserts new item upon new line input action at end of existing item", (tester) async {
447495
final context = await tester //
448496
.createDocument()
@@ -558,6 +606,40 @@ void main() {
558606
);
559607
});
560608

609+
testWidgetsOnWebAndroid("splits list item into two upon new line insertion in middle of existing item",
610+
(tester) async {
611+
final context = await tester //
612+
.createDocument()
613+
.fromMarkdown('* List Item')
614+
.pump();
615+
616+
final document = context.findEditContext().document;
617+
618+
// Place the caret at "List |Item"
619+
await tester.placeCaretInParagraph(document.first.id, 5);
620+
621+
// On Android Web, pressing ENTER generates both a "\n" insertion and a newline input action.
622+
await tester.pressEnterWithIme(getter: imeClientGetter);
623+
624+
// Ensure that a new item was created with part of the previous item.
625+
expect(document.nodeCount, 2);
626+
expect(document.first, isA<ListItemNode>());
627+
expect((document.first as ListItemNode).text.toPlainText(), "List ");
628+
expect(document.last, isA<ListItemNode>());
629+
expect((document.last as ListItemNode).text.toPlainText(), "Item");
630+
expect((document.last as ListItemNode).type, ListItemType.unordered);
631+
expect((document.last as ListItemNode).indent, 0);
632+
expect(
633+
SuperEditorInspector.findDocumentSelection(),
634+
DocumentSelection.collapsed(
635+
position: DocumentPosition(
636+
nodeId: document.last.id,
637+
nodePosition: const TextNodePosition(offset: 0),
638+
),
639+
),
640+
);
641+
});
642+
561643
testWidgetsOnMobile("splits list item into two upon new line input action in middle of existing item",
562644
(tester) async {
563645
final context = await tester //
@@ -939,6 +1021,43 @@ A paragraph
9391021
);
9401022
});
9411023

1024+
testWidgetsOnWebAndroid("inserts new item upon new line insertion at end of existing item", (tester) async {
1025+
final context = await tester //
1026+
.createDocument()
1027+
.fromMarkdown('1. Item 1')
1028+
.pump();
1029+
1030+
final document = context.findEditContext().document;
1031+
1032+
// Place the caret at the end of the list item.
1033+
await tester.placeCaretInParagraph(document.first.id, 6);
1034+
1035+
// On Android Web, pressing ENTER generates both a "\n" insertion and a newline input action.
1036+
await tester.pressEnterWithIme(getter: imeClientGetter);
1037+
1038+
// Ensure that a new, empty list item was created.
1039+
expect(document.nodeCount, 2);
1040+
1041+
// Ensure the existing item remains the same.
1042+
expect(document.first, isA<ListItemNode>());
1043+
expect((document.first as ListItemNode).text.toPlainText(), "Item 1");
1044+
1045+
// Ensure the new item has the correct list item type and indentation.
1046+
expect(document.last, isA<ListItemNode>());
1047+
expect((document.last as ListItemNode).text.toPlainText(), "");
1048+
expect((document.last as ListItemNode).type, ListItemType.ordered);
1049+
expect((document.last as ListItemNode).indent, 0);
1050+
expect(
1051+
SuperEditorInspector.findDocumentSelection(),
1052+
DocumentSelection.collapsed(
1053+
position: DocumentPosition(
1054+
nodeId: document.last.id,
1055+
nodePosition: const TextNodePosition(offset: 0),
1056+
),
1057+
),
1058+
);
1059+
});
1060+
9421061
testWidgetsOnMobile("inserts new item upon new line input action at end of existing item", (tester) async {
9431062
final context = await tester //
9441063
.createDocument()
@@ -1043,6 +1162,40 @@ A paragraph
10431162
);
10441163
});
10451164

1165+
testWidgetsOnWebAndroid("splits list item into two upon new line insertion in middle of existing item",
1166+
(tester) async {
1167+
final context = await tester //
1168+
.createDocument()
1169+
.fromMarkdown('1. List Item')
1170+
.pump();
1171+
1172+
final document = context.findEditContext().document;
1173+
1174+
// Place the caret at "List |Item"
1175+
await tester.placeCaretInParagraph(document.first.id, 5);
1176+
1177+
// On Android Web, pressing ENTER generates both a "\n" insertion and a newline input action.
1178+
await tester.pressEnterWithIme(getter: imeClientGetter);
1179+
1180+
// Ensure that a new item was created with part of the previous item.
1181+
expect(document.nodeCount, 2);
1182+
expect(document.first, isA<ListItemNode>());
1183+
expect((document.first as ListItemNode).text.toPlainText(), "List ");
1184+
expect(document.last, isA<ListItemNode>());
1185+
expect((document.last as ListItemNode).text.toPlainText(), "Item");
1186+
expect((document.last as ListItemNode).type, ListItemType.ordered);
1187+
expect((document.last as ListItemNode).indent, 0);
1188+
expect(
1189+
SuperEditorInspector.findDocumentSelection(),
1190+
DocumentSelection.collapsed(
1191+
position: DocumentPosition(
1192+
nodeId: document.last.id,
1193+
nodePosition: const TextNodePosition(offset: 0),
1194+
),
1195+
),
1196+
);
1197+
});
1198+
10461199
testWidgetsOnMobile("splits list item into two upon new line input action in middle of existing item",
10471200
(tester) async {
10481201
final context = await tester //

super_editor/test/super_editor/components/task_test.dart

+63
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,38 @@ void main() {
157157
);
158158
});
159159

160+
testWidgetsOnWebAndroid("new task upon new line insertion at end of existing task", (tester) async {
161+
final document = MutableDocument(
162+
nodes: [
163+
TaskNode(id: "1", text: AttributedText("This is a task"), isComplete: false),
164+
],
165+
);
166+
final task = document.getNodeAt(0) as TaskNode;
167+
await _pumpScaffold(tester, document: document);
168+
169+
// Place the caret at the end of the task.
170+
await tester.placeCaretInParagraph("1", task.text.length);
171+
172+
// On Android Web, pressing ENTER generates both a "\n" insertion and a newline input action.
173+
await tester.pressEnterWithIme(getter: imeClientGetter);
174+
175+
// Ensure that a new, empty task was created.
176+
expect(document.nodeCount, 2);
177+
expect(document.first, isA<TaskNode>());
178+
expect((document.first as TaskNode).text.toPlainText(), "This is a task");
179+
expect(document.last, isA<TaskNode>());
180+
expect((document.last as TaskNode).text.toPlainText(), "");
181+
expect(
182+
SuperEditorInspector.findDocumentSelection(),
183+
DocumentSelection.collapsed(
184+
position: DocumentPosition(
185+
nodeId: document.last.id,
186+
nodePosition: const TextNodePosition(offset: 0),
187+
),
188+
),
189+
);
190+
});
191+
160192
testWidgetsOnMobile("new task upon new line input action at end of existing task", (tester) async {
161193
final document = MutableDocument(
162194
nodes: [
@@ -253,6 +285,37 @@ void main() {
253285
);
254286
});
255287

288+
testWidgetsOnWebAndroid("task into two upon new line insertion in middle of existing task", (tester) async {
289+
final document = MutableDocument(
290+
nodes: [
291+
TaskNode(id: "1", text: AttributedText("This is a task"), isComplete: false),
292+
],
293+
);
294+
await _pumpScaffold(tester, document: document);
295+
296+
// Place the caret at "This is |a task"
297+
await tester.placeCaretInParagraph("1", 8);
298+
299+
// On Android Web, pressing ENTER generates both a "\n" insertion and a newline input action.
300+
await tester.pressEnterWithIme(getter: imeClientGetter);
301+
302+
// Ensure that a new task was created with part of the previous task.
303+
expect(document.nodeCount, 2);
304+
expect(document.first, isA<TaskNode>());
305+
expect((document.first as TaskNode).text.toPlainText(), "This is ");
306+
expect(document.last, isA<TaskNode>());
307+
expect((document.last as TaskNode).text.toPlainText(), "a task");
308+
expect(
309+
SuperEditorInspector.findDocumentSelection(),
310+
DocumentSelection.collapsed(
311+
position: DocumentPosition(
312+
nodeId: document.last.id,
313+
nodePosition: const TextNodePosition(offset: 0),
314+
),
315+
),
316+
);
317+
});
318+
256319
testWidgetsOnMobile("task into two upon new line input action in middle of existing task", (tester) async {
257320
final document = MutableDocument(
258321
nodes: [

0 commit comments

Comments
 (0)