@@ -780,13 +780,28 @@ void main() {
780780 focusNode.requestFocus();
781781 await tester.pump();
782782
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+ }
784795 expect(focusNode.hasFocus, isTrue);
785796
786797 focusNode.unfocus();
787798 await tester.pump();
788799
789- expect(controller.value, value);
800+ if (kIsWeb) {
801+ expect(controller.value, webValue);
802+ } else {
803+ expect(controller.value, value);
804+ }
790805 expect(focusNode.hasFocus, isFalse);
791806 });
792807
@@ -4349,7 +4364,10 @@ void main() {
43494364 ],
43504365 value: expectedValue,
43514366 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),
43534371 ),
43544372 ],
43554373 ),
@@ -15062,7 +15080,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
1506215080 ),
1506315081 ),
1506415082 );
15065- }
15083+ },
1506615084 ),
1506715085 ),
1506815086 );
@@ -15088,6 +15106,217 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
1508815106
1508915107 EditableText.debugDeterministicCursor = false;
1509015108 });
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+ });
1509115320}
1509215321
1509315322class UnsettableController extends TextEditingController {
0 commit comments