Unverified Commit b3d12ebd authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Revert "[EditableText] call `onSelectionChanged` only when there're actual...

Revert "[EditableText] call `onSelectionChanged` only when there're actual selection/cause changes (#87971)" (#88183)
parent 72a1e65a
......@@ -593,6 +593,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
bool _wasSelectingVerticallyWithKeyboard = false;
void _setTextEditingValue(TextEditingValue newValue, SelectionChangedCause cause) {
textSelectionDelegate.textEditingValue = newValue;
textSelectionDelegate.userUpdateTextEditingValue(newValue, cause);
}
......@@ -612,11 +613,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
extentOffset: math.min(nextSelection.extentOffset, textLength),
);
}
_handleSelectionChange(nextSelection, cause);
_setTextEditingValue(
textSelectionDelegate.textEditingValue.copyWith(selection: nextSelection),
cause,
);
_handleSelectionChange(nextSelection, cause);
}
void _handleSelectionChange(
......
......@@ -1079,9 +1079,6 @@ class EditableText extends StatefulWidget {
/// {@template flutter.widgets.editableText.onSelectionChanged}
/// Called when the user changes the selection of text (including the cursor
/// location).
///
/// This callback is only called when the selected text is changed, or the
/// same range of text is selected via a different [SelectionChangedCause].
/// {@endtemplate}
final SelectionChangedCallback? onSelectionChanged;
......@@ -1538,18 +1535,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
TextInputConnection? _textInputConnection;
TextSelectionOverlay? _selectionOverlay;
// The source of the most recent selection change.
//
// Changing the selection programmatically does not update
// _selectionChangedCause.
SelectionChangedCause? _selectionChangedCause;
ScrollController? _scrollController;
late final AnimationController _cursorBlinkOpacityController = AnimationController(
vsync: this,
duration: _fadeDuration,
)..addListener(_onCursorColorTick);
late AnimationController _cursorBlinkOpacityController;
final LayerLink _toolbarLayerLink = LayerLink();
final LayerLink _startHandleLayerLink = LayerLink();
......@@ -1624,6 +1612,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
widget.focusNode.addListener(_handleFocusChanged);
_scrollController = widget.scrollController ?? ScrollController();
_scrollController!.addListener(() { _selectionOverlay?.updateForScroll(); });
_cursorBlinkOpacityController = AnimationController(vsync: this, duration: _fadeDuration);
_cursorBlinkOpacityController.addListener(_onCursorColorTick);
_floatingCursorResetController = AnimationController(vsync: this);
_floatingCursorResetController.addListener(_onFloatingCursorResetTick);
_cursorVisibilityNotifier.value = widget.showCursor;
......@@ -1760,22 +1750,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
_lastKnownRemoteTextEditingValue = value;
final bool shouldShowCaret = widget.readOnly
? _value.selection != value.selection
: _value != value;
if (shouldShowCaret) {
_scheduleShowCaretOnScreen();
if (value == _value) {
// This is possible, for example, when the numeric keyboard is input,
// the engine will notify twice for the same value.
// Track at https://github.com/flutter/flutter/issues/65811
return;
}
// Wherever the value is changed by the user, schedule a showCaretOnScreen
// to make sure the user can see the changes they just made. Programmatical
// changes to `textEditingValue` do not trigger the behavior even if the
// text field is focused.
_scheduleShowCaretOnScreen();
// Apply the input formatters.
value = _formatUserInput(value);
if (value.text != _value.text || value.composing != _value.composing) {
if (value.text == _value.text && value.composing == _value.composing) {
// `selection` is the only change.
_handleSelectionChanged(value.selection, SelectionChangedCause.keyboard);
} else {
hideToolbar();
_currentPromptRectRange = null;
......@@ -1785,9 +1770,21 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_obscureLatestCharIndex = _value.selection.baseOffset;
}
}
_formatAndSetValue(value, SelectionChangedCause.keyboard);
}
_updateEditingValueForUserInteraction(value, SelectionChangedCause.keyboard);
// Wherever the value is changed by the user, schedule a showCaretOnScreen
// to make sure the user can see the changes they just made. Programmatical
// changes to `textEditingValue` do not trigger the behavior even if the
// text field is focused.
_scheduleShowCaretOnScreen();
if (_hasInputConnection) {
// To keep the cursor from blinking while typing, we want to restart the
// cursor timer every time a new character is typed.
_stopCursorTimer(resetCharTicks: false);
_startCursorTimer();
}
}
@override
......@@ -1885,13 +1882,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset;
if (_floatingCursorResetController.isCompleted) {
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!);
if (_lastTextPosition!.offset != renderEditable.selection!.baseOffset) {
if (_lastTextPosition!.offset != renderEditable.selection!.baseOffset)
// The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same.
final TextEditingValue newValue = _value.copyWith(
selection: TextSelection.fromPosition(_lastTextPosition!),
);
_updateEditingValueForUserInteraction(newValue, SelectionChangedCause.forcePress);
}
_handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition!.offset), SelectionChangedCause.forcePress);
_startCaretRect = null;
_lastTextPosition = null;
_pointOffsetOrigin = null;
......@@ -2159,30 +2152,63 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
void _updateSelectionOverlayForNewEditingValue(TextEditingValue newEditingValue) {
@pragma('vm:notify-debugger-on-exception')
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
// We return early if the selection is not valid. This can happen when the
// text of [EditableText] is updated at the same time as the selection is
// changed by a gesture event.
if (!widget.controller.isSelectionWithinTextBounds(selection))
return;
widget.controller.selection = selection;
// This will show the keyboard for all selection changes on the
// EditableWidget, not just changes triggered by user gestures.
requestKeyboard();
if (widget.selectionControls == null) {
_selectionOverlay?.dispose();
_selectionOverlay = null;
return;
} else {
if (_selectionOverlay == null) {
_selectionOverlay = TextSelectionOverlay(
clipboardStatus: _clipboardStatus,
context: context,
value: _value,
debugRequiredFor: widget,
toolbarLayerLink: _toolbarLayerLink,
startHandleLayerLink: _startHandleLayerLink,
endHandleLayerLink: _endHandleLayerLink,
renderObject: renderEditable,
selectionControls: widget.selectionControls,
selectionDelegate: this,
dragStartBehavior: widget.dragStartBehavior,
onSelectionHandleTapped: widget.onSelectionHandleTapped,
);
} else {
_selectionOverlay!.update(_value);
}
_selectionOverlay!.handlesVisible = widget.showSelectionHandles;
_selectionOverlay!.showHandles();
}
// TODO(chunhtai): we should make sure selection actually changed before
// we call the onSelectionChanged.
// https://github.com/flutter/flutter/issues/76349.
try {
widget.onSelectionChanged?.call(selection, cause);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets',
context: ErrorDescription('while calling onSelectionChanged for $cause'),
));
}
_selectionOverlay?.update(newEditingValue);
_selectionOverlay ??= TextSelectionOverlay(
clipboardStatus: _clipboardStatus,
context: context,
value: newEditingValue,
debugRequiredFor: widget,
toolbarLayerLink: _toolbarLayerLink,
startHandleLayerLink: _startHandleLayerLink,
endHandleLayerLink: _endHandleLayerLink,
renderObject: renderEditable,
selectionControls: widget.selectionControls,
selectionDelegate: this,
dragStartBehavior: widget.dragStartBehavior,
onSelectionHandleTapped: widget.onSelectionHandleTapped,
);
_selectionOverlay?.handlesVisible = widget.showSelectionHandles;
_selectionOverlay?.showHandles();
// To keep the cursor from blinking while it moves, restart the timer here.
if (_cursorTimer != null) {
_stopCursorTimer(resetCharTicks: false);
_startCursorTimer();
}
}
Rect? _currentCaretRect;
......@@ -2266,7 +2292,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
@pragma('vm:notify-debugger-on-exception')
TextEditingValue _formatUserInput(TextEditingValue newValue) {
void _formatAndSetValue(TextEditingValue value, SelectionChangedCause? cause, {bool userInteraction = false}) {
// Only apply input formatters if the text has changed (including uncommitted
// text in the composing region), or when the user committed the composing
// text.
......@@ -2275,41 +2301,32 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// current composing region) is very infinite-loop-prone: the formatters
// will keep trying to modify the composing region while Gboard will keep
// trying to restore the original composing region.
final bool needsFormatting = _value.text != newValue.text
|| (!_value.composing.isCollapsed && newValue.composing.isCollapsed);
return needsFormatting
? widget.inputFormatters?.fold<TextEditingValue>(
newValue,
(TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(_value, newValue),
) ?? newValue
: newValue;
}
// Update the TextEditingValue in the controller in response to user
// interactions (via hardware/software keyboards and gesture events).
//
// This method should not be called for programmatical changes made by
// directly modifying the TextEditingValue in the controller.
//
// Do not call request keyboard in this method: this method can be called
// when the text field does not have focus and should not request focus (for
// instance, during autofill).
@pragma('vm:notify-debugger-on-exception')
void _updateEditingValueForUserInteraction(TextEditingValue value, SelectionChangedCause? cause) {
final TextEditingValue previousValue = _value;
final bool textChanged = _value.text != value.text
|| (!_value.composing.isCollapsed && value.composing.isCollapsed);
final bool selectionChanged = _value.selection != value.selection;
if (textChanged) {
value = widget.inputFormatters?.fold<TextEditingValue>(
value,
(TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(_value, newValue),
) ?? value;
}
// Put all optional user callback invocations in a batch edit to prevent
// sending multiple `TextInput.updateEditingValue` messages.
beginBatchEdit();
// Set the value before we invoke the onChanged callbacks.
// This is going to notify the listeners, which may potentially further
// modify the text editing value.
_value = value;
// Call the onChanged callback first in case it changes the selection.
if (_value.text != previousValue.text) {
// Changes made by the keyboard can sometimes be "out of band" for listening
// components, so always send those events, even if we didn't think it
// changed. Also, the user long pressing should always send a selection change
// as well.
if (selectionChanged ||
(userInteraction &&
(cause == SelectionChangedCause.longPress ||
cause == SelectionChangedCause.keyboard))) {
_handleSelectionChanged(_value.selection, cause);
}
if (textChanged) {
try {
widget.onChanged?.call(_value.text);
} catch (exception, stack) {
......@@ -2322,30 +2339,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
final bool selectionChanged = _value.selection != previousValue.selection;
if (selectionChanged || cause != _selectionChangedCause) {
try {
widget.onSelectionChanged?.call(_value.selection, cause);
_selectionChangedCause = cause;
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets',
context: ErrorDescription('while calling onSelectionChanged for $cause'),
));
}
// TODO(LongCatIsLoong): find a better place to populate the selection
// overlay. See: https://github.com/flutter/flutter/issues/87963.
_updateSelectionOverlayForNewEditingValue(_value);
// To keep the cursor from blinking while it moves, restart the timer here.
if (_cursorTimer != null) {
_stopCursorTimer(resetCharTicks: false);
_startCursorTimer();
}
}
endBatchEdit();
}
......@@ -2459,10 +2452,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
if (!_value.selection.isValid) {
// Place cursor at the end if the selection is invalid when we receive focus.
final TextEditingValue valueWithValidSelection = _value.copyWith(
selection: TextSelection.collapsed(offset: _value.text.length),
);
_updateEditingValueForUserInteraction(valueWithValidSelection, null);
_handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), null);
}
} else {
WidgetsBinding.instance!.removeObserver(this);
......@@ -2493,6 +2483,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
Rect? composingRect = renderEditable.getRectForComposingRange(composingRange);
// Send the caret location instead if there's no marked text yet.
if (composingRect == null) {
assert(!composingRange.isValid || composingRange.isCollapsed);
final int offset = composingRange.isValid ? composingRange.start : 0;
composingRect = renderEditable.getLocalRectForCaret(TextPosition(offset: offset));
}
......@@ -2534,11 +2525,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
double get _devicePixelRatio => MediaQuery.of(context).devicePixelRatio;
// This method is similar to updateEditingValue, but is used to handle user
// input caused by hardware keyboard events and gesture events, while
// updateEditingValue handles IME/software keyboard input.
@override
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) {
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause? cause) {
// Compare the current TextEditingValue with the pre-format new
// TextEditingValue value, in case the formatter would reject the change.
final bool shouldShowCaret = widget.readOnly
......@@ -2547,24 +2535,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (shouldShowCaret) {
_scheduleShowCaretOnScreen();
}
final TextEditingValue formattedValue = _formatUserInput(value);
if (value.selection != _value.selection) {
requestKeyboard();
}
if (value.text != _value.text || value.composing != _value.composing) {
hideToolbar();
_currentPromptRectRange = null;
if (_hasInputConnection) {
if (widget.obscureText && value.text.length == _value.text.length + 1) {
_obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks;
_obscureLatestCharIndex = _value.selection.baseOffset;
}
}
}
_updateEditingValueForUserInteraction(formattedValue, cause);
_formatAndSetValue(value, cause, userInteraction: true);
}
@override
......
......@@ -1226,7 +1226,7 @@ class TextSelectionGestureDetectorBuilder {
@protected
void onDoubleTapDown(TapDownDetails details) {
if (delegate.selectionEnabled) {
renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
renderEditable.selectWord(cause: SelectionChangedCause.tap);
if (shouldShowSelectionToolbar)
editableText.showToolbar();
}
......
......@@ -2381,7 +2381,6 @@ void main() {
await gesture.moveBy(const Offset(600, 0));
// To the edge of the screen basically.
await tester.pump();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 54, affinity: TextAffinity.upstream),
......@@ -2389,14 +2388,12 @@ void main() {
// Keep moving out.
await gesture.moveBy(const Offset(1, 0));
await tester.pump();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 61, affinity: TextAffinity.upstream),
);
await gesture.moveBy(const Offset(1, 0));
await tester.pump();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
......
......@@ -3871,7 +3871,7 @@ void main() {
editableTextState.textEditingValue.copyWith(
selection: TextSelection.collapsed(offset: longText.length),
),
SelectionChangedCause.tap,
null,
);
await tester.pump(); // TODO(ianh): Figure out why this extra pump is needed.
......@@ -3908,7 +3908,7 @@ void main() {
editableTextState.textEditingValue.copyWith(
selection: const TextSelection.collapsed(offset: tallText.length),
),
SelectionChangedCause.tap,
null,
);
await tester.pump();
await skipPastScrollingAnimation(tester);
......@@ -7709,7 +7709,6 @@ void main() {
await gesture.moveBy(const Offset(600, 0));
// To the edge of the screen basically.
await tester.pump();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 56, affinity: TextAffinity.downstream),
......@@ -7717,14 +7716,12 @@ void main() {
// Keep moving out.
await gesture.moveBy(const Offset(1, 0));
await tester.pump();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 62, affinity: TextAffinity.downstream),
);
await gesture.moveBy(const Offset(1, 0));
await tester.pump();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
......
......@@ -34,9 +34,7 @@ class FakeEditableTextState with TextSelectionDelegate {
void hideToolbar([bool hideHandles = true]) { }
@override
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) {
textEditingValue = value;
}
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) { }
@override
void bringIntoView(TextPosition position) { }
......
......@@ -650,7 +650,7 @@ void main() {
// false.
state.userUpdateTextEditingValue(
state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 90)),
SelectionChangedCause.tap,
null,
);
await tester.pumpAndSettle();
expect(isCaretOnScreen(tester), isTrue);
......@@ -674,7 +674,7 @@ void main() {
state.userUpdateTextEditingValue(
state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 100)),
SelectionChangedCause.tap,
null,
);
await tester.pumpAndSettle();
expect(isCaretOnScreen(tester), isTrue);
......
......@@ -4938,19 +4938,21 @@ void main() {
testWidgets('keyboard text selection works (RawKeyEvent)', (WidgetTester tester) async {
debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData;
addTearDown(() { debugKeyEventSimulatorTransitModeOverride = null; });
await testTextEditing(tester, targetPlatform: defaultTargetPlatform);
debugKeyEventSimulatorTransitModeOverride = null;
// On web, using keyboard for selection is handled by the browser.
}, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended]
testWidgets('keyboard text selection works (ui.KeyData then RawKeyEvent)', (WidgetTester tester) async {
debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData;
addTearDown(() { debugKeyEventSimulatorTransitModeOverride = null; });
await testTextEditing(tester, targetPlatform: defaultTargetPlatform);
debugKeyEventSimulatorTransitModeOverride = null;
// On web, using keyboard for selection is handled by the browser.
}, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended]
......@@ -6045,6 +6047,7 @@ void main() {
'TextInput.setStyle',
'TextInput.setEditingState',
'TextInput.setEditingState',
'TextInput.show',
'TextInput.setCaretRect',
];
expect(
......@@ -6088,14 +6091,16 @@ void main() {
'TextInput.setStyle',
'TextInput.setEditingState',
'TextInput.setEditingState',
'TextInput.show',
'TextInput.setCaretRect',
'TextInput.show',
];
expect(
tester.testTextInput.log.map((MethodCall methodCall) => methodCall.method),
logOrder,
);
expect(tester.testTextInput.log.length, logOrder.length);
int index = 0;
for (final MethodCall m in tester.testTextInput.log) {
expect(m.method, logOrder[index]);
index++;
}
expect(tester.testTextInput.editingState!['text'], 'flutter is the best!');
});
......@@ -6136,6 +6141,7 @@ void main() {
'TextInput.setStyle',
'TextInput.setEditingState',
'TextInput.setEditingState',
'TextInput.show',
'TextInput.setCaretRect',
'TextInput.setEditingState',
];
......@@ -6209,7 +6215,7 @@ void main() {
selection: controller.selection,
));
expect(log, isEmpty);
expect(log.length, 0);
// setEditingState is called when remote value modified by the formatter.
state.updateEditingValue(TextEditingValue(
......@@ -7454,115 +7460,6 @@ void main() {
});
});
group('onChanged callbacks are edge-triggered', () {
SelectionChangedCallback? onSelectionChanged;
ValueChanged<String>? onChanged;
final TextEditingController controller = TextEditingController();
final Widget editableText = EditableText(
showSelectionHandles: false,
controller: controller,
focusNode: FocusNode(),
cursorColor: Colors.red,
backgroundCursorColor: Colors.blue,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1!.copyWith(fontFamily: 'Roboto'),
keyboardType: TextInputType.text,
selectionControls: materialTextSelectionControls,
onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) => onSelectionChanged?.call(selection, cause),
onChanged: (String value) => onChanged?.call(value),
);
tearDown(() {
onSelectionChanged = null;
onChanged = null;
});
testWidgets('onSelectionChanged', (WidgetTester tester) async {
TextSelection? selection;
SelectionChangedCause? cause;
onSelectionChanged = (TextSelection newSelection, SelectionChangedCause? newCause) {
selection = newSelection;
cause = newCause;
};
controller.value = const TextEditingValue(text: 'text', selection: TextSelection(baseOffset: 1, extentOffset: 2));
await tester.pumpWidget(MaterialApp(
home: editableText,
));
final EditableTextState state = tester.state(find.byWidget(editableText));
await tester.showKeyboard(find.byWidget(editableText));
await tester.pump();
// No user input.
expect(selection, isNull);
expect(cause, isNull);
// Selection didn't change but the cause did (keyboard).
state.updateEditingValue(
const TextEditingValue(text: 'test text', selection: TextSelection(baseOffset: 1, extentOffset: 2)),
);
expect(selection, const TextSelection(baseOffset: 1, extentOffset: 2));
expect(cause, SelectionChangedCause.keyboard);
// Selection and cause both changed
await tester.enterText(find.byWidget(editableText), 'test text');
expect(selection, const TextSelection.collapsed(offset: 9));
expect(cause, SelectionChangedCause.keyboard);
selection = null;
cause = null;
// Nothing changes.
state.userUpdateTextEditingValue(
const TextEditingValue(text: 'test text', selection: TextSelection.collapsed(offset: 9)),
SelectionChangedCause.keyboard
);
expect(selection, isNull);
expect(cause, isNull);
// The cause changes.
state.userUpdateTextEditingValue(
const TextEditingValue(text: 'test text', selection: TextSelection.collapsed(offset: 9)),
SelectionChangedCause.toolBar,
);
expect(selection, const TextSelection.collapsed(offset: 9));
expect(cause, SelectionChangedCause.toolBar);
});
testWidgets('onChanged', (WidgetTester tester) async {
String? newText;
onChanged = (String text) => newText = text;
controller.value = const TextEditingValue(text: 'text', selection: TextSelection(baseOffset: 1, extentOffset: 2));
await tester.pumpWidget(MaterialApp(
home: editableText,
));
final EditableTextState state = tester.state(find.byWidget(editableText));
await tester.showKeyboard(find.byWidget(editableText));
await tester.pump();
// No user input.
expect(newText, isNull);
state.updateEditingValue(
const TextEditingValue(text: 'text', selection: TextSelection(baseOffset: 1, extentOffset: 3)),
);
// Selection & cause changed but the text didn't;
expect(newText, isNull);
state.updateEditingValue(
const TextEditingValue(text: 'test text', selection: TextSelection(baseOffset: 1, extentOffset: 3)),
);
// Now the text is changed.
expect(newText, 'test text');
});
});
group('callback errors', () {
const String errorText = 'Test EditableText callback error';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment