Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
62 changes: 52 additions & 10 deletions lib/web_ui/lib/src/engine/text_editing/text_editing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -147,13 +143,45 @@ 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
/// (the default) on the current input field. See the [fromFrameworkMessage]
/// static method.
class EngineAutofillForm {
EngineAutofillForm({
required this.viewId,
required this.formElement,
this.elements,
this.items,
Expand All @@ -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.
///
Expand All @@ -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,
) {
Expand Down Expand Up @@ -312,6 +344,7 @@ class EngineAutofillForm {
insertionReferenceNode ??= submitButton;

return EngineAutofillForm(
viewId: viewId,
formElement: formElement,
elements: elements,
items: items,
Expand All @@ -330,7 +363,7 @@ class EngineAutofillForm {
}

formElement.insertBefore(mainTextEditingElement, insertionReferenceNode);
defaultTextEditingRoot.append(formElement);
_insertEditingElementInView(formElement, viewId);
}

void storeForm() {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -1757,7 +1799,7 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
if (hasAutofillGroup) {
placeForm();
} else {
defaultTextEditingRoot.append(activeDomElement);
_insertEditingElementInView(activeDomElement, inputConfig.viewId);
}
inputConfig.textCapitalization.setAutocapitalizeAttribute(
activeDomElement);
Expand Down
20 changes: 11 additions & 9 deletions lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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() {
Expand Down
14 changes: 8 additions & 6 deletions lib/web_ui/test/engine/composition_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion lib/web_ui/test/engine/semantics/text_field_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
Expand Down
Loading