Skip to content

Commit 65f66ac

Browse files
chunhtaigspencergoog
authored andcommitted
Fix iOS selectWordEdge doesn't account for affinity (flutter#115849)
* Fix iOS selectWordEdge doesn't account for affinity * fix test * update
1 parent 4652f59 commit 65f66ac

File tree

4 files changed

+61
-37
lines changed

4 files changed

+61
-37
lines changed

packages/flutter/lib/src/rendering/editable.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2149,7 +2149,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
21492149
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition! - _paintOffset));
21502150
final TextRange word = _textPainter.getWordBoundary(position);
21512151
late TextSelection newSelection;
2152-
if (position.offset - word.start <= 1) {
2152+
if (position.offset <= word.start) {
21532153
newSelection = TextSelection.collapsed(offset: word.start);
21542154
} else {
21552155
newSelection = TextSelection.collapsed(offset: word.end, affinity: TextAffinity.upstream);

packages/flutter/test/cupertino/text_field_test.dart

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2173,7 +2173,7 @@ void main() {
21732173
await tester.pump(const Duration(milliseconds: 50));
21742174
// First tap moved the cursor.
21752175
expect(controller.selection.isCollapsed, isTrue);
2176-
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9);
2176+
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 12 : 9);
21772177

21782178
await tester.tapAt(pPos);
21792179
await tester.pumpAndSettle();
@@ -2275,7 +2275,7 @@ void main() {
22752275
await tester.pump(const Duration(milliseconds: 50));
22762276
// First tap moved the cursor.
22772277
expect(controller.selection.isCollapsed, isTrue);
2278-
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9);
2278+
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 12 : 9);
22792279

22802280
await tester.tapAt(pPos);
22812281
await tester.pump(const Duration(milliseconds: 500));
@@ -3134,7 +3134,7 @@ void main() {
31343134
await tester.pump(const Duration(milliseconds: 50));
31353135
// First tap moved the cursor to the beginning of the second word.
31363136
expect(controller.selection.isCollapsed, isTrue);
3137-
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9);
3137+
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 12 : 9);
31383138
await tester.tapAt(pPos);
31393139
await tester.pump(const Duration(milliseconds: 500));
31403140

@@ -3201,7 +3201,7 @@ void main() {
32013201
// First tap moved the cursor.
32023202
expect(find.byType(CupertinoButton), findsNothing);
32033203
expect(controller.selection.isCollapsed, isTrue);
3204-
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9);
3204+
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 12 : 9);
32053205

32063206
await tester.tapAt(pPos);
32073207
await tester.pumpAndSettle();
@@ -3270,7 +3270,7 @@ void main() {
32703270
// First tap moved the cursor and hides the toolbar.
32713271
expect(
32723272
controller.selection,
3273-
const TextSelection.collapsed(offset: 8),
3273+
const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream),
32743274
);
32753275
expect(find.byType(CupertinoButton), findsNothing);
32763276
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
@@ -3375,7 +3375,7 @@ void main() {
33753375
// Fall back to a single tap which selects the edge of the word on iOS, and
33763376
// a precise position on macOS.
33773377
expect(controller.selection.isCollapsed, isTrue);
3378-
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9);
3378+
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 12 : 9);
33793379

33803380
await tester.pump();
33813381
// Falling back to a single tap doesn't trigger a toolbar.
@@ -3410,7 +3410,7 @@ void main() {
34103410
await tester.tapAt(ePos, pointer: 7);
34113411
await tester.pump(const Duration(milliseconds: 50));
34123412
expect(controller.selection.isCollapsed, isTrue);
3413-
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 4 : 5);
3413+
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 5);
34143414
await tester.tapAt(ePos, pointer: 7);
34153415
await tester.pumpAndSettle();
34163416
expect(controller.selection.baseOffset, 4);
@@ -3911,14 +3911,14 @@ void main() {
39113911
await touchGesture.up();
39123912
await tester.pumpAndSettle(kDoubleTapTimeout);
39133913
// On iOS, a tap to select, selects the word edge instead of the exact tap position.
3914-
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5);
3915-
expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5);
3914+
expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5);
3915+
expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5);
39163916

