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 2 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 @@ -90,13 +90,8 @@ final class ViewFocusBinding {
}

static int? _viewId(DomElement? element) {
final DomElement? viewElement = element?.closest(
DomManager.flutterViewTagName,
);
final String? viewIdAttribute = viewElement?.getAttribute(
GlobalHtmlAttributes.flutterViewIdAttributeName,
);
return viewIdAttribute == null ? null : int.tryParse(viewIdAttribute);
final FlutterViewManager viewManager = EnginePlatformDispatcher.instance.viewManager;
return viewManager.findViewForElement(element)?.viewId;
}

static const String _focusin = 'focusin';
Expand Down
78 changes: 67 additions & 11 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,19 +143,53 @@ 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];
if (view == null) {
// `viewId` points to a non-existent view, is this even possible? Should we
// throw?
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe convert this in one of those assert(viewId != null, 'This should never happen, create a bug!');, rather than bailing out silently?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


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 int viewId,
required this.formElement,
this.elements,
this.items,
this.formIdentifier = '',
this.insertionReferenceNode,
});
}) : _viewId = viewId;

final DomHTMLFormElement formElement;

Expand All @@ -177,6 +207,16 @@ class EngineAutofillForm {
/// See [formsOnTheDom].
final String formIdentifier;

int _viewId;
int get viewId => _viewId;
set viewId(int value) {
if (_viewId == value) {
return;
}
_viewId = value;
_ensureEditingElementInView(formElement, _viewId);
}

/// Creates an [EngineAutofillFrom] from the JSON representation of a Flutter
/// framework `TextInputConfiguration` object.
///
Expand All @@ -189,6 +229,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 +353,7 @@ class EngineAutofillForm {
insertionReferenceNode ??= submitButton;

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

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

void storeForm() {
Expand Down Expand Up @@ -944,6 +986,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 +1001,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 +1020,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 @@ -1238,6 +1286,8 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
DomHTMLFormElement? get focusedFormElement =>
inputConfiguration.autofillGroup?.formElement;

FlutterViewManager get viewManager => EnginePlatformDispatcher.instance.viewManager;

@override
void initializeTextEditing(
InputConfiguration inputConfig, {
Expand All @@ -1257,7 +1307,8 @@ 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);
final DomElement textEditingHost = viewManager[inputConfig.viewId]!.dom.textEditingHost;
textEditingHost.append(activeDomElement);
_appendedToForm = false;
}

Expand Down Expand Up @@ -1291,8 +1342,12 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
final AutofillInfo? autofill = config.autofill;
if (autofill != null) {
autofill.applyToDomElement(activeDomElement, focusedElement: true);
config.autofillGroup!.viewId = config.viewId;
} 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 @@ -1748,7 +1803,8 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
if (hasAutofillGroup) {
placeForm();
} else {
defaultTextEditingRoot.append(activeDomElement);
final DomElement textEditingHost = viewManager[inputConfig.viewId]!.dom.textEditingHost;
textEditingHost.append(activeDomElement);
}
inputConfig.textCapitalization.setAutocapitalizeAttribute(
activeDomElement);
Expand Down
11 changes: 11 additions & 0 deletions lib/web_ui/lib/src/engine/util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import 'dom.dart';
import 'safe_browser_api.dart';
import 'services.dart';
import 'vector_math.dart';
import 'view_embedder/dom_manager.dart';
import 'view_embedder/global_html_attributes.dart';

/// Generic callback signature, used by [_futurize].
typedef Callback<T> = void Function(T result);
Expand Down Expand Up @@ -649,6 +651,15 @@ int? tryViewId(Object? arguments) {
return null;
}

int? findParentViewId(DomElement element) {
DomElement? current = element;
while (current != null && element.tagName.toLowerCase() != DomManager.flutterViewTagName) {
current = element.parent;
}
final String? viewIdAttribute = current?.getAttribute(GlobalHtmlAttributes.flutterViewIdAttributeName);
return viewIdAttribute == null ? null : int.parse(viewIdAttribute);
}

/// Prints a list of bytes in hex format.
///
/// Bytes are separated by one space and are padded on the left to always show
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@ 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>{};

// A map of root elements to their corresponding EngineFlutterView.
final Map<DomElement, EngineFlutterView> _elementToView =
<DomElement, EngineFlutterView>{};

// 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 @@ -60,6 +67,7 @@ class FlutterViewManager {

// Store the view, and the jsViewOptions, if any...
_viewData[viewId] = view;
_elementToView[view.dom.rootElement] = view;
if (jsViewOptions != null) {
_jsViewOptions[viewId] = jsViewOptions;
}
Expand All @@ -82,7 +90,10 @@ class FlutterViewManager {
///
/// Returns its [JsFlutterViewOptions] (if any).
JsFlutterViewOptions? unregisterView(int viewId) {
_viewData.remove(viewId); // .dispose();
final EngineFlutterView? unregisteredView = _viewData.remove(viewId);
if (unregisteredView != null) {
_elementToView.remove(unregisteredView.dom.rootElement);
}
final JsFlutterViewOptions? jsViewOptions = _jsViewOptions.remove(viewId);
_onViewDisposedController.add(viewId);
return jsViewOptions;
Expand All @@ -96,6 +107,18 @@ class FlutterViewManager {
return _jsViewOptions[viewId];
}

EngineFlutterView? findViewForElement(DomElement? element) {
DomElement? current = element;
while (current != null) {
final EngineFlutterView? view = _elementToView[current];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this while and not closest? Is iterating our own cache faster than a brower querySelector/closest?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the idea of depending on tag name and html attributes. But I'm not feeling strongly about it, so I'll switch to closest since it might be faster.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if (view != null) {
return view;
}
current = current.parent;
}
return null;
}

void dispose() {
// We need to call `toList()` in order to avoid concurrent modification
// inside the loop.
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