Unverified Commit 42988d1b authored by Mouad Debbar's avatar Mouad Debbar Committed by GitHub

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: https://github.com/flutter/engine/pull/51099
Fixes https://github.com/flutter/flutter/issues/137344
parent ee5e6fac
......@@ -801,7 +801,9 @@ class _AutofillScopeTextInputConfiguration extends TextInputConfiguration {
_AutofillScopeTextInputConfiguration({
required this.allConfigurations,
required TextInputConfiguration currentClientConfiguration,
}) : super(inputType: currentClientConfiguration.inputType,
}) : super(
viewId: currentClientConfiguration.viewId,
inputType: currentClientConfiguration.inputType,
obscureText: currentClientConfiguration.obscureText,
autocorrect: currentClientConfiguration.autocorrect,
smartDashesType: currentClientConfiguration.smartDashesType,
......@@ -811,7 +813,8 @@ class _AutofillScopeTextInputConfiguration extends TextInputConfiguration {
textCapitalization: currentClientConfiguration.textCapitalization,
keyboardAppearance: currentClientConfiguration.keyboardAppearance,
actionLabel: currentClientConfiguration.actionLabel,
autofillConfiguration: currentClientConfiguration.autofillConfiguration,
autofillConfiguration:
currentClientConfiguration.autofillConfiguration,
);
final Iterable<TextInputConfiguration> allConfigurations;
......
......@@ -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<String, dynamic> toJson() {
final Map<String, dynamic>? autofill = autofillConfiguration.toJson();
return <String, dynamic>{
'viewId': viewId,
'inputType': inputType.toJson(),
'readOnly': readOnly,
'obscureText': obscureText,
......
......@@ -2982,6 +2982,14 @@ class EditableTextState extends State<EditableText> 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<EditableText> with AutomaticKeepAliveClien
@override
String get autofillId => 'EditableText-$hashCode';
int? _viewId;
@override
TextInputConfiguration get textInputConfiguration {
final List<String>? autofillHints = widget.autofillHints?.toList(growable: false);
......@@ -4738,7 +4748,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
)
: AutofillConfiguration.disabled;
_viewId = View.of(context).viewId;
return TextInputConfiguration(
viewId: _viewId,
inputType: widget.keyboardType,
readOnly: widget.readOnly,
obscureText: widget.obscureText,
......
......@@ -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<EditableTextState>(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<EditableTextState>(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;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment