Unverified Commit 909e29ed authored by takashi kasai's avatar takashi kasai Committed by GitHub

moving the left handle automatically scrolls EditableText to the right handle,...

moving the left handle automatically scrolls EditableText to the right handle, which doesn't happen on native (#105836)

Fixes a scrolling issue when dragging selection handles.
parent d48aef0e
......@@ -996,8 +996,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
case TargetPlatform.windows:
case TargetPlatform.fuchsia:
case TargetPlatform.android:
if (cause == SelectionChangedCause.longPress
|| cause == SelectionChangedCause.drag) {
if (cause == SelectionChangedCause.longPress) {
_editableText.bringIntoView(selection.extent);
}
break;
......
......@@ -1125,8 +1125,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
case TargetPlatform.windows:
case TargetPlatform.fuchsia:
case TargetPlatform.android:
if (cause == SelectionChangedCause.longPress
|| cause == SelectionChangedCause.drag) {
if (cause == SelectionChangedCause.longPress) {
_editableText?.bringIntoView(selection.extent);
}
break;
......
......@@ -3557,6 +3557,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
final TextSelection oldTextSelection = textEditingValue.selection;
// Put all optional user callback invocations in a batch edit to prevent
// sending multiple `TextInput.updateEditingValue` messages.
beginBatchEdit();
......@@ -3570,6 +3572,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
(cause == SelectionChangedCause.longPress ||
cause == SelectionChangedCause.keyboard))) {
_handleSelectionChanged(_value.selection, cause);
_bringIntoViewBySelectionState(oldTextSelection, value.selection, cause);
}
final String currentText = _value.text;
if (oldValue.text != currentText) {
......@@ -3587,6 +3590,30 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
endBatchEdit();
}
void _bringIntoViewBySelectionState(TextSelection oldSelection, TextSelection newSelection, SelectionChangedCause? cause) {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
if (cause == SelectionChangedCause.longPress ||
cause == SelectionChangedCause.drag) {
bringIntoView(newSelection.extent);
}
break;
case TargetPlatform.linux:
case TargetPlatform.windows:
case TargetPlatform.fuchsia:
case TargetPlatform.android:
if (cause == SelectionChangedCause.drag) {
if (oldSelection.baseOffset != newSelection.baseOffset) {
bringIntoView(newSelection.base);
} else if (oldSelection.extentOffset != newSelection.extentOffset) {
bringIntoView(newSelection.extent);
}
}
break;
}
}
void _onCursorColorTick() {
renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
_cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController.value > 0;
......
......@@ -718,7 +718,7 @@ class TextSelectionOverlay {
));
final TextSelection currentSelection = TextSelection.fromPosition(position);
_handleSelectionHandleChanged(currentSelection, isEnd: true);
_handleSelectionHandleChanged(currentSelection);
return;
}
......@@ -749,7 +749,7 @@ class TextSelectionOverlay {
break;
}
_handleSelectionHandleChanged(newSelection, isEnd: true);
_handleSelectionHandleChanged(newSelection);
_selectionOverlay.updateMagnifier(_buildMagnifier(
currentTextPosition: newSelection.extent,
......@@ -814,7 +814,7 @@ class TextSelectionOverlay {
));
final TextSelection currentSelection = TextSelection.fromPosition(position);
_handleSelectionHandleChanged(currentSelection, isEnd: false);
_handleSelectionHandleChanged(currentSelection);
return;
}
......@@ -851,7 +851,7 @@ class TextSelectionOverlay {
renderEditable: renderObject,
));
_handleSelectionHandleChanged(newSelection, isEnd: false);
_handleSelectionHandleChanged(newSelection);
}
void _handleAnyDragEnd(DragEndDetails details) {
......@@ -874,13 +874,11 @@ class TextSelectionOverlay {
}
}
void _handleSelectionHandleChanged(TextSelection newSelection, {required bool isEnd}) {
final TextPosition textPosition = isEnd ? newSelection.extent : newSelection.base;
void _handleSelectionHandleChanged(TextSelection newSelection) {
selectionDelegate.userUpdateTextEditingValue(
_value.copyWith(selection: newSelection),
SelectionChangedCause.drag,
);
selectionDelegate.bringIntoView(textPosition);
}
TextSelectionHandleType _chooseType(
......
......@@ -4853,6 +4853,163 @@ void main() {
);
});
testWidgets(
'Can drag the left handle while the right handle remains off-screen',
(WidgetTester tester) async {
// Text is longer than textfield width.
const String testValue = 'aaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbb';
final TextEditingController controller = TextEditingController(text: testValue);
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
scrollController: scrollController,
),
),
),
);
// Double tap 'b' to show handles.
final Offset bPos = textOffsetToPosition(tester, testValue.indexOf('b'));
await tester.tapAt(bPos);
await tester.pump(kDoubleTapTimeout ~/ 2);
await tester.tapAt(bPos);
await tester.pumpAndSettle();
final TextSelection selection = controller.selection;
expect(selection.baseOffset, 28);
expect(selection.extentOffset, testValue.length);
// Move to the left edge.
scrollController.jumpTo(0);
await tester.pumpAndSettle();
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
expect(endpoints.length, 2);
// Left handle should appear between textfield's left and right position.
final Offset textFieldLeftPosition =
tester.getTopLeft(find.byType(CupertinoTextField));
expect(endpoints[0].point.dx - textFieldLeftPosition.dx, isPositive);
final Offset textFieldRightPosition =
tester.getTopRight(find.byType(CupertinoTextField));
expect(textFieldRightPosition.dx - endpoints[0].point.dx, isPositive);
// Right handle should remain off-screen.
expect(endpoints[1].point.dx - textFieldRightPosition.dx, isPositive);
// Drag the left handle to the right by 25 offset.
const int toOffset = 25;
final double beforeScrollOffset = scrollController.offset;
final Offset handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
final Offset newHandlePos = textOffsetToPosition(tester, toOffset);
final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
// On Apple platforms, dragging the base handle makes it the extent.
expect(controller.selection.baseOffset, testValue.length);
expect(controller.selection.extentOffset, toOffset);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(controller.selection.baseOffset, toOffset);
expect(controller.selection.extentOffset, testValue.length);
break;
}
// The scroll area of text field should not move.
expect(scrollController.offset, beforeScrollOffset);
},
);
testWidgets(
'Can drag the right handle while the left handle remains off-screen',
(WidgetTester tester) async {
// Text is longer than textfield width.
const String testValue = 'aaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbb';
final TextEditingController controller = TextEditingController(text: testValue);
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
scrollController: scrollController,
),
),
),
);
// Double tap 'a' to show handles.
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
await tester.tapAt(aPos);
await tester.pump(kDoubleTapTimeout ~/ 2);
await tester.tapAt(aPos);
await tester.pumpAndSettle();
final TextSelection selection = controller.selection;
expect(selection.baseOffset, 0);
expect(selection.extentOffset, 27);
// Move to the right edge.
scrollController.jumpTo(800);
await tester.pumpAndSettle();
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
expect(endpoints.length, 2);
// Right handle should appear between textfield's left and right position.
final Offset textFieldLeftPosition =
tester.getTopLeft(find.byType(CupertinoTextField));
expect(endpoints[1].point.dx - textFieldLeftPosition.dx, isPositive);
final Offset textFieldRightPosition =
tester.getTopRight(find.byType(CupertinoTextField));
expect(textFieldRightPosition.dx - endpoints[1].point.dx, isPositive);
// Left handle should remain off-screen.
expect(endpoints[0].point.dx, isNegative);
// Drag the right handle to the left by 50 offset.
const int toOffset = 50;
final double beforeScrollOffset = scrollController.offset;
final Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
final Offset newHandlePos = textOffsetToPosition(tester, toOffset);
final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, toOffset);
// The scroll area of text field should not move.
expect(scrollController.offset, beforeScrollOffset);
},
);
group('Text selection toolbar', () {
testWidgets('Collapsed selection works', (WidgetTester tester) async {
EditableText.debugDeterministicCursor = true;
......
......@@ -2566,6 +2566,171 @@ void main() {
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets(
'Can drag the left handle while the right handle remains off-screen',
(WidgetTester tester) async {
// Text is longer than textfield width.
const String testValue =
'aaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbb';
final TextEditingController controller = TextEditingController(text: testValue);
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MediaQuery(
data: const MediaQueryData(size: Size(800.0, 600.0)),
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
scrollController: scrollController,
),
),
),
),
);
// Double tap 'b' to show handles.
final Offset bPos = textOffsetToPosition(tester, testValue.indexOf('b'));
await tester.tapAt(bPos);
await tester.pump(kDoubleTapTimeout ~/ 2);
await tester.tapAt(bPos);
await tester.pumpAndSettle();
final TextSelection selection = controller.selection;
expect(selection.baseOffset, 28);
expect(selection.extentOffset, testValue.length);
// Move to the left edge.
scrollController.jumpTo(0);
await tester.pumpAndSettle();
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
expect(endpoints.length, 2);
// Left handle should appear between textfield's left and right position.
final Offset textFieldLeftPosition =
tester.getTopLeft(find.byType(TextField));
expect(endpoints[0].point.dx - textFieldLeftPosition.dx, isPositive);
final Offset textFieldRightPosition =
tester.getTopRight(find.byType(TextField));
expect(textFieldRightPosition.dx - endpoints[0].point.dx, isPositive);
// Right handle should remain off-screen.
expect(endpoints[1].point.dx - textFieldRightPosition.dx, isPositive);
// Drag the left handle to the right by 25 offset.
const int toOffset = 25;
final double beforeScrollOffset = scrollController.offset;
final Offset handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
final Offset newHandlePos = textOffsetToPosition(tester, toOffset);
final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
// On Apple platforms, dragging the base handle makes it the extent.
expect(controller.selection.baseOffset, testValue.length);
expect(controller.selection.extentOffset, toOffset);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(controller.selection.baseOffset, toOffset);
expect(controller.selection.extentOffset, testValue.length);
break;
}
// The scroll area of text field should not move.
expect(scrollController.offset, beforeScrollOffset);
},
);
testWidgets(
'Can drag the right handle while the left handle remains off-screen',
(WidgetTester tester) async {
// Text is longer than textfield width.
const String testValue =
'aaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbb';
final TextEditingController controller = TextEditingController(text: testValue);
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MediaQuery(
data: const MediaQueryData(size: Size(800.0, 600.0)),
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
scrollController: scrollController,
),
),
),
),
);
// Double tap 'a' to show handles.
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
await tester.tapAt(aPos);
await tester.pump(kDoubleTapTimeout ~/ 2);
await tester.tapAt(aPos);
await tester.pumpAndSettle();
final TextSelection selection = controller.selection;
expect(selection.baseOffset, 0);
expect(selection.extentOffset, 27);
// Move to the right edge.
scrollController.jumpTo(800);
await tester.pumpAndSettle();
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
expect(endpoints.length, 2);
// Right handle should appear between textfield's left and right position.
final Offset textFieldLeftPosition =
tester.getTopLeft(find.byType(TextField));
expect(endpoints[1].point.dx - textFieldLeftPosition.dx, isPositive);
final Offset textFieldRightPosition =
tester.getTopRight(find.byType(TextField));
expect(textFieldRightPosition.dx - endpoints[1].point.dx, isPositive);
// Left handle should remain off-screen.
expect(endpoints[0].point.dx, isNegative);
// Drag the right handle to the left by 50 offset.
const int toOffset = 50;
final double beforeScrollOffset = scrollController.offset;
final Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
final Offset newHandlePos = textOffsetToPosition(tester, toOffset);
final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, toOffset);
// The scroll area of text field should not move.
expect(scrollController.offset, beforeScrollOffset);
},
);
testWidgets('Drag handles trigger feedback', (WidgetTester tester) async {
final FeedbackTester feedback = FeedbackTester();
addTearDown(feedback.dispose);
......
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