fix(editor): a rich text field focused without a cursor puts it at the end

AppFlowy's editor takes the focus without a selection when it is focused
from outside, by Tab or by a field moving it there (Todo's quick add, its
down arrow into the description), so it showed no cursor and took no
typing until clicked. Focused so, it now places the cursor at the end of
its last text.

Whats-New: Moving into a text with Tab or the arrow keys now puts the cursor there, ready to type.
Whats-New-De: Wer mit Tab oder den Pfeiltasten in einen Text wechselt, hat den Cursor jetzt gleich dort und kann tippen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-09 23:12:29 +01:00
commit 6356bed37d
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 80 additions and 0 deletions

View file

@ -117,6 +117,26 @@ class _RichContentEditorState() extends State<RichContentEditor> {
if (_focus.hasFocus != _focused) {
setState(() => _focused = _focus.hasFocus);
}
// Focus given without a click (asked for, or Tab from the field before)
// leaves the editor without a caret, and what is typed goes nowhere: it
// goes to the end of the text then.
if (_focus.hasFocus && _editorState.selection == null) {
if (_lastText(_editorState.document.root) case final node?) {
_editorState.selection = Selection.collapsed(
Position(path: node.path, offset: node.delta!.length),
);
}
}
}
/// The last node under [node] holding text, if any.
static Node? _lastText(Node node) {
for (final child in node.children.reversed) {
if (_lastText(child) case final found?) {
return found;
}
}
return node.delta != null ? node : null;
}
void _onTransaction(EditorTransactionValue value) {

View file

@ -1,4 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_editor/halkyon_editor.dart';
import 'package:material_ui/material_ui.dart';
@ -45,4 +46,63 @@ void main() {
.editorState;
expect(editorState.selection?.start.path, [2]);
});
group('Given an editor without a caret,', () {
late FocusNode focusNode;
Future<EditorState> pumpEditor(WidgetTester tester) async {
focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: editorTestDelegates,
home: Scaffold(
body: Column(
children: [
const TextField(key: Key('before'), autofocus: true),
SizedBox(
height: 200,
child: RichContentEditor(
initialContent: RichContent.plain('Water'),
focusNode: focusNode,
onChanged: (_) {},
),
),
],
),
),
),
);
await tester.pump(const Duration(seconds: 1));
return tester
.widget<AppFlowyEditor>(find.byType(AppFlowyEditor))
.editorState;
}
testWidgets('when its focus is asked for, then the caret is at the end '
'of its text.', (tester) async {
final editorState = await pumpEditor(tester);
expect(editorState.selection, isNull);
focusNode.requestFocus();
await tester.pump();
expect(focusNode.hasFocus, isTrue);
expect(
editorState.selection,
Selection.collapsed(Position(path: [0], offset: 5)),
);
});
testWidgets('when Tab leaves the field before it, then the caret is in '
'it.', (tester) async {
final editorState = await pumpEditor(tester);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusNode.hasFocus, isTrue);
expect(editorState.selection, isNotNull);
});
});
}