Skip to content

Commit 42988d1

Browse files
authored
Add viewId to TextInputConfiguration (#145708)
In order for text fields to work correctly in multi-view on the web, we need to have the `viewId` information sent to the engine (`TextInput.setClient`). And while the text field is active, if it somehow moves to a new `View`, we need to inform the engine about such change (`TextInput.updateConfig`). Engine PR: flutter/engine#51099 Fixes flutter/flutter#137344
1 parent ee5e6fa commit 42988d1

File tree

4 files changed

+157
-12
lines changed

4 files changed

+157
-12
lines changed

packages/flutter/lib/src/services/autofill.dart

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -801,18 +801,21 @@ class _AutofillScopeTextInputConfiguration extends TextInputConfiguration {
801801
_AutofillScopeTextInputConfiguration({
802802
required this.allConfigurations,
803803
required TextInputConfiguration currentClientConfiguration,
804-
}) : super(inputType: currentClientConfiguration.inputType,
805-
obscureText: currentClientConfiguration.obscureText,
806-
autocorrect: currentClientConfiguration.autocorrect,
807-
smartDashesType: currentClientConfiguration.smartDashesType,
808-
smartQuotesType: currentClientConfiguration.smartQuotesType,
809-
enableSuggestions: currentClientConfiguration.enableSuggestions,
810-
inputAction: currentClientConfiguration.inputAction,
811-
textCapitalization: currentClientConfiguration.textCapitalization,
812-
keyboardAppearance: currentClientConfiguration.keyboardAppearance,
813-
actionLabel: currentClientConfiguration.actionLabel,
814-
autofillConfiguration: currentClientConfiguration.autofillConfiguration,
815-
);
804+
}) : super(
805+
viewId: currentClientConfiguration.viewId,
806+
inputType: currentClientConfiguration.inputType,
807+
obscureText: currentClientConfiguration.obscureText,
808+
autocorrect: currentClientConfiguration.autocorrect,
809+
smartDashesType: currentClientConfiguration.smartDashesType,
810+
smartQuotesType: currentClientConfiguration.smartQuotesType,
811+
enableSuggestions: currentClientConfiguration.enableSuggestions,
812+
inputAction: currentClientConfiguration.inputAction,
813+
textCapitalization: currentClientConfiguration.textCapitalization,
814+
keyboardAppearance: currentClientConfiguration.keyboardAppearance,
815+
actionLabel: currentClientConfiguration.actionLabel,
816+
autofillConfiguration:
817+
currentClientConfiguration.autofillConfiguration,
818+
);
816819

817820
final Iterable<TextInputConfiguration> allConfigurations;
818821

