mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Use viewId for text editing (flutter/engine#51099)
Handle `viewId` for text fields. Part of https://github.com/flutter/flutter/issues/137344
This commit is contained in:
parent
2b1bbfe870
commit
01ea911472
@ -95,11 +95,8 @@ final class ViewFocusBinding {
|
||||
}
|
||||
|
||||
int? _viewId(DomElement? element) {
|
||||
final DomElement? rootElement = element?.closest(DomManager.flutterViewTagName);
|
||||
if (rootElement == null) {
|
||||
return null;
|
||||
}
|
||||
return _viewManager.viewIdForRootElement(rootElement);
|
||||
final FlutterViewManager viewManager = EnginePlatformDispatcher.instance.viewManager;
|
||||
return viewManager.findViewForElement(element)?.viewId;
|
||||
}
|
||||
|
||||
void _handleViewCreated(int viewId) {
|
||||
|
||||
@ -18,6 +18,8 @@ import '../semantics.dart';
|
||||
import '../services.dart';
|
||||
import '../text/paragraph.dart';
|
||||
import '../util.dart';
|
||||
import '../view_embedder/flutter_view_manager.dart';
|
||||
import '../window.dart';
|
||||
import 'autofill_hint.dart';
|
||||
import 'composition_aware_mixin.dart';
|
||||
import 'input_action.dart';
|
||||
@ -48,12 +50,6 @@ const String transparentTextEditingClass = 'transparentTextEditing';
|
||||
|
||||
void _emptyCallback(dynamic _) {}
|
||||
|
||||
/// The default [HostNode] that hosts all DOM required for text editing when a11y is not enabled.
|
||||
@visibleForTesting
|
||||
// TODO(mdebbar): There could be multiple views with multiple text editing hosts.
|
||||
// https://github.com/flutter/flutter/issues/137344
|
||||
DomElement get defaultTextEditingRoot => EnginePlatformDispatcher.instance.implicitView!.dom.textEditingHost;
|
||||
|
||||
/// These style attributes are constant throughout the life time of an input
|
||||
/// element.
|
||||
///
|
||||
@ -147,6 +143,37 @@ void _styleAutofillElements(
|
||||
elementStyle.setProperty('caret-color', 'transparent');
|
||||
}
|
||||
|
||||
void _ensureEditingElementInView(DomElement element, int viewId) {
|
||||
final bool isAlreadyAppended = element.isConnected ?? false;
|
||||
if (!isAlreadyAppended) {
|
||||
// If the element is not already appended to a view, we don't need to move
|
||||
// it anywhere.
|
||||
return;
|
||||
}
|
||||
|
||||
final FlutterViewManager viewManager = EnginePlatformDispatcher.instance.viewManager;
|
||||
final EngineFlutterView? currentView = viewManager.findViewForElement(element);
|
||||
if (currentView == null) {
|
||||
// For some reason, the input element was in the DOM, but it wasn't part of
|
||||
// any Flutter view. Should we throw?
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentView.viewId != viewId) {
|
||||
_insertEditingElementInView(element, viewId);
|
||||
}
|
||||
}
|
||||
|
||||
void _insertEditingElementInView(DomElement element, int viewId) {
|
||||
final FlutterViewManager viewManager = EnginePlatformDispatcher.instance.viewManager;
|
||||
final EngineFlutterView? view = viewManager[viewId];
|
||||
assert(
|
||||
view != null,
|
||||
'Could not find View with id $viewId. This should never happen, please file a bug!',
|
||||
);
|
||||
view!.dom.textEditingHost.append(element);
|
||||
}
|
||||
|
||||
/// Form that contains all the fields in the same AutofillGroup.
|
||||
///
|
||||
/// An [EngineAutofillForm] will only be constructed when autofill is enabled
|
||||
@ -154,6 +181,7 @@ void _styleAutofillElements(
|
||||
/// static method.
|
||||
class EngineAutofillForm {
|
||||
EngineAutofillForm({
|
||||
required this.viewId,
|
||||
required this.formElement,
|
||||
this.elements,
|
||||
this.items,
|
||||
@ -177,6 +205,9 @@ class EngineAutofillForm {
|
||||
/// See [formsOnTheDom].
|
||||
final String formIdentifier;
|
||||
|
||||
/// The ID of the view that this form is rendered into.
|
||||
final int viewId;
|
||||
|
||||
/// Creates an [EngineAutofillFrom] from the JSON representation of a Flutter
|
||||
/// framework `TextInputConfiguration` object.
|
||||
///
|
||||
@ -189,6 +220,7 @@ class EngineAutofillForm {
|
||||
///
|
||||
/// Returns null if autofill is disabled for the input field.
|
||||
static EngineAutofillForm? fromFrameworkMessage(
|
||||
int viewId,
|
||||
Map<String, dynamic>? focusedElementAutofill,
|
||||
List<dynamic>? fields,
|
||||
) {
|
||||
@ -312,6 +344,7 @@ class EngineAutofillForm {
|
||||
insertionReferenceNode ??= submitButton;
|
||||
|
||||
return EngineAutofillForm(
|
||||
viewId: viewId,
|
||||
formElement: formElement,
|
||||
elements: elements,
|
||||
items: items,
|
||||
@ -330,7 +363,7 @@ class EngineAutofillForm {
|
||||
}
|
||||
|
||||
formElement.insertBefore(mainTextEditingElement, insertionReferenceNode);
|
||||
defaultTextEditingRoot.append(formElement);
|
||||
_insertEditingElementInView(formElement, viewId);
|
||||
}
|
||||
|
||||
void storeForm() {
|
||||
@ -944,6 +977,7 @@ class EditingState {
|
||||
/// This corresponds to Flutter's [TextInputConfiguration].
|
||||
class InputConfiguration {
|
||||
InputConfiguration({
|
||||
required this.viewId,
|
||||
this.inputType = EngineInputType.text,
|
||||
this.inputAction = 'TextInputAction.done',
|
||||
this.obscureText = false,
|
||||
@ -958,7 +992,8 @@ class InputConfiguration {
|
||||
|
||||
InputConfiguration.fromFrameworkMessage(
|
||||
Map<String, dynamic> flutterInputConfiguration)
|
||||
: inputType = EngineInputType.fromName(
|
||||
: viewId = flutterInputConfiguration.tryInt('viewId') ?? kImplicitViewId,
|
||||
inputType = EngineInputType.fromName(
|
||||
flutterInputConfiguration.readJson('inputType').readString('name'),
|
||||
isDecimal: flutterInputConfiguration.readJson('inputType').tryBool('decimal') ?? false,
|
||||
isMultiline: flutterInputConfiguration.readJson('inputType').tryBool('isMultiline') ?? false,
|
||||
@ -976,11 +1011,15 @@ class InputConfiguration {
|
||||
flutterInputConfiguration.readJson('autofill'))
|
||||
: null,
|
||||
autofillGroup = EngineAutofillForm.fromFrameworkMessage(
|
||||
flutterInputConfiguration.tryInt('viewId') ?? kImplicitViewId,
|
||||
flutterInputConfiguration.tryJson('autofill'),
|
||||
flutterInputConfiguration.tryList('fields'),
|
||||
),
|
||||
enableDeltaModel = flutterInputConfiguration.tryBool('enableDeltaModel') ?? false;
|
||||
|
||||
/// The ID of the view that contains the text field.
|
||||
final int viewId;
|
||||
|
||||
/// The type of information being edited in the input control.
|
||||
final EngineInputType inputType;
|
||||
|
||||
@ -1257,7 +1296,7 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
|
||||
// DOM later, when the first location information arrived.
|
||||
// Otherwise, on Blink based Desktop browsers, the autofill menu appears
|
||||
// on top left of the screen.
|
||||
defaultTextEditingRoot.append(activeDomElement);
|
||||
_insertEditingElementInView(activeDomElement, inputConfig.viewId);
|
||||
_appendedToForm = false;
|
||||
}
|
||||
|
||||
@ -1293,6 +1332,9 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
|
||||
autofill.applyToDomElement(activeDomElement, focusedElement: true);
|
||||
} else {
|
||||
activeDomElement.setAttribute('autocomplete', 'off');
|
||||
// When the new input configuration contains a different view ID, we need
|
||||
// to move the input element to the new view.
|
||||
_ensureEditingElementInView(activeDomElement, inputConfiguration.viewId);
|
||||
}
|
||||
|
||||
final String autocorrectValue = config.autocorrect ? 'on' : 'off';
|
||||
@ -1757,7 +1799,7 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
if (hasAutofillGroup) {
|
||||
placeForm();
|
||||
} else {
|
||||
defaultTextEditingRoot.append(activeDomElement);
|
||||
_insertEditingElementInView(activeDomElement, inputConfig.viewId);
|
||||
}
|
||||
inputConfig.textCapitalization.setAutocapitalizeAttribute(
|
||||
activeDomElement);
|
||||
|
||||
@ -12,12 +12,15 @@ class FlutterViewManager {
|
||||
|
||||
// A map of EngineFlutterViews indexed by their viewId.
|
||||
final Map<int, EngineFlutterView> _viewData = <int, EngineFlutterView>{};
|
||||
|
||||
// A map of (optional) JsFlutterViewOptions, indexed by their viewId.
|
||||
final Map<int, JsFlutterViewOptions> _jsViewOptions =
|
||||
<int, JsFlutterViewOptions>{};
|
||||
|
||||
// The controller of the [onViewCreated] stream.
|
||||
final StreamController<int> _onViewCreatedController =
|
||||
StreamController<int>.broadcast(sync: true);
|
||||
|
||||
// The controller of the [onViewDisposed] stream.
|
||||
final StreamController<int> _onViewDisposedController =
|
||||
StreamController<int>.broadcast(sync: true);
|
||||
@ -82,7 +85,7 @@ class FlutterViewManager {
|
||||
///
|
||||
/// Returns its [JsFlutterViewOptions] (if any).
|
||||
JsFlutterViewOptions? unregisterView(int viewId) {
|
||||
_viewData.remove(viewId); // .dispose();
|
||||
_viewData.remove(viewId);
|
||||
final JsFlutterViewOptions? jsViewOptions = _jsViewOptions.remove(viewId);
|
||||
_onViewDisposedController.add(viewId);
|
||||
return jsViewOptions;
|
||||
@ -96,14 +99,13 @@ class FlutterViewManager {
|
||||
return _jsViewOptions[viewId];
|
||||
}
|
||||
|
||||
/// Returns the [viewId] if [rootElement] corresponds to any of the [views].
|
||||
int? viewIdForRootElement(DomElement rootElement) {
|
||||
for(final EngineFlutterView view in views) {
|
||||
if (view.dom.rootElement == rootElement) {
|
||||
return view.viewId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
EngineFlutterView? findViewForElement(DomElement? element) {
|
||||
const String viewRootSelector =
|
||||
'${DomManager.flutterViewTagName}[${GlobalHtmlAttributes.flutterViewIdAttributeName}]';
|
||||
final DomElement? viewRoot = element?.closest(viewRootSelector);
|
||||
final String? viewIdAttribute = viewRoot?.getAttribute(GlobalHtmlAttributes.flutterViewIdAttributeName);
|
||||
final int? viewId = viewIdAttribute == null ? null : int.parse(viewIdAttribute);
|
||||
return viewId == null ? null : _viewData[viewId];
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
|
||||
@ -6,14 +6,13 @@ import 'dart:async';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine/browser_detection.dart';
|
||||
|
||||
import 'package:ui/src/engine/dom.dart';
|
||||
import 'package:ui/src/engine/text_editing/composition_aware_mixin.dart';
|
||||
import 'package:ui/src/engine/text_editing/text_editing.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
|
||||
import '../common/test_initialization.dart';
|
||||
|
||||
DomElement get defaultTextEditingRoot =>
|
||||
EnginePlatformDispatcher.instance.implicitView!.dom.textEditingHost;
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
@ -36,7 +35,10 @@ GloballyPositionedTextEditingStrategy _enableEditingStrategy({
|
||||
}) {
|
||||
final HybridTextEditing owner = HybridTextEditing();
|
||||
|
||||
owner.configuration = InputConfiguration(enableDeltaModel: deltaModel);
|
||||
owner.configuration = InputConfiguration(
|
||||
viewId: kImplicitViewId,
|
||||
enableDeltaModel: deltaModel,
|
||||
);
|
||||
|
||||
final GloballyPositionedTextEditingStrategy editingStrategy =
|
||||
GloballyPositionedTextEditingStrategy(owner);
|
||||
|
||||
@ -15,9 +15,10 @@ import 'package:ui/ui.dart' as ui;
|
||||
import '../../common/test_initialization.dart';
|
||||
import 'semantics_tester.dart';
|
||||
|
||||
final InputConfiguration singlelineConfig = InputConfiguration();
|
||||
final InputConfiguration singlelineConfig = InputConfiguration(viewId: kImplicitViewId);
|
||||
|
||||
final InputConfiguration multilineConfig = InputConfiguration(
|
||||
viewId: kImplicitViewId,
|
||||
inputType: EngineInputType.multiline,
|
||||
inputAction: 'TextInputAction.newline',
|
||||
);
|
||||
|
||||
@ -8,16 +8,7 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:ui/src/engine/browser_detection.dart';
|
||||
import 'package:ui/src/engine/dom.dart';
|
||||
import 'package:ui/src/engine/raw_keyboard.dart';
|
||||
import 'package:ui/src/engine/services.dart';
|
||||
import 'package:ui/src/engine/text_editing/autofill_hint.dart';
|
||||
import 'package:ui/src/engine/text_editing/input_type.dart';
|
||||
import 'package:ui/src/engine/text_editing/text_editing.dart';
|
||||
import 'package:ui/src/engine/util.dart';
|
||||
import 'package:ui/src/engine/vector_math.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../common/spy.dart';
|
||||
@ -28,6 +19,11 @@ const int _kReturnKeyCode = 13;
|
||||
|
||||
const MethodCodec codec = JSONMethodCodec();
|
||||
|
||||
EnginePlatformDispatcher get dispatcher => EnginePlatformDispatcher.instance;
|
||||
|
||||
DomElement get defaultTextEditingRoot =>
|
||||
dispatcher.implicitView!.dom.textEditingHost;
|
||||
|
||||
/// Add unit tests for [FirefoxTextEditingStrategy].
|
||||
// TODO(mdebbar): https://github.com/flutter/flutter/issues/46891
|
||||
|
||||
@ -36,11 +32,14 @@ EditingState? lastEditingState;
|
||||
TextEditingDeltaState? editingDeltaState;
|
||||
String? lastInputAction;
|
||||
|
||||
final InputConfiguration singlelineConfig = InputConfiguration();
|
||||
final InputConfiguration singlelineConfig = InputConfiguration(
|
||||
viewId: kImplicitViewId,
|
||||
);
|
||||
final Map<String, dynamic> flutterSinglelineConfig =
|
||||
createFlutterConfig('text');
|
||||
|
||||
final InputConfiguration multilineConfig = InputConfiguration(
|
||||
viewId: kImplicitViewId,
|
||||
inputType: EngineInputType.multiline,
|
||||
inputAction: 'TextInputAction.newline',
|
||||
);
|
||||
@ -129,8 +128,38 @@ Future<void> testMain() async {
|
||||
domDocument.body);
|
||||
});
|
||||
|
||||
test('inserts element in the correct view', () {
|
||||
final DomElement host = createDomElement('div');
|
||||
domDocument.body!.append(host);
|
||||
final EngineFlutterView view = EngineFlutterView(dispatcher, host);
|
||||
dispatcher.viewManager.registerView(view);
|
||||
final DomElement textEditingHost = view.dom.textEditingHost;
|
||||
|
||||
expect(domDocument.getElementsByTagName('input'), hasLength(0));
|
||||
expect(textEditingHost.getElementsByTagName('input'), hasLength(0));
|
||||
|
||||
final InputConfiguration config = InputConfiguration(viewId: view.viewId);
|
||||
editingStrategy!.enable(
|
||||
config,
|
||||
onChange: trackEditingState,
|
||||
onAction: trackInputAction,
|
||||
);
|
||||
final DomElement input = editingStrategy!.domElement!;
|
||||
|
||||
// Input is appended to the right view.
|
||||
expect(textEditingHost.contains(input), isTrue);
|
||||
|
||||
// Cleanup.
|
||||
editingStrategy!.disable();
|
||||
expect(textEditingHost.querySelectorAll('input'), hasLength(0));
|
||||
dispatcher.viewManager.unregisterView(view.viewId);
|
||||
view.dispose();
|
||||
host.remove();
|
||||
});
|
||||
|
||||
test('Respects read-only config', () {
|
||||
final InputConfiguration config = InputConfiguration(
|
||||
viewId: kImplicitViewId,
|
||||
readOnly: true,
|
||||
);
|
||||
editingStrategy!.enable(
|
||||
@ -148,6 +177,7 @@ Future<void> testMain() async {
|
||||
|
||||
test('Knows how to create password fields', () {
|
||||
final InputConfiguration config = InputConfiguration(
|
||||
viewId: kImplicitViewId,
|
||||
obscureText: true,
|
||||
);
|
||||
editingStrategy!.enable(
|
||||
@ -165,6 +195,7 @@ Future<void> testMain() async {
|
||||
|
||||
test('Knows how to create non-default text actions', () {
|
||||
final InputConfiguration config = InputConfiguration(
|
||||
viewId: kImplicitViewId,
|
||||
inputAction: 'TextInputAction.send'
|
||||
);
|
||||
editingStrategy!.enable(
|
||||
@ -186,6 +217,7 @@ Future<void> testMain() async {
|
||||
|
||||
test('Knows to turn autocorrect off', () {
|
||||
final InputConfiguration config = InputConfiguration(
|
||||
viewId: kImplicitViewId,
|
||||
autocorrect: false,
|
||||
);
|
||||
editingStrategy!.enable(
|
||||
@ -202,7 +234,7 @@ Future<void> testMain() async {
|
||||
});
|
||||
|
||||
test('Knows to turn autocorrect on', () {
|
||||
final InputConfiguration config = InputConfiguration();
|
||||
final InputConfiguration config = InputConfiguration(viewId: kImplicitViewId);
|
||||
editingStrategy!.enable(
|
||||
config,
|
||||
onChange: trackEditingState,
|
||||
@ -217,7 +249,7 @@ Future<void> testMain() async {
|
||||
});
|
||||
|
||||
test('Knows to turn autofill off', () {
|
||||
final InputConfiguration config = InputConfiguration();
|
||||
final InputConfiguration config = InputConfiguration(viewId: kImplicitViewId);
|
||||
editingStrategy!.enable(
|
||||
config,
|
||||
onChange: trackEditingState,
|
||||
@ -352,7 +384,7 @@ Future<void> testMain() async {
|
||||
});
|
||||
|
||||
test('Triggers input action', () {
|
||||
final InputConfiguration config = InputConfiguration();
|
||||
final InputConfiguration config = InputConfiguration(viewId: kImplicitViewId);
|
||||
editingStrategy!.enable(
|
||||
config,
|
||||
onChange: trackEditingState,
|
||||
@ -371,10 +403,10 @@ Future<void> testMain() async {
|
||||
});
|
||||
|
||||
test('handling keyboard event prevents triggering input action', () {
|
||||
final ui.PlatformMessageCallback? savedCallback = ui.PlatformDispatcher.instance.onPlatformMessage;
|
||||
final ui.PlatformMessageCallback? savedCallback = dispatcher.onPlatformMessage;
|
||||
|
||||
bool markTextEventHandled = false;
|
||||
ui.PlatformDispatcher.instance.onPlatformMessage = (String channel, ByteData? data,
|
||||
dispatcher.onPlatformMessage = (String channel, ByteData? data,
|
||||
ui.PlatformMessageResponseCallback? callback) {
|
||||
final ByteData response = const JSONMessageCodec()
|
||||
.encodeMessage(<String, dynamic>{'handled': markTextEventHandled})!;
|
||||
@ -382,7 +414,7 @@ Future<void> testMain() async {
|
||||
};
|
||||
RawKeyboard.initialize();
|
||||
|
||||
final InputConfiguration config = InputConfiguration();
|
||||
final InputConfiguration config = InputConfiguration(viewId: kImplicitViewId);
|
||||
editingStrategy!.enable(
|
||||
config,
|
||||
onChange: trackEditingState,
|
||||
@ -412,12 +444,13 @@ Future<void> testMain() async {
|
||||
// Input action received.
|
||||
expect(lastInputAction, 'TextInputAction.done');
|
||||
|
||||
ui.PlatformDispatcher.instance.onPlatformMessage = savedCallback;
|
||||
dispatcher.onPlatformMessage = savedCallback;
|
||||
RawKeyboard.instance?.dispose();
|
||||
});
|
||||
|
||||
test('Triggers input action in multi-line mode', () {
|
||||
final InputConfiguration config = InputConfiguration(
|
||||
viewId: kImplicitViewId,
|
||||
inputType: EngineInputType.multiline,
|
||||
);
|
||||
editingStrategy!.enable(
|
||||
@ -443,6 +476,7 @@ Future<void> testMain() async {
|
||||
|
||||
test('Triggers input action in multiline-none mode', () {
|
||||
final InputConfiguration config = InputConfiguration(
|
||||
viewId: kImplicitViewId,
|
||||
inputType: EngineInputType.multilineNone,
|
||||
);
|
||||
editingStrategy!.enable(
|
||||
@ -468,7 +502,7 @@ Future<void> testMain() async {
|
||||
|
||||
test('Triggers input action and prevent new line key event for single line field', () {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/113559
|
||||
final InputConfiguration config = InputConfiguration();
|
||||
final InputConfiguration config = InputConfiguration(viewId: kImplicitViewId);
|
||||
editingStrategy!.enable(
|
||||
config,
|
||||
onChange: trackEditingState,
|
||||
@ -590,16 +624,24 @@ Future<void> testMain() async {
|
||||
/// Returns the `clientId` used in the platform message.
|
||||
int showKeyboard({
|
||||
required String inputType,
|
||||
int? viewId,
|
||||
String? inputAction,
|
||||
bool decimal = false,
|
||||
bool isMultiline = false,
|
||||
bool autofillEnabled = true,
|
||||
}) {
|
||||
final MethodCall setClient = MethodCall(
|
||||
'TextInput.setClient',
|
||||
<dynamic>[
|
||||
++clientId,
|
||||
createFlutterConfig(inputType,
|
||||
inputAction: inputAction, decimal: decimal, isMultiline: isMultiline),
|
||||
createFlutterConfig(
|
||||
inputType,
|
||||
viewId: viewId,
|
||||
inputAction: inputAction,
|
||||
decimal: decimal,
|
||||
isMultiline: isMultiline,
|
||||
autofillEnabled: autofillEnabled,
|
||||
),
|
||||
],
|
||||
);
|
||||
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
||||
@ -2481,6 +2523,186 @@ Future<void> testMain() async {
|
||||
expect(event.defaultPrevented, isFalse);
|
||||
});
|
||||
|
||||
test('inserts element in the correct view', () async {
|
||||
final DomElement host = createDomElement('div');
|
||||
domDocument.body!.append(host);
|
||||
final EngineFlutterView view = EngineFlutterView(dispatcher, host);
|
||||
dispatcher.viewManager.registerView(view);
|
||||
|
||||
textEditing = HybridTextEditing();
|
||||
showKeyboard(inputType: 'text', viewId: view.viewId);
|
||||
// The Safari strategy doesn't insert the input element into the DOM until
|
||||
// it has received the geometry information.
|
||||
final List<double> transform = Matrix4.identity().storage.toList();
|
||||
final MethodCall setSizeAndTransform = configureSetSizeAndTransformMethodCall(10, 10, transform);
|
||||
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
|
||||
|
||||
await waitForDesktopSafariFocus();
|
||||
|
||||
final DomElement input = textEditing!.strategy.domElement!;
|
||||
|
||||
|
||||
// Input is appended to the right view.
|
||||
expect(view.dom.textEditingHost.contains(input), isTrue);
|
||||
|
||||
// Cleanup.
|
||||
hideKeyboard();
|
||||
dispatcher.viewManager.unregisterView(view.viewId);
|
||||
view.dispose();
|
||||
host.remove();
|
||||
});
|
||||
|
||||
test('moves element to correct view', () {
|
||||
final DomElement host1 = createDomElement('div');
|
||||
domDocument.body!.append(host1);
|
||||
final EngineFlutterView view1 = EngineFlutterView(dispatcher, host1);
|
||||
dispatcher.viewManager.registerView(view1);
|
||||
|
||||
final DomElement host2 = createDomElement('div');
|
||||
domDocument.body!.append(host2);
|
||||
final EngineFlutterView view2 = EngineFlutterView(dispatcher, host2);
|
||||
dispatcher.viewManager.registerView(view2);
|
||||
|
||||
textEditing = HybridTextEditing();
|
||||
showKeyboard(inputType: 'text', viewId: view1.viewId, autofillEnabled: false);
|
||||
|
||||
final DomElement input = textEditing!.strategy.domElement!;
|
||||
|
||||
// Input is appended to view1.
|
||||
expect(view1.dom.textEditingHost.contains(input), isTrue);
|
||||
|
||||
sendFrameworkMessage(codec.encodeMethodCall(MethodCall(
|
||||
'TextInput.updateConfig',
|
||||
createFlutterConfig('text', viewId: view2.viewId, autofillEnabled: false),
|
||||
)));
|
||||
|
||||
// The input element is the same (no new element was created), but it has
|
||||
// moved to view2.
|
||||
expect(textEditing!.strategy.domElement, input);
|
||||
expect(view2.dom.textEditingHost.contains(input), isTrue);
|
||||
|
||||
// Cleanup.
|
||||
hideKeyboard();
|
||||
dispatcher.viewManager.unregisterView(view1.viewId);
|
||||
view1.dispose();
|
||||
dispatcher.viewManager.unregisterView(view2.viewId);
|
||||
view2.dispose();
|
||||
host1.remove();
|
||||
host2.remove();
|
||||
});
|
||||
|
||||
test('places autofill form in the correct view', () async {
|
||||
final DomElement host = createDomElement('div');
|
||||
domDocument.body!.append(host);
|
||||
final EngineFlutterView view = EngineFlutterView(dispatcher, host);
|
||||
dispatcher.viewManager.registerView(view);
|
||||
|
||||
textEditing = HybridTextEditing();
|
||||
|
||||
// Create a configuration with an AutofillGroup of three text fields.
|
||||
final Map<String, dynamic> flutterMultiAutofillElementConfig =
|
||||
createFlutterConfig(
|
||||
'text',
|
||||
viewId: view.viewId,
|
||||
autofillHint: 'username',
|
||||
autofillHintsForFields: <String>['username', 'email', 'name'],
|
||||
);
|
||||
final MethodCall setClient = MethodCall(
|
||||
'TextInput.setClient',
|
||||
<dynamic>[123, flutterMultiAutofillElementConfig],
|
||||
);
|
||||
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
||||
|
||||
const MethodCall show = MethodCall('TextInput.show');
|
||||
sendFrameworkMessage(codec.encodeMethodCall(show));
|
||||
// The Safari strategy doesn't insert the input element into the DOM until
|
||||
// it has received the geometry information.
|
||||
final List<double> transform = Matrix4.identity().storage.toList();
|
||||
final MethodCall setSizeAndTransform = configureSetSizeAndTransformMethodCall(10, 10, transform);
|
||||
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
|
||||
|
||||
await waitForDesktopSafariFocus();
|
||||
|
||||
final DomElement input = textEditing!.strategy.domElement!;
|
||||
final DomElement form = textEditing!.configuration!.autofillGroup!.formElement;
|
||||
|
||||
// Input and form are appended to the right view.
|
||||
expect(view.dom.textEditingHost.contains(input), isTrue);
|
||||
expect(view.dom.textEditingHost.contains(form), isTrue);
|
||||
|
||||
// Cleanup.
|
||||
hideKeyboard();
|
||||
dispatcher.viewManager.unregisterView(view.viewId);
|
||||
view.dispose();
|
||||
host.remove();
|
||||
});
|
||||
|
||||
test('moves autofill form to the correct view', () async {
|
||||
final DomElement host1 = createDomElement('div');
|
||||
domDocument.body!.append(host1);
|
||||
final EngineFlutterView view1 = EngineFlutterView(dispatcher, host1);
|
||||
dispatcher.viewManager.registerView(view1);
|
||||
|
||||
final DomElement host2 = createDomElement('div');
|
||||
domDocument.body!.append(host2);
|
||||
final EngineFlutterView view2 = EngineFlutterView(dispatcher, host2);
|
||||
dispatcher.viewManager.registerView(view2);
|
||||
|
||||
textEditing = HybridTextEditing();
|
||||
|
||||
// Create a configuration with an AutofillGroup of three text fields.
|
||||
final Map<String, dynamic> autofillConfig1 = createFlutterConfig(
|
||||
'text',
|
||||
viewId: view1.viewId,
|
||||
autofillHint: 'username',
|
||||
autofillHintsForFields: <String>['username', 'email', 'name'],
|
||||
);
|
||||
final MethodCall setClient = MethodCall(
|
||||
'TextInput.setClient',
|
||||
<dynamic>[123, autofillConfig1],
|
||||
);
|
||||
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
||||
|
||||
const MethodCall show = MethodCall('TextInput.show');
|
||||
sendFrameworkMessage(codec.encodeMethodCall(show));
|
||||
|
||||
await waitForDesktopSafariFocus();
|
||||
|
||||
final DomElement input = textEditing!.strategy.domElement!;
|
||||
final DomElement form = textEditing!.configuration!.autofillGroup!.formElement;
|
||||
|
||||
// Input and form are appended to view1.
|
||||
expect(view1.dom.textEditingHost.contains(input), isTrue);
|
||||
expect(view1.dom.textEditingHost.contains(form), isTrue);
|
||||
|
||||
// Move the input and form to view2.
|
||||
final Map<String, dynamic> autofillConfig2 = createFlutterConfig(
|
||||
'text',
|
||||
viewId: view2.viewId,
|
||||
autofillHint: 'username',
|
||||
autofillHintsForFields: <String>['username', 'email', 'name'],
|
||||
);
|
||||
sendFrameworkMessage(codec.encodeMethodCall(MethodCall(
|
||||
'TextInput.updateConfig',
|
||||
autofillConfig2,
|
||||
)));
|
||||
|
||||
// Input and form are in view2.
|
||||
expect(view2.dom.textEditingHost.contains(input), isTrue);
|
||||
expect(view2.dom.textEditingHost.contains(form), isTrue);
|
||||
|
||||
// Cleanup.
|
||||
hideKeyboard();
|
||||
dispatcher.viewManager.unregisterView(view1.viewId);
|
||||
view1.dispose();
|
||||
dispatcher.viewManager.unregisterView(view2.viewId);
|
||||
view2.dispose();
|
||||
host1.remove();
|
||||
host2.remove();
|
||||
// TODO(mdebbar): Autofill forms don't get updated in the current system.
|
||||
// https://github.com/flutter/flutter/issues/145101
|
||||
}, skip: true);
|
||||
|
||||
tearDown(() {
|
||||
clearForms();
|
||||
});
|
||||
@ -2493,7 +2715,10 @@ Future<void> testMain() async {
|
||||
<String>['field1', 'field2', 'field3']);
|
||||
final EngineAutofillForm autofillForm =
|
||||
EngineAutofillForm.fromFrameworkMessage(
|
||||
createAutofillInfo('username', 'field1'), fields)!;
|
||||
kImplicitViewId,
|
||||
createAutofillInfo('username', 'field1'),
|
||||
fields,
|
||||
)!;
|
||||
|
||||
// Number of elements if number of fields sent to the constructor minus
|
||||
// one (for the focused text element).
|
||||
@ -2550,7 +2775,10 @@ Future<void> testMain() async {
|
||||
<String>['zzyyxx', 'aabbcc', 'jjkkll']);
|
||||
final EngineAutofillForm autofillForm =
|
||||
EngineAutofillForm.fromFrameworkMessage(
|
||||
createAutofillInfo('username', 'field1'), fields)!;
|
||||
kImplicitViewId,
|
||||
createAutofillInfo('username', 'field1'),
|
||||
fields,
|
||||
)!;
|
||||
|
||||
expect(autofillForm.formIdentifier, 'aabbcc*jjkkll*zzyyxx');
|
||||
});
|
||||
@ -2563,7 +2791,10 @@ Future<void> testMain() async {
|
||||
<String>['field1', 'fields2', 'field3']);
|
||||
final EngineAutofillForm autofillForm =
|
||||
EngineAutofillForm.fromFrameworkMessage(
|
||||
createAutofillInfo('username', 'field1'), fields)!;
|
||||
kImplicitViewId,
|
||||
createAutofillInfo('username', 'field1'),
|
||||
fields,
|
||||
)!;
|
||||
|
||||
final DomHTMLInputElement testInputElement = createDomHTMLInputElement();
|
||||
autofillForm.placeForm(testInputElement);
|
||||
@ -2590,7 +2821,10 @@ Future<void> testMain() async {
|
||||
);
|
||||
final EngineAutofillForm autofillForm =
|
||||
EngineAutofillForm.fromFrameworkMessage(
|
||||
createAutofillInfo('username', 'field1'), fields)!;
|
||||
kImplicitViewId,
|
||||
createAutofillInfo('username', 'field1'),
|
||||
fields,
|
||||
)!;
|
||||
|
||||
// The focused element is the only field. Form should be empty after
|
||||
// the initialization (focus element is appended later).
|
||||
@ -2615,7 +2849,7 @@ Future<void> testMain() async {
|
||||
<String>['field1'],
|
||||
);
|
||||
final EngineAutofillForm? autofillForm =
|
||||
EngineAutofillForm.fromFrameworkMessage(null, fields);
|
||||
EngineAutofillForm.fromFrameworkMessage(kImplicitViewId, null, fields);
|
||||
|
||||
expect(autofillForm, isNull);
|
||||
});
|
||||
@ -2632,7 +2866,10 @@ Future<void> testMain() async {
|
||||
]);
|
||||
final EngineAutofillForm autofillForm =
|
||||
EngineAutofillForm.fromFrameworkMessage(
|
||||
createAutofillInfo('email', 'field1'), fields)!;
|
||||
kImplicitViewId,
|
||||
createAutofillInfo('email', 'field1'),
|
||||
fields,
|
||||
)!;
|
||||
|
||||
expect(autofillForm.elements, hasLength(2));
|
||||
|
||||
@ -2675,7 +2912,10 @@ Future<void> testMain() async {
|
||||
]);
|
||||
final EngineAutofillForm autofillForm =
|
||||
EngineAutofillForm.fromFrameworkMessage(
|
||||
createAutofillInfo('email', 'field1'), fields)!;
|
||||
kImplicitViewId,
|
||||
createAutofillInfo('email', 'field1'),
|
||||
fields,
|
||||
)!;
|
||||
final List<DomHTMLInputElement> formChildNodes =
|
||||
autofillForm.formElement.childNodes.toList()
|
||||
as List<DomHTMLInputElement>;
|
||||
@ -2707,7 +2947,10 @@ Future<void> testMain() async {
|
||||
]);
|
||||
final EngineAutofillForm autofillForm =
|
||||
EngineAutofillForm.fromFrameworkMessage(
|
||||
createAutofillInfo('email', 'field1'), fields)!;
|
||||
kImplicitViewId,
|
||||
createAutofillInfo('email', 'field1'),
|
||||
fields,
|
||||
)!;
|
||||
final List<DomHTMLInputElement> formChildNodes =
|
||||
autofillForm.formElement.childNodes.toList()
|
||||
as List<DomHTMLInputElement>;
|
||||
@ -2738,7 +2981,10 @@ Future<void> testMain() async {
|
||||
]);
|
||||
final EngineAutofillForm autofillForm =
|
||||
EngineAutofillForm.fromFrameworkMessage(
|
||||
createAutofillInfo('email', 'field1'), fields)!;
|
||||
kImplicitViewId,
|
||||
createAutofillInfo('email', 'field1'),
|
||||
fields,
|
||||
)!;
|
||||
|
||||
final DomHTMLInputElement testInputElement = createDomHTMLInputElement();
|
||||
testInputElement.name = 'email';
|
||||
@ -3378,6 +3624,7 @@ void checkTextAreaEditingState(
|
||||
/// simplicity.
|
||||
Map<String, dynamic> createFlutterConfig(
|
||||
String inputType, {
|
||||
int? viewId,
|
||||
bool readOnly = false,
|
||||
bool obscureText = false,
|
||||
bool autocorrect = true,
|
||||
@ -3397,6 +3644,7 @@ Map<String, dynamic> createFlutterConfig(
|
||||
if (decimal) 'decimal': true,
|
||||
if (isMultiline) 'isMultiline': true,
|
||||
},
|
||||
if (viewId != null) 'viewId': viewId,
|
||||
'readOnly': readOnly,
|
||||
'obscureText': obscureText,
|
||||
'autocorrect': autocorrect,
|
||||
|
||||
@ -102,13 +102,74 @@ Future<void> doTests() async {
|
||||
});
|
||||
});
|
||||
|
||||
group('viewIdForRootElement', () {
|
||||
test('works', () {
|
||||
final EngineFlutterView view = EngineFlutterView(platformDispatcher, createDomElement('div'));
|
||||
final int viewId = view.viewId;
|
||||
group('findViewForElement', () {
|
||||
test('finds view for root and descendant elements', () {
|
||||
final DomElement host = createDomElement('div');
|
||||
final EngineFlutterView view = EngineFlutterView(platformDispatcher, host);
|
||||
|
||||
viewManager.registerView(view);
|
||||
|
||||
expect(viewManager.viewIdForRootElement(view.dom.rootElement), viewId);
|
||||
final DomElement rootElement = view.dom.rootElement;
|
||||
final DomElement child1 = createDomElement('div');
|
||||
final DomElement child2 = createDomElement('div');
|
||||
final DomElement child3 = createDomElement('div');
|
||||
rootElement.append(child1);
|
||||
rootElement.append(child2);
|
||||
child2.append(child3);
|
||||
|
||||
expect(viewManager.findViewForElement(rootElement), view);
|
||||
expect(viewManager.findViewForElement(child1), view);
|
||||
expect(viewManager.findViewForElement(child2), view);
|
||||
expect(viewManager.findViewForElement(child3), view);
|
||||
});
|
||||
|
||||
test('returns null for host element', () {
|
||||
final DomElement host = createDomElement('div');
|
||||
final EngineFlutterView view = EngineFlutterView(platformDispatcher, host);
|
||||
viewManager.registerView(view);
|
||||
|
||||
expect(viewManager.findViewForElement(host), isNull);
|
||||
});
|
||||
|
||||
test("returns null for elements that don't belong to any view", () {
|
||||
final DomElement host = createDomElement('div');
|
||||
final EngineFlutterView view = EngineFlutterView(platformDispatcher, host);
|
||||
viewManager.registerView(view);
|
||||
|
||||
final DomElement disconnectedElement = createDomElement('div');
|
||||
final DomElement childOfBody = createDomElement('div');
|
||||
|
||||
domDocument.body!.append(childOfBody);
|
||||
|
||||
expect(viewManager.findViewForElement(disconnectedElement), isNull);
|
||||
expect(viewManager.findViewForElement(childOfBody), isNull);
|
||||
expect(viewManager.findViewForElement(domDocument.body), isNull);
|
||||
});
|
||||
|
||||
test('does not recognize elements from unregistered views', () {
|
||||
final DomElement host = createDomElement('div');
|
||||
final EngineFlutterView view = EngineFlutterView(platformDispatcher, host);
|
||||
viewManager.registerView(view);
|
||||
|
||||
final DomElement rootElement = view.dom.rootElement;
|
||||
final DomElement child1 = createDomElement('div');
|
||||
final DomElement child2 = createDomElement('div');
|
||||
final DomElement child3 = createDomElement('div');
|
||||
rootElement.append(child1);
|
||||
rootElement.append(child2);
|
||||
child2.append(child3);
|
||||
|
||||
expect(viewManager.findViewForElement(rootElement), view);
|
||||
expect(viewManager.findViewForElement(child1), view);
|
||||
expect(viewManager.findViewForElement(child2), view);
|
||||
expect(viewManager.findViewForElement(child3), view);
|
||||
|
||||
viewManager.unregisterView(view.viewId);
|
||||
|
||||
expect(viewManager.findViewForElement(rootElement), isNull);
|
||||
expect(viewManager.findViewForElement(child1), isNull);
|
||||
expect(viewManager.findViewForElement(child2), isNull);
|
||||
expect(viewManager.findViewForElement(child3), isNull);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user