39173917
// Selection should stay the same since it is set on tap up for mobile platforms.
39183918
await touchGesture.down(gPos);
39193919
await tester.pump();
3920-
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5);
3921-
expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5);
3920+
expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5);
3921+
expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5);
39223922

39233923
await touchGesture.up();
39243924
await tester.pumpAndSettle();
@@ -7219,7 +7219,7 @@ void main() {
72197219
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
72207220
await tester.pumpAndSettle(const Duration(milliseconds: 300));
72217221
expect(controller.selection.isCollapsed, true);
7222-
expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 5 : 4);
7222+
expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 5 : 7);
72237223
expect(find.byKey(fakeMagnifier.key!), findsNothing);
72247224

72257225
// Long press the 'e' to move the cursor in front of the 'e' and show the magnifier.

packages/flutter/test/material/text_field_test.dart

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,7 +1017,6 @@ void main() {
10171017
);
10181018
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
10191019
// On macOS, we select the precise position of the tap.
1020-
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
10211020
await tester.pumpWidget(
10221021
MaterialApp(
10231022
home: Material(
@@ -1031,21 +1030,19 @@ void main() {
10311030
),
10321031
);
10331032

1034-
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
1035-
10361033
// This tap just puts the cursor somewhere different than where the double
10371034
// tap will occur to test that the double tap moves the existing cursor first.
1038-
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
1035+
await tester.tapAt(textOffsetToPosition(tester, 3));
10391036
await tester.pump(const Duration(milliseconds: 500));
10401037

1041-
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
1038+
await tester.tapAt(textOffsetToPosition(tester, 8));
10421039
await tester.pump(const Duration(milliseconds: 50));
10431040
// First tap moved the cursor.
10441041
expect(
10451042
controller.selection,
1046-
TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9),
1043+
const TextSelection.collapsed(offset: 8),
10471044
);
1048-
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
1045+
await tester.tapAt(textOffsetToPosition(tester, 8));
10491046
await tester.pump();
10501047

10511048
// Second tap selects the word around the cursor.
@@ -2088,14 +2085,14 @@ void main() {
20882085
await touchGesture.up();
20892086
await tester.pumpAndSettle(kDoubleTapTimeout);
20902087
// On iOS a tap to select, selects the word edge instead of the exact tap position.
2091-
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5);
2092-
expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5);
2088+
expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5);
2089+
expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5);
20932090

20942091
// Selection should stay the same since it is set on tap up for mobile platforms.
20952092
await touchGesture.down(gPos);
20962093
await tester.pump();
2097-
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5);
2098-
expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5);
2094+
expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5);
2095+
expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5);
20992096

21002097
await touchGesture.up();
21012098
await tester.pumpAndSettle();
@@ -8414,14 +8411,11 @@ void main() {
84148411
);
84158412

