Fix a11y tab traversal (flutter/engine#25797)
* fix race with framework when using tab to traverse in a11y mode
This commit is contained in:
@@ -92,8 +92,8 @@ class DomRenderer {
|
||||
/// 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.
|
||||
|
||||
@@ -49,12 +49,9 @@ class LabelAndValue extends RoleManager {
|
||||
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();
|
||||
|
||||
@@ -269,8 +269,8 @@ class SemanticsObject {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -583,7 +583,7 @@ class SemanticsObject {
|
||||
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;
|
||||
@@ -784,15 +784,24 @@ class SemanticsObject {
|
||||
/// > 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);
|
||||
@@ -1144,7 +1153,7 @@ class EngineSemanticsOwner {
|
||||
_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.
|
||||
@@ -1221,8 +1230,8 @@ class EngineSemanticsOwner {
|
||||
/// 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;
|
||||
|
||||
@@ -20,6 +20,10 @@ class Tappable extends RoleManager {
|
||||
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));
|
||||
|
||||
@@ -48,6 +52,11 @@ class Tappable extends RoleManager {
|
||||
_stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
// Request focus so that the AT shifts a11y focus to this node.
|
||||
if (semanticsObject.isFlagsDirty && semanticsObject.hasFocus) {
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
void _stopListening() {
|
||||
|
||||
@@ -14,28 +14,94 @@ part of engine;
|
||||
/// 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;
|
||||
@@ -47,28 +113,15 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
_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
|
||||
@@ -79,27 +132,15 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -107,31 +148,47 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
_applyConfiguration(inputConfig);
|
||||
}
|
||||
|
||||
@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!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,20 +203,15 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
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
|
||||
@@ -167,13 +219,13 @@ class TextField extends RoleManager {
|
||||
// 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.
|
||||
//
|
||||
@@ -188,7 +240,7 @@ class TextField extends RoleManager {
|
||||
..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:
|
||||
@@ -211,12 +263,11 @@ class TextField extends RoleManager {
|
||||
/// 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);
|
||||
});
|
||||
@@ -236,14 +287,13 @@ class TextField extends RoleManager {
|
||||
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) {
|
||||
@@ -251,7 +301,7 @@ class TextField extends RoleManager {
|
||||
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
|
||||
//
|
||||
@@ -272,15 +322,75 @@ class TextField extends RoleManager {
|
||||
}, 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ part of engine;
|
||||
/// 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;
|
||||
|
||||
@@ -485,6 +488,14 @@ class EditingState {
|
||||
///
|
||||
/// [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;
|
||||
@@ -494,6 +505,25 @@ class EditingState {
|
||||
html.TextAreaElement element = domElement;
|
||||
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');
|
||||
}
|
||||
@@ -652,9 +682,9 @@ class GloballyPositionedTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -685,7 +715,7 @@ class SafariDesktopTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
/// 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
|
||||
@@ -702,16 +732,16 @@ class SafariDesktopTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -744,12 +774,20 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
|
||||
@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;
|
||||
@@ -783,18 +821,18 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
|
||||
}) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -809,19 +847,19 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
|
||||
_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
|
||||
@@ -837,16 +875,16 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -860,12 +898,11 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
@mustCallSuper
|
||||
@override
|
||||
void updateElementStyle(EditableTextStyle style) {
|
||||
_style = style;
|
||||
if (isEnabled) {
|
||||
_style!.applyToDomElement(domElement);
|
||||
_style!.applyToDomElement(activeDomElement);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,16 +925,15 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
|
||||
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;
|
||||
@@ -908,18 +944,18 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -962,7 +998,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
|
||||
}
|
||||
|
||||
// Re-focuses after setting editing state.
|
||||
domElement.focus();
|
||||
activeDomElement.focus();
|
||||
}
|
||||
|
||||
/// Prevent default behavior for mouse down, up and move.
|
||||
@@ -971,15 +1007,15 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
|
||||
/// 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();
|
||||
}));
|
||||
}
|
||||
@@ -1038,11 +1074,11 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
}) {
|
||||
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
|
||||
@@ -1050,7 +1086,7 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
/// 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;
|
||||
}
|
||||
@@ -1063,14 +1099,14 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
}
|
||||
|
||||
// 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();
|
||||
}));
|
||||
@@ -1082,7 +1118,7 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
//
|
||||
// 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();
|
||||
}));
|
||||
}
|
||||
@@ -1120,7 +1156,7 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
/// [_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.
|
||||
@@ -1144,8 +1180,8 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
|
||||
@override
|
||||
void placeElement() {
|
||||
domElement.focus();
|
||||
_geometry?.applyToDomElement(domElement);
|
||||
activeDomElement.focus();
|
||||
_geometry?.applyToDomElement(activeDomElement);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1167,13 +1203,13 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
}) {
|
||||
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
|
||||
@@ -1184,19 +1220,19 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
@@ -1205,8 +1241,8 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
|
||||
@override
|
||||
void placeElement() {
|
||||
domElement.focus();
|
||||
_geometry?.applyToDomElement(domElement);
|
||||
activeDomElement.focus();
|
||||
_geometry?.applyToDomElement(activeDomElement);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1238,9 +1274,9 @@ class FirefoxTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
@@ -1255,18 +1291,18 @@ class FirefoxTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
//
|
||||
// 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();
|
||||
}));
|
||||
|
||||
@@ -1279,23 +1315,214 @@ class FirefoxTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
// 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].
|
||||
///
|
||||
@@ -1311,103 +1538,84 @@ class TextEditingChannel {
|
||||
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(
|
||||
@@ -1477,118 +1685,15 @@ class HybridTextEditing {
|
||||
/// 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.
|
||||
@@ -1597,13 +1702,30 @@ class HybridTextEditing {
|
||||
@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);
|
||||
},
|
||||
@@ -1616,7 +1738,7 @@ class HybridTextEditing {
|
||||
void stopEditing() {
|
||||
assert(isEditing);
|
||||
isEditing = false;
|
||||
editingElement.disable();
|
||||
strategy.disable();
|
||||
}
|
||||
|
||||
void sendTextConnectionClosedToFrameworkIfAny() {
|
||||
|
||||
@@ -17,7 +17,7 @@ import 'package:test/test.dart';
|
||||
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 @@ void main() {
|
||||
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 @@ void _testTappable() {
|
||||
..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 _testLiveRegion() {
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import 'package:test/test.dart';
|
||||
|
||||
import 'package:ui/src/engine.dart' hide window;
|
||||
|
||||
import 'matchers.dart';
|
||||
import 'spy.dart';
|
||||
|
||||
/// The `keyCode` of the "Enter" key.
|
||||
@@ -24,7 +23,7 @@ const MethodCodec codec = JSONMethodCodec();
|
||||
/// 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 @@ void testMain() {
|
||||
tearDown(() {
|
||||
lastEditingState = null;
|
||||
lastInputAction = null;
|
||||
cleanTextEditingElement();
|
||||
cleanTextEditingStrategy();
|
||||
cleanTestFlags();
|
||||
clearBackUpDomElementIfExists();
|
||||
});
|
||||
@@ -67,8 +66,9 @@ void testMain() {
|
||||
|
||||
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 @@ void testMain() {
|
||||
// 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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
});
|
||||
|
||||
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 @@ void testMain() {
|
||||
});
|
||||
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
);
|
||||
|
||||
// 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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
});
|
||||
|
||||
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 @@ void testMain() {
|
||||
expect(lastInputAction, isNull);
|
||||
|
||||
dispatchKeyboardEvent(
|
||||
editingElement.domElement,
|
||||
editingStrategy.domElement,
|
||||
'keydown',
|
||||
keyCode: _kReturnKeyCode,
|
||||
);
|
||||
@@ -313,7 +322,7 @@ void testMain() {
|
||||
inputType: EngineInputType.multiline,
|
||||
inputAction: 'TextInputAction.done',
|
||||
);
|
||||
editingElement.enable(
|
||||
editingStrategy.enable(
|
||||
config,
|
||||
onChange: trackEditingState,
|
||||
onAction: trackInputAction,
|
||||
@@ -323,7 +332,7 @@ void testMain() {
|
||||
expect(lastInputAction, isNull);
|
||||
|
||||
final KeyboardEvent event = dispatchKeyboardEvent(
|
||||
editingElement.domElement,
|
||||
editingStrategy.domElement,
|
||||
'keydown',
|
||||
keyCode: _kReturnKeyCode,
|
||||
);
|
||||
@@ -335,270 +344,29 @@ void testMain() {
|
||||
});
|
||||
|
||||
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 @@ void testMain() {
|
||||
}
|
||||
|
||||
String getEditingInputMode() {
|
||||
return textEditing.editingElement.domElement.getAttribute('inputmode');
|
||||
return textEditing.strategy.domElement.getAttribute('inputmode');
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
@@ -671,7 +439,7 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
|
||||
// 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 @@ void testMain() {
|
||||
|
||||
// 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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
|
||||
// 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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
if (browserEngine == BrowserEngine.webkit &&
|
||||
operatingSystem == OperatingSystem.iOs) {
|
||||
expect(
|
||||
textEditing.editingElement.domElement
|
||||
textEditing.strategy.domElement
|
||||
.getAttribute('autocapitalize'),
|
||||
'characters');
|
||||
}
|
||||
@@ -1349,7 +1120,7 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
});
|
||||
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 @@ void testMain() {
|
||||
'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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
test('Canonicalizes font family', () {
|
||||
showKeyboard();
|
||||
|
||||
final HtmlElement input = textEditing.editingElement.domElement;
|
||||
final HtmlElement input = textEditing.strategy.domElement;
|
||||
|
||||
MethodCall setStyle;
|
||||
|
||||
@@ -1529,7 +1300,7 @@ void testMain() {
|
||||
|
||||
// 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 @@ void testMain() {
|
||||
|
||||
// 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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
|
||||
// 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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
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 @@ void testMain() {
|
||||
expect(lastInputAction, isNull);
|
||||
|
||||
dispatchKeyboardEvent(
|
||||
textEditing.editingElement.domElement,
|
||||
textEditing.strategy.domElement,
|
||||
'keydown',
|
||||
keyCode: _kReturnKeyCode,
|
||||
);
|
||||
@@ -1860,7 +1631,7 @@ void testMain() {
|
||||
);
|
||||
|
||||
final KeyboardEvent event = dispatchKeyboardEvent(
|
||||
textEditing.editingElement.domElement,
|
||||
textEditing.strategy.domElement,
|
||||
'keydown',
|
||||
keyCode: _kReturnKeyCode,
|
||||
);
|
||||
@@ -2088,9 +1859,9 @@ void testMain() {
|
||||
EditingState _editingState;
|
||||
|
||||
setUp(() {
|
||||
editingElement =
|
||||
editingStrategy =
|
||||
GloballyPositionedTextEditingStrategy(HybridTextEditing());
|
||||
editingElement.enable(
|
||||
editingStrategy.enable(
|
||||
singlelineConfig,
|
||||
onChange: trackEditingState,
|
||||
onAction: trackInputAction,
|
||||
@@ -2128,8 +1899,8 @@ void testMain() {
|
||||
});
|
||||
|
||||
test('Configure text area element from the editing state', () {
|
||||
cleanTextEditingElement();
|
||||
editingElement.enable(
|
||||
cleanTextEditingStrategy();
|
||||
editingStrategy.enable(
|
||||
multilineConfig,
|
||||
onChange: trackEditingState,
|
||||
onAction: trackInputAction,
|
||||
@@ -2161,8 +1932,8 @@ void testMain() {
|
||||
});
|
||||
|
||||
test('Get Editing State from text area element', () {
|
||||
cleanTextEditingElement();
|
||||
editingElement.enable(
|
||||
cleanTextEditingStrategy();
|
||||
editingStrategy.enable(
|
||||
multilineConfig,
|
||||
onChange: trackEditingState,
|
||||
onAction: trackInputAction,
|
||||
@@ -2242,10 +2013,10 @@ MethodCall configureSetSizeAndTransformMethodCall(
|
||||
|
||||
/// 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user