[flutter_releases] Flutter Stable 2.2.1 Engine Cherrypicks (#26405)
* 'Update Dart SDK to 375a2d7c66c8eb5d40872bdb57dd664b5f850d94'
* Fix a11y tab traversal (#25797)
* fix race with framework when using tab to traverse in a11y mode
* Fix CanvasKit SVG clipPath leak (#26227)
Co-authored-by: Yegor <yjbanov@google.com>
Co-authored-by: Harry Terkelsen <hterkelsen@users.noreply.github.com>
diff --git a/DEPS b/DEPS
index 9229478..d522f9e 100644
--- a/DEPS
+++ b/DEPS
@@ -35,7 +35,7 @@
# Dart is: https://github.com/dart-lang/sdk/blob/master/DEPS.
# You can use //tools/dart/create_updated_flutter_deps.py to produce
# updated revision list of existing dependencies.
- 'dart_revision': '9094f738083c746a15b496ea5f882362a3dc4889',
+ 'dart_revision': '375a2d7c66c8eb5d40872bdb57dd664b5f850d94',
# WARNING: DO NOT EDIT MANUALLY
# The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py
diff --git a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
index aa9055f..9c3e978 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
@@ -187,7 +187,7 @@
);
_rootViews[viewId] = newPlatformViewRoot;
}
- _applyMutators(params.mutators, platformView);
+ _applyMutators(params.mutators, platformView, viewId);
}
int _countClips(MutatorsStack mutators) {
@@ -233,11 +233,33 @@
return head;
}
- void _applyMutators(MutatorsStack mutators, html.Element embeddedView) {
+ /// Clean up the old SVG clip definitions, as this platform view is about to
+ /// be recomposited.
+ void _cleanUpClipDefs(int viewId) {
+ if (_svgClipDefs.containsKey(viewId)) {
+ final html.Element clipDefs =
+ _svgPathDefs!.querySelector('#sk_path_defs')!;
+ final List<html.Element> nodesToRemove = <html.Element>[];
+ final Set<String> oldDefs = _svgClipDefs[viewId]!;
+ for (html.Element child in clipDefs.children) {
+ if (oldDefs.contains(child.id)) {
+ nodesToRemove.add(child);
+ }
+ }
+ for (html.Element node in nodesToRemove) {
+ node.remove();
+ }
+ _svgClipDefs[viewId]!.clear();
+ }
+ }
+
+ void _applyMutators(
+ MutatorsStack mutators, html.Element embeddedView, int viewId) {
html.Element head = embeddedView;
Matrix4 headTransform = Matrix4.identity();
double embeddedOpacity = 1.0;
_resetAnchor(head);
+ _cleanUpClipDefs(viewId);
for (final Mutator mutator in mutators) {
switch (mutator.type) {
@@ -265,28 +287,38 @@
html.Element pathDefs =
_svgPathDefs!.querySelector('#sk_path_defs')!;
_clipPathCount += 1;
+ final String clipId = 'svgClip$_clipPathCount';
html.Node newClipPath = html.DocumentFragment.svg(
- '<clipPath id="svgClip$_clipPathCount">'
+ '<clipPath id="$clipId">'
'<path d="${path.toSvgString()}">'
'</path></clipPath>',
treeSanitizer: _NullTreeSanitizer(),
);
pathDefs.append(newClipPath);
- clipView.style.clipPath = 'url(#svgClip$_clipPathCount)';
+ // Store the id of the node instead of [newClipPath] directly. For
+ // some reason, calling `newClipPath.remove()` doesn't remove it
+ // from the DOM.
+ _svgClipDefs.putIfAbsent(viewId, () => <String>{}).add(clipId);
+ clipView.style.clipPath = 'url(#$clipId)';
} else if (mutator.path != null) {
final CkPath path = mutator.path as CkPath;
_ensureSvgPathDefs();
html.Element pathDefs =
_svgPathDefs!.querySelector('#sk_path_defs')!;
_clipPathCount += 1;
+ final String clipId = 'svgClip$_clipPathCount';
html.Node newClipPath = html.DocumentFragment.svg(
- '<clipPath id="svgClip$_clipPathCount">'
+ '<clipPath id="$clipId">'
'<path d="${path.toSvgString()}">'
'</path></clipPath>',
treeSanitizer: _NullTreeSanitizer(),
);
pathDefs.append(newClipPath);
- clipView.style.clipPath = 'url(#svgClip$_clipPathCount)';
+ // Store the id of the node instead of [newClipPath] directly. For
+ // some reason, calling `newClipPath.remove()` doesn't remove it
+ // from the DOM.
+ _svgClipDefs.putIfAbsent(viewId, () => <String>{}).add(clipId);
+ clipView.style.clipPath = 'url(#$clipId)';
}
_resetAnchor(clipView);
head = clipView;
@@ -323,6 +355,9 @@
html.Element? _svgPathDefs;
+ /// The nodes containing the SVG clip definitions needed to clip this view.
+ Map<int, Set<String>> _svgClipDefs = <int, Set<String>>{};
+
/// Ensures we add a container of SVG path defs to the DOM so they can
/// be referred to in clip-path: url(#blah).
void _ensureSvgPathDefs() {
@@ -411,6 +446,8 @@
_currentCompositionParams.remove(viewId);
_clipCount.remove(viewId);
_viewsToRecomposite.remove(viewId);
+ _cleanUpClipDefs(viewId);
+ _svgClipDefs.remove(viewId);
}
_viewsToDispose.clear();
}
@@ -439,6 +476,14 @@
_overlays[viewId] = overlay;
}
+
+ /// Deletes SVG clip paths, useful for tests.
+ void debugCleanupSvgClipPaths() {
+ _svgPathDefs?.children.single.children.forEach((element) {
+ element.remove();
+ });
+ _svgClipDefs.clear();
+ }
}
/// Caches surfaces used to overlay platform views.
diff --git a/lib/web_ui/lib/src/engine/dom_renderer.dart b/lib/web_ui/lib/src/engine/dom_renderer.dart
index cc0dff0..1f0eb83 100644
--- a/lib/web_ui/lib/src/engine/dom_renderer.dart
+++ b/lib/web_ui/lib/src/engine/dom_renderer.dart
@@ -93,8 +93,8 @@
/// This getter calls the `hasFocus` method of the `Document` interface.
/// See for more details:
/// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasFocus
- bool? get windowHasFocus =>
- js_util.callMethod(html.document, 'hasFocus', <dynamic>[]);
+ bool get windowHasFocus =>
+ js_util.callMethod(html.document, 'hasFocus', <dynamic>[]) ?? false;
void _setupHotRestart() {
// This persists across hot restarts to clear stale DOM.
diff --git a/lib/web_ui/lib/src/engine/semantics/label_and_value.dart b/lib/web_ui/lib/src/engine/semantics/label_and_value.dart
index 7975ba3..c573b09 100644
--- a/lib/web_ui/lib/src/engine/semantics/label_and_value.dart
+++ b/lib/web_ui/lib/src/engine/semantics/label_and_value.dart
@@ -50,12 +50,9 @@
final bool hasValue = semanticsObject.hasValue;
final bool hasLabel = semanticsObject.hasLabel;
- // If the node is incrementable or a text field the value is reported to the
- // browser via the respective role managers. We do not need to also render
- // it again here.
- final bool shouldDisplayValue = hasValue &&
- !semanticsObject.isIncrementable &&
- !semanticsObject.isTextField;
+ // If the node is incrementable the value is reported to the browser via
+ // the respective role manager. We do not need to also render it again here.
+ final bool shouldDisplayValue = hasValue && !semanticsObject.isIncrementable;
if (!hasLabel && !shouldDisplayValue) {
_cleanUpDom();
diff --git a/lib/web_ui/lib/src/engine/semantics/semantics.dart b/lib/web_ui/lib/src/engine/semantics/semantics.dart
index 3472695..5c05e65 100644
--- a/lib/web_ui/lib/src/engine/semantics/semantics.dart
+++ b/lib/web_ui/lib/src/engine/semantics/semantics.dart
@@ -270,8 +270,8 @@
}
/// See [ui.SemanticsUpdateBuilder.updateNode].
- int? get flags => _flags;
- int? _flags;
+ int get flags => _flags;
+ int _flags = 0;
/// Whether the [flags] field has been updated but has not been applied to the
/// DOM yet.
@@ -584,7 +584,7 @@
SemanticsObject? _parent;
/// Whether this node currently has a given [SemanticsFlag].
- bool hasFlag(ui.SemanticsFlag flag) => _flags! & flag.index != 0;
+ bool hasFlag(ui.SemanticsFlag flag) => _flags & flag.index != 0;
/// Whether [actions] contains the given action.
bool hasAction(ui.SemanticsAction action) => (_actions! & action.index) != 0;
@@ -785,15 +785,24 @@
/// > A map literal is ordered: iterating over the keys and/or values of the maps always happens in the order the keys appeared in the source code.
final Map<Role, RoleManager?> _roleManagers = <Role, RoleManager?>{};
+ /// Returns the role manager for the given [role].
+ ///
+ /// If a role manager does not exist for the given role, returns null.
+ RoleManager? debugRoleManagerFor(Role role) => _roleManagers[role];
+
/// Detects the roles that this semantics object corresponds to and manages
/// the lifecycles of [SemanticsObjectRole] objects.
void _updateRoles() {
- _updateRole(Role.labelAndValue, (hasLabel || hasValue) && !isVisualOnly);
+ _updateRole(Role.labelAndValue, (hasLabel || hasValue) && !isTextField && !isVisualOnly);
_updateRole(Role.textField, isTextField);
- _updateRole(
- Role.tappable,
- hasAction(ui.SemanticsAction.tap) ||
- hasFlag(ui.SemanticsFlag.isButton));
+
+ bool shouldUseTappableRole =
+ (hasAction(ui.SemanticsAction.tap) || hasFlag(ui.SemanticsFlag.isButton)) &&
+ // Text fields manage their own focus/tap interactions. We don't need the
+ // tappable role manager. It only confuses AT.
+ !isTextField;
+
+ _updateRole(Role.tappable, shouldUseTappableRole);
_updateRole(Role.incrementable, isIncrementable);
_updateRole(Role.scrollable,
isVerticalScrollContainer || isHorizontalScrollContainer);
@@ -1145,7 +1154,7 @@
_instance = null;
}
- final Map<int?, SemanticsObject?> _semanticsTree = <int?, SemanticsObject?>{};
+ final Map<int, SemanticsObject> _semanticsTree = <int, SemanticsObject>{};
/// Map [SemanticsObject.id] to parent [SemanticsObject] it was attached to
/// this frame.
@@ -1222,8 +1231,8 @@
/// Returns the entire semantics tree for testing.
///
/// Works only in debug mode.
- Map<int?, SemanticsObject?>? get debugSemanticsTree {
- Map<int?, SemanticsObject?>? result;
+ Map<int, SemanticsObject>? get debugSemanticsTree {
+ Map<int, SemanticsObject>? result;
assert(() {
result = _semanticsTree;
return true;
diff --git a/lib/web_ui/lib/src/engine/semantics/tappable.dart b/lib/web_ui/lib/src/engine/semantics/tappable.dart
index f7edf34..dea3e86 100644
--- a/lib/web_ui/lib/src/engine/semantics/tappable.dart
+++ b/lib/web_ui/lib/src/engine/semantics/tappable.dart
@@ -21,6 +21,10 @@
void update() {
final html.Element element = semanticsObject.element;
+ // "tab-index=0" is used to allow keyboard traversal of non-form elements.
+ // See also: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets
+ element.tabIndex = 0;
+
semanticsObject.setAriaRole(
'button', semanticsObject.hasFlag(ui.SemanticsFlag.isButton));
@@ -49,6 +53,11 @@
_stopListening();
}
}
+
+ // Request focus so that the AT shifts a11y focus to this node.
+ if (semanticsObject.isFlagsDirty && semanticsObject.hasFocus) {
+ element.focus();
+ }
}
void _stopListening() {
diff --git a/lib/web_ui/lib/src/engine/semantics/text_field.dart b/lib/web_ui/lib/src/engine/semantics/text_field.dart
index fe01510..c428b83 100644
--- a/lib/web_ui/lib/src/engine/semantics/text_field.dart
+++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart
@@ -15,28 +15,94 @@
/// This class is still responsible for hooking up the DOM element with the
/// [HybridTextEditing] instance so that changes are communicated to Flutter.
class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy {
- /// The semantics object which this text editing element belongs to.
- final SemanticsObject semanticsObject;
+ /// Initializes the [SemanticsTextEditingStrategy] singleton.
+ ///
+ /// This method must be called prior to accessing [instance].
+ static SemanticsTextEditingStrategy ensureInitialized(HybridTextEditing owner) {
+ if (_instance != null && instance.owner == owner) {
+ return instance;
+ }
+ return _instance = SemanticsTextEditingStrategy(owner);
+ }
+
+ /// The [SemanticsTextEditingStrategy] singleton.
+ static SemanticsTextEditingStrategy get instance => _instance!;
+ static SemanticsTextEditingStrategy? _instance;
/// Creates a [SemanticsTextEditingStrategy] that eagerly instantiates
/// [domElement] so the caller can insert it before calling
/// [SemanticsTextEditingStrategy.enable].
- SemanticsTextEditingStrategy(SemanticsObject semanticsObject,
- HybridTextEditing owner, html.HtmlElement domElement)
- : this.semanticsObject = semanticsObject,
- super(owner) {
- // Make sure the DOM element is of a type that we support for text editing.
- // TODO(yjbanov): move into initializer list when https://github.com/dart-lang/sdk/issues/37881 is fixed.
- assert((domElement is html.InputElement) ||
- (domElement is html.TextAreaElement));
- super.domElement = domElement;
+ SemanticsTextEditingStrategy(HybridTextEditing owner)
+ : super(owner);
+
+ /// The text field whose DOM element is currently used for editing.
+ ///
+ /// If this field is null, no editing takes place.
+ TextField? activeTextField;
+
+ /// Current input configuration supplied by the "flutter/textinput" channel.
+ InputConfiguration? inputConfig;
+
+ _OnChangeCallback? onChange;
+ _OnActionCallback? onAction;
+
+ /// The semantics implementation does not operate on DOM nodes, but only
+ /// remembers the config and callbacks. This is because the DOM nodes are
+ /// supplied in the semantics update and enabled by [activate].
+ @override
+ void enable(
+ InputConfiguration inputConfig, {
+ required _OnChangeCallback onChange,
+ required _OnActionCallback onAction,
+ }) {
+ this.inputConfig = inputConfig;
+ this.onChange = onChange;
+ this.onAction = onAction;
+ }
+
+ /// Attaches the DOM element owned by [textField] to the text editing
+ /// strategy.
+ ///
+ /// This method must be called after [enable] to name sure that [inputConfig],
+ /// [onChange], and [onAction] are not null.
+ void activate(TextField textField) {
+ assert(
+ inputConfig != null && onChange != null && onAction != null,
+ '"enable" should be called before "enableFromSemantics" and initialize input configuration',
+ );
+
+ if (activeTextField == textField) {
+ // The specified field is already active. Skip.
+ return;
+ } else if (activeTextField != null) {
+ // Another text field is currently active. Deactivate it before switching.
+ disable();
+ }
+
+ activeTextField = textField;
+ domElement = textField.editableElement;
+ _syncStyle();
+ super.enable(inputConfig!, onChange: onChange!, onAction: onAction!);
+ }
+
+ /// Detaches the DOM element owned by [textField] from this text editing
+ /// strategy.
+ ///
+ /// Typically at this point the element loses focus (blurs) and stops being
+ /// used for editing.
+ void deactivate(TextField textField) {
+ if (activeTextField == textField) {
+ disable();
+ }
}
@override
void disable() {
// We don't want to remove the DOM element because the caller is responsible
// for that. However we still want to stop editing, cleanup the handlers.
- assert(isEnabled);
+ if (!isEnabled) {
+ return;
+ }
isEnabled = false;
_style = null;
@@ -48,28 +114,15 @@
_subscriptions.clear();
_lastEditingState = null;
- // If focused element is a part of a form, it needs to stay on the DOM
- // until the autofill context of the form is finalized.
- // More details on `TextInput.finishAutofillContext` call.
- if (_appendedToForm &&
- _inputConfiguration.autofillGroup?.formElement != null) {
- // We want to save the domElement with the form. However we still
- // need to keep the text editing domElement attached to the semantics
- // tree. In order to simplify the logic we will create a clone of the
- // element.
- final html.Node textFieldClone = domElement.clone(false);
- domElement = textFieldClone as html.HtmlElement;
- _inputConfiguration.autofillGroup?.storeForm();
- }
-
// If the text element still has focus, remove focus from the editable
- // element to cause the keyboard to hide.
+ // element to cause the on-screen keyboard, if any, to hide (e.g. on iOS,
+ // Android).
// Otherwise, the keyboard stays on screen even when the user navigates to
// a different screen (e.g. by hitting the "back" button).
- if (operatingSystem == OperatingSystem.android ||
- operatingSystem == OperatingSystem.iOs) {
- domElement.blur();
- }
+ domElement?.blur();
+ domElement = null;
+ activeTextField = null;
+ _queuedStyle = null;
}
@override
@@ -80,27 +133,15 @@
}
// Subscribe to text and selection changes.
- _subscriptions.add(domElement.onInput.listen(_handleChange));
-
- _subscriptions.add(domElement.onKeyDown.listen(_maybeSendAction));
-
+ _subscriptions.add(activeDomElement.onInput.listen(_handleChange));
+ _subscriptions.add(activeDomElement.onKeyDown.listen(_maybeSendAction));
_subscriptions.add(html.document.onSelectionChange.listen(_handleChange));
-
preventDefaultForMouseEvents();
}
@override
- void initializeElementPlacement() {
- // Element placement is done by [TextField].
- }
-
- @override
void initializeTextEditing(InputConfiguration inputConfig,
{_OnChangeCallback? onChange, _OnActionCallback? onAction}) {
- // In accesibilty mode, the user of this class is supposed to insert the
- // [domElement] on their own. Let's make sure they did.
- assert(html.document.body!.contains(domElement));
-
isEnabled = true;
_inputConfiguration = inputConfig;
_onChange = onChange;
@@ -109,30 +150,46 @@
}
@override
- void setEditingState(EditingState? editingState) {
- super.setEditingState(editingState);
-
- // Refocus after setting editing state.
- domElement.focus();
- }
-
- @override
void placeElement() {
// If this text editing element is a part of an autofill group.
if (hasAutofillGroup) {
placeForm();
}
- domElement.focus();
+ activeDomElement.focus();
+ }
+
+ @override
+ void initializeElementPlacement() {
+ // Element placement is done by [TextField].
}
@override
void placeForm() {
- // Switch domElement's parent from semantics object to form.
- domElement.remove();
- _inputConfiguration.autofillGroup!.formElement.append(domElement);
- semanticsObject.element
- .append(_inputConfiguration.autofillGroup!.formElement);
- _appendedToForm = true;
+ }
+
+ @override
+ void updateElementPlacement(EditableTextGeometry geometry) {
+ // Element placement is done by [TextField].
+ }
+
+ EditableTextStyle? _queuedStyle;
+
+ @override
+ void updateElementStyle(EditableTextStyle style) {
+ _queuedStyle = style;
+ _syncStyle();
+ }
+
+ /// Apply style to the element, if both style and element are available.
+ ///
+ /// Because style is supplied by the "flutter/textinput" channel and the DOM
+ /// element is supplied by the semantics tree, the existence of both at the
+ /// same time is not guaranteed.
+ void _syncStyle() {
+ if (_queuedStyle == null || domElement == null) {
+ return;
+ }
+ super.updateElementStyle(_queuedStyle!);
}
}
@@ -147,20 +204,15 @@
class TextField extends RoleManager {
TextField(SemanticsObject semanticsObject)
: super(Role.textField, semanticsObject) {
- final html.HtmlElement editableDomElement =
+ editableElement =
semanticsObject.hasFlag(ui.SemanticsFlag.isMultiline)
? html.TextAreaElement()
: html.InputElement();
- textEditingElement = SemanticsTextEditingStrategy(
- semanticsObject,
- textEditing,
- editableDomElement,
- );
_setupDomElement();
}
- SemanticsTextEditingStrategy? textEditingElement;
- html.HtmlElement get _textFieldElement => textEditingElement!.domElement;
+ /// The element used for editing, e.g. `<input>`, `<textarea>`.
+ late final html.HtmlElement editableElement;
void _setupDomElement() {
// On iOS, even though the semantic text field is transparent, the cursor
@@ -168,13 +220,13 @@
// are made invisible by CSS in [DomRenderer.reset].
// But there's one more case where iOS highlights text. That's when there's
// and autocorrect suggestion. To disable that, we have to do the following:
- _textFieldElement
+ editableElement
..spellcheck = false
..setAttribute('autocorrect', 'off')
..setAttribute('autocomplete', 'off')
..setAttribute('data-semantics-role', 'text-field');
- _textFieldElement.style
+ editableElement.style
..position = 'absolute'
// `top` and `left` are intentionally set to zero here.
//
@@ -189,7 +241,7 @@
..left = '0'
..width = '${semanticsObject.rect!.width}px'
..height = '${semanticsObject.rect!.height}px';
- semanticsObject.element.append(_textFieldElement);
+ semanticsObject.element.append(editableElement);
switch (browserEngine) {
case BrowserEngine.blink:
@@ -212,12 +264,11 @@
/// When in browser gesture mode, the focus is forwarded to the framework as
/// a tap to initialize editing.
void _initializeForBlink() {
- _textFieldElement.addEventListener('focus', (html.Event event) {
+ editableElement.addEventListener('focus', (html.Event event) {
if (semanticsObject.owner.gestureMode != GestureMode.browserGestures) {
return;
}
- textEditing.useCustomEditableElement(textEditingElement);
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
semanticsObject.id, ui.SemanticsAction.tap, null);
});
@@ -237,14 +288,13 @@
num? lastTouchStartOffsetX;
num? lastTouchStartOffsetY;
- _textFieldElement.addEventListener('touchstart', (html.Event event) {
- textEditing.useCustomEditableElement(textEditingElement);
+ editableElement.addEventListener('touchstart', (html.Event event) {
final html.TouchEvent touchEvent = event as html.TouchEvent;
lastTouchStartOffsetX = touchEvent.changedTouches!.last.client.x;
lastTouchStartOffsetY = touchEvent.changedTouches!.last.client.y;
}, true);
- _textFieldElement.addEventListener('touchend', (html.Event event) {
+ editableElement.addEventListener('touchend', (html.Event event) {
final html.TouchEvent touchEvent = event as html.TouchEvent;
if (lastTouchStartOffsetX != null) {
@@ -252,7 +302,7 @@
final num offsetX = touchEvent.changedTouches!.last.client.x;
final num offsetY = touchEvent.changedTouches!.last.client.y;
- // This should match the similar constant define in:
+ // This should match the similar constant defined in:
//
// lib/src/gestures/constants.dart
//
@@ -273,15 +323,75 @@
}, true);
}
+ bool _hasFocused = false;
+
@override
void update() {
// The user is editing the semantic text field directly, so there's no need
// to do any update here.
+ if (semanticsObject.hasLabel) {
+ editableElement.setAttribute(
+ 'aria-label',
+ semanticsObject.label!,
+ );
+ } else {
+ editableElement.removeAttribute('aria-label');
+ }
+
+ editableElement.style
+ ..width = '${semanticsObject.rect!.width}px'
+ ..height = '${semanticsObject.rect!.height}px';
+
+ // Whether we should request that the browser shift focus to the editable
+ // element, so that both the framework and the browser agree on what's
+ // currently focused.
+ bool needsDomFocusRequest = false;
+ final EditingState editingState = EditingState(
+ text: semanticsObject.value,
+ baseOffset: semanticsObject.textSelectionBase,
+ extentOffset: semanticsObject.textSelectionExtent,
+ );
+ if (semanticsObject.hasFocus) {
+ if (!_hasFocused) {
+ _hasFocused = true;
+ SemanticsTextEditingStrategy.instance.activate(this);
+ needsDomFocusRequest = true;
+ }
+ if (html.document.activeElement != editableElement) {
+ needsDomFocusRequest = true;
+ }
+ // Focused elements should have full text editing state applied.
+ SemanticsTextEditingStrategy.instance.setEditingState(editingState);
+ } else if (_hasFocused) {
+ SemanticsTextEditingStrategy.instance.deactivate(this);
+
+ // Only apply text, because this node is not focused.
+ editingState.applyTextToDomElement(editableElement);
+
+ if (_hasFocused && html.document.activeElement == editableElement) {
+ // Unlike `editableElement.focus()` we don't need to schedule `blur`
+ // post-update because `document.activeElement` implies that the
+ // element is already attached to the DOM. If it's not, it can't
+ // possibly be focused and therefore there's no need to blur.
+ editableElement.blur();
+ }
+ _hasFocused = false;
+ }
+
+ if (needsDomFocusRequest) {
+ // Schedule focus post-update to make sure the element is attached to
+ // the document. Otherwise focus() has no effect.
+ semanticsObject.owner.addOneTimePostUpdateCallback(() {
+ if (html.document.activeElement != editableElement) {
+ editableElement.focus();
+ }
+ });
+ }
}
@override
void dispose() {
- _textFieldElement.remove();
- textEditing.stopUsingCustomEditableElement();
+ editableElement.remove();
+ SemanticsTextEditingStrategy.instance.deactivate(this);
}
}
diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
index c5d908f..1d190ea 100644
--- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
+++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
@@ -8,6 +8,9 @@
/// Make the content editable span visible to facilitate debugging.
bool _debugVisibleTextEditing = false;
+/// Set this to `true` to print when text input commands are scheduled and run.
+bool _debugPrintTextInputCommands = false;
+
/// The `keyCode` of the "Enter" key.
const int _kReturnKeyCode = 13;
@@ -486,6 +489,14 @@
///
/// [domElement] can be a [InputElement] or a [TextAreaElement] depending on
/// the [InputType] of the text field.
+ ///
+ /// This should only be used by focused elements only, because only focused
+ /// elements can have their text selection range set. Attempting to set
+ /// selection range on a non-focused element will cause it to request focus.
+ ///
+ /// See also:
+ ///
+ /// * [applyTextToDomElement], which is used for non-focused elements.
void applyToDomElement(html.HtmlElement? domElement) {
if (domElement is html.InputElement) {
html.InputElement element = domElement;
@@ -496,6 +507,25 @@
element.value = text;
element.setSelectionRange(baseOffset!, extentOffset!);
} else {
+ throw UnsupportedError('Unsupported DOM element type: <${domElement?.tagName}> (${domElement.runtimeType})');
+ }
+ }
+
+ /// Applies the [text] to the [domElement].
+ ///
+ /// This is used by non-focused elements.
+ ///
+ /// See also:
+ ///
+ /// * [applyToDomElement], which is used for focused elements.
+ void applyTextToDomElement(html.HtmlElement? domElement) {
+ if (domElement is html.InputElement) {
+ html.InputElement element = domElement;
+ element.value = text;
+ } else if (domElement is html.TextAreaElement) {
+ html.TextAreaElement element = domElement;
+ element.value = text;
+ } else {
throw UnsupportedError('Unsupported DOM element type');
}
}
@@ -653,9 +683,9 @@
// does not appear on top-left of the page.
// Refocus on the elements after applying the geometry.
focusedFormElement!.focus();
- domElement.focus();
+ activeDomElement.focus();
} else {
- _geometry?.applyToDomElement(domElement);
+ _geometry?.applyToDomElement(activeDomElement);
}
}
}
@@ -686,7 +716,7 @@
/// Making an extra `focus` request causes flickering in Safari.
@override
void placeElement() {
- _geometry?.applyToDomElement(domElement);
+ _geometry?.applyToDomElement(activeDomElement);
if (hasAutofillGroup) {
placeForm();
// On Safari Desktop, when a form is focused, it opens an autofill menu
@@ -703,16 +733,16 @@
// users ongoing work to continue uninterrupted when there is an update to
// the transform.
// If domElement is not focused cursor location will not be correct.
- domElement.focus();
+ activeDomElement.focus();
if (_lastEditingState != null) {
- _lastEditingState!.applyToDomElement(domElement);
+ _lastEditingState!.applyToDomElement(activeDomElement);
}
}
}
@override
void initializeElementPlacement() {
- domElement.focus();
+ activeDomElement.focus();
}
}
@@ -745,12 +775,20 @@
@visibleForTesting
bool isEnabled = false;
- html.HtmlElement get domElement => _domElement!;
- set domElement(html.HtmlElement element) {
- _domElement = element;
- }
+ /// The DOM element used for editing, if any.
+ html.HtmlElement? domElement;
- html.HtmlElement? _domElement;
+ /// Same as [domElement] but null-checked.
+ ///
+ /// This must only be called in places that know for sure that a DOM element
+ /// is currently available for editing.
+ html.HtmlElement get activeDomElement {
+ assert(
+ domElement != null,
+ 'The DOM element of this text editing strategy is not currently active.',
+ );
+ return domElement!;
+ }
late InputConfiguration _inputConfiguration;
EditingState? _lastEditingState;
@@ -784,18 +822,18 @@
}) {
assert(!isEnabled);
- _domElement = inputConfig.inputType.createDomElement();
+ domElement = inputConfig.inputType.createDomElement();
_applyConfiguration(inputConfig);
- _setStaticStyleAttributes(domElement);
- _style?.applyToDomElement(domElement);
+ _setStaticStyleAttributes(activeDomElement);
+ _style?.applyToDomElement(activeDomElement);
if (!hasAutofillGroup) {
// If there is an Autofill Group the `FormElement`, it will be appended to the
// DOM later, when the first location information arrived.
// Otherwise, on Blink based Desktop browsers, the autofill menu appears
// on top left of the screen.
- domRenderer.glassPaneElement!.append(domElement);
+ domRenderer.glassPaneElement!.append(activeDomElement);
_appendedToForm = false;
}
@@ -810,19 +848,19 @@
_inputConfiguration = config;
if (config.readOnly) {
- domElement.setAttribute('readonly', 'readonly');
+ activeDomElement.setAttribute('readonly', 'readonly');
} else {
- domElement.removeAttribute('readonly');
+ activeDomElement.removeAttribute('readonly');
}
if (config.obscureText) {
- domElement.setAttribute('type', 'password');
+ activeDomElement.setAttribute('type', 'password');
}
- config.autofill?.applyToDomElement(domElement, focusedElement: true);
+ config.autofill?.applyToDomElement(activeDomElement, focusedElement: true);
final String autocorrectValue = config.autocorrect ? 'on' : 'off';
- domElement.setAttribute('autocorrect', autocorrectValue);
+ activeDomElement.setAttribute('autocorrect', autocorrectValue);
}
@override
@@ -838,16 +876,16 @@
}
// Subscribe to text and selection changes.
- _subscriptions.add(domElement.onInput.listen(_handleChange));
+ _subscriptions.add(activeDomElement.onInput.listen(_handleChange));
- _subscriptions.add(domElement.onKeyDown.listen(_maybeSendAction));
+ _subscriptions.add(activeDomElement.onKeyDown.listen(_maybeSendAction));
_subscriptions.add(html.document.onSelectionChange.listen(_handleChange));
- // Refocus on the domElement after blur, so that user can keep editing the
+ // Refocus on the activeDomElement after blur, so that user can keep editing the
// text field.
- _subscriptions.add(domElement.onBlur.listen((_) {
- domElement.focus();
+ _subscriptions.add(activeDomElement.onBlur.listen((_) {
+ activeDomElement.focus();
}));
preventDefaultForMouseEvents();
@@ -861,12 +899,11 @@
}
}
- @mustCallSuper
@override
void updateElementStyle(EditableTextStyle style) {
_style = style;
if (isEnabled) {
- _style!.applyToDomElement(domElement);
+ _style!.applyToDomElement(activeDomElement);
}
}
@@ -889,16 +926,15 @@
if (_appendedToForm &&
_inputConfiguration.autofillGroup?.formElement != null) {
// Subscriptions are removed, listeners won't be triggered.
- domElement.blur();
- _hideAutofillElements(domElement, isOffScreen: true);
+ activeDomElement.blur();
+ _hideAutofillElements(activeDomElement, isOffScreen: true);
_inputConfiguration.autofillGroup?.storeForm();
} else {
- domElement.remove();
+ activeDomElement.remove();
}
- _domElement = null;
+ domElement = null;
}
- @mustCallSuper
@override
void setEditingState(EditingState? editingState) {
_lastEditingState = editingState;
@@ -909,18 +945,18 @@
}
void placeElement() {
- domElement.focus();
+ activeDomElement.focus();
}
void placeForm() {
- _inputConfiguration.autofillGroup!.placeForm(domElement);
+ _inputConfiguration.autofillGroup!.placeForm(activeDomElement);
_appendedToForm = true;
}
void _handleChange(html.Event event) {
assert(isEnabled);
- EditingState newEditingState = EditingState.fromDomElement(domElement,
+ EditingState newEditingState = EditingState.fromDomElement(activeDomElement,
textCapitalization: _inputConfiguration.textCapitalization);
if (newEditingState != _lastEditingState) {
@@ -963,7 +999,7 @@
}
// Re-focuses after setting editing state.
- domElement.focus();
+ activeDomElement.focus();
}
/// Prevent default behavior for mouse down, up and move.
@@ -972,15 +1008,15 @@
/// selection conflicts with selection sent from the framework, which creates
/// flickering during selection by mouse.
void preventDefaultForMouseEvents() {
- _subscriptions.add(domElement.onMouseDown.listen((_) {
+ _subscriptions.add(activeDomElement.onMouseDown.listen((_) {
_.preventDefault();
}));
- _subscriptions.add(domElement.onMouseUp.listen((_) {
+ _subscriptions.add(activeDomElement.onMouseUp.listen((_) {
_.preventDefault();
}));
- _subscriptions.add(domElement.onMouseMove.listen((_) {
+ _subscriptions.add(activeDomElement.onMouseMove.listen((_) {
_.preventDefault();
}));
}
@@ -1039,11 +1075,11 @@
}) {
super.initializeTextEditing(inputConfig,
onChange: onChange, onAction: onAction);
- inputConfig.inputType.configureInputMode(domElement);
+ inputConfig.inputType.configureInputMode(activeDomElement);
if (hasAutofillGroup) {
placeForm();
}
- inputConfig.textCapitalization.setAutocapitalizeAttribute(domElement);
+ inputConfig.textCapitalization.setAutocapitalizeAttribute(activeDomElement);
}
@override
@@ -1051,7 +1087,7 @@
/// Position the element outside of the page before focusing on it. This is
/// useful for not triggering a scroll when iOS virtual keyboard is
/// coming up.
- domElement.style.transform = 'translate(-9999px, -9999px)';
+ activeDomElement.style.transform = 'translate(-9999px, -9999px)';
_canPosition = false;
}
@@ -1064,14 +1100,14 @@
}
// Subscribe to text and selection changes.
- _subscriptions.add(domElement.onInput.listen(_handleChange));
+ _subscriptions.add(activeDomElement.onInput.listen(_handleChange));
- _subscriptions.add(domElement.onKeyDown.listen(_maybeSendAction));
+ _subscriptions.add(activeDomElement.onKeyDown.listen(_maybeSendAction));
_subscriptions.add(html.document.onSelectionChange.listen(_handleChange));
// Position the DOM element after it is focused.
- _subscriptions.add(domElement.onFocus.listen((_) {
+ _subscriptions.add(activeDomElement.onFocus.listen((_) {
// Cancel previous timer if exists.
_schedulePlacement();
}));
@@ -1083,7 +1119,7 @@
//
// Since in all these cases, the connection needs to be closed,
// [domRenderer.windowHasFocus] is not checked in [IOSTextEditingStrategy].
- _subscriptions.add(domElement.onBlur.listen((_) {
+ _subscriptions.add(activeDomElement.onBlur.listen((_) {
owner.sendTextConnectionClosedToFrameworkIfAny();
}));
}
@@ -1121,7 +1157,7 @@
/// [_positionInputElementTimer] timer is restarted. The element will be
/// placed to its correct position after [_delayBeforePlacement].
void _addTapListener() {
- _subscriptions.add(domElement.onClick.listen((_) {
+ _subscriptions.add(activeDomElement.onClick.listen((_) {
// Check if the element is already positioned. If not this does not fall
// under `The user was using the long press, now they want to enter text
// via keyboard` journey.
@@ -1145,8 +1181,8 @@
@override
void placeElement() {
- domElement.focus();
- _geometry?.applyToDomElement(domElement);
+ activeDomElement.focus();
+ _geometry?.applyToDomElement(activeDomElement);
}
}
@@ -1168,13 +1204,13 @@
}) {
super.initializeTextEditing(inputConfig,
onChange: onChange, onAction: onAction);
- inputConfig.inputType.configureInputMode(domElement);
+ inputConfig.inputType.configureInputMode(activeDomElement);
if (hasAutofillGroup) {
placeForm();
} else {
- domRenderer.glassPaneElement!.append(domElement);
+ domRenderer.glassPaneElement!.append(activeDomElement);
}
- inputConfig.textCapitalization.setAutocapitalizeAttribute(domElement);
+ inputConfig.textCapitalization.setAutocapitalizeAttribute(activeDomElement);
}
@override
@@ -1185,19 +1221,19 @@
}
// Subscribe to text and selection changes.
- _subscriptions.add(domElement.onInput.listen(_handleChange));
+ _subscriptions.add(activeDomElement.onInput.listen(_handleChange));
- _subscriptions.add(domElement.onKeyDown.listen(_maybeSendAction));
+ _subscriptions.add(activeDomElement.onKeyDown.listen(_maybeSendAction));
_subscriptions.add(html.document.onSelectionChange.listen(_handleChange));
- _subscriptions.add(domElement.onBlur.listen((_) {
- if (domRenderer.windowHasFocus!) {
+ _subscriptions.add(activeDomElement.onBlur.listen((_) {
+ if (domRenderer.windowHasFocus) {
// Chrome on Android will hide the onscreen keyboard when you tap outside
// the text box. Instead, we want the framework to tell us to hide the
// keyboard via `TextInput.clearClient` or `TextInput.hide`. Therefore
// refocus as long as [domRenderer.windowHasFocus] is true.
- domElement.focus();
+ activeDomElement.focus();
} else {
owner.sendTextConnectionClosedToFrameworkIfAny();
}
@@ -1206,8 +1242,8 @@
@override
void placeElement() {
- domElement.focus();
- _geometry?.applyToDomElement(domElement);
+ activeDomElement.focus();
+ _geometry?.applyToDomElement(activeDomElement);
}
}
@@ -1239,9 +1275,9 @@
}
// Subscribe to text and selection changes.
- _subscriptions.add(domElement.onInput.listen(_handleChange));
+ _subscriptions.add(activeDomElement.onInput.listen(_handleChange));
- _subscriptions.add(domElement.onKeyDown.listen(_maybeSendAction));
+ _subscriptions.add(activeDomElement.onKeyDown.listen(_maybeSendAction));
// Detects changes in text selection.
//
@@ -1256,18 +1292,18 @@
//
// After each keyup, the start/end values of the selection is compared to
// the previously saved editing state.
- _subscriptions.add(domElement.onKeyUp.listen((event) {
+ _subscriptions.add(activeDomElement.onKeyUp.listen((event) {
_handleChange(event);
}));
// In Firefox the context menu item "Select All" does not work without
// listening to onSelect. On the other browsers onSelectionChange is
// enough for covering "Select All" functionality.
- _subscriptions.add(domElement.onSelect.listen(_handleChange));
+ _subscriptions.add(activeDomElement.onSelect.listen(_handleChange));
- // Refocus on the domElement after blur, so that user can keep editing the
+ // Refocus on the activeDomElement after blur, so that user can keep editing the
// text field.
- _subscriptions.add(domElement.onBlur.listen((_) {
+ _subscriptions.add(activeDomElement.onBlur.listen((_) {
_postponeFocus();
}));
@@ -1280,23 +1316,214 @@
// Calling focus inside a Timer for `0` milliseconds guarantee that it is
// called after blur event propagation is completed.
Timer(const Duration(milliseconds: 0), () {
- domElement.focus();
+ activeDomElement.focus();
});
}
@override
void placeElement() {
- domElement.focus();
- _geometry?.applyToDomElement(domElement);
+ activeDomElement.focus();
+ _geometry?.applyToDomElement(activeDomElement);
// Set the last editing state if it exists, this is critical for a
// users ongoing work to continue uninterrupted when there is an update to
// the transform.
if (_lastEditingState != null) {
- _lastEditingState!.applyToDomElement(domElement);
+ _lastEditingState!.applyToDomElement(activeDomElement);
}
}
}
+/// Base class for all `TextInput` commands sent through the `flutter/textinput`
+/// channel.
+@immutable
+abstract class TextInputCommand {
+ const TextInputCommand();
+
+ /// Executes the logic for this command.
+ void run(HybridTextEditing textEditing);
+}
+
+/// Responds to the 'TextInput.setClient' message.
+class TextInputSetClient extends TextInputCommand {
+ TextInputSetClient({
+ required this.clientId,
+ required this.configuration,
+ });
+
+ final int clientId;
+ final InputConfiguration configuration;
+
+ void run(HybridTextEditing textEditing) {
+ final bool clientIdChanged = textEditing._clientId != null && textEditing._clientId != clientId;
+ if (clientIdChanged && textEditing.isEditing) {
+ // We're connecting a new client. Any pending command for the previous client
+ // are irrelevant at this point.
+ textEditing.stopEditing();
+ }
+ textEditing._clientId = clientId;
+ textEditing.configuration = configuration;
+ }
+}
+
+/// Creates the text editing strategy used in non-a11y mode.
+DefaultTextEditingStrategy createDefaultTextEditingStrategy(HybridTextEditing textEditing) {
+ DefaultTextEditingStrategy strategy;
+ if (browserEngine == BrowserEngine.webkit &&
+ operatingSystem == OperatingSystem.iOs) {
+ strategy = IOSTextEditingStrategy(textEditing);
+ } else if (browserEngine == BrowserEngine.webkit) {
+ strategy = SafariDesktopTextEditingStrategy(textEditing);
+ } else if (browserEngine == BrowserEngine.blink &&
+ operatingSystem == OperatingSystem.android) {
+ strategy = AndroidTextEditingStrategy(textEditing);
+ } else if (browserEngine == BrowserEngine.firefox) {
+ strategy = FirefoxTextEditingStrategy(textEditing);
+ } else {
+ strategy = GloballyPositionedTextEditingStrategy(textEditing);
+ }
+ return strategy;
+}
+
+/// Responds to the 'TextInput.updateConfig' message.
+class TextInputUpdateConfig extends TextInputCommand {
+ TextInputUpdateConfig();
+
+ void run(HybridTextEditing textEditing) {
+ textEditing.strategy._applyConfiguration(textEditing.configuration!);
+ }
+}
+
+/// Responds to the 'TextInput.setEditingState' message.
+class TextInputSetEditingState extends TextInputCommand {
+ TextInputSetEditingState({
+ required this.state,
+ });
+
+ final EditingState state;
+
+ void run(HybridTextEditing textEditing) {
+ textEditing.strategy.setEditingState(state);
+ }
+}
+
+/// Responds to the 'TextInput.show' message.
+class TextInputShow extends TextInputCommand {
+ const TextInputShow();
+
+ void run(HybridTextEditing textEditing) {
+ if (!textEditing.isEditing) {
+ textEditing._startEditing();
+ }
+ }
+}
+
+/// Responds to the 'TextInput.setEditableSizeAndTransform' message.
+class TextInputSetEditableSizeAndTransform extends TextInputCommand {
+ TextInputSetEditableSizeAndTransform({
+ required this.geometry,
+ });
+
+ final EditableTextGeometry geometry;
+
+ void run(HybridTextEditing textEditing) {
+ textEditing.strategy.updateElementPlacement(geometry);
+ }
+}
+
+/// Responds to the 'TextInput.setStyle' message.
+class TextInputSetStyle extends TextInputCommand {
+ TextInputSetStyle({
+ required this.style,
+ });
+
+ final EditableTextStyle style;
+
+ void run(HybridTextEditing textEditing) {
+ textEditing.strategy.updateElementStyle(style);
+ }
+}
+
+/// Responds to the 'TextInput.clearClient' message.
+class TextInputClearClient extends TextInputCommand {
+ const TextInputClearClient();
+
+ void run(HybridTextEditing textEditing) {
+ if (textEditing.isEditing) {
+ textEditing.stopEditing();
+ }
+ }
+}
+
+/// Responds to the 'TextInput.hide' message.
+class TextInputHide extends TextInputCommand {
+ const TextInputHide();
+
+ void run(HybridTextEditing textEditing) {
+ if (textEditing.isEditing) {
+ textEditing.stopEditing();
+ }
+ }
+}
+
+class TextInputSetMarkedTextRect extends TextInputCommand {
+ const TextInputSetMarkedTextRect();
+
+ void run(HybridTextEditing textEditing) {
+ // No-op: this message is currently only used on iOS to implement
+ // UITextInput.firstRecForRange.
+ }
+}
+
+class TextInputSetCaretRect extends TextInputCommand {
+ const TextInputSetCaretRect();
+
+ void run(HybridTextEditing textEditing) {
+ // No-op: not supported on this platform.
+ }
+}
+
+class TextInputFinishAutofillContext extends TextInputCommand {
+ TextInputFinishAutofillContext({
+ required this.saveForm,
+ });
+
+ final bool saveForm;
+
+ void run(HybridTextEditing textEditing) {
+ // Close the text editing connection. Form is finalizing.
+ textEditing.sendTextConnectionClosedToFrameworkIfAny();
+ if (saveForm) {
+ saveForms();
+ }
+ // Clean the forms from DOM after submitting them.
+ cleanForms();
+ }
+}
+
+/// Submits the forms currently attached to the DOM.
+///
+/// Browser will save the information entered to the form.
+///
+/// Called when the form is finalized with save option `true`.
+/// See: https://github.com/flutter/flutter/blob/bf9f3a3dcfea3022f9cf2dfc3ab10b120b48b19d/packages/flutter/lib/src/services/text_input.dart#L1277
+void saveForms() {
+ formsOnTheDom.forEach((String identifier, html.FormElement form) {
+ final html.InputElement submitBtn =
+ form.getElementsByClassName('submitBtn').first as html.InputElement;
+ submitBtn.click();
+ });
+}
+
+/// Removes the forms from the DOM.
+///
+/// Called when the form is finalized.
+void cleanForms() {
+ for (html.FormElement form in formsOnTheDom.values) {
+ form.remove();
+ }
+ formsOnTheDom.clear();
+}
+
/// Translates the message-based communication between the framework and the
/// engine [implementation].
///
@@ -1312,103 +1539,84 @@
ByteData? data, ui.PlatformMessageResponseCallback? callback) {
const JSONMethodCodec codec = JSONMethodCodec();
final MethodCall call = codec.decodeMethodCall(data);
+ late final TextInputCommand command;
switch (call.method) {
case 'TextInput.setClient':
- implementation.setClient(
- call.arguments[0],
- InputConfiguration.fromFrameworkMessage(call.arguments[1]),
+ command = TextInputSetClient(
+ clientId: call.arguments[0],
+ configuration: InputConfiguration.fromFrameworkMessage(call.arguments[1]),
);
break;
case 'TextInput.updateConfig':
- final config = InputConfiguration.fromFrameworkMessage(call.arguments);
- implementation.updateConfig(config);
+ // Set configuration eagerly because it contains data about the text
+ // field used to flush the command queue. However, delaye applying the
+ // configuration because the strategy may not be available yet.
+ implementation.configuration = InputConfiguration.fromFrameworkMessage(call.arguments);
+ command = TextInputUpdateConfig();
break;
case 'TextInput.setEditingState':
- implementation
- .setEditingState(EditingState.fromFrameworkMessage(call.arguments));
+ command = TextInputSetEditingState(
+ state: EditingState.fromFrameworkMessage(call.arguments),
+ );
break;
case 'TextInput.show':
- implementation.show();
+ command = const TextInputShow();
break;
case 'TextInput.setEditableSizeAndTransform':
- implementation.setEditableSizeAndTransform(
- EditableTextGeometry.fromFrameworkMessage(call.arguments));
+ command = TextInputSetEditableSizeAndTransform(
+ geometry: EditableTextGeometry.fromFrameworkMessage(call.arguments),
+ );
break;
case 'TextInput.setStyle':
- implementation
- .setStyle(EditableTextStyle.fromFrameworkMessage(call.arguments));
+ command = TextInputSetStyle(
+ style: EditableTextStyle.fromFrameworkMessage(call.arguments),
+ );
break;
case 'TextInput.clearClient':
- implementation.clearClient();
+ command = const TextInputClearClient();
break;
case 'TextInput.hide':
- implementation.hide();
+ command = const TextInputHide();
break;
case 'TextInput.requestAutofill':
- // No-op: This message is sent by the framework to requests the platform autofill UI to appear.
- // Since autofill UI is a part of the browser, web engine does not need to utilize this method.
+ // There's no API to request autofill on the web. Instead we let the
+ // browser show autofill options automatically, if available. We
+ // therefore simply ignore this message.
break;
case 'TextInput.finishAutofillContext':
- final bool saveForm = call.arguments as bool;
- // Close the text editing connection. Form is finalizing.
- implementation.sendTextConnectionClosedToFrameworkIfAny();
- if (saveForm) {
- saveForms();
- }
- // Clean the forms from DOM after submitting them.
- cleanForms();
+ command = TextInputFinishAutofillContext(
+ saveForm: call.arguments as bool,
+ );
break;
case 'TextInput.setMarkedTextRect':
- // No-op: this message is currently only used on iOS to implement
- // UITextInput.firstRecForRange.
+ command = const TextInputSetMarkedTextRect();
break;
case 'TextInput.setCaretRect':
- // No-op: not supported on this platform.
+ command = const TextInputSetCaretRect();
break;
default:
EnginePlatformDispatcher.instance._replyToPlatformMessage(callback, null);
return;
}
- EnginePlatformDispatcher.instance
- ._replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
- }
- /// Used for submitting the forms attached on the DOM.
- ///
- /// Browser will save the information entered to the form.
- ///
- /// Called when the form is finalized with save option `true`.
- /// See: https://github.com/flutter/flutter/blob/bf9f3a3dcfea3022f9cf2dfc3ab10b120b48b19d/packages/flutter/lib/src/services/text_input.dart#L1277
- void saveForms() {
- formsOnTheDom.forEach((String identifier, html.FormElement form) {
- final html.InputElement submitBtn =
- form.getElementsByClassName('submitBtn').first as html.InputElement;
- submitBtn.click();
+ implementation.acceptCommand(command, () {
+ EnginePlatformDispatcher.instance
+ ._replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
});
}
- /// Used for removing the forms on the DOM.
- ///
- /// Called when the form is finalized.
- void cleanForms() {
- for (html.FormElement form in formsOnTheDom.values) {
- form.remove();
- }
- formsOnTheDom.clear();
- }
-
/// Sends the 'TextInputClient.updateEditingState' message to the framework.
void updateEditingState(int? clientId, EditingState? editingState) {
EnginePlatformDispatcher.instance.invokeOnPlatformMessage(
@@ -1478,118 +1686,15 @@
/// The constructor also decides which text editing strategy to use depending
/// on the operating system and browser engine.
HybridTextEditing() {
- if (browserEngine == BrowserEngine.webkit &&
- operatingSystem == OperatingSystem.iOs) {
- this._defaultEditingElement = IOSTextEditingStrategy(this);
- } else if (browserEngine == BrowserEngine.webkit) {
- this._defaultEditingElement = SafariDesktopTextEditingStrategy(this);
- } else if ((browserEngine == BrowserEngine.blink ||
- browserEngine == BrowserEngine.samsung) &&
- operatingSystem == OperatingSystem.android) {
- this._defaultEditingElement = AndroidTextEditingStrategy(this);
- } else if (browserEngine == BrowserEngine.firefox) {
- this._defaultEditingElement = FirefoxTextEditingStrategy(this);
- } else {
- this._defaultEditingElement = GloballyPositionedTextEditingStrategy(this);
- }
channel = TextEditingChannel(this);
}
late TextEditingChannel channel;
- /// The text editing stategy used. It can change depending on the
- /// formfactor/browser.
- ///
- /// It uses an HTML element to manage editing state when a custom element is
- /// not provided via [useCustomEditableElement]
- late final DefaultTextEditingStrategy _defaultEditingElement;
-
- /// The HTML element used to manage editing state.
- ///
- /// This field is populated using [useCustomEditableElement]. If `null` the
- /// [_defaultEditingElement] is used instead.
- DefaultTextEditingStrategy? _customEditingElement;
-
- DefaultTextEditingStrategy get editingElement {
- return _customEditingElement ?? _defaultEditingElement;
- }
-
- /// Responds to the 'TextInput.setClient' message.
- void setClient(int? clientId, InputConfiguration configuration) {
- final bool clientIdChanged = _clientId != null && _clientId != clientId;
- if (clientIdChanged && isEditing) {
- stopEditing();
- }
- _clientId = clientId;
- _configuration = configuration;
- }
-
- void updateConfig(InputConfiguration configuration) {
- _configuration = configuration;
- editingElement._applyConfiguration(_configuration);
- }
-
- /// Responds to the 'TextInput.setEditingState' message.
- void setEditingState(EditingState state) {
- editingElement.setEditingState(state);
- }
-
- /// Responds to the 'TextInput.show' message.
- void show() {
- if (!isEditing) {
- _startEditing();
- }
- }
-
- /// Responds to the 'TextInput.setEditableSizeAndTransform' message.
- void setEditableSizeAndTransform(EditableTextGeometry geometry) {
- editingElement.updateElementPlacement(geometry);
- }
-
- /// Responds to the 'TextInput.setStyle' message.
- void setStyle(EditableTextStyle style) {
- editingElement.updateElementStyle(style);
- }
-
- /// Responds to the 'TextInput.clearClient' message.
- void clearClient() {
- // We do not distinguish between "clearClient" and "hide" on the Web.
- hide();
- }
-
- /// Responds to the 'TextInput.hide' message.
- void hide() {
- if (isEditing) {
- stopEditing();
- }
- }
-
/// A CSS class name used to identify all elements used for text editing.
@visibleForTesting
static const String textEditingClass = 'flt-text-editing';
- static bool isEditingElement(html.Element element) {
- return element.classes.contains(textEditingClass);
- }
-
- /// Requests that [customEditingElement] is used for managing text editing state
- /// instead of the hidden default element.
- ///
- /// Use [stopUsingCustomEditableElement] to switch back to default element.
- void useCustomEditableElement(
- DefaultTextEditingStrategy? customEditingElement) {
- if (isEditing && customEditingElement != _customEditingElement) {
- stopEditing();
- }
- _customEditingElement = customEditingElement;
- }
-
- /// Switches back to using the built-in default element for managing text
- /// editing state.
- void stopUsingCustomEditableElement() {
- useCustomEditableElement(null);
- }
-
int? _clientId;
/// Flag which shows if there is an ongoing editing.
@@ -1598,13 +1703,30 @@
@visibleForTesting
bool isEditing = false;
- late InputConfiguration _configuration;
+ InputConfiguration? configuration;
+
+ DefaultTextEditingStrategy? debugTextEditingStrategyOverride;
+
+ /// Supplies the DOM element used for editing.
+ late final DefaultTextEditingStrategy strategy =
+ debugTextEditingStrategyOverride ??
+ (EngineSemanticsOwner.instance.semanticsEnabled
+ ? SemanticsTextEditingStrategy.ensureInitialized(this)
+ : createDefaultTextEditingStrategy(this));
+
+ void acceptCommand(TextInputCommand command, ui.VoidCallback callback) {
+ if (_debugPrintTextInputCommands) {
+ print('flutter/textinput channel command: ${command.runtimeType}');
+ }
+ command.run(this);
+ callback();
+ }
void _startEditing() {
assert(!isEditing);
isEditing = true;
- editingElement.enable(
- _configuration,
+ strategy.enable(
+ configuration!,
onChange: (EditingState? editingState) {
channel.updateEditingState(_clientId, editingState);
},
@@ -1617,7 +1739,7 @@
void stopEditing() {
assert(isEditing);
isEditing = false;
- editingElement.disable();
+ strategy.disable();
}
void sendTextConnectionClosedToFrameworkIfAny() {
diff --git a/lib/web_ui/test/canvaskit/common.dart b/lib/web_ui/test/canvaskit/common.dart
index 45b3bbd..3bb74c9 100644
--- a/lib/web_ui/test/canvaskit/common.dart
+++ b/lib/web_ui/test/canvaskit/common.dart
@@ -9,8 +9,9 @@
/// Whether the current browser is Safari on iOS.
// TODO: https://github.com/flutter/flutter/issues/60040
-bool get isIosSafari => browserEngine == BrowserEngine.webkit &&
- operatingSystem == OperatingSystem.iOs;
+bool get isIosSafari =>
+ browserEngine == BrowserEngine.webkit &&
+ operatingSystem == OperatingSystem.iOs;
/// Whether the current browser is Firefox.
bool get isFirefox => browserEngine == BrowserEngine.firefox;
@@ -24,8 +25,7 @@
/// Common test setup for all CanvasKit unit-tests.
void setUpCanvasKitTest() {
setUpAll(() async {
- expect(useCanvasKit, true,
- reason: 'This test must run in CanvasKit mode.');
+ expect(useCanvasKit, true, reason: 'This test must run in CanvasKit mode.');
debugResetBrowserSupportsFinalizationRegistry();
await ui.webOnlyInitializePlatform(assetManager: WebOnlyMockAssetManager());
});
@@ -81,8 +81,10 @@
///
/// Tests should use [collectNow] and [collectAfterTest] to trigger collections.
class TestCollector implements Collector {
- final List<_TestFinalizerRegistration> _activeRegistrations = <_TestFinalizerRegistration>[];
- final List<_TestFinalizerRegistration> _collectedRegistrations = <_TestFinalizerRegistration>[];
+ final List<_TestFinalizerRegistration> _activeRegistrations =
+ <_TestFinalizerRegistration>[];
+ final List<_TestFinalizerRegistration> _collectedRegistrations =
+ <_TestFinalizerRegistration>[];
final List<_TestCollection> _pendingCollections = <_TestCollection>[];
final List<_TestCollection> _completedCollections = <_TestCollection>[];
@@ -113,7 +115,8 @@
}
if (activeRegistration == null) {
late final _TestFinalizerRegistration? collectedRegistration;
- for (_TestFinalizerRegistration registration in _collectedRegistrations) {
+ for (_TestFinalizerRegistration registration
+ in _collectedRegistrations) {
if (identical(registration.deletable, collection.deletable)) {
collectedRegistration = registration;
break;
@@ -121,16 +124,15 @@
}
if (collectedRegistration == null) {
fail(
- 'Attempted to collect an object that was never registered for finalization.\n'
- 'The collection was requested here:\n'
- '${collection.stackTrace}'
- );
+ 'Attempted to collect an object that was never registered for finalization.\n'
+ 'The collection was requested here:\n'
+ '${collection.stackTrace}');
} else {
- final _TestCollection firstCollection = _completedCollections.firstWhere(
- (_TestCollection completedCollection) {
- return identical(completedCollection.deletable, collection.deletable);
- }
- );
+ final _TestCollection firstCollection = _completedCollections
+ .firstWhere((_TestCollection completedCollection) {
+ return identical(
+ completedCollection.deletable, collection.deletable);
+ });
fail(
'Attempted to collect an object that was previously collected.\n'
'The object was registered for finalization here:\n'
@@ -138,7 +140,7 @@
'The first collection was requested here:\n'
'${firstCollection.stackTrace}\n\n'
'The second collection was requested here:\n'
- '${collection.stackTrace}'
+ '${collection.stackTrace}',
);
}
} else {
diff --git a/lib/web_ui/test/canvaskit/embedded_views_test.dart b/lib/web_ui/test/canvaskit/embedded_views_test.dart
index 4788d61..970d595 100644
--- a/lib/web_ui/test/canvaskit/embedded_views_test.dart
+++ b/lib/web_ui/test/canvaskit/embedded_views_test.dart
@@ -28,6 +28,11 @@
window.debugOverrideDevicePixelRatio(1);
});
+ tearDown(() {
+ EnginePlatformDispatcher.instance.rasterizer?.surface.viewEmbedder
+ .debugCleanupSvgClipPaths();
+ });
+
test('embeds interactive platform views', () async {
ui.platformViewRegistry.registerViewFactory(
'test-platform-view',
@@ -123,7 +128,7 @@
List<String> getTransformChain(html.Element viewHost) {
final List<String> chain = <String>[];
html.Element? element = viewHost;
- while(element != null && element.tagName.toLowerCase() != 'flt-scene') {
+ while (element != null && element.tagName.toLowerCase() != 'flt-scene') {
chain.add(element.style.transform);
element = element.parent;
}
@@ -146,9 +151,8 @@
sb.pushOffset(3, 3);
sb.addPlatformView(0, width: 10, height: 10);
dispatcher.rasterizer!.draw(sb.build().layerTree);
- final html.Element viewHost = domRenderer.sceneElement!
- .querySelectorAll('#view-0')
- .single;
+ final html.Element viewHost =
+ domRenderer.sceneElement!.querySelectorAll('#view-0').single;
expect(
getTransformChain(viewHost),
@@ -174,9 +178,8 @@
sb.pushOffset(9, 9);
sb.addPlatformView(0, width: 10, height: 10);
dispatcher.rasterizer!.draw(sb.build().layerTree);
- final html.Element viewHost = domRenderer.sceneElement!
- .querySelectorAll('#view-0')
- .single;
+ final html.Element viewHost =
+ domRenderer.sceneElement!.querySelectorAll('#view-0').single;
expect(
getTransformChain(viewHost),
@@ -190,12 +193,10 @@
test('renders overlays on top of platform views', () async {
expect(OverlayCache.instance.debugLength, 0);
- final CkPicture testPicture = paintPicture(
- ui.Rect.fromLTRB(0, 0, 10, 10),
- (CkCanvas canvas) {
- canvas.drawCircle(ui.Offset(5, 5), 5, CkPaint());
- }
- );
+ final CkPicture testPicture =
+ paintPicture(ui.Rect.fromLTRB(0, 0, 10, 10), (CkCanvas canvas) {
+ canvas.drawCircle(ui.Offset(5, 5), 5, CkPaint());
+ });
// Initialize all platform views to be used in the test.
final List<int> platformViewIds = <int>[];
@@ -211,7 +212,7 @@
final EnginePlatformDispatcher dispatcher =
ui.window.platformDispatcher as EnginePlatformDispatcher;
- void renderTestScene({ required int viewCount }) {
+ void renderTestScene({required int viewCount}) {
LayerSceneBuilder sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
for (int i = 0; i < viewCount; i++) {
@@ -361,6 +362,44 @@
hasLength(0),
);
});
+
+ test(
+ 'removes old SVG clip definitions from the DOM when the view is recomposited',
+ () async {
+ ui.platformViewRegistry.registerViewFactory(
+ 'test-platform-view',
+ (viewId) => html.DivElement()..id = 'test-view',
+ );
+ await _createPlatformView(0, 'test-platform-view');
+
+ final EnginePlatformDispatcher dispatcher =
+ ui.window.platformDispatcher as EnginePlatformDispatcher;
+
+ void renderTestScene() {
+ LayerSceneBuilder sb = LayerSceneBuilder();
+ sb.pushOffset(0, 0);
+ sb.pushClipRRect(
+ ui.RRect.fromLTRBR(0, 0, 10, 10, ui.Radius.circular(3)));
+ sb.addPlatformView(0, width: 10, height: 10);
+ dispatcher.rasterizer!.draw(sb.build().layerTree);
+ }
+
+ final html.Node skPathDefs =
+ domRenderer.sceneElement!.querySelector('#sk_path_defs')!;
+
+ expect(skPathDefs.childNodes, hasLength(0));
+
+ renderTestScene();
+ expect(skPathDefs.childNodes, hasLength(1));
+
+ await Future<void>.delayed(Duration.zero);
+ renderTestScene();
+ expect(skPathDefs.childNodes, hasLength(1));
+
+ await Future<void>.delayed(Duration.zero);
+ renderTestScene();
+ expect(skPathDefs.childNodes, hasLength(1));
+ });
// TODO: https://github.com/flutter/flutter/issues/60040
}, skip: isIosSafari);
}
diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart
index 45b47f1..4b2aee5 100644
--- a/lib/web_ui/test/engine/semantics/semantics_test.dart
+++ b/lib/web_ui/test/engine/semantics/semantics_test.dart
@@ -17,7 +17,7 @@
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
-import '../../matchers.dart';
+import 'semantics_tester.dart';
DateTime _testTime = DateTime(2018, 12, 17);
@@ -27,13 +27,8 @@
internalBootstrapBrowserTest(() => testMain);
}
-String rootSemanticStyle = '';
-
void testMain() {
setUp(() {
- rootSemanticStyle = browserEngine != BrowserEngine.edge
- ? 'filter: opacity(0%); color: rgba(0, 0, 0, 0)' :
- 'color: rgba(0, 0, 0, 0); filter: opacity(0%)';
EngineSemanticsOwner.debugResetSemantics();
});
@@ -1198,24 +1193,23 @@
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
- final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
- updateNode(
- builder,
+ final SemanticsTester tester = SemanticsTester(semantics());
+ tester.updateNode(
id: 0,
- actions: 0 | ui.SemanticsAction.tap.index,
- flags: 0 |
- ui.SemanticsFlag.hasEnabledState.index |
- ui.SemanticsFlag.isEnabled.index |
- ui.SemanticsFlag.isButton.index,
- transform: Matrix4.identity().toFloat64(),
+ hasTap: true,
+ hasEnabledState: true,
+ isEnabled: true,
+ isButton: true,
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
);
+ tester.apply();
- semantics().updateSemantics(builder.build());
expectSemanticsTree('''
<sem role="button" style="$rootSemanticStyle"></sem>
''');
+ expect(tester.getSemanticsObject(0).element.tabIndex, 0);
+
semantics().semanticsEnabled = false;
},
// TODO(nurhan): https://github.com/flutter/flutter/issues/50754
@@ -1417,53 +1411,9 @@
});
}
-void expectSemanticsTree(String semanticsHtml) {
- expect(
- canonicalizeHtml(html.document.querySelector('flt-semantics').outerHtml),
- canonicalizeHtml(semanticsHtml),
- );
-}
-
-html.Element findScrollable() {
- return html.document.querySelectorAll('flt-semantics').firstWhere(
- (html.Element element) =>
- element.style.overflow == 'hidden' ||
- element.style.overflowY == 'scroll' ||
- element.style.overflowX == 'scroll',
- orElse: () => null,
- );
-}
-
-class SemanticsActionLogger {
- StreamController<int> idLogController;
- StreamController<ui.SemanticsAction> actionLogController;
- Stream<int> idLog;
- Stream<ui.SemanticsAction> actionLog;
-
- SemanticsActionLogger() {
- idLogController = StreamController<int>();
- actionLogController = StreamController<ui.SemanticsAction>();
- idLog = idLogController.stream.asBroadcastStream();
- actionLog = actionLogController.stream.asBroadcastStream();
-
- // The browser kicks us out of the test zone when the browser event happens.
- // We memorize the test zone so we can call expect when the callback is
- // fired.
- final Zone testZone = Zone.current;
-
- ui.window.onSemanticsAction =
- (int id, ui.SemanticsAction action, ByteData args) {
- idLogController.add(id);
- actionLogController.add(action);
- testZone.run(() {
- expect(args, null);
- });
- };
- }
-}
-
/// A facade in front of [ui.SemanticsUpdateBuilder.updateNode] that
/// supplies default values for semantics attributes.
+// TODO(yjbanov): move this to TestSemanticsBuilder
void updateNode(
ui.SemanticsUpdateBuilder builder, {
int id = 0,
diff --git a/lib/web_ui/test/engine/semantics/semantics_tester.dart b/lib/web_ui/test/engine/semantics/semantics_tester.dart
new file mode 100644
index 0000000..2b0b6fd
--- /dev/null
+++ b/lib/web_ui/test/engine/semantics/semantics_tester.dart
@@ -0,0 +1,388 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:html' as html;
+import 'dart:typed_data';
+
+import 'package:test/test.dart';
+import 'package:ui/src/engine.dart';
+import 'package:ui/ui.dart' as ui;
+
+import '../../matchers.dart';
+
+/// CSS style applied to the root of the semantics tree.
+// TODO(yjbanov): this should be handled internally by [expectSemanticsTree].
+// No need for every test to inject it.
+final String rootSemanticStyle = browserEngine != BrowserEngine.edge
+ ? 'filter: opacity(0%); color: rgba(0, 0, 0, 0)'
+ : 'color: rgba(0, 0, 0, 0); filter: opacity(0%)';
+
+/// A convenience wrapper of the semantics API for building and inspecting the
+/// semantics tree in unit tests.
+class SemanticsTester {
+ SemanticsTester(this.owner);
+
+ final EngineSemanticsOwner owner;
+ final List<SemanticsNodeUpdate> _nodeUpdates = <SemanticsNodeUpdate>[];
+
+ /// Updates one semantics node.
+ ///
+ /// Provides reasonable defaults for the missing attributes, and conveniences
+ /// for specifying flags, such as [isTextField].
+ SemanticsNodeUpdate updateNode({
+ required int id,
+
+ // Flags
+ int flags = 0,
+ bool? hasCheckedState,
+ bool? isChecked,
+ bool? isSelected,
+ bool? isButton,
+ bool? isLink,
+ bool? isTextField,
+ bool? isReadOnly,
+ bool? isFocusable,
+ bool? isFocused,
+ bool? hasEnabledState,
+ bool? isEnabled,
+ bool? isInMutuallyExclusiveGroup,
+ bool? isHeader,
+ bool? isObscured,
+ bool? scopesRoute,
+ bool? namesRoute,
+ bool? isHidden,
+ bool? isImage,
+ bool? isLiveRegion,
+ bool? hasToggledState,
+ bool? isToggled,
+ bool? hasImplicitScrolling,
+ bool? isMultiline,
+ bool? isSlider,
+ bool? isKeyboardKey,
+
+ // Actions
+ int actions = 0,
+ bool? hasTap,
+ bool? hasLongPress,
+ bool? hasScrollLeft,
+ bool? hasScrollRight,
+ bool? hasScrollUp,
+ bool? hasScrollDown,
+ bool? hasIncrease,
+ bool? hasDecrease,
+ bool? hasShowOnScreen,
+ bool? hasMoveCursorForwardByCharacter,
+ bool? hasMoveCursorBackwardByCharacter,
+ bool? hasSetSelection,
+ bool? hasCopy,
+ bool? hasCut,
+ bool? hasPaste,
+ bool? hasDidGainAccessibilityFocus,
+ bool? hasDidLoseAccessibilityFocus,
+ bool? hasCustomAction,
+ bool? hasDismiss,
+ bool? hasMoveCursorForwardByWord,
+ bool? hasMoveCursorBackwardByWord,
+ bool? hasSetText,
+
+ // Other attributes
+ int? maxValueLength,
+ int? currentValueLength,
+ int? textSelectionBase,
+ int? textSelectionExtent,
+ int? platformViewId,
+ int? scrollChildren,
+ int? scrollIndex,
+ double? scrollPosition,
+ double? scrollExtentMax,
+ double? scrollExtentMin,
+ double? elevation,
+ double? thickness,
+ ui.Rect? rect,
+ String? label,
+ String? hint,
+ String? value,
+ String? increasedValue,
+ String? decreasedValue,
+ ui.TextDirection? textDirection,
+ Float64List? transform,
+ Int32List? additionalActions,
+ List<SemanticsNodeUpdate>? children,
+ }) {
+ // Flags
+ if (hasCheckedState == true) {
+ flags |= ui.SemanticsFlag.hasCheckedState.index;
+ }
+ if (isChecked == true) {
+ flags |= ui.SemanticsFlag.isChecked.index;
+ }
+ if (isSelected == true) {
+ flags |= ui.SemanticsFlag.isSelected.index;
+ }
+ if (isButton == true) {
+ flags |= ui.SemanticsFlag.isButton.index;
+ }
+ if (isLink == true) {
+ flags |= ui.SemanticsFlag.isLink.index;
+ }
+ if (isTextField == true) {
+ flags |= ui.SemanticsFlag.isTextField.index;
+ }
+ if (isReadOnly == true) {
+ flags |= ui.SemanticsFlag.isReadOnly.index;
+ }
+ if (isFocusable == true) {
+ flags |= ui.SemanticsFlag.isFocusable.index;
+ }
+ if (isFocused == true) {
+ flags |= ui.SemanticsFlag.isFocused.index;
+ }
+ if (hasEnabledState == true) {
+ flags |= ui.SemanticsFlag.hasEnabledState.index;
+ }
+ if (isEnabled == true) {
+ flags |= ui.SemanticsFlag.isEnabled.index;
+ }
+ if (isInMutuallyExclusiveGroup == true) {
+ flags |= ui.SemanticsFlag.isInMutuallyExclusiveGroup.index;
+ }
+ if (isHeader == true) {
+ flags |= ui.SemanticsFlag.isHeader.index;
+ }
+ if (isObscured == true) {
+ flags |= ui.SemanticsFlag.isObscured.index;
+ }
+ if (scopesRoute == true) {
+ flags |= ui.SemanticsFlag.scopesRoute.index;
+ }
+ if (namesRoute == true) {
+ flags |= ui.SemanticsFlag.namesRoute.index;
+ }
+ if (isHidden == true) {
+ flags |= ui.SemanticsFlag.isHidden.index;
+ }
+ if (isImage == true) {
+ flags |= ui.SemanticsFlag.isImage.index;
+ }
+ if (isLiveRegion == true) {
+ flags |= ui.SemanticsFlag.isLiveRegion.index;
+ }
+ if (hasToggledState == true) {
+ flags |= ui.SemanticsFlag.hasToggledState.index;
+ }
+ if (isToggled == true) {
+ flags |= ui.SemanticsFlag.isToggled.index;
+ }
+ if (hasImplicitScrolling == true) {
+ flags |= ui.SemanticsFlag.hasImplicitScrolling.index;
+ }
+ if (isMultiline == true) {
+ flags |= ui.SemanticsFlag.isMultiline.index;
+ }
+ if (isSlider == true) {
+ flags |= ui.SemanticsFlag.isSlider.index;
+ }
+ if (isKeyboardKey == true) {
+ flags |= ui.SemanticsFlag.isKeyboardKey.index;
+ }
+
+ // Actions
+ if (hasTap == true) {
+ actions != ui.SemanticsAction.tap.index;
+ }
+ if (hasLongPress == true) {
+ actions != ui.SemanticsAction.longPress.index;
+ }
+ if (hasScrollLeft == true) {
+ actions != ui.SemanticsAction.scrollLeft.index;
+ }
+ if (hasScrollRight == true) {
+ actions != ui.SemanticsAction.scrollRight.index;
+ }
+ if (hasScrollUp == true) {
+ actions != ui.SemanticsAction.scrollUp.index;
+ }
+ if (hasScrollDown == true) {
+ actions != ui.SemanticsAction.scrollDown.index;
+ }
+ if (hasIncrease == true) {
+ actions != ui.SemanticsAction.increase.index;
+ }
+ if (hasDecrease == true) {
+ actions != ui.SemanticsAction.decrease.index;
+ }
+ if (hasShowOnScreen == true) {
+ actions != ui.SemanticsAction.showOnScreen.index;
+ }
+ if (hasMoveCursorForwardByCharacter == true) {
+ actions != ui.SemanticsAction.moveCursorForwardByCharacter.index;
+ }
+ if (hasMoveCursorBackwardByCharacter == true) {
+ actions != ui.SemanticsAction.moveCursorBackwardByCharacter.index;
+ }
+ if (hasSetSelection == true) {
+ actions != ui.SemanticsAction.setSelection.index;
+ }
+ if (hasCopy == true) {
+ actions != ui.SemanticsAction.copy.index;
+ }
+ if (hasCut == true) {
+ actions != ui.SemanticsAction.cut.index;
+ }
+ if (hasPaste == true) {
+ actions != ui.SemanticsAction.paste.index;
+ }
+ if (hasDidGainAccessibilityFocus == true) {
+ actions != ui.SemanticsAction.didGainAccessibilityFocus.index;
+ }
+ if (hasDidLoseAccessibilityFocus == true) {
+ actions != ui.SemanticsAction.didLoseAccessibilityFocus.index;
+ }
+ if (hasCustomAction == true) {
+ actions != ui.SemanticsAction.customAction.index;
+ }
+ if (hasDismiss == true) {
+ actions != ui.SemanticsAction.dismiss.index;
+ }
+ if (hasMoveCursorForwardByWord == true) {
+ actions != ui.SemanticsAction.moveCursorForwardByWord.index;
+ }
+ if (hasMoveCursorBackwardByWord == true) {
+ actions != ui.SemanticsAction.moveCursorBackwardByWord.index;
+ }
+ if (hasSetText == true) {
+ actions != ui.SemanticsAction.setText.index;
+ }
+
+ // Other attributes
+ ui.Rect childRect(SemanticsNodeUpdate child) {
+ return transformRect(Matrix4.fromFloat32List(child.transform), child.rect);
+ }
+
+ // If a rect is not provided, generate one than covers all children.
+ ui.Rect effectiveRect = rect ?? ui.Rect.zero;
+ if (children != null && children.isNotEmpty) {
+ effectiveRect = childRect(children.first);
+ for (SemanticsNodeUpdate child in children.skip(1)) {
+ effectiveRect = effectiveRect.expandToInclude(childRect(child));
+ }
+ }
+
+ final Int32List childIds = Int32List(children?.length ?? 0);
+ if (children != null) {
+ for (int i = 0; i < children.length; i++) {
+ childIds[i] = children[i].id;
+ }
+ }
+
+ final SemanticsNodeUpdate update = SemanticsNodeUpdate(
+ id: id,
+ flags: flags,
+ actions: actions,
+ maxValueLength: maxValueLength ?? 0,
+ currentValueLength: currentValueLength ?? 0,
+ textSelectionBase: textSelectionBase ?? 0,
+ textSelectionExtent: textSelectionExtent ?? 0,
+ platformViewId: platformViewId ?? 0,
+ scrollChildren: scrollChildren ?? 0,
+ scrollIndex: scrollIndex ?? 0,
+ scrollPosition: scrollPosition ?? 0,
+ scrollExtentMax: scrollExtentMax ?? 0,
+ scrollExtentMin: scrollExtentMin ?? 0,
+ rect: effectiveRect,
+ label: label ?? '',
+ hint: hint ?? '',
+ value: value ?? '',
+ increasedValue: increasedValue ?? '',
+ decreasedValue: decreasedValue ?? '',
+ transform: transform != null ? toMatrix32(transform) : Matrix4.identity().storage,
+ elevation: elevation ?? 0,
+ thickness: thickness ?? 0,
+ childrenInTraversalOrder: childIds,
+ childrenInHitTestOrder: childIds,
+ additionalActions: additionalActions ?? Int32List(0),
+ );
+ _nodeUpdates.add(update);
+ return update;
+ }
+
+ /// Updates the HTML tree from semantics updates accumulated by this builder.
+ ///
+ /// This builder forgets previous updates and may be reused in future updates.
+ Map<int, SemanticsObject> apply() {
+ owner.updateSemantics(SemanticsUpdate(nodeUpdates: _nodeUpdates));
+ _nodeUpdates.clear();
+ return owner.debugSemanticsTree!;
+ }
+
+ /// Locates the semantics object with the given [id].
+ SemanticsObject getSemanticsObject(int id) {
+ return owner.debugSemanticsTree![id]!;
+ }
+
+ /// Locates the role manager of the semantics object with the give [id].
+ RoleManager? getRoleManager(int id, Role role) {
+ return getSemanticsObject(id).debugRoleManagerFor(role);
+ }
+
+ /// Locates the [TextField] role manager of the semantics object with the give [id].
+ TextField getTextField(int id) {
+ return getRoleManager(id, Role.textField) as TextField;
+ }
+}
+
+/// Verifies the HTML structure of the current semantics tree.
+void expectSemanticsTree(String semanticsHtml) {
+ expect(
+ canonicalizeHtml(html.document.querySelector('flt-semantics')!.outerHtml!),
+ canonicalizeHtml(semanticsHtml),
+ );
+}
+
+/// Finds the first HTML element in the semantics tree used for scrolling.
+html.Element? findScrollable() {
+ return html.document.querySelectorAll('flt-semantics').cast<html.Element?>().firstWhere(
+ (html.Element? element) =>
+ element!.style.overflow == 'hidden' ||
+ element.style.overflowY == 'scroll' ||
+ element.style.overflowX == 'scroll',
+ orElse: () => null,
+ );
+}
+
+/// Logs semantics actions dispatched to [ui.window].
+class SemanticsActionLogger {
+ late StreamController<int> _idLogController;
+ late StreamController<ui.SemanticsAction> _actionLogController;
+
+ /// Semantics object ids that dispatched the actions.
+ Stream<int> get idLog => _idLog;
+ late Stream<int> _idLog;
+
+ /// The actions that were dispatched to [ui.window].
+ Stream<ui.SemanticsAction> get actionLog => _actionLog;
+ late Stream<ui.SemanticsAction> _actionLog;
+
+ SemanticsActionLogger() {
+ _idLogController = StreamController<int>();
+ _actionLogController = StreamController<ui.SemanticsAction>();
+ _idLog = _idLogController.stream.asBroadcastStream();
+ _actionLog = _actionLogController.stream.asBroadcastStream();
+
+ // The browser kicks us out of the test zone when the browser event happens.
+ // We memorize the test zone so we can call expect when the callback is
+ // fired.
+ final Zone testZone = Zone.current;
+
+ ui.window.onSemanticsAction =
+ (int id, ui.SemanticsAction action, ByteData? args) {
+ _idLogController.add(id);
+ _actionLogController.add(action);
+ testZone.run(() {
+ expect(args, null);
+ });
+ };
+ }
+}
diff --git a/lib/web_ui/test/engine/semantics/text_field_test.dart b/lib/web_ui/test/engine/semantics/text_field_test.dart
new file mode 100644
index 0000000..2d79d42
--- /dev/null
+++ b/lib/web_ui/test/engine/semantics/text_field_test.dart
@@ -0,0 +1,416 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@TestOn('chrome || safari || firefox')
+
+import 'dart:html' as html;
+
+import 'package:test/bootstrap/browser.dart';
+import 'package:test/test.dart';
+
+import 'package:ui/src/engine.dart' hide window;
+import 'package:ui/ui.dart' as ui;
+
+import 'semantics_tester.dart';
+
+final InputConfiguration singlelineConfig = InputConfiguration(
+ inputType: EngineInputType.text,
+);
+
+final InputConfiguration multilineConfig = InputConfiguration(
+ inputType: EngineInputType.multiline,
+ inputAction: 'TextInputAction.newline',
+);
+
+EngineSemanticsOwner semantics() => EngineSemanticsOwner.instance;
+
+const MethodCodec codec = JSONMethodCodec();
+
+DateTime _testTime = DateTime(2021, 4, 16);
+
+void main() {
+ internalBootstrapBrowserTest(() => testMain);
+}
+
+void testMain() {
+ setUp(() {
+ EngineSemanticsOwner.debugResetSemantics();
+ });
+
+ group('$SemanticsTextEditingStrategy', () {
+ late HybridTextEditing testTextEditing;
+ late SemanticsTextEditingStrategy strategy;
+
+ setUp(() {
+ testTextEditing = HybridTextEditing();
+ SemanticsTextEditingStrategy.ensureInitialized(testTextEditing);
+ strategy = SemanticsTextEditingStrategy.instance;
+ testTextEditing.debugTextEditingStrategyOverride = strategy;
+ testTextEditing.configuration = singlelineConfig;
+ });
+
+ test('renders a text field', () async {
+ semantics()
+ ..debugOverrideTimestampFunction(() => _testTime)
+ ..semanticsEnabled = true;
+
+ createTextFieldSemantics(value: 'hello');
+
+ expectSemanticsTree('''
+<sem style="$rootSemanticStyle">
+ <input value="hello" />
+</sem>''');
+
+ semantics().semanticsEnabled = false;
+ });
+
+ // TODO(yjbanov): this test will need to be adjusted for Safari when we add
+ // Safari testing.
+ test('sends a tap action when browser requests focus', () async {
+ final SemanticsActionLogger logger = SemanticsActionLogger();
+ semantics()
+ ..debugOverrideTimestampFunction(() => _testTime)
+ ..semanticsEnabled = true;
+
+ createTextFieldSemantics(value: 'hello');
+
+ final html.Element textField = html.document
+ .querySelectorAll('input[data-semantics-role="text-field"]')
+ .single;
+
+ expect(html.document.activeElement, isNot(textField));
+
+ textField.focus();
+
+ expect(html.document.activeElement, textField);
+ expect(await logger.idLog.first, 0);
+ expect(await logger.actionLog.first, ui.SemanticsAction.tap);
+
+ semantics().semanticsEnabled = false;
+ }, // TODO(nurhan): https://github.com/flutter/flutter/issues/46638
+ // TODO(nurhan): https://github.com/flutter/flutter/issues/50590
+ // TODO(nurhan): https://github.com/flutter/flutter/issues/50754
+ skip: (browserEngine != BrowserEngine.blink));
+
+ test('Syncs editing state from framework', () async {
+ semantics()
+ ..debugOverrideTimestampFunction(() => _testTime)
+ ..semanticsEnabled = true;
+
+ expect(html.document.activeElement, html.document.body);
+
+ int changeCount = 0;
+ int actionCount = 0;
+ strategy.enable(
+ singlelineConfig,
+ onChange: (_) {
+ changeCount++;
+ },
+ onAction: (_) {
+ actionCount++;
+ },
+ );
+
+ // Create
+ SemanticsObject textFieldSemantics = createTextFieldSemantics(
+ value: 'hello',
+ label: 'greeting',
+ isFocused: true,
+ rect: ui.Rect.fromLTWH(0, 0, 10, 15),
+ );
+
+ TextField textField = textFieldSemantics.debugRoleManagerFor(Role.textField) as TextField;
+ expect(textField.editableElement, strategy.domElement);
+ expect(html.document.activeElement, strategy.domElement);
+ expect((textField.editableElement as dynamic).value, 'hello');
+ expect(textField.editableElement.getAttribute('aria-label'), 'greeting');
+ expect(textField.editableElement.style.width, '10px');
+ expect(textField.editableElement.style.height, '15px');
+
+ // Update
+ createTextFieldSemantics(
+ value: 'bye',
+ label: 'farewell',
+ isFocused: false,
+ rect: ui.Rect.fromLTWH(0, 0, 12, 17),
+ );
+
+ expect(html.document.activeElement, html.document.body);
+ expect(strategy.domElement, null);
+ expect((textField.editableElement as dynamic).value, 'bye');
+ expect(textField.editableElement.getAttribute('aria-label'), 'farewell');
+ expect(textField.editableElement.style.width, '12px');
+ expect(textField.editableElement.style.height, '17px');
+
+ strategy.disable();
+ semantics().semanticsEnabled = false;
+
+ // There was no user interaction with the <input> element,
+ // so we should expect no engine-to-framework feedback.
+ expect(changeCount, 0);
+ expect(actionCount, 0);
+ });
+
+ test('Gives up focus after DOM blur', () async {
+ semantics()
+ ..debugOverrideTimestampFunction(() => _testTime)
+ ..semanticsEnabled = true;
+
+ expect(html.document.activeElement, html.document.body);
+ strategy.enable(
+ singlelineConfig,
+ onChange: (_) {},
+ onAction: (_) {},
+ );
+ final SemanticsObject textFieldSemantics = createTextFieldSemantics(
+ value: 'hello',
+ isFocused: true,
+ );
+
+ final TextField textField = textFieldSemantics.debugRoleManagerFor(Role.textField) as TextField;
+ expect(textField.editableElement, strategy.domElement);
+ expect(html.document.activeElement, strategy.domElement);
+
+ // The input should not refocus after blur.
+ textField.editableElement.blur();
+ expect(html.document.activeElement, html.document.body);
+ strategy.disable();
+ semantics().semanticsEnabled = false;
+ });
+
+ test('Does not dispose and recreate dom elements in persistent mode', () {
+ semantics()
+ ..debugOverrideTimestampFunction(() => _testTime)
+ ..semanticsEnabled = true;
+
+ strategy.enable(
+ singlelineConfig,
+ onChange: (_) {},
+ onAction: (_) {},
+ );
+
+ // It doesn't create a new DOM element.
+ expect(strategy.domElement, isNull);
+
+ // During the semantics update the DOM element is created and is focused on.
+ final SemanticsObject textFieldSemantics = createTextFieldSemantics(
+ value: 'hello',
+ isFocused: true,
+ );
+ expect(strategy.domElement, isNotNull);
+ expect(html.document.activeElement, strategy.domElement);
+
+ strategy.disable();
+ expect(strategy.domElement, isNull);
+
+ // It doesn't remove the DOM element.
+ final TextField textField = textFieldSemantics.debugRoleManagerFor(Role.textField) as TextField;
+ expect(html.document.body!.contains(textField.editableElement), isTrue);
+ // Editing element is not enabled.
+ expect(strategy.isEnabled, isFalse);
+ expect(html.document.activeElement, html.document.body);
+ semantics().semanticsEnabled = false;
+ });
+
+ test('Refocuses when setting editing state', () {
+ semantics()
+ ..debugOverrideTimestampFunction(() => _testTime)
+ ..semanticsEnabled = true;
+
+ strategy.enable(
+ singlelineConfig,
+ onChange: (_) {},
+ onAction: (_) {},
+ );
+
+ createTextFieldSemantics(
+ value: 'hello',
+ isFocused: true,
+ );
+ expect(strategy.domElement, isNotNull);
+ expect(html.document.activeElement, strategy.domElement);
+
+ // Blur the element without telling the framework.
+ strategy.activeDomElement.blur();
+ expect(html.document.activeElement, html.document.body);
+
+ // The input will have focus after editing state is set and semantics updated.
+ strategy.setEditingState(EditingState(text: 'foo'));
+
+ // NOTE: at this point some browsers, e.g. some versions of Safari will
+ // have set the focus on the editing element as a result of setting
+ // the test selection range. Other browsers require an explicit call
+ // to `element.focus()` for the element to acquire focus. So far,
+ // this discrepancy hasn't caused issues, so we're not checking for
+ // any particular focus state between setEditingState and
+ // createTextFieldSemantics. However, this is something for us to
+ // keep in mind in case this causes issues in the future.
+
+ createTextFieldSemantics(
+ value: 'hello',
+ isFocused: true,
+ );
+ expect(html.document.activeElement, strategy.domElement);
+
+ strategy.disable();
+ semantics().semanticsEnabled = false;
+ });
+
+ test('Works in multi-line mode', () {
+ semantics()
+ ..debugOverrideTimestampFunction(() => _testTime)
+ ..semanticsEnabled = true;
+
+ strategy.enable(
+ multilineConfig,
+ onChange: (_) {},
+ onAction: (_) {},
+ );
+ createTextFieldSemantics(
+ value: 'hello',
+ isFocused: true,
+ isMultiline: true,
+ );
+
+ final html.TextAreaElement textArea = strategy.domElement as html.TextAreaElement;
+ expect(html.document.activeElement, textArea);
+ strategy.enable(
+ singlelineConfig,
+ onChange: (_) {},
+ onAction: (_) {},
+ );
+
+ textArea.blur();
+ expect(html.document.activeElement, html.document.body);
+
+ strategy.disable();
+ // It doesn't remove the textarea from the DOM.
+ expect(html.document.body!.contains(textArea), isTrue);
+ // Editing element is not enabled.
+ expect(strategy.isEnabled, isFalse);
+ semantics().semanticsEnabled = false;
+ });
+
+ test('Does not position or size its DOM element', () {
+ semantics()
+ ..debugOverrideTimestampFunction(() => _testTime)
+ ..semanticsEnabled = true;
+
+ strategy.enable(
+ singlelineConfig,
+ onChange: (_) {},
+ onAction: (_) {},
+ );
+
+ // Send width and height that are different from semantics values on
+ // purpose.
+ final EditableTextGeometry geometry = EditableTextGeometry(
+ height: 12,
+ width: 13,
+ globalTransform: Matrix4.translationValues(14, 15, 0).storage,
+ );
+ final ui.Rect semanticsRect = ui.Rect.fromLTRB(0, 0, 100, 50);
+
+ testTextEditing.acceptCommand(
+ TextInputSetEditableSizeAndTransform(geometry: geometry),
+ () {},
+ );
+
+ createTextFieldSemantics(
+ value: 'hello',
+ isFocused: true,
+ rect: semanticsRect,
+ );
+
+ // Checks that the placement attributes come from semantics and not from
+ // EditableTextGeometry.
+ void checkPlacementIsSetBySemantics() {
+ expect(strategy.activeDomElement.style.transform, '');
+ expect(strategy.activeDomElement.style.width, '${semanticsRect.width}px');
+ expect(strategy.activeDomElement.style.height, '${semanticsRect.height}px');
+ }
+
+ checkPlacementIsSetBySemantics();
+ strategy.placeElement();
+ checkPlacementIsSetBySemantics();
+ semantics().semanticsEnabled = false;
+ });
+
+ Map<int, SemanticsObject> createTwoFieldSemantics(SemanticsTester builder, { int? focusFieldId }) {
+ builder.updateNode(
+ id: 0,
+ children: <SemanticsNodeUpdate>[
+ builder.updateNode(
+ id: 1,
+ isTextField: true,
+ value: 'Hello',
+ isFocused: focusFieldId == 1,
+ rect: ui.Rect.fromLTRB(0, 0, 50, 10),
+ ),
+ builder.updateNode(
+ id: 2,
+ isTextField: true,
+ value: 'World',
+ isFocused: focusFieldId == 2,
+ rect: ui.Rect.fromLTRB(0, 20, 50, 10),
+ ),
+ ],
+ );
+ return builder.apply();
+ }
+
+ test('Changes focus from one text field to another through a semantics update', () async {
+ semantics()
+ ..debugOverrideTimestampFunction(() => _testTime)
+ ..semanticsEnabled = true;
+
+ strategy.enable(
+ singlelineConfig,
+ onChange: (_) {},
+ onAction: (_) {},
+ );
+
+ // Switch between the two fields a few times.
+ for (int i = 0; i < 5; i++) {
+ final SemanticsTester tester = SemanticsTester(semantics());
+ createTwoFieldSemantics(tester, focusFieldId: 1);
+ expect(tester.apply().length, 3);
+ expect(html.document.activeElement, tester.getTextField(1).editableElement);
+ expect(strategy.domElement, tester.getTextField(1).editableElement);
+
+ createTwoFieldSemantics(tester, focusFieldId: 2);
+ expect(tester.apply().length, 3);
+ expect(html.document.activeElement, tester.getTextField(2).editableElement);
+ expect(strategy.domElement, tester.getTextField(2).editableElement);
+ }
+
+ semantics().semanticsEnabled = false;
+ });
+ },
+ // TODO(nurhan): https://github.com/flutter/flutter/issues/50769
+ skip: browserEngine == BrowserEngine.edge);
+}
+
+SemanticsObject createTextFieldSemantics({
+ required String value,
+ String label = '',
+ bool isFocused = false,
+ bool isMultiline = false,
+ ui.Rect rect = const ui.Rect.fromLTRB(0, 0, 100, 50),
+}) {
+ final SemanticsTester tester = SemanticsTester(semantics());
+ tester.updateNode(
+ id: 0,
+ label: label,
+ value: value,
+ isTextField: true,
+ isFocused: isFocused,
+ isMultiline: isMultiline,
+ hasTap: true,
+ rect: rect,
+ textDirection: ui.TextDirection.ltr,
+ );
+ tester.apply();
+ return tester.getSemanticsObject(0);
+}
diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart
index 6c9d9a4..394604e 100644
--- a/lib/web_ui/test/text_editing_test.dart
+++ b/lib/web_ui/test/text_editing_test.dart
@@ -13,7 +13,6 @@
import 'package:ui/src/engine.dart' hide window;
-import 'matchers.dart';
import 'spy.dart';
/// The `keyCode` of the "Enter" key.
@@ -24,7 +23,7 @@
/// Add unit tests for [FirefoxTextEditingStrategy].
/// TODO(nurhan): https://github.com/flutter/flutter/issues/46891
-DefaultTextEditingStrategy editingElement;
+DefaultTextEditingStrategy editingStrategy;
EditingState lastEditingState;
String lastInputAction;
@@ -57,7 +56,7 @@
tearDown(() {
lastEditingState = null;
lastInputAction = null;
- cleanTextEditingElement();
+ cleanTextEditingStrategy();
cleanTestFlags();
clearBackUpDomElementIfExists();
});
@@ -67,8 +66,9 @@
setUp(() {
testTextEditing = HybridTextEditing();
- editingElement = GloballyPositionedTextEditingStrategy(testTextEditing);
- testTextEditing.useCustomEditableElement(editingElement);
+ editingStrategy = GloballyPositionedTextEditingStrategy(testTextEditing);
+ testTextEditing.debugTextEditingStrategyOverride = editingStrategy;
+ testTextEditing.configuration = singlelineConfig;
});
test('Creates element when enabled and removes it when disabled', () {
@@ -79,7 +79,7 @@
// The focus initially is on the body.
expect(document.activeElement, document.body);
- editingElement.enable(
+ editingStrategy.enable(
singlelineConfig,
onChange: trackEditingState,
onAction: trackInputAction,
@@ -91,14 +91,14 @@
final InputElement input = document.getElementsByTagName('input')[0];
// Now the editing element should have focus.
expect(document.activeElement, input);
- expect(editingElement.domElement, input);
+ expect(editingStrategy.domElement, input);
expect(input.getAttribute('type'), null);
// Input is appended to the glass pane.
- expect(domRenderer.glassPaneElement.contains(editingElement.domElement),
+ expect(domRenderer.glassPaneElement.contains(editingStrategy.domElement),
isTrue);
- editingElement.disable();
+ editingStrategy.disable();
expect(
document.getElementsByTagName('input'),
hasLength(0),
@@ -108,73 +108,81 @@
});
test('Respects read-only config', () {
- final InputConfiguration config = InputConfiguration(readOnly: true);
- editingElement.enable(
+ final InputConfiguration config = InputConfiguration(
+ readOnly: true,
+ );
+ editingStrategy.enable(
config,
onChange: trackEditingState,
onAction: trackInputAction,
);
expect(document.getElementsByTagName('input'), hasLength(1));
final InputElement input = document.getElementsByTagName('input')[0];
- expect(editingElement.domElement, input);
+ expect(editingStrategy.domElement, input);
expect(input.getAttribute('readonly'), 'readonly');
- editingElement.disable();
+ editingStrategy.disable();
});
test('Knows how to create password fields', () {
- final InputConfiguration config = InputConfiguration(obscureText: true);
- editingElement.enable(
+ final InputConfiguration config = InputConfiguration(
+ obscureText: true,
+ );
+ editingStrategy.enable(
config,
onChange: trackEditingState,
onAction: trackInputAction,
);
expect(document.getElementsByTagName('input'), hasLength(1));
final InputElement input = document.getElementsByTagName('input')[0];
- expect(editingElement.domElement, input);
+ expect(editingStrategy.domElement, input);
expect(input.getAttribute('type'), 'password');
- editingElement.disable();
+ editingStrategy.disable();
});
test('Knows to turn autocorrect off', () {
- final InputConfiguration config = InputConfiguration(autocorrect: false);
- editingElement.enable(
+ final InputConfiguration config = InputConfiguration(
+ autocorrect: false,
+ );
+ editingStrategy.enable(
config,
onChange: trackEditingState,
onAction: trackInputAction,
);
expect(document.getElementsByTagName('input'), hasLength(1));
final InputElement input = document.getElementsByTagName('input')[0];
- expect(editingElement.domElement, input);
+ expect(editingStrategy.domElement, input);
expect(input.getAttribute('autocorrect'), 'off');
- editingElement.disable();
+ editingStrategy.disable();
});
test('Knows to turn autocorrect on', () {
- final InputConfiguration config = InputConfiguration(autocorrect: true);
- editingElement.enable(
+ final InputConfiguration config = InputConfiguration(
+ autocorrect: true,
+ );
+ editingStrategy.enable(
config,
onChange: trackEditingState,
onAction: trackInputAction,
);
expect(document.getElementsByTagName('input'), hasLength(1));
final InputElement input = document.getElementsByTagName('input')[0];
- expect(editingElement.domElement, input);
+ expect(editingStrategy.domElement, input);
expect(input.getAttribute('autocorrect'), 'on');
- editingElement.disable();
+ editingStrategy.disable();
});
test('Can read editing state correctly', () {
- editingElement.enable(
+ editingStrategy.enable(
singlelineConfig,
onChange: trackEditingState,
onAction: trackInputAction,
);
- final InputElement input = editingElement.domElement;
+ final InputElement input = editingStrategy.domElement;
input.value = 'foo bar';
input.dispatchEvent(Event.eventType('Event', 'input'));
expect(
@@ -194,15 +202,15 @@
});
test('Can set editing state correctly', () {
- editingElement.enable(
+ editingStrategy.enable(
singlelineConfig,
onChange: trackEditingState,
onAction: trackInputAction,
);
- editingElement.setEditingState(
+ editingStrategy.setEditingState(
EditingState(text: 'foo bar baz', baseOffset: 2, extentOffset: 7));
- checkInputEditingState(editingElement.domElement, 'foo bar baz', 2, 7);
+ checkInputEditingState(editingStrategy.domElement, 'foo bar baz', 2, 7);
// There should be no input action.
expect(lastInputAction, isNull);
@@ -211,7 +219,7 @@
test('Multi-line mode also works', () {
// The textarea element is created lazily.
expect(document.getElementsByTagName('textarea'), hasLength(0));
- editingElement.enable(
+ editingStrategy.enable(
multilineConfig,
onChange: trackEditingState,
onAction: trackInputAction,
@@ -222,7 +230,7 @@
document.getElementsByTagName('textarea')[0];
// Now the textarea should have focus.
expect(document.activeElement, textarea);
- expect(editingElement.domElement, textarea);
+ expect(editingStrategy.domElement, textarea);
textarea.value = 'foo\nbar';
textarea.dispatchEvent(Event.eventType('Event', 'input'));
@@ -235,11 +243,11 @@
);
// Can set textarea state correctly (and preserves new lines).
- editingElement.setEditingState(
+ editingStrategy.setEditingState(
EditingState(text: 'bar\nbaz', baseOffset: 2, extentOffset: 7));
checkTextAreaEditingState(textarea, 'bar\nbaz', 2, 7);
- editingElement.disable();
+ editingStrategy.disable();
// The textarea should be cleaned up.
expect(document.getElementsByTagName('textarea'), hasLength(0));
// The focus is back to the body.
@@ -255,7 +263,7 @@
expect(document.getElementsByTagName('textarea'), hasLength(0));
// Use single-line config and expect an `<input>` to be created.
- editingElement.enable(
+ editingStrategy.enable(
singlelineConfig,
onChange: trackEditingState,
onAction: trackInputAction,
@@ -264,12 +272,12 @@
expect(document.getElementsByTagName('textarea'), hasLength(0));
// Disable and check that all DOM elements were removed.
- editingElement.disable();
+ editingStrategy.disable();
expect(document.getElementsByTagName('input'), hasLength(0));
expect(document.getElementsByTagName('textarea'), hasLength(0));
// Use multi-line config and expect an `<textarea>` to be created.
- editingElement.enable(
+ editingStrategy.enable(
multilineConfig,
onChange: trackEditingState,
onAction: trackInputAction,
@@ -278,7 +286,7 @@
expect(document.getElementsByTagName('textarea'), hasLength(1));
// Disable again and check that all DOM elements were removed.
- editingElement.disable();
+ editingStrategy.disable();
expect(document.getElementsByTagName('input'), hasLength(0));
expect(document.getElementsByTagName('textarea'), hasLength(0));
@@ -287,9 +295,10 @@
});
test('Triggers input action', () {
- final InputConfiguration config =
- InputConfiguration(inputAction: 'TextInputAction.done');
- editingElement.enable(
+ final InputConfiguration config = InputConfiguration(
+ inputAction: 'TextInputAction.done',
+ );
+ editingStrategy.enable(
config,
onChange: trackEditingState,
onAction: trackInputAction,
@@ -299,7 +308,7 @@
expect(lastInputAction, isNull);
dispatchKeyboardEvent(
- editingElement.domElement,
+ editingStrategy.domElement,
'keydown',
keyCode: _kReturnKeyCode,
);
@@ -313,7 +322,7 @@
inputType: EngineInputType.multiline,
inputAction: 'TextInputAction.done',
);
- editingElement.enable(
+ editingStrategy.enable(
config,
onChange: trackEditingState,
onAction: trackInputAction,
@@ -323,7 +332,7 @@
expect(lastInputAction, isNull);
final KeyboardEvent event = dispatchKeyboardEvent(
- editingElement.domElement,
+ editingStrategy.domElement,
'keydown',
keyCode: _kReturnKeyCode,
);
@@ -335,270 +344,29 @@
});
test('globally positions and sizes its DOM element', () {
- editingElement.enable(
+ editingStrategy.enable(
singlelineConfig,
onChange: trackEditingState,
onAction: trackInputAction,
);
- expect(editingElement.isEnabled, isTrue);
+ expect(editingStrategy.isEnabled, isTrue);
// No geometry should be set until setEditableSizeAndTransform is called.
- expect(editingElement.domElement.style.transform, '');
- expect(editingElement.domElement.style.width, '');
- expect(editingElement.domElement.style.height, '');
+ expect(editingStrategy.domElement.style.transform, '');
+ expect(editingStrategy.domElement.style.width, '');
+ expect(editingStrategy.domElement.style.height, '');
- testTextEditing.setEditableSizeAndTransform(EditableTextGeometry(
+ testTextEditing.acceptCommand(TextInputSetEditableSizeAndTransform(geometry: EditableTextGeometry(
width: 13,
height: 12,
globalTransform: Matrix4.translationValues(14, 15, 0).storage,
- ));
+ )), () {});
// setEditableSizeAndTransform calls placeElement, so expecting geometry to be applied.
- expect(editingElement.domElement.style.transform,
+ expect(editingStrategy.domElement.style.transform,
'matrix(1, 0, 0, 1, 14, 15)');
- expect(editingElement.domElement.style.width, '13px');
- expect(editingElement.domElement.style.height, '12px');
- });
- });
-
- group('$SemanticsTextEditingStrategy', () {
- InputElement testInputElement;
- HybridTextEditing testTextEditing;
-
- final PlatformMessagesSpy spy = PlatformMessagesSpy();
-
- /// Emulates sending of a message by the framework to the engine.
- void sendFrameworkMessage(dynamic message) {
- textEditing.channel.handleTextInput(message, (ByteData data) {});
- }
-
- setUp(() {
- testInputElement = InputElement();
- testTextEditing = HybridTextEditing();
- editingElement = GloballyPositionedTextEditingStrategy(testTextEditing);
- });
-
- tearDown(() {
- testInputElement = null;
- });
-
- test('autofill form lifecycle works', () async {
- editingElement = SemanticsTextEditingStrategy(
- SemanticsObject(5, null), testTextEditing, testInputElement);
- // Create a configuration with an AutofillGroup of four text fields.
- final Map<String, dynamic> flutterMultiAutofillElementConfig =
- createFlutterConfig('text',
- autofillHint: 'username',
- autofillHintsForFields: [
- 'username',
- 'email',
- 'name',
- 'telephoneNumber'
- ]);
- final MethodCall setClient = MethodCall('TextInput.setClient',
- <dynamic>[123, flutterMultiAutofillElementConfig]);
- sendFrameworkMessage(codec.encodeMethodCall(setClient));
-
- const MethodCall setEditingState1 =
- MethodCall('TextInput.setEditingState', <String, dynamic>{
- 'text': 'abcd',
- 'selectionBase': 2,
- 'selectionExtent': 3,
- });
- sendFrameworkMessage(codec.encodeMethodCall(setEditingState1));
-
- const MethodCall show = MethodCall('TextInput.show');
- sendFrameworkMessage(codec.encodeMethodCall(show));
-
- // The transform is changed. For example after a validation error, red
- // line appeared under the input field.
- final MethodCall setSizeAndTransform =
- configureSetSizeAndTransformMethodCall(150, 50,
- Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
- sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
-
- // Form is added to DOM.
- expect(document.getElementsByTagName('form'), isNotEmpty);
-
- const MethodCall clearClient = MethodCall('TextInput.clearClient');
- sendFrameworkMessage(codec.encodeMethodCall(clearClient));
-
- // Confirm that [HybridTextEditing] didn't send any messages.
- expect(spy.messages, isEmpty);
- // Form stays on the DOM until autofill context is finalized.
- expect(document.getElementsByTagName('form'), isNotEmpty);
- expect(formsOnTheDom, hasLength(1));
-
- const MethodCall finishAutofillContext =
- MethodCall('TextInput.finishAutofillContext', false);
- sendFrameworkMessage(codec.encodeMethodCall(finishAutofillContext));
-
- // Form element is removed from DOM.
- expect(document.getElementsByTagName('form'), hasLength(0));
- expect(formsOnTheDom, hasLength(0));
- },
- // TODO(nurhan): https://github.com/flutter/flutter/issues/50769
- skip: browserEngine == BrowserEngine.edge);
-
- test('Does not accept dom elements of a wrong type', () {
- // A regular <span> shouldn't be accepted.
- final HtmlElement span = SpanElement();
- expect(
- () => SemanticsTextEditingStrategy(
- SemanticsObject(5, null), HybridTextEditing(), span),
- throwsAssertionError,
- );
- });
-
- test('Do not re-acquire focus', () {
- editingElement = SemanticsTextEditingStrategy(
- SemanticsObject(5, null), HybridTextEditing(), testInputElement);
-
- expect(document.activeElement, document.body);
-
- document.body.append(testInputElement);
- editingElement.enable(
- singlelineConfig,
- onChange: trackEditingState,
- onAction: trackInputAction,
- );
- expect(document.activeElement, testInputElement);
-
- // The input should not refocus after blur.
- editingElement.domElement.blur();
- expect(document.activeElement, document.body);
-
- editingElement.disable();
- },
- // TODO(nurhan): https://github.com/flutter/flutter/issues/50769
- skip: browserEngine == BrowserEngine.edge);
-
- test('Does not dispose and recreate dom elements in persistent mode', () {
- editingElement = SemanticsTextEditingStrategy(
- SemanticsObject(5, null), HybridTextEditing(), testInputElement);
-
- // The DOM element should've been eagerly created.
- expect(testInputElement, isNotNull);
- // But doesn't have focus.
- expect(document.activeElement, document.body);
-
- // Can't enable before the input element is inserted into the DOM.
- expect(
- () => editingElement.enable(
- singlelineConfig,
- onChange: trackEditingState,
- onAction: trackInputAction,
- ),
- throwsAssertionError,
- );
-
- document.body.append(testInputElement);
- editingElement.enable(
- singlelineConfig,
- onChange: trackEditingState,
- onAction: trackInputAction,
- );
- expect(document.activeElement, editingElement.domElement);
- // It doesn't create a new DOM element.
- expect(editingElement.domElement, testInputElement);
-
- editingElement.disable();
- // It doesn't remove the DOM element.
- expect(editingElement.domElement, testInputElement);
- expect(document.body.contains(editingElement.domElement), isTrue);
- // Editing element is not enabled.
- expect(editingElement.isEnabled, isFalse);
- // For mobile browsers `blur` is called to close the onscreen keyboard.
- if (operatingSystem == OperatingSystem.iOs &&
- browserEngine == BrowserEngine.webkit) {
- expect(document.activeElement, document.body);
- } else {
- expect(document.activeElement, editingElement.domElement);
- }
- },
- // TODO(nurhan): https://github.com/flutter/flutter/issues/50769
- skip: browserEngine == BrowserEngine.edge);
-
- test('Refocuses when setting editing state', () {
- editingElement = SemanticsTextEditingStrategy(
- SemanticsObject(5, null), HybridTextEditing(), testInputElement);
-
- document.body.append(testInputElement);
- editingElement.enable(
- singlelineConfig,
- onChange: trackEditingState,
- onAction: trackInputAction,
- );
- // The input will have focus after editing state is set.
- editingElement.setEditingState(EditingState(text: 'foo'));
- expect(document.activeElement, testInputElement);
-
- editingElement.disable();
- },
- // TODO(nurhan): https://github.com/flutter/flutter/issues/50769
- skip: browserEngine == BrowserEngine.edge);
-
- test('Works in multi-line mode', () {
- final TextAreaElement textarea = TextAreaElement();
- editingElement = SemanticsTextEditingStrategy(
- SemanticsObject(5, null), HybridTextEditing(), textarea);
-
- expect(editingElement.domElement, textarea);
- expect(document.activeElement, document.body);
-
- // Can't enable before the textarea is inserted into the DOM.
- expect(
- () => editingElement.enable(
- singlelineConfig,
- onChange: trackEditingState,
- onAction: trackInputAction,
- ),
- throwsAssertionError,
- );
-
- document.body.append(textarea);
- editingElement.enable(
- multilineConfig,
- onChange: trackEditingState,
- onAction: trackInputAction,
- );
- // Focuses the textarea.
- expect(document.activeElement, textarea);
-
- textarea.blur();
- // The textArea loses focus.
- expect(document.activeElement, document.body);
-
- editingElement.disable();
- // It doesn't remove the textarea from the DOM.
- expect(document.body.contains(editingElement.domElement), isTrue);
- // Editing element is not enabled.
- expect(editingElement.isEnabled, isFalse);
- },
- // TODO(nurhan): https://github.com/flutter/flutter/issues/50769
- skip: browserEngine == BrowserEngine.edge);
-
- test('Does not position or size its DOM element', () {
- editingElement.enable(
- singlelineConfig,
- onChange: trackEditingState,
- onAction: trackInputAction,
- );
- testTextEditing.setEditableSizeAndTransform(EditableTextGeometry(
- height: 12,
- width: 13,
- globalTransform: Matrix4.translationValues(14, 15, 0).storage,
- ));
-
- void checkPlacementIsEmpty() {
- expect(editingElement.domElement.style.transform, '');
- expect(editingElement.domElement.style.width, '');
- expect(editingElement.domElement.style.height, '');
- }
-
- checkPlacementIsEmpty();
- editingElement.placeElement();
- checkPlacementIsEmpty();
+ expect(editingStrategy.domElement.style.width, '13px');
+ expect(editingStrategy.domElement.style.height, '12px');
});
});
@@ -644,7 +412,7 @@
}
String getEditingInputMode() {
- return textEditing.editingElement.domElement.getAttribute('inputmode');
+ return textEditing.strategy.domElement.getAttribute('inputmode');
}
setUp(() {
@@ -671,7 +439,7 @@
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
- checkInputEditingState(textEditing.editingElement.domElement, '', 0, 0);
+ checkInputEditingState(textEditing.strategy.domElement, '', 0, 0);
const MethodCall setEditingState =
MethodCall('TextInput.setEditingState', <String, dynamic>{
@@ -682,7 +450,7 @@
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
checkInputEditingState(
- textEditing.editingElement.domElement, 'abcd', 2, 3);
+ textEditing.strategy.domElement, 'abcd', 2, 3);
const MethodCall hide = MethodCall('TextInput.hide');
sendFrameworkMessage(codec.encodeMethodCall(hide));
@@ -714,7 +482,7 @@
sendFrameworkMessage(codec.encodeMethodCall(show));
checkInputEditingState(
- textEditing.editingElement.domElement, 'abcd', 2, 3);
+ textEditing.strategy.domElement, 'abcd', 2, 3);
const MethodCall clearClient = MethodCall('TextInput.clearClient');
sendFrameworkMessage(codec.encodeMethodCall(clearClient));
@@ -745,7 +513,7 @@
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
- final HtmlElement element = textEditing.editingElement.domElement;
+ final HtmlElement element = textEditing.strategy.domElement;
expect(element.getAttribute('readonly'), 'readonly');
// Update the read-only config.
@@ -787,10 +555,11 @@
sendFrameworkMessage(codec.encodeMethodCall(show));
checkInputEditingState(
- textEditing.editingElement.domElement, 'abcd', 2, 3);
+ textEditing.strategy.domElement, 'abcd', 2, 3);
+ expect(textEditing.isEditing, isTrue);
// DOM element is blurred.
- textEditing.editingElement.domElement.blur();
+ textEditing.strategy.domElement.blur();
// For ios-safari the connection is closed.
if (browserEngine == BrowserEngine.webkit &&
@@ -807,11 +576,11 @@
expect(spy.messages, hasLength(0));
await Future<void>.delayed(Duration.zero);
// DOM element still keeps the focus.
- expect(document.activeElement, textEditing.editingElement.domElement);
+ expect(document.activeElement, textEditing.strategy.domElement);
}
},
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
- skip: (browserEngine == BrowserEngine.edge));
+ skip: browserEngine == BrowserEngine.edge);
test('finishAutofillContext closes connection no autofill element',
() async {
@@ -834,7 +603,7 @@
sendFrameworkMessage(codec.encodeMethodCall(show));
checkInputEditingState(
- textEditing.editingElement.domElement, 'abcd', 2, 3);
+ textEditing.strategy.domElement, 'abcd', 2, 3);
const MethodCall finishAutofillContext =
MethodCall('TextInput.finishAutofillContext', false);
@@ -859,14 +628,16 @@
test('finishAutofillContext removes form from DOM', () async {
// Create a configuration with an AutofillGroup of four text fields.
final Map<String, dynamic> flutterMultiAutofillElementConfig =
- createFlutterConfig('text',
- autofillHint: 'username',
- autofillHintsForFields: [
- 'username',
- 'email',
- 'name',
- 'telephoneNumber'
- ]);
+ createFlutterConfig(
+ 'text',
+ autofillHint: 'username',
+ autofillHintsForFields: [
+ 'username',
+ 'email',
+ 'name',
+ 'telephoneNumber'
+ ],
+ );
final MethodCall setClient = MethodCall('TextInput.setClient',
<dynamic>[123, flutterMultiAutofillElementConfig]);
sendFrameworkMessage(codec.encodeMethodCall(setClient));
@@ -1044,7 +815,7 @@
sendFrameworkMessage(codec.encodeMethodCall(show));
checkInputEditingState(
- textEditing.editingElement.domElement, 'abcd', 2, 3);
+ textEditing.strategy.domElement, 'abcd', 2, 3);
final MethodCall setClient2 = MethodCall(
'TextInput.setClient', <dynamic>[567, flutterSinglelineConfig]);
@@ -1086,7 +857,7 @@
// The second [setEditingState] should override the first one.
checkInputEditingState(
- textEditing.editingElement.domElement, 'xyz', 0, 2);
+ textEditing.strategy.domElement, 'xyz', 0, 2);
const MethodCall clearClient = MethodCall('TextInput.clearClient');
sendFrameworkMessage(codec.encodeMethodCall(clearClient));
@@ -1123,7 +894,7 @@
// The second [setEditingState] should override the first one.
checkInputEditingState(
- textEditing.editingElement.domElement, 'abcd', 2, 3);
+ textEditing.strategy.domElement, 'abcd', 2, 3);
final FormElement formElement = document.getElementsByTagName('form')[0];
// The form has one input element and one submit button.
@@ -1161,7 +932,7 @@
sendFrameworkMessage(codec.encodeMethodCall(show));
final InputElement inputElement =
- textEditing.editingElement.domElement as InputElement;
+ textEditing.strategy.domElement as InputElement;
expect(inputElement.value, 'abcd');
if (!(browserEngine == BrowserEngine.webkit &&
operatingSystem == OperatingSystem.macOs)) {
@@ -1181,11 +952,11 @@
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
// Check the element still has focus. User can keep editing.
- expect(document.activeElement, textEditing.editingElement.domElement);
+ expect(document.activeElement, textEditing.strategy.domElement);
// Check the cursor location is the same.
checkInputEditingState(
- textEditing.editingElement.domElement, 'abcd', 2, 3);
+ textEditing.strategy.domElement, 'abcd', 2, 3);
const MethodCall clearClient = MethodCall('TextInput.clearClient');
sendFrameworkMessage(codec.encodeMethodCall(clearClient));
@@ -1232,7 +1003,7 @@
// The second [setEditingState] should override the first one.
checkInputEditingState(
- textEditing.editingElement.domElement, 'abcd', 2, 3);
+ textEditing.strategy.domElement, 'abcd', 2, 3);
final FormElement formElement = document.getElementsByTagName('form')[0];
// The form has 4 input elements and one submit button.
@@ -1274,12 +1045,12 @@
if (browserEngine == BrowserEngine.webkit &&
operatingSystem == OperatingSystem.iOs) {
expect(
- textEditing.editingElement.domElement
+ textEditing.strategy.domElement
.getAttribute('autocapitalize'),
'off');
} else {
expect(
- textEditing.editingElement.domElement
+ textEditing.strategy.domElement
.getAttribute('autocapitalize'),
isNull);
}
@@ -1313,7 +1084,7 @@
if (browserEngine == BrowserEngine.webkit &&
operatingSystem == OperatingSystem.iOs) {
expect(
- textEditing.editingElement.domElement
+ textEditing.strategy.domElement
.getAttribute('autocapitalize'),
'characters');
}
@@ -1349,7 +1120,7 @@
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
- final HtmlElement domElement = textEditing.editingElement.domElement;
+ final HtmlElement domElement = textEditing.strategy.domElement;
checkInputEditingState(domElement, 'abcd', 2, 3);
@@ -1360,7 +1131,7 @@
const Point<double>(160.0, 70.0)));
expect(domElement.style.transform,
'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, 20, 30, 1)');
- expect(textEditing.editingElement.domElement.style.font,
+ expect(textEditing.strategy.domElement.style.font,
'500 12px sans-serif');
const MethodCall clearClient = MethodCall('TextInput.clearClient');
@@ -1405,7 +1176,7 @@
});
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
- final HtmlElement domElement = textEditing.editingElement.domElement;
+ final HtmlElement domElement = textEditing.strategy.domElement;
checkInputEditingState(domElement, 'abcd', 2, 3);
@@ -1420,7 +1191,7 @@
'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, 20, 30, 1)',
);
expect(
- textEditing.editingElement.domElement.style.font,
+ textEditing.strategy.domElement.style.font,
'500 12px sans-serif',
);
@@ -1428,11 +1199,11 @@
if (browserEngine == BrowserEngine.blink ||
browserEngine == BrowserEngine.samsung ||
browserEngine == BrowserEngine.webkit) {
- expect(textEditing.editingElement.domElement.classes,
+ expect(textEditing.strategy.domElement.classes,
contains('transparentTextEditing'));
} else {
expect(
- textEditing.editingElement.domElement.classes.any(
+ textEditing.strategy.domElement.classes.any(
(element) => element.toString() == 'transparentTextEditing'),
isFalse);
}
@@ -1468,7 +1239,7 @@
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
- final HtmlElement domElement = textEditing.editingElement.domElement;
+ final HtmlElement domElement = textEditing.strategy.domElement;
checkInputEditingState(domElement, 'abcd', 2, 3);
@@ -1480,7 +1251,7 @@
expect(domElement.style.transform,
'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, 20, 30, 1)');
expect(
- textEditing.editingElement.domElement.style.font, '12px sans-serif');
+ textEditing.strategy.domElement.style.font, '12px sans-serif');
hideKeyboard();
},
@@ -1490,7 +1261,7 @@
test('Canonicalizes font family', () {
showKeyboard();
- final HtmlElement input = textEditing.editingElement.domElement;
+ final HtmlElement input = textEditing.strategy.domElement;
MethodCall setStyle;
@@ -1529,7 +1300,7 @@
// Check if the selection range is correct.
checkInputEditingState(
- textEditing.editingElement.domElement, 'xyz', 1, 2);
+ textEditing.strategy.domElement, 'xyz', 1, 2);
const MethodCall setEditingState2 =
MethodCall('TextInput.setEditingState', <String, dynamic>{
@@ -1541,7 +1312,7 @@
// The negative offset values are applied to the dom element as 0.
checkInputEditingState(
- textEditing.editingElement.domElement, 'xyz', 0, 0);
+ textEditing.strategy.domElement, 'xyz', 0, 0);
hideKeyboard();
});
@@ -1562,7 +1333,7 @@
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
- final InputElement input = textEditing.editingElement.domElement;
+ final InputElement input = textEditing.strategy.domElement;
input.value = 'something';
input.dispatchEvent(Event.eventType('Event', 'input'));
@@ -1586,7 +1357,7 @@
input.setSelectionRange(2, 5);
if (browserEngine == BrowserEngine.firefox) {
Event keyup = KeyboardEvent('keyup');
- textEditing.editingElement.domElement.dispatchEvent(keyup);
+ textEditing.strategy.domElement.dispatchEvent(keyup);
} else {
document.dispatchEvent(Event.eventType('Event', 'selectionchange'));
}
@@ -1644,7 +1415,7 @@
// The second [setEditingState] should override the first one.
checkInputEditingState(
- textEditing.editingElement.domElement, 'abcd', 2, 3);
+ textEditing.strategy.domElement, 'abcd', 2, 3);
final FormElement formElement = document.getElementsByTagName('form')[0];
// The form has 4 input elements and one submit button.
@@ -1695,7 +1466,7 @@
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
- final TextAreaElement textarea = textEditing.editingElement.domElement;
+ final TextAreaElement textarea = textEditing.strategy.domElement;
checkTextAreaEditingState(textarea, '', 0, 0);
// Can set editing state and preserve new lines.
@@ -1713,7 +1484,7 @@
textarea.dispatchEvent(Event.eventType('Event', 'input'));
textarea.setSelectionRange(2, 5);
if (browserEngine == BrowserEngine.firefox) {
- textEditing.editingElement.domElement
+ textEditing.strategy.domElement
.dispatchEvent(KeyboardEvent('keyup'));
} else {
document.dispatchEvent(Event.eventType('Event', 'selectionchange'));
@@ -1837,7 +1608,7 @@
expect(lastInputAction, isNull);
dispatchKeyboardEvent(
- textEditing.editingElement.domElement,
+ textEditing.strategy.domElement,
'keydown',
keyCode: _kReturnKeyCode,
);
@@ -1860,7 +1631,7 @@
);
final KeyboardEvent event = dispatchKeyboardEvent(
- textEditing.editingElement.domElement,
+ textEditing.strategy.domElement,
'keydown',
keyCode: _kReturnKeyCode,
);
@@ -2088,9 +1859,9 @@
EditingState _editingState;
setUp(() {
- editingElement =
+ editingStrategy =
GloballyPositionedTextEditingStrategy(HybridTextEditing());
- editingElement.enable(
+ editingStrategy.enable(
singlelineConfig,
onChange: trackEditingState,
onAction: trackInputAction,
@@ -2128,8 +1899,8 @@
});
test('Configure text area element from the editing state', () {
- cleanTextEditingElement();
- editingElement.enable(
+ cleanTextEditingStrategy();
+ editingStrategy.enable(
multilineConfig,
onChange: trackEditingState,
onAction: trackInputAction,
@@ -2161,8 +1932,8 @@
});
test('Get Editing State from text area element', () {
- cleanTextEditingElement();
- editingElement.enable(
+ cleanTextEditingStrategy();
+ editingStrategy.enable(
multilineConfig,
onChange: trackEditingState,
onAction: trackInputAction,
@@ -2242,10 +2013,10 @@
/// Will disable editing element which will also clean the backup DOM
/// element from the page.
-void cleanTextEditingElement() {
- if (editingElement != null && editingElement.isEnabled) {
+void cleanTextEditingStrategy() {
+ if (editingStrategy != null && editingStrategy.isEnabled) {
// Clean up all the DOM elements and event listeners.
- editingElement.disable();
+ editingStrategy.disable();
}
}