diff --git a/packages/flutter/lib/src/services/autofill.dart b/packages/flutter/lib/src/services/autofill.dart index 72ed11205c9e..89ec50a4e5df 100644 --- a/packages/flutter/lib/src/services/autofill.dart +++ b/packages/flutter/lib/src/services/autofill.dart @@ -801,18 +801,21 @@ class _AutofillScopeTextInputConfiguration extends TextInputConfiguration { _AutofillScopeTextInputConfiguration({ required this.allConfigurations, required TextInputConfiguration currentClientConfiguration, - }) : super(inputType: currentClientConfiguration.inputType, - obscureText: currentClientConfiguration.obscureText, - autocorrect: currentClientConfiguration.autocorrect, - smartDashesType: currentClientConfiguration.smartDashesType, - smartQuotesType: currentClientConfiguration.smartQuotesType, - enableSuggestions: currentClientConfiguration.enableSuggestions, - inputAction: currentClientConfiguration.inputAction, - textCapitalization: currentClientConfiguration.textCapitalization, - keyboardAppearance: currentClientConfiguration.keyboardAppearance, - actionLabel: currentClientConfiguration.actionLabel, - autofillConfiguration: currentClientConfiguration.autofillConfiguration, - ); + }) : super( + viewId: currentClientConfiguration.viewId, + inputType: currentClientConfiguration.inputType, + obscureText: currentClientConfiguration.obscureText, + autocorrect: currentClientConfiguration.autocorrect, + smartDashesType: currentClientConfiguration.smartDashesType, + smartQuotesType: currentClientConfiguration.smartQuotesType, + enableSuggestions: currentClientConfiguration.enableSuggestions, + inputAction: currentClientConfiguration.inputAction, + textCapitalization: currentClientConfiguration.textCapitalization, + keyboardAppearance: currentClientConfiguration.keyboardAppearance, + actionLabel: currentClientConfiguration.actionLabel, + autofillConfiguration: + currentClientConfiguration.autofillConfiguration, + ); final Iterable allConfigurations; diff --git a/packages/flutter/lib/src/services/text_input.dart b/packages/flutter/lib/src/services/text_input.dart index 1db2dad2e55f..d133d6857a3d 100644 --- a/packages/flutter/lib/src/services/text_input.dart +++ b/packages/flutter/lib/src/services/text_input.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:io' show Platform; import 'dart:ui' show + FlutterView, FontWeight, Offset, Rect, @@ -464,6 +465,7 @@ class TextInputConfiguration { /// All arguments have default values, except [actionLabel]. Only /// [actionLabel] may be null. const TextInputConfiguration({ + this.viewId, this.inputType = TextInputType.text, this.readOnly = false, this.obscureText = false, @@ -483,6 +485,14 @@ class TextInputConfiguration { }) : smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled); + /// The ID of the view that the text input belongs to. + /// + /// See also: + /// + /// * [FlutterView], which is the view that the ID points to. + /// * [View], which is a widget that wraps a [FlutterView]. + final int? viewId; + /// The type of information for which to optimize the text input control. final TextInputType inputType; @@ -626,6 +636,7 @@ class TextInputConfiguration { /// Creates a copy of this [TextInputConfiguration] with the given fields /// replaced with new values. TextInputConfiguration copyWith({ + int? viewId, TextInputType? inputType, bool? readOnly, bool? obscureText, @@ -644,6 +655,7 @@ class TextInputConfiguration { bool? enableDeltaModel, }) { return TextInputConfiguration( + viewId: viewId ?? this.viewId, inputType: inputType ?? this.inputType, readOnly: readOnly ?? this.readOnly, obscureText: obscureText ?? this.obscureText, @@ -691,6 +703,7 @@ class TextInputConfiguration { Map toJson() { final Map? autofill = autofillConfiguration.toJson(); return { + 'viewId': viewId, 'inputType': inputType.toJson(), 'readOnly': readOnly, 'obscureText': obscureText, diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index be3d8e447f5d..f139488a68bd 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -2982,6 +2982,14 @@ class EditableTextState extends State with AutomaticKeepAliveClien } } + // Check for changes in viewId. + if (_hasInputConnection) { + final int newViewId = View.of(context).viewId; + if (newViewId != _viewId) { + _textInputConnection!.updateConfig(_effectiveAutofillClient.textInputConfiguration); + } + } + if (defaultTargetPlatform != TargetPlatform.iOS && defaultTargetPlatform != TargetPlatform.android) { return; } @@ -4727,6 +4735,8 @@ class EditableTextState extends State with AutomaticKeepAliveClien @override String get autofillId => 'EditableText-$hashCode'; + int? _viewId; + @override TextInputConfiguration get textInputConfiguration { final List? autofillHints = widget.autofillHints?.toList(growable: false); @@ -4738,7 +4748,9 @@ class EditableTextState extends State with AutomaticKeepAliveClien ) : AutofillConfiguration.disabled; + _viewId = View.of(context).viewId; return TextInputConfiguration( + viewId: _viewId, inputType: widget.keyboardType, readOnly: widget.readOnly, obscureText: widget.obscureText, diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 6d3e6dcc1d6f..3c2bb0d21690 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -907,6 +907,64 @@ void main() { expect(state.textInputConfiguration.enableInteractiveSelection, isFalse); }); + testWidgets('EditableText sends viewId to config', (WidgetTester tester) async { + await tester.pumpWidget( + wrapWithView: false, + View( + view: FakeFlutterView(tester.view, viewId: 77), + child: MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.ltr, + child: FocusScope( + node: focusScopeNode, + autofocus: true, + child: EditableText( + controller: controller, + backgroundCursorColor: Colors.grey, + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + ), + ), + ), + ), + ), + ); + + EditableTextState state = tester.state(find.byType(EditableText)); + expect(state.textInputConfiguration.viewId, 77); + + await tester.pumpWidget( + wrapWithView: false, + View( + view: FakeFlutterView(tester.view, viewId: 88), + child: MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.ltr, + child: FocusScope( + node: focusScopeNode, + autofocus: true, + child: EditableText( + enableInteractiveSelection: false, + controller: controller, + backgroundCursorColor: Colors.grey, + focusNode: focusNode, + keyboardType: TextInputType.multiline, + style: textStyle, + cursorColor: cursorColor, + ), + ), + ), + ), + ), + ); + + state = tester.state(find.byType(EditableText)); + expect(state.textInputConfiguration.viewId, 88); + }); + testWidgets('selection persists when unfocused', (WidgetTester tester) async { const TextEditingValue value = TextEditingValue( text: 'test test', @@ -3289,6 +3347,53 @@ void main() { expect(tester.testTextInput.setClientArgs!['obscureText'], isFalse); }); + testWidgets('Sends viewId and updates config when it changes', (WidgetTester tester) async { + int viewId = 14; + late StateSetter setState; + final GlobalKey key = GlobalKey(); + + await tester.pumpWidget( + wrapWithView: false, + StatefulBuilder( + builder: (BuildContext context, StateSetter stateSetter) { + setState = stateSetter; + return View( + view: FakeFlutterView(tester.view, viewId: viewId), + child: MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.ltr, + child: EditableText( + key: key, + controller: controller, + backgroundCursorColor: Colors.grey, + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + ), + ), + ), + ); + }, + ), + ); + + // Focus the field to establish the input connection. + focusNode.requestFocus(); + await tester.pump(); + + expect(tester.testTextInput.setClientArgs!['viewId'], 14); + expect(tester.testTextInput.log, contains(matchesMethodCall('TextInput.setClient'))); + tester.testTextInput.log.clear(); + + setState(() { viewId = 15; }); + await tester.pump(); + + expect(tester.testTextInput.setClientArgs!['viewId'], 15); + expect(tester.testTextInput.log, contains(matchesMethodCall('TextInput.updateConfig'))); + tester.testTextInput.log.clear(); + }); + testWidgets('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async { late String changedValue; final Widget widget = MaterialApp( @@ -17510,3 +17615,15 @@ class _TestScrollController extends ScrollController { } class FakeSpellCheckService extends DefaultSpellCheckService {} + +class FakeFlutterView extends TestFlutterView { + FakeFlutterView(TestFlutterView view, {required this.viewId}) + : super( + view: view, + display: view.display, + platformDispatcher: view.platformDispatcher, + ); + + @override + final int viewId; +}