@@ -780,13 +780,28 @@ void main() {
780
780
focusNode.requestFocus();
781
781
await tester.pump();
782
782
783
- expect(controller.value, value);
783
+ // On web, focusing a single-line input selects the entire field.
784
+ final TextEditingValue webValue = value.copyWith(
785
+ selection: TextSelection(
786
+ baseOffset: 0,
787
+ extentOffset: controller.value.text.length,
788
+ ),
789
+ );
790
+ if (kIsWeb) {
791
+ expect(controller.value, webValue);
792
+ } else {
793
+ expect(controller.value, value);
794
+ }
784
795
expect(focusNode.hasFocus, isTrue);
785
796
786
797
focusNode.unfocus();
787
798
await tester.pump();
788
799
789
- expect(controller.value, value);
800
+ if (kIsWeb) {
801
+ expect(controller.value, webValue);
802
+ } else {
803
+ expect(controller.value, value);
804
+ }
790
805
expect(focusNode.hasFocus, isFalse);
791
806
});
792
807
@@ -4349,7 +4364,10 @@ void main() {
4349
4364
],
4350
4365
value: expectedValue,
4351
4366
textDirection: TextDirection.ltr,
4352
- textSelection: const TextSelection.collapsed(offset: 24),
4367
+ // Focusing a single-line field on web selects it.
4368
+ textSelection: kIsWeb
4369
+ ? const TextSelection(baseOffset: 0, extentOffset: 24)
4370
+ : const TextSelection.collapsed(offset: 24),
4353
4371
),
4354
4372
],
4355
4373
),
@@ -15062,7 +15080,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
15062
15080
),
15063
15081
),
15064
15082
);
15065
- }
15083
+ },
15066
15084
),
15067
15085
),
15068
15086
);
@@ -15088,6 +15106,217 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
15088
15106
15089
15107
EditableText.debugDeterministicCursor = false;
15090
15108
});
15109
+
15110
+ group('selection behavior when receiving focus', () {
15111
+ testWidgets('tabbing between fields', (WidgetTester tester) async {
15112
+ final TextEditingController controller1 = TextEditingController();
15113
+ final TextEditingController controller2 = TextEditingController();
15114
+ controller1.text = 'Text1';
15115
+ controller2.text = 'Text2\nLine2';
15116
+ final FocusNode focusNode1 = FocusNode();
15117
+ final FocusNode focusNode2 = FocusNode();
15118
+
15119
+ await tester.pumpWidget(
15120
+ MaterialApp(
15121
+ home: Column(
15122
+ crossAxisAlignment: CrossAxisAlignment.start,
15123
+ children: <Widget>[
15124
+ EditableText(
15125
+ key: ValueKey<String>(controller1.text),
15126
+ controller: controller1,
15127
+ focusNode: focusNode1,
15128
+ style: Typography.material2018().black.titleMedium!,
15129
+ cursorColor: Colors.blue,
15130
+ backgroundCursorColor: Colors.grey,
15131
+ ),
15132
+ const SizedBox(height: 200.0),
15133
+ EditableText(
15134
+ key: ValueKey<String>(controller2.text),
15135
+ controller: controller2,
15136
+ focusNode: focusNode2,
15137
+ style: Typography.material2018().black.titleMedium!,
15138
+ cursorColor: Colors.blue,
15139
+ backgroundCursorColor: Colors.grey,
15140
+ minLines: 10,
15141
+ maxLines: 20,
15142
+ ),
15143
+ const SizedBox(height: 100.0),
15144
+ ],
15145
+ ),
15146
+ ),
15147
+ );
15148
+
15149
+ expect(focusNode1.hasFocus, isFalse);
15150
+ expect(focusNode2.hasFocus, isFalse);
15151
+ expect(
15152
+ controller1.selection,
15153
+ const TextSelection.collapsed(offset: -1),
15154
+ );
15155
+ expect(
15156
+ controller2.selection,
15157
+ const TextSelection.collapsed(offset: -1),
15158
+ );
15159
+
15160
+ // Tab to the first field (single line).
15161
+ await tester.sendKeyEvent(LogicalKeyboardKey.tab);
15162
+ await tester.pumpAndSettle();
15163
+ expect(focusNode1.hasFocus, isTrue);
15164
+ expect(focusNode2.hasFocus, isFalse);
15165
+ expect(
15166
+ controller1.selection,
15167
+ kIsWeb
15168
+ ? TextSelection(
15169
+ baseOffset: 0,
15170
+ extentOffset: controller1.text.length,
15171
+ )
15172
+ : TextSelection.collapsed(
15173
+ offset: controller1.text.length,
15174
+ ),
15175
+ );
15176
+
15177
+ // Move the cursor to another position in the first field.
15178
+ await tester.tapAt(textOffsetToPosition(tester, controller1.text.length - 1));
15179
+ await tester.pumpAndSettle();
15180
+ expect(
15181
+ controller1.selection,
15182
+ TextSelection.collapsed(
15183
+ offset: controller1.text.length - 1,
15184
+ ),
15185
+ );
15186
+
15187
+ // Tab to the second field (multiline).
15188
+ await tester.sendKeyEvent(LogicalKeyboardKey.tab);
15189
+ await tester.pumpAndSettle();
15190
+ expect(focusNode1.hasFocus, isFalse);
15191
+ expect(focusNode2.hasFocus, isTrue);
15192
+ expect(
15193
+ controller2.selection,
15194
+ TextSelection.collapsed(
15195
+ offset: controller2.text.length,
15196
+ ),
15197
+ );
15198
+
15199
+ // Move the cursor to another position in the second field.
15200
+ await tester.tapAt(textOffsetToPosition(tester, controller2.text.length - 1, index: 1));
15201
+ await tester.pumpAndSettle();
15202
+ expect(
15203
+ controller2.selection,
15204
+ TextSelection.collapsed(
15205
+ offset: controller2.text.length - 1,
15206
+ ),
15207
+ );
15208
+
15209
+ // On web, the document root is also focusable.
15210
+ if (kIsWeb) {
15211
+ await tester.sendKeyEvent(LogicalKeyboardKey.tab);
15212
+ await tester.pumpAndSettle();
15213
+ expect(focusNode1.hasFocus, isFalse);
15214
+ expect(focusNode2.hasFocus, isFalse);
15215
+ }
15216
+
15217
+ // Tabbing again goes back to the first field and reselects the field.
15218
+ await tester.sendKeyEvent(LogicalKeyboardKey.tab);
15219
+ await tester.pumpAndSettle();
15220
+ expect(focusNode1.hasFocus, isTrue);
15221
+ expect(focusNode2.hasFocus, isFalse);
15222
+ expect(
15223
+ controller1.selection,
15224
+ kIsWeb
15225
+ ? TextSelection(
15226
+ baseOffset: 0,
15227
+ extentOffset: controller1.text.length,
15228
+ )
15229
+ : TextSelection.collapsed(
15230
+ offset: controller1.text.length - 1,
15231
+ ),
15232
+ );
15233
+
15234
+ // Tabbing to the second field again retains the moved selection.
15235
+ await tester.sendKeyEvent(LogicalKeyboardKey.tab);
15236
+ await tester.pumpAndSettle();
15237
+ expect(focusNode1.hasFocus, isFalse);
15238
+ expect(focusNode2.hasFocus, isTrue);
15239
+ expect(
15240
+ controller2.selection,
15241
+ TextSelection.collapsed(
15242
+ offset: controller2.text.length - 1,
15243
+ ),
15244
+ );
15245
+ });
15246
+
15247
+ testWidgets('when having focus stolen between frames on web', (WidgetTester tester) async {
15248
+ final TextEditingController controller1 = TextEditingController();
15249
+ controller1.text = 'Text1';
15250
+ final FocusNode focusNode1 = FocusNode();
15251
+ final FocusNode focusNode2 = FocusNode();
15252
+
15253
+ await tester.pumpWidget(
15254
+ MaterialApp(
15255
+ home: Column(
15256
+ crossAxisAlignment: CrossAxisAlignment.start,
15257
+ children: <Widget>[
15258
+ EditableText(
15259
+ key: ValueKey<String>(controller1.text),
15260
+ controller: controller1,
15261
+ focusNode: focusNode1,
15262
+ style: Typography.material2018().black.titleMedium!,
15263
+ cursorColor: Colors.blue,
15264
+ backgroundCursorColor: Colors.grey,
15265
+ ),
15266
+ const SizedBox(height: 200.0),
15267
+ Focus(
15268
+ focusNode: focusNode2,
15269
+ child: const SizedBox.shrink(),
15270
+ ),
15271
+ const SizedBox(height: 100.0),
15272
+ ],
15273
+ ),
15274
+ ),
15275
+ );
15276
+
15277
+ expect(focusNode1.hasFocus, isFalse);
15278
+ expect(focusNode2.hasFocus, isFalse);
15279
+ expect(
15280
+ controller1.selection,
15281
+ const TextSelection.collapsed(offset: -1),
15282
+ );
15283
+
15284
+ final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText).first);
15285
+
15286
+ // Set the text editing value in order to trigger an internal call to
15287
+ // requestFocus.
15288
+ state.userUpdateTextEditingValue(
15289
+ controller1.value,
15290
+ SelectionChangedCause.keyboard,
15291
+ );
15292
+ // Focus takes a frame to update, so it hasn't changed yet.
15293
+ expect(focusNode1.hasFocus, isFalse);
15294
+ expect(focusNode2.hasFocus, isFalse);
15295
+
15296
+ // Before EditableText's listener on widget.focusNode can be called, change
15297
+ // the focus again
15298
+ focusNode2.requestFocus();
15299
+ await tester.pump();
15300
+ expect(focusNode1.hasFocus, isFalse);
15301
+ expect(focusNode2.hasFocus, isTrue);
15302
+
15303
+ // Focus the EditableText again, which should cause the field to be selected
15304
+ // on web.
15305
+ focusNode1.requestFocus();
15306
+ await tester.pumpAndSettle();
15307
+ expect(focusNode1.hasFocus, isTrue);
15308
+ expect(focusNode2.hasFocus, isFalse);
15309
+ expect(
15310
+ controller1.selection,
15311
+ TextSelection(
15312
+ baseOffset: 0,
15313
+ extentOffset: controller1.text.length,
15314
+ ),
15315
+ );
15316
+ },
15317
+ skip: !kIsWeb, // [intended]
15318
+ );
15319
+ });
15091
15320
}
15092
15321
15093
15322
class UnsettableController extends TextEditingController {
0 commit comments