Selecting spaces (#68227)
When attempting to select a space on mobile, Flutter will try to select the word before the space too, if one exists.
This commit is contained in:
parent
891bd95e1a
commit
09dc5599dd
@ -1930,6 +1930,40 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
// If text is obscured, the entire sentence should be treated as one word.
|
||||
if (obscureText) {
|
||||
return TextSelection(baseOffset: 0, extentOffset: _plainText.length);
|
||||
// If the word is a space, on iOS try to select the previous word instead.
|
||||
} else if (text?.text != null
|
||||
&& _isWhitespace(text!.text!.codeUnitAt(position.offset))
|
||||
&& position.offset > 0) {
|
||||
assert(defaultTargetPlatform != null);
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.iOS:
|
||||
int startIndex = position.offset - 1;
|
||||
while (startIndex > 0
|
||||
&& (_isWhitespace(text!.text!.codeUnitAt(startIndex))
|
||||
|| text!.text! == '\u200e' || text!.text! == '\u200f')) {
|
||||
startIndex--;
|
||||
}
|
||||
if (startIndex > 0) {
|
||||
final TextPosition positionBeforeSpace = TextPosition(
|
||||
offset: startIndex,
|
||||
affinity: position.affinity,
|
||||
);
|
||||
final TextRange wordBeforeSpace = _textPainter.getWordBoundary(
|
||||
positionBeforeSpace,
|
||||
);
|
||||
startIndex = wordBeforeSpace.start;
|
||||
}
|
||||
return TextSelection(
|
||||
baseOffset: startIndex,
|
||||
extentOffset: position.offset,
|
||||
);
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return TextSelection(baseOffset: word.start, extentOffset: word.end);
|
||||
}
|
||||
|
@ -1720,6 +1720,126 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('double tapping a space selects the previous word on iOS', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: ' blah blah \n blah',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, -1);
|
||||
expect(controller.value.selection.extentOffset, -1);
|
||||
|
||||
// Put the cursor at the end of the field.
|
||||
await tester.tapAt(textOffsetToPosition(tester, 19));
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 19);
|
||||
expect(controller.value.selection.extentOffset, 19);
|
||||
|
||||
// Double tapping the second space selects the previous word.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 1);
|
||||
expect(controller.value.selection.extentOffset, 5);
|
||||
|
||||
// Put the cursor at the end of the field.
|
||||
await tester.tapAt(textOffsetToPosition(tester, 19));
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 19);
|
||||
expect(controller.value.selection.extentOffset, 19);
|
||||
|
||||
// Double tapping the first space selects the space.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 0);
|
||||
expect(controller.value.selection.extentOffset, 1);
|
||||
|
||||
// Put the cursor at the end of the field.
|
||||
await tester.tapAt(textOffsetToPosition(tester, 19));
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 19);
|
||||
expect(controller.value.selection.extentOffset, 19);
|
||||
|
||||
// Double tapping the last space selects all previous contiguous spaces on
|
||||
// both lines and the previous word.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 14));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 14));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 6);
|
||||
expect(controller.value.selection.extentOffset, 14);
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
|
||||
|
||||
testWidgets('double tapping a space selects the space on Mac', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: ' blah blah',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, -1);
|
||||
expect(controller.value.selection.extentOffset, -1);
|
||||
|
||||
// Put the cursor at the end of the field.
|
||||
await tester.tapAt(textOffsetToPosition(tester, 10));
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 10);
|
||||
expect(controller.value.selection.extentOffset, 10);
|
||||
|
||||
// Double tapping the second space selects it.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 5);
|
||||
expect(controller.value.selection.extentOffset, 6);
|
||||
|
||||
// Put the cursor at the end of the field.
|
||||
await tester.tapAt(textOffsetToPosition(tester, 10));
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 10);
|
||||
expect(controller.value.selection.extentOffset, 10);
|
||||
|
||||
// Double tapping the first space selects it.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 0);
|
||||
expect(controller.value.selection.extentOffset, 1);
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }));
|
||||
|
||||
testWidgets(
|
||||
'An obscured CupertinoTextField is not selectable when disabled',
|
||||
(WidgetTester tester) async {
|
||||
|
@ -7044,6 +7044,129 @@ void main() {
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
||||
|
||||
testWidgets('double tapping a space selects the previous word on iOS', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: ' blah blah \n blah',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, -1);
|
||||
expect(controller.value.selection.extentOffset, -1);
|
||||
|
||||
// Put the cursor at the end of the field.
|
||||
await tester.tapAt(textOffsetToPosition(tester, 19));
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 19);
|
||||
expect(controller.value.selection.extentOffset, 19);
|
||||
|
||||
// Double tapping does the same thing.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.extentOffset, 5);
|
||||
expect(controller.value.selection.baseOffset, 1);
|
||||
|
||||
// Put the cursor at the end of the field.
|
||||
await tester.tapAt(textOffsetToPosition(tester, 19));
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 19);
|
||||
expect(controller.value.selection.extentOffset, 19);
|
||||
|
||||
// Double tapping does the same thing for the first space.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 0);
|
||||
expect(controller.value.selection.extentOffset, 1);
|
||||
|
||||
// Put the cursor at the end of the field.
|
||||
await tester.tapAt(textOffsetToPosition(tester, 19));
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 19);
|
||||
expect(controller.value.selection.extentOffset, 19);
|
||||
|
||||
// Double tapping the last space selects all previous contiguous spaces on
|
||||
// both lines and the previous word.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 14));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 14));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 6);
|
||||
expect(controller.value.selection.extentOffset, 14);
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
|
||||
|
||||
testWidgets('selecting a space selects the space on non-iOS platforms', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: ' blah blah',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, -1);
|
||||
expect(controller.value.selection.extentOffset, -1);
|
||||
|
||||
// Put the cursor at the end of the field.
|
||||
await tester.tapAt(textOffsetToPosition(tester, 10));
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 10);
|
||||
expect(controller.value.selection.extentOffset, 10);
|
||||
|
||||
// Double tapping the second space selects it.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 5);
|
||||
expect(controller.value.selection.extentOffset, 6);
|
||||
|
||||
// Put the cursor at the end of the field.
|
||||
await tester.tapAt(textOffsetToPosition(tester, 10));
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 10);
|
||||
expect(controller.value.selection.extentOffset, 10);
|
||||
|
||||
// Double tapping the second space selects it.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.value.selection, isNotNull);
|
||||
expect(controller.value.selection.baseOffset, 0);
|
||||
expect(controller.value.selection.extentOffset, 1);
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia, TargetPlatform.android }));
|
||||
|
||||
testWidgets('force press does not select a word', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
|
Loading…
x
Reference in New Issue
Block a user