packages/flutter/lib/src/services/text_input.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'dart:async';
66
import 'dart:io' show Platform;
77
import 'dart:ui' show
8+
FlutterView,
89
FontWeight,
910
Offset,
1011
Rect,
@@ -464,6 +465,7 @@ class TextInputConfiguration {
464465
/// All arguments have default values, except [actionLabel]. Only
465466
/// [actionLabel] may be null.
466467
const TextInputConfiguration({
468+
this.viewId,
467469
this.inputType = TextInputType.text,
468470
this.readOnly = false,
469471
this.obscureText = false,
@@ -483,6 +485,14 @@ class TextInputConfiguration {
483485
}) : smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
484486
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled);
485487

488+
/// The ID of the view that the text input belongs to.
489+
///
490+
/// See also:
491+
///
492+
/// * [FlutterView], which is the view that the ID points to.
493+
/// * [View], which is a widget that wraps a [FlutterView].
494+
final int? viewId;
495+
486496
/// The type of information for which to optimize the text input control.
487497
final TextInputType inputType;
488498

@@ -626,6 +636,7 @@ class TextInputConfiguration {
626636
/// Creates a copy of this [TextInputConfiguration] with the given fields
627637
/// replaced with new values.
628638
TextInputConfiguration copyWith({
639+
int? viewId,
629640
TextInputType? inputType,
630641
bool? readOnly,
631642
bool? obscureText,
@@ -644,6 +655,7 @@ class TextInputConfiguration {
644655
bool? enableDeltaModel,
645656
}) {
646657
return TextInputConfiguration(
658+
viewId: viewId ?? this.viewId,
647659
inputType: inputType ?? this.inputType,
648660
readOnly: readOnly ?? this.readOnly,
649661
obscureText: obscureText ?? this.obscureText,
@@ -691,6 +703,7 @@ class TextInputConfiguration {
691703
Map<String, dynamic> toJson() {
692704
final Map<String, dynamic>? autofill = autofillConfiguration.toJson();
693705
return <String, dynamic>{
706+
'viewId': viewId,
694707
'inputType': inputType.toJson(),
695708
'readOnly': readOnly,
696709
'obscureText': obscureText,

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2982,6 +2982,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
29822982
}
29832983
}
29842984

2985+
// Check for changes in viewId.
2986+
if (_hasInputConnection) {
2987+
final int newViewId = View.of(context).viewId;
2988+
if (newViewId != _viewId) {
2989+
_textInputConnection!.updateConfig(_effectiveAutofillClient.textInputConfiguration);
2990+
}
2991+
}
2992+
29852993
if (defaultTargetPlatform != TargetPlatform.iOS && defaultTargetPlatform != TargetPlatform.android) {
29862994
return;
29872995
}
@@ -4727,6 +4735,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
47274735
@override
47284736
String get autofillId => 'EditableText-$hashCode';
47294737

4738+
int? _viewId;
4739+
47304740
@override
47314741
TextInputConfiguration get textInputConfiguration {
47324742
final List<String>? autofillHints = widget.autofillHints?.toList(growable: false);
@@ -4738,7 +4748,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
47384748
)
47394749
: AutofillConfiguration.disabled;
47404750

4751+
_viewId = View.of(context).viewId;
47414752
return TextInputConfiguration(
4753+
viewId: _viewId,
47424754
inputType: widget.keyboardType,
47434755
readOnly: widget.readOnly,
47444756
obscureText: widget.obscureText,

packages/flutter/test/widgets/editable_text_test.dart

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,64 @@ void main() {
907907
expect(state.textInputConfiguration.enableInteractiveSelection, isFalse);
908908
});
909909

910+
testWidgets('EditableText sends viewId to config', (WidgetTester tester) async {
911+
await tester.pumpWidget(
912+
wrapWithView: false,
913+
View(
914+
view: FakeFlutterView(tester.view, viewId: 77),
915+
child: MediaQuery(
916+
data: const MediaQueryData(),
917+
child: Directionality(
918+
textDirection: TextDirection.ltr,
919+
child: FocusScope(
920+
node: focusScopeNode,
921+
autofocus: true,
922+
child: EditableText(
923+
controller: controller,
924+
backgroundCursorColor: Colors.grey,
925+
focusNode: focusNode,
926+
style: textStyle,
927+
cursorColor: cursorColor,
928+
),
929+
),
930+
),
931+
),
932+
),
933+
);
934+
935+
EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
936+
expect(state.textInputConfiguration.viewId, 77);
937+
938+
await tester.pumpWidget(
939+
wrapWithView: false,
940+
View(
941+
view: FakeFlutterView(tester.view, viewId: 88),
942+
child: MediaQuery(
943+
data: const MediaQueryData(),
944+
child: Directionality(
945+
textDirection: TextDirection.ltr,
946+
child: FocusScope(
947+
node: focusScopeNode,
948+
autofocus: true,
949+
child: EditableText(
950+
enableInteractiveSelection: false,
951+
controller: controller,
952+
backgroundCursorColor: Colors.grey,
953+
focusNode: focusNode,
954+
keyboardType: TextInputType.multiline,
955+
style: textStyle,
956+
cursorColor: cursorColor,
957+
),
958+
),
959+
),
960+
),
961+
),
962+
);
963+
964+
state = tester.state<EditableTextState>(find.byType(EditableText));
965+
expect(state.textInputConfiguration.viewId, 88);
966+
});
967+
910968
testWidgets('selection persists when unfocused', (WidgetTester tester) async {
911969
const TextEditingValue value = TextEditingValue(
912970
text: 'test test',
@@ -3289,6 +3347,53 @@ void main() {
32893347
expect(tester.testTextInput.setClientArgs!['obscureText'], isFalse);
32903348
});
32913349

3350+
testWidgets('Sends viewId and updates config when it changes', (WidgetTester tester) async {
3351+
int viewId = 14;
3352+
late StateSetter setState;
3353+
final GlobalKey key = GlobalKey();
3354+
3355+
await tester.pumpWidget(
3356+
wrapWithView: false,
3357+
StatefulBuilder(
3358+
builder: (BuildContext context, StateSetter stateSetter) {
3359+
setState = stateSetter;
3360+
return View(
3361+
view: FakeFlutterView(tester.view, viewId: viewId),
3362+
child: MediaQuery(
3363+
data: const MediaQueryData(),
3364+
child: Directionality(
3365+
textDirection: TextDirection.ltr,
3366+
child: EditableText(
3367+
key: key,
3368+
controller: controller,
3369+
backgroundCursorColor: Colors.grey,
3370+
focusNode: focusNode,
3371+
style: textStyle,
3372+
cursorColor: cursorColor,
3373+
),
3374+
),
3375+
),
3376+
);
3377+
},
3378+
),
3379+
);
3380+
3381+
// Focus the field to establish the input connection.
3382+
focusNode.requestFocus();
3383+
await tester.pump();
3384+
3385+
expect(tester.testTextInput.setClientArgs!['viewId'], 14);
3386+
expect(tester.testTextInput.log, contains(matchesMethodCall('TextInput.setClient')));
3387+
tester.testTextInput.log.clear();
3388+
3389+
setState(() { viewId = 15; });
3390+
await tester.pump();
3391+
3392+
expect(tester.testTextInput.setClientArgs!['viewId'], 15);
3393+
expect(tester.testTextInput.log, contains(matchesMethodCall('TextInput.updateConfig')));
3394+
tester.testTextInput.log.clear();
3395+
});
3396+
32923397
testWidgets('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async {
32933398
late String changedValue;
32943399
final Widget widget = MaterialApp(
@@ -17510,3 +17615,15 @@ class _TestScrollController extends ScrollController {
1751017615
}
1751117616

1751217617
class FakeSpellCheckService extends DefaultSpellCheckService {}
17618+
17619+
class FakeFlutterView extends TestFlutterView {
17620+
FakeFlutterView(TestFlutterView view, {required this.viewId})
17621+
: super(
17622+
view: view,
17623+
display: view.display,
17624+
platformDispatcher: view.platformDispatcher,
17625+
);
17626+
17627+
@override
17628+
final int viewId;
17629+
}

0 commit comments

Comments
 (0)