diff --git a/lib/web_ui/lib/src/engine/dom_renderer.dart b/lib/web_ui/lib/src/engine/dom_renderer.dart index 75d04623a2bdc..fd9bb48d88488 100644 --- a/lib/web_ui/lib/src/engine/dom_renderer.dart +++ b/lib/web_ui/lib/src/engine/dom_renderer.dart @@ -317,19 +317,8 @@ flt-glass-pane * { setElementStyle(bodyElement, 'font', defaultCssFont); setElementStyle(bodyElement, 'color', 'red'); - // TODO(flutter_web): send the location during the scroll for more frequent - // location updates from the framework. Remove spellcheck=false property. - /// The spell check is being disabled for now. - /// - /// Flutter web is positioning the input box on top of editable widget. - /// This location is updated only in the paint phase of the widget. - /// It is wrong during the scroll. It is not important for text editing - /// since the content is already invisible. On the other hand, the red - /// indicator for spellcheck gets confusing due to the wrong positioning. - /// We are disabling spellcheck until the location starts getting updated - /// via scroll. This is possible since we can listen to the scroll on - /// Flutter. - /// See [HybridTextEditing]. + // TODO(flutter_web): Disable spellcheck until changes in the framework and + // engine are complete. bodyElement.spellcheck = false; for (html.Element viewportMeta diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index 5a8cdf8cb4ff1..3e2ee972fb66f 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -323,11 +323,6 @@ class TouchAdapter extends BaseAdapter { event.preventDefault(); _updateButtonDownState(_kPrimaryMouseButton, false); _callback(_convertEventToPointerData(ui.PointerChange.up, event)); - if (textEditing.needsKeyboard && - browserEngine == BrowserEngine.webkit && - operatingSystem == OperatingSystem.iOs) { - textEditing.editingElement.configureInputElementForIOS(); - } }); _addEventListener('touchcancel', (html.Event event) { 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 9cee4586d5936..580a83108a71d 100644 --- a/lib/web_ui/lib/src/engine/semantics/text_field.dart +++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart @@ -38,7 +38,6 @@ class TextField extends RoleManager { // and autocorrect suggestion. To disable that, we have to do the following: _textFieldElement ..spellcheck = false - ..setAttribute('spellcheck', 'false') ..setAttribute('autocorrect', 'off') ..setAttribute('autocomplete', 'off') ..setAttribute('data-semantics-role', 'text-field'); diff --git a/lib/web_ui/lib/src/engine/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing.dart index 526899a5a1846..116401de8c609 100644 --- a/lib/web_ui/lib/src/engine/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing.dart @@ -203,6 +203,22 @@ class TextEditingElement { /// See [TextEditingElement.persistent] to understand what persistent mode is. TextEditingElement(this.owner); + /// Timer that times when to set the location of the input text. + /// + /// This is only used for iOS. In iOS, virtual keyboard shifts the screen. + /// There is no callback to know if the keyboard is up and how much the screen + /// has shifted. Therefore instead of listening to the shift and passing this + /// information to Flutter Framework, we are trying to stop the shift. + /// + /// In iOS, the virtual keyboard shifts the screen up if the focused input + /// element is under the keyboard or very close to the keyboard. Before the + /// focus is called we are positioning it offscreen. The location of the input + /// in iOS is set to correct place, 100ms after focus. We use this timer for + /// timing this delay. + Timer _positionInputElementTimer; + static const Duration _delayBeforePositioning = + const Duration(milliseconds: 100); + final HybridTextEditing owner; bool _enabled = false; @@ -222,20 +238,22 @@ class TextEditingElement { /// On iOS, sets the location of the input element after focusing on it. /// /// On iOS, keyboard causes scrolling in the UI. This scrolling does not - /// trigger an event. In order to position the input element correctly, it is + /// trigger an event. In order not to trigger a shift on the page, it is /// important we set it's final location after focusing on it (after keyboard /// is up). /// - /// This method is called in the end of the 'touchend' event, therefore it is - /// called after the editing state is set. + /// This method is called after a delay. + /// See [_positionInputElementTimer]. void configureInputElementForIOS() { if (browserEngine != BrowserEngine.webkit || operatingSystem != OperatingSystem.iOs) { - // Only relevant on Safari. + // Only relevant on Safari-based on iOS. return; } + if (domElement != null) { owner.setStyle(domElement); + owner.inputPositioned = true; } } @@ -274,6 +292,9 @@ class TextEditingElement { })); } + if (owner.doesKeyboardShiftInput) { + _preventShiftDuringFocus(); + } domElement.focus(); if (_lastEditingState != null) { @@ -299,6 +320,9 @@ class TextEditingElement { _subscriptions[i].cancel(); } _subscriptions.clear(); + _positionInputElementTimer?.cancel(); + _positionInputElementTimer = null; + owner.inputPositioned = false; _removeDomElement(); } @@ -328,6 +352,32 @@ class TextEditingElement { domElement.focus(); } + void _preventShiftDuringFocus() { + // Position the element outside of the page before focusing on it. + // + // See [_positionInputElementTimer]. + owner.setStyleOutsideOfScreen(domElement); + + _subscriptions.add(domElement.onFocus.listen((_) { + // Cancel previous timer if exists. + _positionInputElementTimer?.cancel(); + _positionInputElementTimer = Timer(_delayBeforePositioning, () { + if (textEditing.inputElementNeedsToBePositioned) { + configureInputElementForIOS(); + } + }); + + // When the virtual keyboard is closed on iOS, onBlur is triggered. + _subscriptions.add(domElement.onBlur.listen((_) { + // Cancel the timer since there is no need to set the location of the + // input element anymore. It needs to be focused again to be editable + // by the user. + _positionInputElementTimer?.cancel(); + _positionInputElementTimer = null; + })); + })); + } + void setEditingState(EditingState editingState) { _lastEditingState = editingState; if (!_enabled || !editingState.isValid) { @@ -362,8 +412,12 @@ class TextEditingElement { break; } - // Safari on iOS requires that we focus explicitly. Otherwise, the on-screen - // keyboard won't show up. + + if(owner.inputElementNeedsToBePositioned) { + _preventShiftDuringFocus(); + } + + // Re-focuses when setting editing state. domElement.focus(); } @@ -583,8 +637,18 @@ class HybridTextEditing { /// Also used to define if a keyboard is needed. bool _isEditing = false; - /// Flag indicating if the flutter framework requested a keyboard. - bool get needsKeyboard => _isEditing; + /// Indicates whether the input element needs to be positioned. + /// + /// See [TextEditingElement._delayBeforePositioning]. + bool get inputElementNeedsToBePositioned => + !inputPositioned && + _isEditing && + doesKeyboardShiftInput; + + /// Flag indicating whether the input element's position is set. + /// + /// See [inputElementNeedsToBePositioned]. + bool inputPositioned = false; Map _configuration; @@ -710,15 +774,30 @@ class HybridTextEditing { ); } + /// Positioning of input element is only done if we are not expecting input + /// to be shifted by a virtual keyboard or if the input is already positioned. + /// + /// Otherwise positioning will be done after focusing on the input. + /// See [TextEditingElement._delayBeforePositioning]. + bool get _canPositionInput => inputPositioned || !doesKeyboardShiftInput; + + /// Indicates whether virtual keyboard shifts the location of input element. + /// + /// Value decided using the operating system and the browser engine. + /// + /// In iOS, the virtual keyboard might shifts the screen up to make input + /// visible depending on the location of the focused input element. + bool get doesKeyboardShiftInput => + browserEngine == BrowserEngine.webkit && + operatingSystem == OperatingSystem.iOs; + /// These style attributes are dynamic throughout the life time of an input /// element. /// /// They are changed depending on the messages coming from method calls: /// "TextInput.setStyle", "TextInput.setEditableSizeAndTransform". void _setDynamicStyleAttributes(html.HtmlElement domElement) { - if (_editingLocationAndSize != null && - !(browserEngine == BrowserEngine.webkit && - operatingSystem == OperatingSystem.iOs)) { + if (_editingLocationAndSize != null && _canPositionInput) { setStyle(domElement); } } @@ -741,6 +820,19 @@ class HybridTextEditing { ..transform = transformCss; } + // TODO(flutter_web): After the browser closes and re-opens the virtual + // shifts the page in iOS. Call this method from visibility change listener + // attached to body. + /// Set the dom element's location somewhere outside of the screen. + /// + /// This is useful for not triggering a scroll when iOS virtual keyboard is + /// coming up. + /// + /// See [TextEditingElement._delayBeforePositioning]. + void setStyleOutsideOfScreen(html.HtmlElement domElement) { + domElement.style.transform = 'translate(-9999px, -9999px)'; + } + html.InputElement createInputElement() { final html.InputElement input = html.InputElement(); _setStaticStyleAttributes(input); @@ -785,8 +877,6 @@ class _EditingStyle { /// /// This information is received via "TextInput.setEditableSizeAndTransform" /// message. Framework currently sends this information on paint. -// TODO(flutter_web): send the location during the scroll for more frequent -// updates from the framework. class _EditableSizeAndTransform { _EditableSizeAndTransform({ @required this.width,