84168413
testWidgets(
8417-
'double tap selects word and first tap of double tap moves cursor',
8414+
'double tap selects word and first tap of double tap moves cursor (iOS)',
84188415
(WidgetTester tester) async {
84198416
final TextEditingController controller = TextEditingController(
84208417
text: 'Atwater Peel Sherbrooke Bonaventure',
84218418
);
8422-
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
8423-
// On macOS, we select the precise position of the tap.
8424-
final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS;
84258419
await tester.pumpWidget(
84268420
MaterialApp(
84278421
home: Material(
@@ -8447,7 +8441,7 @@ void main() {
84478441
// First tap moved the cursor.
84488442
expect(
84498443
controller.selection,
8450-
TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9),
8444+
const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream),
84518445
);
84528446
await tester.tapAt(pPos);
84538447
await tester.pumpAndSettle();
@@ -8464,6 +8458,37 @@ void main() {
84648458
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
84658459
);
84668460

8461+
testWidgets('iOS selectWordEdge works correctly', (WidgetTester tester) async {
8462+
final TextEditingController controller = TextEditingController(
8463+
text: 'blah1 blah2',
8464+
);
8465+
await tester.pumpWidget(
8466+
MaterialApp(
8467+
home: Material(
8468+
child: TextField(
8469+
controller: controller,
8470+
),
8471+
),
8472+
),
8473+
);
8474+
8475+
// Initially, the menu is not shown and there is no selection.
8476+
expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1));
8477+
final Offset pos1 = textOffsetToPosition(tester, 1);
8478+
TestGesture gesture = await tester.startGesture(pos1);
8479+
await tester.pump();
8480+
await gesture.up();
8481+
await tester.pumpAndSettle();
8482+
expect(controller.selection, const TextSelection.collapsed(offset: 5, affinity: TextAffinity.upstream));
8483+
8484+
final Offset pos0 = textOffsetToPosition(tester, 0);
8485+
gesture = await tester.startGesture(pos0);
8486+
await tester.pump();
8487+
await gesture.up();
8488+
await tester.pumpAndSettle();
8489+
expect(controller.selection, const TextSelection.collapsed(offset: 0));
8490+
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS));
8491+
84678492
testWidgets(
84688493
'double tap does not select word on read-only obscured field',
84698494
(WidgetTester tester) async {
@@ -8952,7 +8977,7 @@ void main() {
89528977
// First tap moved the cursor.
89538978
expect(
89548979
controller.selection,
8955-
TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9),
8980+
isTargetPlatformMobile ? const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream) : const TextSelection.collapsed(offset: 9),
89568981
);
89578982
await tester.tapAt(pPos);
89588983
await tester.pump(const Duration(milliseconds: 500));
@@ -9813,7 +9838,7 @@ void main() {
98139838
// First tap moved the cursor to the beginning of the second word.
98149839
expect(
98159840
controller.selection,
9816-
TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9),
9841+
isTargetPlatformMobile ? const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream) : const TextSelection.collapsed(offset: 9),
98179842
);
98189843
await tester.tapAt(pPos);
98199844
await tester.pump(const Duration(milliseconds: 500));
@@ -9875,7 +9900,7 @@ void main() {
98759900
// First tap moved the cursor.
98769901
expect(
98779902
controller.selection,
9878-
TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9),
9903+
isTargetPlatformMobile ? const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream) : const TextSelection.collapsed(offset: 9),
98799904
);
98809905
await tester.tapAt(pPos);
98819906
await tester.pumpAndSettle();
@@ -10006,7 +10031,7 @@ void main() {
1000610031
// First tap moved the cursor and hid the toolbar.
1000710032
expect(
1000810033
controller.selection,
10009-
const TextSelection.collapsed(offset: 8),
10034+
const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream)
1001010035
);
1001110036
expect(find.byType(CupertinoButton), findsNothing);
1001210037
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
@@ -10441,7 +10466,7 @@ void main() {
1044110466
// Single taps selects the edge of the word.
1044210467
expect(
1044310468
controller.selection,
10444-
const TextSelection.collapsed(offset: 8),
10469+
const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream),
1044510470
);
1044610471

1044710472
await tester.pump();
@@ -13418,7 +13443,7 @@ void main() {
1341813443
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
1341913444
await tester.pumpAndSettle(const Duration(milliseconds: 300));
1342013445
expect(controller.selection.isCollapsed, true);
13421-
expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 5 : 4);
13446+
expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 5 : 7);
1342213447
expect(find.byKey(fakeMagnifier.key!), findsNothing);
1342313448

1342413449
// Long press the 'e' to select 'def' on Android and show magnifier.

packages/flutter/test/widgets/selectable_text_test.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3885,7 +3885,6 @@ void main() {
38853885
),
38863886
),
38873887
);
3888-
38893888
final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));
38903889

38913890
await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0));
@@ -3912,7 +3911,7 @@ void main() {
39123911
// First tap moved the cursor.
39133912
expect(
39143913
controller.selection,
3915-
const TextSelection.collapsed(offset: 0),
3914+
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
39163915
);
39173916
await tester.tapAt(selectableTextStart + const Offset(10.0, 5.0));
39183917
await tester.pump(const Duration(milliseconds: 50));

0 commit comments

Comments
 (0)