Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit d8d0d3f

Browse files
authored
Reflect selection changes in Firefox for text editing (#12447)
* reflect selection changes in Firefox. With this change if the keyboard arrow keys to move the cursor the selection change is synced to Flutter Framework * Addresing PR comments. Renaming. Adding the domelement to SelectionChangeDetection constructor. * add initial value to selection start/end add initial value to selection start/end
1 parent a5b23d8 commit d8d0d3f

File tree

2 files changed

+129
-7
lines changed

2 files changed

+129
-7
lines changed

lib/web_ui/lib/src/engine/text_editing.dart

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ class TextEditingElement {
226226
EditingState _lastEditingState;
227227
_OnChangeCallback _onChange;
228228

229+
SelectionChangeDetection _selectionDetection;
230+
229231
final List<StreamSubscription<html.Event>> _subscriptions =
230232
<StreamSubscription<html.Event>>[];
231233

@@ -273,6 +275,7 @@ class TextEditingElement {
273275

274276
_initDomElement(inputConfig);
275277
_enabled = true;
278+
_selectionDetection = SelectionChangeDetection(domElement);
276279
_onChange = onChange;
277280

278281
// Chrome on Android will hide the onscreen keyboard when you tap outside
@@ -305,6 +308,18 @@ class TextEditingElement {
305308
_subscriptions
306309
..add(html.document.onSelectionChange.listen(_handleChange))
307310
..add(domElement.onInput.listen(_handleChange));
311+
312+
// In Firefox, when cursor moves, nor selectionChange neither onInput
313+
// events are triggered. We are listening to keyup event to decide
314+
// if the user shifted the cursor.
315+
// See [SelectionChangeDetection].
316+
if (browserEngine == BrowserEngine.firefox) {
317+
_subscriptions.add(domElement.onKeyUp.listen((event) {
318+
if (_selectionDetection.detectChange()) {
319+
_handleChange(event);
320+
}
321+
}));
322+
}
308323
}
309324

310325
/// Disables the element so it's no longer used for text editing.
@@ -324,6 +339,7 @@ class TextEditingElement {
324339
_positionInputElementTimer = null;
325340
owner.inputPositioned = false;
326341
_removeDomElement();
342+
_selectionDetection = null;
327343
}
328344

329345
void _initDomElement(InputConfiguration inputConfig) {
@@ -412,8 +428,7 @@ class TextEditingElement {
412428
break;
413429
}
414430

415-
416-
if(owner.inputElementNeedsToBePositioned) {
431+
if (owner.inputElementNeedsToBePositioned) {
417432
_preventShiftDuringFocus();
418433
}
419434

@@ -641,9 +656,7 @@ class HybridTextEditing {
641656
///
642657
/// See [TextEditingElement._delayBeforePositioning].
643658
bool get inputElementNeedsToBePositioned =>
644-
!inputPositioned &&
645-
_isEditing &&
646-
doesKeyboardShiftInput;
659+
!inputPositioned && _isEditing && doesKeyboardShiftInput;
647660

648661
/// Flag indicating whether the input element's position is set.
649662
///
@@ -788,8 +801,8 @@ class HybridTextEditing {
788801
/// In iOS, the virtual keyboard might shifts the screen up to make input
789802
/// visible depending on the location of the focused input element.
790803
bool get doesKeyboardShiftInput =>
791-
browserEngine == BrowserEngine.webkit &&
792-
operatingSystem == OperatingSystem.iOs;
804+
browserEngine == BrowserEngine.webkit &&
805+
operatingSystem == OperatingSystem.iOs;
793806

794807
/// These style attributes are dynamic throughout the life time of an input
795808
/// element.
@@ -888,3 +901,62 @@ class _EditableSizeAndTransform {
888901
final double height;
889902
final Float64List transform;
890903
}
904+
905+
/// Detects changes in text selection.
906+
///
907+
/// Currently only used in Firefox.
908+
///
909+
/// In Firefox, when cursor moves, neither selectionChange nor onInput
910+
/// events are triggered. We are listening to keyup event. Selection start,
911+
/// end values are used to decide if the text cursor moved.
912+
///
913+
/// Specific keycodes are not checked since users/applicatins can bind their own
914+
/// keys to move the text cursor.
915+
class SelectionChangeDetection {
916+
final html.HtmlElement _domElement;
917+
int _start = -1;
918+
int _end = -1;
919+
920+
SelectionChangeDetection(this._domElement) {
921+
if (_domElement is html.InputElement) {
922+
html.InputElement element = _domElement;
923+
_saveSelection(element.selectionStart, element.selectionEnd);
924+
} else if (_domElement is html.TextAreaElement) {
925+
html.TextAreaElement element = _domElement;
926+
_saveSelection(element.selectionStart, element.selectionEnd);
927+
} else {
928+
throw UnsupportedError('Initialized with unsupported input type');
929+
}
930+
}
931+
932+
/// Decides if the selection has changed (cursor moved) compared to the
933+
/// previous values.
934+
///
935+
/// After each keyup, the start/end values of the selection is compared to the
936+
/// previously saved start/end values.
937+
bool detectChange() {
938+
if (_domElement is html.InputElement) {
939+
html.InputElement element = _domElement;
940+
return _compareSelection(element.selectionStart, element.selectionEnd);
941+
}
942+
if (_domElement is html.TextAreaElement) {
943+
html.TextAreaElement element = _domElement;
944+
return _compareSelection(element.selectionStart, element.selectionEnd);
945+
}
946+
throw UnsupportedError('Unsupported input type');
947+
}
948+
949+
void _saveSelection(int selectionStart, int selectionEnd) {
950+
_start = selectionStart;
951+
_end = selectionEnd;
952+
}
953+
954+
bool _compareSelection(int selectionStart, int selectionEnd) {
955+
if (selectionStart != _start || selectionEnd != _end) {
956+
_saveSelection(selectionStart, selectionEnd);
957+
return true;
958+
} else {
959+
return false;
960+
}
961+
}
962+
}

lib/web_ui/test/text_editing_test.dart

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,56 @@ void main() {
676676
expect(spy.messages, isEmpty);
677677
});
678678
});
679+
680+
group('SelectionChangeDetection', () {
681+
SelectionChangeDetection _selectionChangeDetection;
682+
683+
test('Change detected on an input field', () {
684+
final InputElement input = document.getElementsByTagName('input')[0];
685+
_selectionChangeDetection = SelectionChangeDetection(input);
686+
687+
input.value = 'foo\nbar';
688+
input.setSelectionRange(1, 3);
689+
690+
expect(_selectionChangeDetection.detectChange(), true);
691+
expect(_selectionChangeDetection.detectChange(), false);
692+
693+
input.setSelectionRange(1, 5);
694+
695+
expect(_selectionChangeDetection.detectChange(), true);
696+
});
697+
698+
test('Change detected on an text area', () {
699+
final TextAreaElement textarea =
700+
document.getElementsByTagName('textarea')[0];
701+
_selectionChangeDetection = SelectionChangeDetection(textarea);
702+
703+
textarea.value = 'foo\nbar';
704+
textarea.setSelectionRange(4, 6);
705+
706+
expect(_selectionChangeDetection.detectChange(), true);
707+
expect(_selectionChangeDetection.detectChange(), false);
708+
709+
textarea.setSelectionRange(4, 5);
710+
711+
expect(_selectionChangeDetection.detectChange(), true);
712+
});
713+
714+
test('No change if selection stayed the same', () {
715+
final InputElement input = document.getElementsByTagName('input')[0];
716+
_selectionChangeDetection = SelectionChangeDetection(input);
717+
718+
input.value = 'foo\nbar';
719+
input.setSelectionRange(1, 3);
720+
721+
expect(_selectionChangeDetection.detectChange(), true);
722+
expect(_selectionChangeDetection.detectChange(), false);
723+
724+
input.setSelectionRange(1, 3);
725+
726+
expect(_selectionChangeDetection.detectChange(), false);
727+
});
728+
});
679729
}
680730

681731
MethodCall configureSetStyleMethodCall(int fontSize, String fontFamily,

0 commit comments

Comments
 (0)