Skip to content

Commit 79f1530

Browse files
authored
[web] Use viewId for text editing (flutter#51099)
Handle `viewId` for text fields. Part of flutter#137344
1 parent 7176173 commit 79f1530

7 files changed

Lines changed: 419 additions & 66 deletions

File tree

lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,8 @@ final class ViewFocusBinding {
9595
}
9696

9797
int? _viewId(DomElement? element) {
98-
final DomElement? rootElement = element?.closest(DomManager.flutterViewTagName);
99-
if (rootElement == null) {
100-
return null;
101-
}
102-
return _viewManager.viewIdForRootElement(rootElement);
98+
final FlutterViewManager viewManager = EnginePlatformDispatcher.instance.viewManager;
99+
return viewManager.findViewForElement(element)?.viewId;
103100
}
104101

105102
void _handleViewCreated(int viewId) {

lib/web_ui/lib/src/engine/text_editing/text_editing.dart

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import '../semantics.dart';
1818
import '../services.dart';
1919
import '../text/paragraph.dart';
2020
import '../util.dart';
21+
import '../view_embedder/flutter_view_manager.dart';
22+
import '../window.dart';
2123
import 'autofill_hint.dart';
2224
import 'composition_aware_mixin.dart';
2325
import 'input_action.dart';
@@ -48,12 +50,6 @@ const String transparentTextEditingClass = 'transparentTextEditing';
4850

4951
void _emptyCallback(dynamic _) {}
5052

51-
/// The default [HostNode] that hosts all DOM required for text editing when a11y is not enabled.
52-
@visibleForTesting
53-
// TODO(mdebbar): There could be multiple views with multiple text editing hosts.
54-
// https://github.com/flutter/flutter/issues/137344
55-
DomElement get defaultTextEditingRoot => EnginePlatformDispatcher.instance.implicitView!.dom.textEditingHost;
56-
5753
/// These style attributes are constant throughout the life time of an input
5854
/// element.
5955
///
@@ -147,13 +143,45 @@ void _styleAutofillElements(
147143
elementStyle.setProperty('caret-color', 'transparent');
148144
}
149145

146+
void _ensureEditingElementInView(DomElement element, int viewId) {
147+
final bool isAlreadyAppended = element.isConnected ?? false;
148+
if (!isAlreadyAppended) {
149+
// If the element is not already appended to a view, we don't need to move
150+
// it anywhere.
151+
return;
152+
}
153+
154+
final FlutterViewManager viewManager = EnginePlatformDispatcher.instance.viewManager;
155+
final EngineFlutterView? currentView = viewManager.findViewForElement(element);
156+
if (currentView == null) {
157+
// For some reason, the input element was in the DOM, but it wasn't part of
158+
// any Flutter view. Should we throw?
159+
return;
160+
}
161+
162+
if (currentView.viewId != viewId) {
163+
_insertEditingElementInView(element, viewId);
164+
}
165+
}
166+
167+
void _insertEditingElementInView(DomElement element, int viewId) {
168+
final FlutterViewManager viewManager = EnginePlatformDispatcher.instance.viewManager;
169+
final EngineFlutterView? view = viewManager[viewId];
170+
assert(
171+
view != null,
172+
'Could not find View with id $viewId. This should never happen, please file a bug!',
173+
);
174+
view!.dom.textEditingHost.append(element);
175+
}
176+
150177
/// Form that contains all the fields in the same AutofillGroup.
151178
///
152179
/// An [EngineAutofillForm] will only be constructed when autofill is enabled
153180
/// (the default) on the current input field. See the [fromFrameworkMessage]
154181
/// static method.
155182
class EngineAutofillForm {
156183
EngineAutofillForm({
184+
required this.viewId,
157185
required this.formElement,
158186
this.elements,
159187
this.items,
@@ -177,6 +205,9 @@ class EngineAutofillForm {
177205
/// See [formsOnTheDom].
178206
final String formIdentifier;
179207

208+
/// The ID of the view that this form is rendered into.
209+
final int viewId;
210+
180211
/// Creates an [EngineAutofillFrom] from the JSON representation of a Flutter
181212
/// framework `TextInputConfiguration` object.
182213
///
@@ -189,6 +220,7 @@ class EngineAutofillForm {
189220
///
190221
/// Returns null if autofill is disabled for the input field.
191222
static EngineAutofillForm? fromFrameworkMessage(
223+
int viewId,
192224
Map<String, dynamic>? focusedElementAutofill,
193225
List<dynamic>? fields,
194226
) {
@@ -312,6 +344,7 @@ class EngineAutofillForm {
312344
insertionReferenceNode ??= submitButton;
313345

314346
return EngineAutofillForm(
347+
viewId: viewId,
315348
formElement: formElement,
316349
elements: elements,
317350
items: items,
@@ -330,7 +363,7 @@ class EngineAutofillForm {
330363
}
331364

332365
formElement.insertBefore(mainTextEditingElement, insertionReferenceNode);
333-
defaultTextEditingRoot.append(formElement);
366+
_insertEditingElementInView(formElement, viewId);
334367
}
335368

336369
void storeForm() {
@@ -944,6 +977,7 @@ class EditingState {
944977
/// This corresponds to Flutter's [TextInputConfiguration].
945978
class InputConfiguration {
946979
InputConfiguration({
980+
required this.viewId,
947981
this.inputType = EngineInputType.text,
948982
this.inputAction = 'TextInputAction.done',
949983
this.obscureText = false,
@@ -958,7 +992,8 @@ class InputConfiguration {
958992

959993
InputConfiguration.fromFrameworkMessage(
960994
Map<String, dynamic> flutterInputConfiguration)
961-
: inputType = EngineInputType.fromName(
995+
: viewId = flutterInputConfiguration.tryInt('viewId') ?? kImplicitViewId,
996+
inputType = EngineInputType.fromName(
962997
flutterInputConfiguration.readJson('inputType').readString('name'),
963998
isDecimal: flutterInputConfiguration.readJson('inputType').tryBool('decimal') ?? false,
964999
isMultiline: flutterInputConfiguration.readJson('inputType').tryBool('isMultiline') ?? false,
@@ -976,11 +1011,15 @@ class InputConfiguration {
9761011
flutterInputConfiguration.readJson('autofill'))
9771012
: null,
9781013
autofillGroup = EngineAutofillForm.fromFrameworkMessage(
1014+
flutterInputConfiguration.tryInt('viewId') ?? kImplicitViewId,
9791015
flutterInputConfiguration.tryJson('autofill'),
9801016
flutterInputConfiguration.tryList('fields'),
9811017
),
9821018
enableDeltaModel = flutterInputConfiguration.tryBool('enableDeltaModel') ?? false;
9831019

1020+
/// The ID of the view that contains the text field.
1021+
final int viewId;
1022+
9841023
/// The type of information being edited in the input control.
9851024
final EngineInputType inputType;
9861025

@@ -1257,7 +1296,7 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
12571296
// DOM later, when the first location information arrived.
12581297
// Otherwise, on Blink based Desktop browsers, the autofill menu appears
12591298
// on top left of the screen.
1260-
defaultTextEditingRoot.append(activeDomElement);
1299+
_insertEditingElementInView(activeDomElement, inputConfig.viewId);
12611300
_appendedToForm = false;
12621301
}
12631302

@@ -1293,6 +1332,9 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
12931332
autofill.applyToDomElement(activeDomElement, focusedElement: true);
12941333
} else {
12951334
activeDomElement.setAttribute('autocomplete', 'off');
1335+
// When the new input configuration contains a different view ID, we need
1336+
// to move the input element to the new view.
1337+
_ensureEditingElementInView(activeDomElement, inputConfiguration.viewId);
12961338
}
12971339

12981340
final String autocorrectValue = config.autocorrect ? 'on' : 'off';
@@ -1757,7 +1799,7 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
17571799
if (hasAutofillGroup) {
17581800
placeForm();
17591801
} else {
1760-
defaultTextEditingRoot.append(activeDomElement);
1802+
_insertEditingElementInView(activeDomElement, inputConfig.viewId);
17611803
}
17621804
inputConfig.textCapitalization.setAutocapitalizeAttribute(
17631805
activeDomElement);

lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ class FlutterViewManager {
1212

1313
// A map of EngineFlutterViews indexed by their viewId.
1414
final Map<int, EngineFlutterView> _viewData = <int, EngineFlutterView>{};
15+
1516
// A map of (optional) JsFlutterViewOptions, indexed by their viewId.
1617
final Map<int, JsFlutterViewOptions> _jsViewOptions =
1718
<int, JsFlutterViewOptions>{};
19+
1820
// The controller of the [onViewCreated] stream.
1921
final StreamController<int> _onViewCreatedController =
2022
StreamController<int>.broadcast(sync: true);
23+
2124
// The controller of the [onViewDisposed] stream.
2225
final StreamController<int> _onViewDisposedController =
2326
StreamController<int>.broadcast(sync: true);
@@ -82,7 +85,7 @@ class FlutterViewManager {
8285
///
8386
/// Returns its [JsFlutterViewOptions] (if any).
8487
JsFlutterViewOptions? unregisterView(int viewId) {
85-
_viewData.remove(viewId); // .dispose();
88+
_viewData.remove(viewId);
8689
final JsFlutterViewOptions? jsViewOptions = _jsViewOptions.remove(viewId);
8790
_onViewDisposedController.add(viewId);
8891
return jsViewOptions;
@@ -96,14 +99,13 @@ class FlutterViewManager {
9699
return _jsViewOptions[viewId];
97100
}
98101

99-
/// Returns the [viewId] if [rootElement] corresponds to any of the [views].
100-
int? viewIdForRootElement(DomElement rootElement) {
101-
for(final EngineFlutterView view in views) {
102-
if (view.dom.rootElement == rootElement) {
103-
return view.viewId;
104-
}
105-
}
106-
return null;
102+
EngineFlutterView? findViewForElement(DomElement? element) {
103+
const String viewRootSelector =
104+
'${DomManager.flutterViewTagName}[${GlobalHtmlAttributes.flutterViewIdAttributeName}]';
105+
final DomElement? viewRoot = element?.closest(viewRootSelector);
106+
final String? viewIdAttribute = viewRoot?.getAttribute(GlobalHtmlAttributes.flutterViewIdAttributeName);
107+
final int? viewId = viewIdAttribute == null ? null : int.parse(viewIdAttribute);
108+
return viewId == null ? null : _viewData[viewId];
107109
}
108110

109111
void dispose() {

lib/web_ui/test/engine/composition_test.dart

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@ import 'dart:async';
66

77
import 'package:test/bootstrap/browser.dart';
88
import 'package:test/test.dart';
9-
import 'package:ui/src/engine/browser_detection.dart';
10-
11-
import 'package:ui/src/engine/dom.dart';
12-
import 'package:ui/src/engine/text_editing/composition_aware_mixin.dart';
13-
import 'package:ui/src/engine/text_editing/text_editing.dart';
9+
import 'package:ui/src/engine.dart';
1410

1511
import '../common/test_initialization.dart';
1612

13+
DomElement get defaultTextEditingRoot =>
14+
EnginePlatformDispatcher.instance.implicitView!.dom.textEditingHost;
15+
1716
void main() {
1817
internalBootstrapBrowserTest(() => testMain);
1918
}
@@ -36,7 +35,10 @@ GloballyPositionedTextEditingStrategy _enableEditingStrategy({
3635
}) {
3736
final HybridTextEditing owner = HybridTextEditing();
3837

39-
owner.configuration = InputConfiguration(enableDeltaModel: deltaModel);
38+
owner.configuration = InputConfiguration(
39+
viewId: kImplicitViewId,
40+
enableDeltaModel: deltaModel,
41+
);
4042

4143
final GloballyPositionedTextEditingStrategy editingStrategy =
4244
GloballyPositionedTextEditingStrategy(owner);

lib/web_ui/test/engine/semantics/text_field_test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import 'package:ui/ui.dart' as ui;
1515
import '../../common/test_initialization.dart';
1616
import 'semantics_tester.dart';
1717

18-
final InputConfiguration singlelineConfig = InputConfiguration();
18+
final InputConfiguration singlelineConfig = InputConfiguration(viewId: kImplicitViewId);
1919

2020
final InputConfiguration multilineConfig = InputConfiguration(
21+
viewId: kImplicitViewId,
2122
inputType: EngineInputType.multiline,
2223
inputAction: 'TextInputAction.newline',
2324
);

0 commit comments

Comments
 (0)