diff --git a/lib/web_ui/lib/src/engine/semantics/text_field.dart b/lib/web_ui/lib/src/engine/semantics/text_field.dart index 02bbf852d592c..3fd681a61dbea 100644 --- a/lib/web_ui/lib/src/engine/semantics/text_field.dart +++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart @@ -4,6 +4,70 @@ part of engine; +/// Text editing used by accesibility mode. +/// +/// [SemanticsTextEditingStrategy] assumes the caller will own the creation, +/// insertion and disposal of the DOM element. Due to this +/// [initializeElementPlacement], [initializeTextEditing] and +/// [disable] strategies are handled differently. +/// +/// 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 { + /// Creates a [SemanticsTextEditingStrategy] that eagerly instantiates + /// [domElement] so the caller can insert it before calling + /// [SemanticsTextEditingStrategy.enable]. + SemanticsTextEditingStrategy( + HybridTextEditing owner, html.HtmlElement domElement) + : 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; + } + + @override + void disable() { + // We don't want to remove the DOM element because the caller is responsible + // for that. + // + // Remove focus from the editable element to cause the keyboard to hide. + // Otherwise, the keyboard stays on screen even when the user navigates to + // a different screen (e.g. by hitting the "back" button). + domElement.blur(); + } + + @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(domElement != null); + assert(html.document.body.contains(domElement)); + + isEnabled = true; + _inputConfiguration = inputConfig; + _onChange = onChange; + _onAction = onAction; + + domElement.focus(); + } + + @override + void setEditingState(EditingState editingState) { + super.setEditingState(editingState); + + // Refocus after setting editing state. + domElement.focus(); + } +} + /// Manages semantics objects that represent editable text fields. /// /// This role is implemented via a content-editable HTML element. This role does @@ -19,15 +83,15 @@ class TextField extends RoleManager { semanticsObject.hasFlag(ui.SemanticsFlag.isMultiline) ? html.TextAreaElement() : html.InputElement(); - persistentTextEditingElement = PersistentTextEditingElement( + textEditingElement = SemanticsTextEditingStrategy( textEditing, editableDomElement, ); _setupDomElement(); } - PersistentTextEditingElement persistentTextEditingElement; - html.Element get _textFieldElement => persistentTextEditingElement.domElement; + SemanticsTextEditingStrategy textEditingElement; + html.Element get _textFieldElement => textEditingElement.domElement; void _setupDomElement() { // On iOS, even though the semantic text field is transparent, the cursor @@ -61,6 +125,7 @@ class TextField extends RoleManager { switch (browserEngine) { case BrowserEngine.blink: case BrowserEngine.edge: + case BrowserEngine.ie11: case BrowserEngine.firefox: case BrowserEngine.ie11: case BrowserEngine.unknown: @@ -82,7 +147,7 @@ class TextField extends RoleManager { return; } - textEditing.useCustomEditableElement(persistentTextEditingElement); + textEditing.useCustomEditableElement(textEditingElement); ui.window .onSemanticsAction(semanticsObject.id, ui.SemanticsAction.tap, null); }); @@ -98,7 +163,7 @@ class TextField extends RoleManager { num lastTouchStartOffsetY; _textFieldElement.addEventListener('touchstart', (html.Event event) { - textEditing.useCustomEditableElement(persistentTextEditingElement); + textEditing.useCustomEditableElement(textEditingElement); final html.TouchEvent touchEvent = event; lastTouchStartOffsetX = touchEvent.changedTouches.last.client.x; lastTouchStartOffsetY = touchEvent.changedTouches.last.client.y; diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 450de827e367f..b597ebed86192 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -71,7 +71,7 @@ class EditingState { /// Flutter Framework can send the [selectionBase] and [selectionExtent] as /// -1, if so 0 assigned to the [baseOffset] and [extentOffset]. -1 is not a /// valid selection range for input DOM elements. - factory EditingState.fromFlutter(Map flutterEditingState) { + factory EditingState.fromFrameworkMessage(Map flutterEditingState) { final int selectionBase = flutterEditingState['selectionBase']; final int selectionExtent = flutterEditingState['selectionExtent']; final String text = flutterEditingState['text']; @@ -105,7 +105,7 @@ class EditingState { } } - /// The counterpart of [EditingState.fromFlutter]. It generates a Map that + /// The counterpart of [EditingState.fromFrameworkMessage]. It generates a Map that /// can be sent to Flutter. // TODO(mdebbar): Should we get `selectionAffinity` and other properties from flutter's editing state? Map toFlutter() => { @@ -183,7 +183,7 @@ class InputConfiguration { @required this.autocorrect, }); - InputConfiguration.fromFlutter(Map flutterInputConfiguration) + InputConfiguration.fromFrameworkMessage(Map flutterInputConfiguration) : inputType = EngineInputType.fromName( flutterInputConfiguration['inputType']['name']), inputAction = flutterInputConfiguration['inputAction'], @@ -212,12 +212,10 @@ class InputConfiguration { typedef _OnChangeCallback = void Function(EditingState editingState); typedef _OnActionCallback = void Function(String inputAction); -/// Interface defining the template for text editing strategies. +/// Provides HTML DOM functionality for editable text. /// -/// The algorithms will be picked in the runtime depending on the concrete -/// class implementing the interface. -/// -/// These algorithms is expected to differ by operating system and/or browser. +/// A concrete implementation is picked at runtime based on the current +/// operating system, web browser, and accessibility mode. abstract class TextEditingStrategy { void initializeTextEditing( InputConfiguration inputConfig, { @@ -225,14 +223,14 @@ abstract class TextEditingStrategy { @required _OnActionCallback onAction, }); - /// Place the DOM element to its initial position. + /// Sets the initial placement of the DOM element on the UI. /// - /// It will be located exactly in the same place with the editable widgets, - /// however it's contents and cursor will be invisible. + /// The element must be located exactly in the same place with the editable + /// widget. However, its contents and cursor will be invisible. /// - /// Users can interact with the element and use the functionalities of the - /// right-click menu. Such as copy,paste, cut, select, translate... - void initializeElementPosition(); + /// Users can interact with the element and use the functionality of the + /// right-click menu, such as copy, paste, cut, select, translate, etc. + void initializeElementPlacement(); /// Register event listeners to the DOM element. /// @@ -243,7 +241,7 @@ abstract class TextEditingStrategy { /// /// The position will be updated everytime Flutter Framework sends /// 'TextInput.setEditableSizeAndTransform' message. - void updateElementPosition(_GeometricInfo geometricInfo); + void updateElementPlacement(EditableTextGeometry geometry); /// Set editing state of the element. /// @@ -253,7 +251,7 @@ abstract class TextEditingStrategy { void setEditingState(EditingState editingState); /// Set style to the native DOM element used for text editing. - void updateElementStyle(_EditingStyle style); + void updateElementStyle(EditableTextStyle style); /// Disables the element so it's no longer used for text editing. /// @@ -261,11 +259,27 @@ abstract class TextEditingStrategy { void disable(); } +/// A [TextEditingStrategy] that places its [domElement] assuming no +/// prior transform or sizing is applied to it. +/// +/// This implementation is used by text editables when semantics is not +/// enabled. With semantics enabled the placement is provided by the semantics +/// tree. +class GloballyPositionedTextEditingStrategy extends DefaultTextEditingStrategy { + GloballyPositionedTextEditingStrategy(HybridTextEditing owner) : super(owner); + + @override + void placeElement() { + super.placeElement(); + _geometry?.applyToDomElement(domElement); + } +} + /// Class implementing the default editing strategies for text editing. /// /// This class uses a DOM element to provide text editing capabilities. /// -/// The backing DOM element could be one of: +/// The backing DOM element could be one of: /// /// 1. ``. /// 2. `