Unverified Commit 44f87da0 authored by xubaolin's avatar xubaolin Committed by GitHub

Fix Text selection handler disappear when dragged to new words (#108808)

parent 406b4baf
......@@ -2142,6 +2142,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
return;
}
if (_checkNeedsAdjustAffinity(value)) {
value = value.copyWith(selection: value.selection.copyWith(affinity: _value.selection.affinity));
}
if (widget.readOnly) {
// In the read-only case, we only care about selection changes, and reject
// everything else.
......@@ -2160,7 +2164,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// `selection` is the only change.
_handleSelectionChanged(value.selection, (_textInputConnection?.scribbleInProgress ?? false) ? SelectionChangedCause.scribble : SelectionChangedCause.keyboard);
} else {
hideToolbar();
// Only hide the toolbar overlay, the selection handle's visibility will be handled
// by `_handleSelectionChanged`. https://github.com/flutter/flutter/issues/108673
hideToolbar(false);
_currentPromptRectRange = null;
final bool revealObscuredInput = _hasInputConnection
......@@ -2186,6 +2192,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
bool _checkNeedsAdjustAffinity(TextEditingValue value) {
// Trust the engine affinity if the text changes or selection changes.
return value.text == _value.text &&
value.selection.isCollapsed == _value.selection.isCollapsed &&
value.selection.start == _value.selection.start &&
value.selection.affinity != _value.selection.affinity;
}
@override
void performAction(TextInputAction action) {
switch (action) {
......
......@@ -212,6 +212,147 @@ void main() {
);
}
testWidgets('text field selection toolbar should hide when the user starts typing', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: 100,
height: 100,
child: TextField(
decoration: InputDecoration(hintText: 'Placeholder'),
),
),
),
),
),
);
await tester.showKeyboard(find.byType(TextField));
const String testValue = 'A B C';
tester.testTextInput.updateEditingValue(
const TextEditingValue(
text: testValue,
),
);
await tester.pump();
// The selectWordsInRange with SelectionChangedCause.tap seems to be needed to show the toolbar.
// (This is true even if we provide selection parameter to the TextEditingValue above.)
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
state.renderEditable.selectWordsInRange(from: Offset.zero, cause: SelectionChangedCause.tap);
expect(state.showToolbar(), true);
// This is needed for the AnimatedOpacity to turn from 0 to 1 so the toolbar is visible.
await tester.pumpAndSettle();
// Sanity check that the toolbar widget exists.
expect(find.text('Paste'), findsOneWidget);
const String newValue = 'A B C D';
tester.testTextInput.updateEditingValue(
const TextEditingValue(
text: newValue,
),
);
await tester.pump();
expect(state.selectionOverlay!.toolbarIsVisible, isFalse);
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
testWidgets('Composing change does not hide selection handle caret', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/108673
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
),
),
);
const String testValue = 'I Love Flutter!';
await tester.enterText(find.byType(TextField), testValue);
expect(controller.value.text, testValue);
await skipPastScrollingAnimation(tester);
// Handle not shown.
expect(controller.selection.isCollapsed, true);
final Finder fadeFinder = find.byType(FadeTransition);
FadeTransition handle = tester.widget(fadeFinder.at(0));
expect(handle.opacity.value, equals(0.0));
// Tap on the text field to show the handle.
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
expect(fadeFinder, findsNWidgets(1));
handle = tester.widget(fadeFinder.at(0));
expect(handle.opacity.value, equals(1.0));
final RenderObject handleRenderObjectBegin = tester.renderObject(fadeFinder.at(0));
expect(
controller.value,
const TextEditingValue(
text: 'I Love Flutter!',
selection: TextSelection.collapsed(offset: 15, affinity: TextAffinity.upstream),
),
);
// Simulate text composing change.
tester.testTextInput.updateEditingValue(
controller.value.copyWith(
composing: const TextRange(start: 7, end: 15),
),
);
await skipPastScrollingAnimation(tester);
expect(
controller.value,
const TextEditingValue(
text: 'I Love Flutter!',
selection: TextSelection.collapsed(offset: 15, affinity: TextAffinity.upstream),
composing: TextRange(start: 7, end: 15),
),
);
// Handle still shown.
expect(controller.selection.isCollapsed, true);
handle = tester.widget(fadeFinder.at(0));
expect(handle.opacity.value, equals(1.0));
// Simulate text composing and affinity change.
tester.testTextInput.updateEditingValue(
controller.value.copyWith(
selection: controller.value.selection.copyWith(affinity: TextAffinity.downstream),
composing: const TextRange(start: 8, end: 15),
),
);
await skipPastScrollingAnimation(tester);
expect(
controller.value,
const TextEditingValue(
text: 'I Love Flutter!',
selection: TextSelection.collapsed(offset: 15, affinity: TextAffinity.upstream),
composing: TextRange(start: 8, end: 15),
),
);
// Handle still shown.
expect(controller.selection.isCollapsed, true);
handle = tester.widget(fadeFinder.at(0));
expect(handle.opacity.value, equals(1.0));
final RenderObject handleRenderObjectEnd = tester.renderObject(fadeFinder.at(0));
// The RenderObject sub-tree should not be unmounted.
expect(identical(handleRenderObjectBegin, handleRenderObjectEnd), true);
});
testWidgets('can use the desktop cut/copy/paste buttons on Mac', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'blah1 blah2',
......
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