Unverified Commit 4c1d887e authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

Set selection on tap down for desktop platforms and tap up for mobile (#105505)

Co-authored-by: 's avatarRenzo Olivares <roliv@google.com>
parent 2c13db3b
......@@ -1454,6 +1454,9 @@ class TextSelectionGestureDetectorBuilder {
/// * [TextSelectionGestureDetector.onTapDown], which triggers this callback.
@protected
void onTapDown(TapDownDetails details) {
if (!delegate.selectionEnabled) {
return;
}
renderEditable.handleTapDown(details);
// The selection overlay should only be shown when the user is interacting
// through a touch screen (via either a finger or a stylus). A mouse shouldn't
......@@ -1465,13 +1468,21 @@ class TextSelectionGestureDetectorBuilder {
|| kind == PointerDeviceKind.stylus;
// Handle shift + click selection if needed.
if (_isShiftPressed && renderEditable.selection?.baseOffset != null) {
_isShiftTapping = true;
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
// On these platforms, a shift-tapped unfocused field expands from 0,
// not from the previous selection.
final bool isShiftPressedValid = _isShiftPressed && renderEditable.selection?.baseOffset != null;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
// On mobile platforms the selection is set on tap up.
if (_isShiftTapping) {
_isShiftTapping = false;
}
break;
case TargetPlatform.macOS:
// On macOS, a shift-tapped unfocused field expands from 0, not from the
// previous selection.
if (isShiftPressedValid) {
_isShiftTapping = true;
final TextSelection? fromSelection = renderEditable.hasFocus
? null
: const TextSelection.collapsed(offset: 0);
......@@ -1480,14 +1491,23 @@ class TextSelectionGestureDetectorBuilder {
SelectionChangedCause.tap,
fromSelection,
);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return;
}
// On macOS, a tap/click places the selection in a precise position.
// This differs from iOS/iPadOS, where if the gesture is done by a touch
// then the selection moves to the closest word edge, instead of a
// precise position.
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
break;
case TargetPlatform.linux:
case TargetPlatform.windows:
if (isShiftPressedValid) {
_isShiftTapping = true;
_extendSelection(details.globalPosition, SelectionChangedCause.tap);
break;
}
return;
}
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
break;
}
}
......@@ -1547,15 +1567,42 @@ class TextSelectionGestureDetectorBuilder {
/// this callback.
@protected
void onSingleTapUp(TapUpDetails details) {
if (_isShiftTapping) {
_isShiftTapping = false;
return;
}
if (delegate.selectionEnabled) {
// Handle shift + click selection if needed.
final bool isShiftPressedValid = _isShiftPressed && renderEditable.selection?.baseOffset != null;
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
// On desktop platforms the selection is set on tap down.
if (_isShiftTapping) {
_isShiftTapping = false;
}
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
if (isShiftPressedValid) {
_isShiftTapping = true;
_extendSelection(details.globalPosition, SelectionChangedCause.tap);
return;
}
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
break;
case TargetPlatform.iOS:
if (isShiftPressedValid) {
// On iOS, a shift-tapped unfocused field expands from 0, not from
// the previous selection.
_isShiftTapping = true;
final TextSelection? fromSelection = renderEditable.hasFocus
? null
: const TextSelection.collapsed(offset: 0);
_expandSelection(
details.globalPosition,
SelectionChangedCause.tap,
fromSelection,
);
return;
}
switch (details.kind) {
case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
......@@ -1566,18 +1613,11 @@ class TextSelectionGestureDetectorBuilder {
break;
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
// On macOS/iOS/iPadOS a touch tap places the cursor at the edge
// of the word.
// On iOS/iPadOS a touch tap places the cursor at the edge of the word.
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
break;
}
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
break;
}
}
}
......
......@@ -1755,7 +1755,7 @@ void main() {
// But don't trigger the toolbar.
expect(find.byType(CupertinoButton), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
testWidgets(
'slow double tap does not trigger double tap',
......@@ -1763,6 +1763,9 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
......@@ -1773,18 +1776,16 @@ void main() {
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final Offset pos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'.
await tester.tapAt(textFieldStart + const Offset(50.0, 5.0));
await tester.tapAt(pos);
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textFieldStart + const Offset(50.0, 5.0));
await tester.tapAt(pos);
await tester.pump();
// Plain collapsed selection.
expect(
controller.selection,
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
);
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6);
// No toolbar.
expect(find.byType(CupertinoButton), findsNothing);
......@@ -1893,6 +1894,9 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
......@@ -1903,19 +1907,20 @@ void main() {
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'.
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'.
await tester.tapAt(textFieldStart + const Offset(50.0, 5.0));
await tester.tapAt(ePos);
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 8),
);
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9);
await tester.tapAt(pPos);
await tester.pumpAndSettle();
// Second tap selects the word around the cursor.
......@@ -1979,6 +1984,9 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
......@@ -1989,29 +1997,27 @@ void main() {
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'.
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 8),
);
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9);
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textFieldStart + const Offset(100.0, 5.0));
await tester.tapAt(ePos);
await tester.pump();
// Plain collapsed selection at the edge of first word. In iOS 12, the
// first tap after a double tap ends up putting the cursor at where
// you tapped instead of the edge like every other single tap. This is
// likely a bug in iOS 12 and not present in other versions.
expect(
controller.selection,
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
);
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6);
// No toolbar.
expect(find.byType(CupertinoButton), findsNothing);
......@@ -2429,6 +2435,9 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
......@@ -2439,20 +2448,18 @@ void main() {
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
await tester.longPressAt(textFieldStart + const Offset(50.0, 5.0));
await tester.longPressAt(ePos);
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textFieldStart + const Offset(50.0, 5.0));
await tester.tapAt(ePos);
await tester.pump();
// We ended up moving the cursor to the edge of the same word and dismissed
// the toolbar.
expect(
controller.selection,
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
);
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6);
// The toolbar from the long press is now dismissed by the second tap.
expect(find.byType(CupertinoButton), findsNothing);
......@@ -2615,6 +2622,9 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
......@@ -2625,25 +2635,24 @@ void main() {
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor to the beginning of the second word.
expect(
controller.selection,
const TextSelection.collapsed(offset: 8),
);
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9);
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 500));
await tester.longPressAt(textFieldStart + const Offset(100.0, 5.0));
await tester.longPressAt(ePos);
await tester.pumpAndSettle();
// Plain collapsed selection at the exact tap position.
expect(
controller.selection,
const TextSelection.collapsed(offset: 6, affinity: TextAffinity.upstream),
const TextSelection.collapsed(offset: 6),
);
// Long press toolbar.
......@@ -2656,6 +2665,9 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
......@@ -2666,19 +2678,18 @@ void main() {
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'
await tester.longPressAt(textFieldStart + const Offset(50.0, 5.0));
await tester.longPressAt(wPos);
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 8),
);
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9);
await tester.tapAt(pPos);
await tester.pumpAndSettle();
// Double tap selection.
......@@ -2752,7 +2763,7 @@ void main() {
const TextSelection(baseOffset: 8, extentOffset: 12),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('force press selects word', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
......@@ -2798,6 +2809,9 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
......@@ -2808,15 +2822,15 @@ void main() {
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'
final int pointerValue = tester.nextPointer;
final TestGesture gesture = await tester.createGesture();
await gesture.downWithCustomEvent(
textFieldStart + const Offset(150.0, 5.0),
pPos,
PointerDownEvent(
pointer: pointerValue,
position: textFieldStart + const Offset(150.0, 5.0),
position: pPos,
// iPhone 6 and below report 0 across the board.
pressure: 0,
pressureMax: 0,
......@@ -2824,11 +2838,10 @@ void main() {
),
);
await gesture.up();
// Fall back to a single tap which selects the edge of the word.
expect(
controller.selection,
const TextSelection.collapsed(offset: 8),
);
// Fall back to a single tap which selects the edge of the word on iOS, and
// a precise position on macOS.
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9);
await tester.pump();
// Falling back to a single tap doesn't trigger a toolbar.
......@@ -2839,6 +2852,9 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'abc def ghi',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
......@@ -2860,7 +2876,7 @@ void main() {
await tester.tapAt(ePos, pointer: 7);
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, 4);
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 4 : 5);
await tester.tapAt(ePos, pointer: 7);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 4);
......@@ -2897,6 +2913,103 @@ void main() {
expect(controller.selection.extentOffset, 5);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('Selection updates on tap down (Desktop platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(controller: controller),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
final Offset ePos = textOffsetToPosition(tester, 5);
final Offset gPos = textOffsetToPosition(tester, 8);
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 5);
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
await gesture.down(gPos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
// This should do nothing. The selection is set on tap down on desktop platforms.
await gesture.up();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
},
variant: TargetPlatformVariant.desktop(),
);
testWidgets('Selection updates on tap up (Mobile platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(controller: controller),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
final Offset ePos = textOffsetToPosition(tester, 5);
final Offset gPos = textOffsetToPosition(tester, 8);
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
await gesture.down(gPos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 5);
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
final TestGesture touchGesture = await tester.startGesture(ePos);
await touchGesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
// On iOS, a tap to select, selects the word edge instead of the exact tap position.
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5);
expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5);
// Selection should stay the same since it is set on tap up for mobile platforms.
await touchGesture.down(gPos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5);
expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5);
await touchGesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
},
variant: TargetPlatformVariant.mobile(),
);
testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
......@@ -5191,6 +5304,7 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
......@@ -5214,11 +5328,17 @@ void main() {
pointer: 7,
kind: PointerDeviceKind.mouse,
);
if (isTargetPlatformMobile) {
await gesture.up();
}
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Expand the selection a bit.
if (isTargetPlatformMobile) {
await gesture.down(textOffsetToPosition(tester, 24));
}
await gesture.moveTo(textOffsetToPosition(tester, 28));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
......@@ -5287,6 +5407,8 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android
|| defaultTargetPlatform == TargetPlatform.fuchsia;
await tester.pumpWidget(
CupertinoApp(
home: Center(
......@@ -5310,11 +5432,17 @@ void main() {
pointer: 7,
kind: PointerDeviceKind.mouse,
);
if (isTargetPlatformMobile) {
await gesture.up();
}
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Expand the selection a bit.
if (isTargetPlatformMobile) {
await gesture.down(textOffsetToPosition(tester, 24));
}
await gesture.moveTo(textOffsetToPosition(tester, 28));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
......@@ -5383,6 +5511,7 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
......@@ -5406,11 +5535,17 @@ void main() {
pointer: 7,
kind: PointerDeviceKind.mouse,
);
if (isTargetPlatformMobile) {
await gesture.up();
}
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Expand the selection a bit.
if (isTargetPlatformMobile) {
await gesture.down(textOffsetToPosition(tester, 7));
}
await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
......@@ -5479,6 +5614,8 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android
|| defaultTargetPlatform == TargetPlatform.fuchsia;
await tester.pumpWidget(
CupertinoApp(
home: Center(
......@@ -5502,11 +5639,17 @@ void main() {
pointer: 7,
kind: PointerDeviceKind.mouse,
);
if (isTargetPlatformMobile) {
await gesture.up();
}
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Expand the selection a bit.
if (isTargetPlatformMobile) {
await gesture.down(textOffsetToPosition(tester, 7));
}
await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
......
......@@ -475,8 +475,6 @@ void main() {
final TestGesture down1 = await tester.startGesture(tester.getCenter(find.byType(TextField).first), kind: PointerDeviceKind.mouse);
await tester.pump();
await tester.pumpAndSettle();
await down1.up();
await down1.removePointer();
expect(focusNodeA.hasFocus, true);
......
......@@ -908,6 +908,9 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
......@@ -933,7 +936,7 @@ void main() {
// First tap moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 8),
TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9),
);
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump();
......@@ -1084,12 +1087,12 @@ void main() {
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
final Offset pos = textOffsetToPosition(tester, 9); // Index of 'P|eel'
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.tapAt(pos);
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.tapAt(pos);
await tester.pump();
// Selected text shows 'Copy', and not 'Paste', 'Cut', 'Select all'.
......@@ -1901,6 +1904,99 @@ void main() {
expect(controller.selection.baseOffset, testValue.length);
});
testWidgets('Selection updates on tap down (Desktop platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(controller: controller),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset ePos = textOffsetToPosition(tester, 5);
final Offset gPos = textOffsetToPosition(tester, 8);
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 5);
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
await gesture.down(gPos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
// This should do nothing. The selection is set on tap down on desktop platforms.
await gesture.up();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
},
variant: TargetPlatformVariant.desktop(),
);
testWidgets('Selection updates on tap up (Mobile platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(controller: controller),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset ePos = textOffsetToPosition(tester, 5);
final Offset gPos = textOffsetToPosition(tester, 8);
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
await gesture.down(gPos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 5);
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
final TestGesture touchGesture = await tester.startGesture(ePos);
await touchGesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
// On iOS a tap to select, selects the word edge instead of the exact tap position.
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5);
expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5);
// Selection should stay the same since it is set on tap up for mobile platforms.
await touchGesture.down(gPos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5);
expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5);
await touchGesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
},
variant: TargetPlatformVariant.mobile(),
);
testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
......@@ -7110,7 +7206,7 @@ void main() {
// But don't trigger the toolbar.
expect(find.byType(CupertinoButton), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets(
......@@ -7186,6 +7282,9 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
......@@ -7198,18 +7297,16 @@ void main() {
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
final Offset pos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'.
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.tapAt(pos);
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.tapAt(pos);
await tester.pump();
// Plain collapsed selection.
expect(
controller.selection,
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
);
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6);
// No toolbar.
expect(find.byType(CupertinoButton), findsNothing);
......@@ -7223,6 +7320,9 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
......@@ -7235,21 +7335,22 @@ void main() {
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'.
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'.
// This tap just puts the cursor somewhere different than where the double
// tap will occur to test that the double tap moves the existing cursor first.
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.tapAt(wPos);
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 8),
TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9),
);
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.tapAt(pPos);
await tester.pumpAndSettle();
// Second tap selects the word around the cursor.
......@@ -7261,7 +7362,7 @@ void main() {
// Selected text shows 3 toolbar buttons.
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets(
......@@ -7679,6 +7780,9 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
......@@ -7691,29 +7795,28 @@ void main() {
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'.
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 8),
TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9),
);
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.tapAt(ePos);
await tester.pump();
// Plain collapsed selection at the edge of first word. In iOS 12, the
// first tap after a double tap ends up putting the cursor at where
// Plain collapsed selection at the edge of first word on iOS. In iOS 12,
// the first tap after a double tap ends up putting the cursor at where
// you tapped instead of the edge like every other single tap. This is
// likely a bug in iOS 12 and not present in other versions.
expect(
controller.selection,
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
);
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6);
// No toolbar.
expect(find.byType(CupertinoButton), findsNothing);
......@@ -7796,6 +7899,9 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
......@@ -7808,20 +7914,18 @@ void main() {
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0));
await tester.longPressAt(ePos);
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.tapAt(ePos);
await tester.pump();
// We ended up moving the cursor to the edge of the same word and dismissed
// the toolbar.
expect(
controller.selection,
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
);
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6);
// Collapsed toolbar shows 2 buttons.
expect(find.byType(CupertinoButton), findsNothing);
......@@ -8358,6 +8462,9 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
......@@ -8370,19 +8477,20 @@ void main() {
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'
final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor to the beginning of the second word.
expect(
controller.selection,
const TextSelection.collapsed(offset: 8),
TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9),
);
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 500));
await tester.longPressAt(textfieldStart + const Offset(100.0, 9.0));
await tester.longPressAt(ePos);
await tester.pumpAndSettle();
// Plain collapsed selection at the exact tap position.
......@@ -8403,6 +8511,9 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
// On macOS, we select the precise position of the tap.
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
......@@ -8415,23 +8526,24 @@ void main() {
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'
await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0));
await tester.longPressAt(wPos);
await tester.pump(const Duration(milliseconds: 50));
expect(
controller.selection,
const TextSelection.collapsed(offset: 3),
);
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.tapAt(pPos);
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 8),
TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9),
);
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.tapAt(pPos);
await tester.pumpAndSettle();
// Double tap selection.
......@@ -8568,7 +8680,7 @@ void main() {
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets(
......@@ -10959,6 +11071,7 @@ void main() {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
......@@ -10982,11 +11095,18 @@ void main() {
pointer: 7,
kind: PointerDeviceKind.mouse,
);
if (isTargetPlatformMobile) {
await gesture.up();
}
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Expand the selection a bit.
if (isTargetPlatformMobile) {
await gesture.down(textOffsetToPosition(tester, 24));
}
await tester.pumpAndSettle();
await gesture.moveTo(textOffsetToPosition(tester, 28));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
......@@ -11049,12 +11169,14 @@ void main() {
await gesture.up();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 26);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('can shift + tap + drag to select with a keyboard (non-Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android
|| defaultTargetPlatform == TargetPlatform.fuchsia;
await tester.pumpWidget(
MaterialApp(
home: Material(
......@@ -11078,11 +11200,17 @@ void main() {
pointer: 7,
kind: PointerDeviceKind.mouse,
);
if (isTargetPlatformMobile) {
await gesture.up();
}
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Expand the selection a bit.
if (isTargetPlatformMobile) {
await gesture.down(textOffsetToPosition(tester, 23));
}
await gesture.moveTo(textOffsetToPosition(tester, 28));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
......@@ -11145,12 +11273,13 @@ void main() {
await gesture.up();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 26);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }));
testWidgets('can shift + tap + drag to select with a keyboard, reversed (Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
......@@ -11174,11 +11303,17 @@ void main() {
pointer: 7,
kind: PointerDeviceKind.mouse,
);
if (isTargetPlatformMobile) {
await gesture.up();
}
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Expand the selection a bit.
if (isTargetPlatformMobile) {
await gesture.down(textOffsetToPosition(tester, 7));
}
await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
......@@ -11241,12 +11376,14 @@ void main() {
await gesture.up();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 14);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('can shift + tap + drag to select with a keyboard, reversed (non-Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android
|| defaultTargetPlatform == TargetPlatform.fuchsia;
await tester.pumpWidget(
MaterialApp(
home: Material(
......@@ -11270,11 +11407,17 @@ void main() {
pointer: 7,
kind: PointerDeviceKind.mouse,
);
if (isTargetPlatformMobile) {
await gesture.up();
}
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Expand the selection a bit.
if (isTargetPlatformMobile) {
await gesture.down(textOffsetToPosition(tester, 8));
}
await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
......@@ -11337,7 +11480,7 @@ void main() {
await gesture.up();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 14);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }));
// Regression test for https://github.com/flutter/flutter/issues/101587.
testWidgets('Right clicking menu behavior', (WidgetTester tester) async {
......
......@@ -557,9 +557,9 @@ void main() {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
expect(renderEditable.selectWordEdgeCalled, isTrue);
break;
case TargetPlatform.macOS:
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
......
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