Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit fe2fc8d

Browse files
Single tap on the previous selection should toggle the toolbar on iOS… (#108913)
1 parent 237a298 commit fe2fc8d

File tree

7 files changed

+232
-16
lines changed

7 files changed

+232
-16
lines changed

packages/flutter/lib/src/cupertino/text_field.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe
102102

103103
@override
104104
void onSingleTapUp(TapUpDetails details) {
105-
editableText.hideToolbar();
106105
// Because TextSelectionGestureDetector listens to taps that happen on
107106
// widgets in front of it, tapping the clear button will also trigger
108107
// this handler. If the clear button widget recognizes the up event,

packages/flutter/lib/src/material/text_field.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
8888

8989
@override
9090
void onSingleTapUp(TapUpDetails details) {
91-
editableText.hideToolbar();
9291
super.onSingleTapUp(details);
9392
_state._requestKeyboard();
9493
_state.widget.onTap?.call();

packages/flutter/lib/src/widgets/editable_text.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3192,10 +3192,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
31923192
}
31933193

31943194
/// Toggles the visibility of the toolbar.
3195-
void toggleToolbar() {
3195+
void toggleToolbar([bool hideHandles = true]) {
31963196
assert(_selectionOverlay != null);
31973197
if (_selectionOverlay!.toolbarIsVisible) {
3198-
hideToolbar();
3198+
hideToolbar(hideHandles);
31993199
} else {
32003200
showToolbar();
32013201
}

packages/flutter/lib/src/widgets/text_selection.dart

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1591,6 +1591,19 @@ class TextSelectionGestureDetectorBuilder {
15911591
&& renderEditable.selection!.end >= textPosition.offset;
15921592
}
15931593

1594+
bool _tapWasOnSelection(Offset position) {
1595+
if (renderEditable.selection == null) {
1596+
return false;
1597+
}
1598+
1599+
final TextPosition textPosition = renderEditable.getPositionForPoint(
1600+
position,
1601+
);
1602+
1603+
return renderEditable.selection!.start < textPosition.offset
1604+
&& renderEditable.selection!.end > textPosition.offset;
1605+
}
1606+
15941607
// Expand the selection to the given global position.
15951608
//
15961609
// Either base or extent will be moved to the last tapped position, whichever
@@ -1821,13 +1834,15 @@ class TextSelectionGestureDetectorBuilder {
18211834
case TargetPlatform.linux:
18221835
case TargetPlatform.macOS:
18231836
case TargetPlatform.windows:
1837+
editableText.hideToolbar();
18241838
// On desktop platforms the selection is set on tap down.
18251839
if (_isShiftTapping) {
18261840
_isShiftTapping = false;
18271841
}
18281842
break;
18291843
case TargetPlatform.android:
18301844
case TargetPlatform.fuchsia:
1845+
editableText.hideToolbar();
18311846
if (isShiftPressedValid) {
18321847
_isShiftTapping = true;
18331848
_extendSelection(details.globalPosition, SelectionChangedCause.tap);
@@ -1861,7 +1876,16 @@ class TextSelectionGestureDetectorBuilder {
18611876
case PointerDeviceKind.touch:
18621877
case PointerDeviceKind.unknown:
18631878
// On iOS/iPadOS a touch tap places the cursor at the edge of the word.
1864-
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
1879+
final TextSelection previousSelection = editableText.textEditingValue.selection;
1880+
// If the tap was within the previous selection, then the selection should stay the same.
1881+
if (!_tapWasOnSelection(details.globalPosition)) {
1882+
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
1883+
}
1884+
if (previousSelection == editableText.textEditingValue.selection && renderEditable.hasFocus) {
1885+
editableText.toggleToolbar(false);
1886+
} else {
1887+
editableText.hideToolbar(false);
1888+
}
18651889
break;
18661890
}
18671891
break;

packages/flutter/test/cupertino/text_field_test.dart

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1752,10 +1752,86 @@ void main() {
17521752
expect(controller.selection.isCollapsed, isTrue);
17531753
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6);
17541754

1755-
// No toolbar.
1756-
expect(find.byType(CupertinoButton), findsNothing);
1755+
// Toolbar shows on mobile.
1756+
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : isTargetPlatformMobile ? findsNWidgets(2) : findsNothing);
17571757
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
17581758

1759+
testWidgets(
1760+
'Tapping on a non-collapsed selection toggles the toolbar and retains the selection',
1761+
(WidgetTester tester) async {
1762+
final TextEditingController controller = TextEditingController(
1763+
text: 'Atwater Peel Sherbrooke Bonaventure',
1764+
);
1765+
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
1766+
await tester.pumpWidget(
1767+
CupertinoApp(
1768+
home: Center(
1769+
child: CupertinoTextField(
1770+
controller: controller,
1771+
),
1772+
),
1773+
),
1774+
);
1775+
1776+
final Offset vPos = textOffsetToPosition(tester, 29); // Index of 'Bonav|enture'.
1777+
final Offset ePos = textOffsetToPosition(tester, 35) + const Offset(7.0, 0.0); // Index of 'Bonaventure|' + Offset(7.0,0), which taps slightly to the right of the end of the text.
1778+
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'.
1779+
1780+
// This tap just puts the cursor somewhere different than where the double
1781+
// tap will occur to test that the double tap moves the existing cursor first.
1782+
await tester.tapAt(wPos);
1783+
await tester.pump(const Duration(milliseconds: 500));
1784+
1785+
await tester.tapAt(vPos);
1786+
await tester.pump(const Duration(milliseconds: 50));
1787+
// First tap moved the cursor.
1788+
expect(controller.selection.isCollapsed, true);
1789+
expect(
1790+
controller.selection.baseOffset,
1791+
35,
1792+
);
1793+
await tester.tapAt(vPos);
1794+
await tester.pumpAndSettle(const Duration(milliseconds: 500));
1795+
1796+
// Second tap selects the word around the cursor.
1797+
expect(
1798+
controller.selection,
1799+
const TextSelection(baseOffset: 24, extentOffset: 35),
1800+
);
1801+
1802+
// Selected text shows 3 toolbar buttons.
1803+
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
1804+
1805+
// Tap the selected word to hide the toolbar and retain the selection.
1806+
await tester.tapAt(vPos);
1807+
await tester.pumpAndSettle();
1808+
expect(
1809+
controller.selection,
1810+
const TextSelection(baseOffset: 24, extentOffset: 35),
1811+
);
1812+
expect(find.byType(CupertinoButton), findsNothing);
1813+
1814+
// Tap the selected word to show the toolbar and retain the selection.
1815+
await tester.tapAt(vPos);
1816+
await tester.pumpAndSettle();
1817+
expect(
1818+
controller.selection,
1819+
const TextSelection(baseOffset: 24, extentOffset: 35),
1820+
);
1821+
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
1822+
1823+
// Tap past the selected word to move the cursor and hide the toolbar.
1824+
await tester.tapAt(ePos);
1825+
await tester.pumpAndSettle();
1826+
expect(
1827+
controller.selection,
1828+
const TextSelection.collapsed(offset: 35),
1829+
);
1830+
expect(find.byType(CupertinoButton), findsNothing);
1831+
},
1832+
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
1833+
);
1834+
17591835
testWidgets(
17601836
'double tap selects word and first tap of double tap moves cursor',
17611837
(WidgetTester tester) async {
@@ -2701,11 +2777,13 @@ void main() {
27012777
// Double tap selecting the same word somewhere else is fine.
27022778
await tester.tapAt(textFieldStart + const Offset(100.0, 5.0));
27032779
await tester.pump(const Duration(milliseconds: 50));
2704-
// First tap moved the cursor.
2780+
// First tap hides the toolbar, and retains the selection.
27052781
expect(
27062782
controller.selection,
2707-
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
2783+
const TextSelection(baseOffset: 0, extentOffset: 7),
27082784
);
2785+
expect(find.byType(CupertinoButton), findsNothing);
2786+
// Second tap shows the toolbar, and retains the selection.
27092787
await tester.tapAt(textFieldStart + const Offset(100.0, 5.0));
27102788
await tester.pumpAndSettle();
27112789
expect(
@@ -2716,11 +2794,12 @@ void main() {
27162794

27172795
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
27182796
await tester.pump(const Duration(milliseconds: 50));
2719-
// First tap moved the cursor.
2797+
// First tap moved the cursor and hides the toolbar.
27202798
expect(
27212799
controller.selection,
27222800
const TextSelection.collapsed(offset: 8),
27232801
);
2802+
expect(find.byType(CupertinoButton), findsNothing);
27242803
await tester.tapAt(textFieldStart + const Offset(150.0, 5.0));
27252804
await tester.pumpAndSettle();
27262805
expect(

packages/flutter/test/material/text_field_test.dart

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7410,12 +7410,90 @@ void main() {
74107410
expect(controller.selection.isCollapsed, isTrue);
74117411
expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6);
74127412

7413-
// No toolbar.
7414-
expect(find.byType(CupertinoButton), findsNothing);
7413+
// Toolbar shows on iOS.
7414+
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : isTargetPlatformMobile ? findsNWidgets(2) : findsNothing);
74157415
},
74167416
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
74177417
);
74187418

7419+
testWidgets(
7420+
'Tapping on a non-collapsed selection toggles the toolbar and retains the selection',
7421+
(WidgetTester tester) async {
7422+
final TextEditingController controller = TextEditingController(
7423+
text: 'Atwater Peel Sherbrooke Bonaventure',
7424+
);
7425+
// On iOS/iPadOS, during a tap we select the edge of the word closest to the tap.
7426+
await tester.pumpWidget(
7427+
MaterialApp(
7428+
home: Material(
7429+
child: Center(
7430+
child: TextField(
7431+
controller: controller,
7432+
),
7433+
),
7434+
),
7435+
),
7436+
);
7437+
7438+
final Offset vPos = textOffsetToPosition(tester, 29); // Index of 'Bonav|enture'.
7439+
final Offset ePos = textOffsetToPosition(tester, 35) + const Offset(7.0, 0.0); // Index of 'Bonaventure|' + Offset(7.0,0), which taps slightly to the right of the end of the text.
7440+
final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'.
7441+
7442+
// This tap just puts the cursor somewhere different than where the double
7443+
// tap will occur to test that the double tap moves the existing cursor first.
7444+
await tester.tapAt(wPos);
7445+
await tester.pump(const Duration(milliseconds: 500));
7446+
7447+
await tester.tapAt(vPos);
7448+
await tester.pump(const Duration(milliseconds: 50));
7449+
// First tap moved the cursor.
7450+
expect(controller.selection.isCollapsed, true);
7451+
expect(
7452+
controller.selection.baseOffset,
7453+
35,
7454+
);
7455+
await tester.tapAt(vPos);
7456+
await tester.pumpAndSettle(const Duration(milliseconds: 500));
7457+
7458+
// Second tap selects the word around the cursor.
7459+
expect(
7460+
controller.selection,
7461+
const TextSelection(baseOffset: 24, extentOffset: 35),
7462+
);
7463+
7464+
// Selected text shows 3 toolbar buttons.
7465+
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
7466+
7467+
// Tap the selected word to hide the toolbar and retain the selection.
7468+
await tester.tapAt(vPos);
7469+
await tester.pumpAndSettle();
7470+
expect(
7471+
controller.selection,
7472+
const TextSelection(baseOffset: 24, extentOffset: 35),
7473+
);
7474+
expect(find.byType(CupertinoButton), findsNothing);
7475+
7476+
// Tap the selected word to show the toolbar and retain the selection.
7477+
await tester.tapAt(vPos);
7478+
await tester.pumpAndSettle();
7479+
expect(
7480+
controller.selection,
7481+
const TextSelection(baseOffset: 24, extentOffset: 35),
7482+
);
7483+
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
7484+
7485+
// Tap past the selected word to move the cursor and hide the toolbar.
7486+
await tester.tapAt(ePos);
7487+
await tester.pumpAndSettle();
7488+
expect(
7489+
controller.selection,
7490+
const TextSelection.collapsed(offset: 35),
7491+
);
7492+
expect(find.byType(CupertinoButton), findsNothing);
7493+
},
7494+
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
7495+
);
7496+
74197497
testWidgets(
74207498
'double tap selects word and first tap of double tap moves cursor',
74217499
(WidgetTester tester) async {
@@ -8804,11 +8882,13 @@ void main() {
88048882
// Double tap selecting the same word somewhere else is fine.
88058883
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
88068884
await tester.pump(const Duration(milliseconds: 50));
8807-
// First tap moved the cursor.
8885+
// First tap hides the toolbar and retains the selection.
88088886
expect(
88098887
controller.selection,
8810-
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
8888+
const TextSelection(baseOffset: 0, extentOffset: 7),
88118889
);
8890+
expect(find.byType(CupertinoButton), findsNothing);
8891+
// Second tap shows the toolbar and retains the selection.
88128892
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
88138893
await tester.pumpAndSettle();
88148894
expect(
@@ -8819,11 +8899,12 @@ void main() {
88198899

88208900
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
88218901
await tester.pump(const Duration(milliseconds: 50));
8822-
// First tap moved the cursor.
8902+
// First tap moved the cursor and hides the toolbar.
88238903
expect(
88248904
controller.selection,
88258905
const TextSelection.collapsed(offset: 8),
88268906
);
8907+
expect(find.byType(CupertinoButton), findsNothing);
88278908
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
88288909
await tester.pumpAndSettle();
88298910
expect(

packages/flutter/test/widgets/text_selection_test.dart

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,38 @@ void main() {
569569
}
570570
}, variant: TargetPlatformVariant.all());
571571

572+
testWidgets('test TextSelectionGestureDetectorBuilder toggles toolbar on single tap on previous selection iOS', (WidgetTester tester) async {
573+
await pumpTextSelectionGestureDetectorBuilder(tester);
574+
575+
final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
576+
final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
577+
expect(state.showToolbarCalled, isFalse);
578+
expect(state.toggleToolbarCalled, isFalse);
579+
renderEditable.selection = const TextSelection(baseOffset: 2, extentOffset: 6);
580+
renderEditable.hasFocus = true;
581+
582+
final TestGesture gesture = await tester.startGesture(
583+
const Offset(25.0, 200.0),
584+
pointer: 0,
585+
);
586+
await gesture.up();
587+
await tester.pumpAndSettle();
588+
589+
switch (defaultTargetPlatform) {
590+
case TargetPlatform.iOS:
591+
expect(renderEditable.selectWordEdgeCalled, isFalse);
592+
expect(state.toggleToolbarCalled, isTrue);
593+
break;
594+
case TargetPlatform.macOS:
595+
case TargetPlatform.android:
596+
case TargetPlatform.fuchsia:
597+
case TargetPlatform.linux:
598+
case TargetPlatform.windows:
599+
expect(renderEditable.selectPositionAtCalled, isTrue);
600+
break;
601+
}
602+
}, variant: TargetPlatformVariant.all());
603+
572604
testWidgets('test TextSelectionGestureDetectorBuilder double tap', (WidgetTester tester) async {
573605
await pumpTextSelectionGestureDetectorBuilder(tester);
574606
final TestGesture gesture = await tester.startGesture(
@@ -1333,6 +1365,7 @@ class FakeEditableText extends EditableText {
13331365
class FakeEditableTextState extends EditableTextState {
13341366
final GlobalKey _editableKey = GlobalKey();
13351367
bool showToolbarCalled = false;
1368+
bool toggleToolbarCalled = false;
13361369

13371370
@override
13381371
RenderEditable get renderEditable => _editableKey.currentContext!.findRenderObject()! as RenderEditable;
@@ -1344,7 +1377,8 @@ class FakeEditableTextState extends EditableTextState {
13441377
}
13451378

13461379
@override
1347-
void toggleToolbar() {
1380+
void toggleToolbar([bool hideHandles = true]) {
1381+
toggleToolbarCalled = true;
13481382
return;
13491383
}
13501384

0 commit comments

Comments
 (0)