-
Notifications
You must be signed in to change notification settings - Fork 603
Fixed cursor jumping to end issue #925
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…ing initial value and fix controller handling on key change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR fixes a cursor positioning bug where editing text in the middle of a URL field would cause the cursor to jump to the end. The fix preserves the cursor position during text updates while still resetting it when switching between different requests.
Key Changes:
- Modified
didUpdateWidgetto distinguish between keyId changes (new request) and value changes (same request being edited) - Added cursor position preservation logic that clamps the cursor offset within valid bounds
- Updated indentation in the
ExtendedTextFieldwidget
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| _focusNode.unfocus(); | ||
| }, | ||
| readOnly: widget.readOnly, | ||
| obscureText: widget.obscureText |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing comma after the obscureText parameter. This will cause a syntax error and prevent the code from compiling.
Add a comma after widget.obscureText:
obscureText: widget.obscureText,| obscureText: widget.obscureText | |
| obscureText: widget.obscureText, |
| if (oldWidget.keyId != widget.keyId) { | ||
| // Key changed - create new controller with cursor at end | ||
| controller = widget.controller ?? | ||
| TextEditingController.fromValue(TextEditingValue( | ||
| text: widget.initialValue!, | ||
| selection: TextSelection.collapsed( | ||
| offset: widget.initialValue!.length))); |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a new controller without disposing the old one will cause a memory leak. When the keyId changes, the old controller should be disposed before assigning a new one.
Consider adding:
if (oldWidget.keyId != widget.keyId) {
// Key changed - dispose old controller and create new one
if (widget.controller == null) {
controller.dispose();
}
controller = widget.controller ??
TextEditingController.fromValue(TextEditingValue(
text: widget.initialValue!,
selection: TextSelection.collapsed(
offset: widget.initialValue!.length)));
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| final newOffset = | ||
| currentSelection.baseOffset.clamp(0, newText.length); | ||
| controller.value = TextEditingValue( | ||
| text: newText, | ||
| selection: TextSelection.collapsed(offset: newOffset), |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cursor position preservation logic doesn't account for text selection ranges (when text is highlighted). The code only preserves baseOffset but ignores extentOffset, which means any text selection will be lost and collapsed to a cursor position.
Consider preserving the full selection when possible:
final currentSelection = controller.selection;
final newText = widget.initialValue ?? '';
if (controller.text != newText) {
// Preserve selection range, ensuring it's within bounds
final newBaseOffset = currentSelection.baseOffset.clamp(0, newText.length);
final newExtentOffset = currentSelection.extentOffset.clamp(0, newText.length);
controller.value = TextEditingValue(
text: newText,
selection: TextSelection(
baseOffset: newBaseOffset,
extentOffset: newExtentOffset,
),
);
}| final newOffset = | |
| currentSelection.baseOffset.clamp(0, newText.length); | |
| controller.value = TextEditingValue( | |
| text: newText, | |
| selection: TextSelection.collapsed(offset: newOffset), | |
| final newBaseOffset = currentSelection.baseOffset.clamp(0, newText.length); | |
| final newExtentOffset = currentSelection.extentOffset.clamp(0, newText.length); | |
| controller.value = TextEditingValue( | |
| text: newText, | |
| selection: TextSelection( | |
| baseOffset: newBaseOffset, | |
| extentOffset: newExtentOffset, | |
| ), |
| } else if (oldWidget.initialValue != widget.initialValue) { | ||
| // Initial value changed but key is same | ||
| // Preserve cursor position if text is being updated | ||
| if (widget.controller == null) { | ||
| final currentSelection = controller.selection; | ||
| final newText = widget.initialValue ?? ''; | ||
|
|
||
| // Only update if the text actually differs from controller's current text | ||
| if (controller.text != newText) { | ||
| // Preserve cursor position, ensuring it's within bounds | ||
| final newOffset = | ||
| currentSelection.baseOffset.clamp(0, newText.length); | ||
| controller.value = TextEditingValue( | ||
| text: newText, | ||
| selection: TextSelection.collapsed(offset: newOffset), | ||
| ); | ||
| } | ||
| } |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cursor position preservation logic introduced in this PR lacks test coverage. While there are existing tests that verify text updates, there are no tests that specifically validate that the cursor position is preserved when text is updated (the main fix of this PR).
Consider adding a test case like:
testWidgets('Testing EnvironmentTriggerField preserves cursor position on text update',
(WidgetTester tester) async {
final fieldKey = GlobalKey<EnvironmentTriggerFieldState>();
await tester.pumpWidget(
Portal(
child: MaterialApp(
home: Scaffold(
body: EnvironmentTriggerField(
key: fieldKey,
keyId: 'testKey',
initialValue: 'hello world',
),
),
),
),
);
// Set cursor to position 5 (middle of text)
fieldKey.currentState!.controller.selection =
TextSelection.collapsed(offset: 5);
// Update the text value
await tester.pumpWidget(
Portal(
child: MaterialApp(
home: Scaffold(
body: EnvironmentTriggerField(
key: fieldKey,
keyId: 'testKey',
initialValue: 'hello world!',
),
),
),
),
);
// Verify cursor is still at position 5
expect(fieldKey.currentState!.controller.selection.baseOffset, 5);
});
PR Description
Fixed cursor jumping to end issue in URL text field. When editing a character in the middle of the URL, the cursor would automatically move to the end, making it difficult to edit URLs.
Root Cause: The
didUpdateWidgetmethod inEnvironmentTriggerFieldwas recreating theTextEditingControlleron every text change, always positioning the cursor at the end.Solution: Modified the controller update logic to:
.clamp(0, newText.length)Related Issues
Checklist
I have gone through the contributing guide
I have updated my branch and synced it with project
mainbranch before making this PRI am using the latest Flutter stable branch (run
flutter upgradeand verify)I have run the tests (
flutter test) and all tests are passingAdded/updated tests?
We encourage you to add relevant test cases.
OS on which you have developed and tested the feature?
Windows
macOS
Linux