From c8087d104bd00bda5a4af201a5edad99ee0de864 Mon Sep 17 00:00:00 2001 From: Nurhan Turgut Date: Tue, 10 Sep 2019 13:02:19 -0700 Subject: [PATCH 1/5] enabling spellcheck for text editing --- lib/web_ui/lib/src/engine/dom_renderer.dart | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/lib/web_ui/lib/src/engine/dom_renderer.dart b/lib/web_ui/lib/src/engine/dom_renderer.dart index 75d04623a2bdc..8aa08b8939ac7 100644 --- a/lib/web_ui/lib/src/engine/dom_renderer.dart +++ b/lib/web_ui/lib/src/engine/dom_renderer.dart @@ -316,22 +316,7 @@ flt-glass-pane * { // apps fully specifies their text styles. 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]. - bodyElement.spellcheck = false; - + for (html.Element viewportMeta in html.document.head.querySelectorAll('meta[name="viewport"]')) { if (assertionsEnabled) { From c6b10d89e7f30a50af4d7fcf83f25376e753a78a Mon Sep 17 00:00:00 2001 From: Nurhan Turgut Date: Fri, 20 Sep 2019 13:07:28 -0700 Subject: [PATCH 2/5] Finalize location changes in IOS. IOS is ready for spellcheck now. Disable spellcheck since we realized some tag/autocomplete transfer from Framework is necessary for complete spellcheck implementation. --- lib/web_ui/lib/src/engine/dom_renderer.dart | 6 +- .../lib/src/engine/pointer_binding.dart | 5 - .../lib/src/engine/semantics/text_field.dart | 1 - lib/web_ui/lib/src/engine/text_editing.dart | 96 ++++++++++++++++--- 4 files changed, 89 insertions(+), 19 deletions(-) diff --git a/lib/web_ui/lib/src/engine/dom_renderer.dart b/lib/web_ui/lib/src/engine/dom_renderer.dart index 8aa08b8939ac7..fd9bb48d88488 100644 --- a/lib/web_ui/lib/src/engine/dom_renderer.dart +++ b/lib/web_ui/lib/src/engine/dom_renderer.dart @@ -316,7 +316,11 @@ flt-glass-pane * { // apps fully specifies their text styles. setElementStyle(bodyElement, 'font', defaultCssFont); setElementStyle(bodyElement, 'color', 'red'); - + + // TODO(flutter_web): Disable spellcheck until changes in the framework and + // engine are complete. + bodyElement.spellcheck = false; + for (html.Element viewportMeta in html.document.head.querySelectorAll('meta[name="viewport"]')) { if (assertionsEnabled) { 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..ba9dc127df7e8 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 positining 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 _delayBeforePositining = + 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 Mobile and Chrome on IOS. return; } + if (domElement != null) { owner.setStyle(domElement); + owner.inputPositioned = true; } } @@ -274,6 +292,32 @@ class TextEditingElement { })); } + if (browserEngine == BrowserEngine.webkit && + operatingSystem == OperatingSystem.iOs) { + /// 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(_delayBeforePositining, () { + 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(); + })); + })); + } + domElement.focus(); if (_lastEditingState != null) { @@ -299,6 +343,8 @@ class TextEditingElement { _subscriptions[i].cancel(); } _subscriptions.clear(); + _positionInputElementTimer?.cancel(); + owner.inputPositioned = false; _removeDomElement(); } @@ -361,10 +407,6 @@ class TextEditingElement { ..addRange(_createRange(editingState)); break; } - - // Safari on iOS requires that we focus explicitly. Otherwise, the on-screen - // keyboard won't show up. - domElement.focus(); } /// Swap out the current DOM element and replace it with a new one of type @@ -583,8 +625,19 @@ 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; + /// Flag indication the input element needs to be positioned. + /// + /// See [TextEditingElement._delayBeforePositining]. + bool get inputElementNeedsToBePositioned => + !inputPositioned && + _isEditing && + browserEngine == BrowserEngine.webkit && + operatingSystem == OperatingSystem.iOs; + + /// Flag indication wheterher the input element's position is set. + /// + /// See [inputElementNeedsToBePositioned]. + bool inputPositioned = false; Map _configuration; @@ -715,10 +768,16 @@ class HybridTextEditing { /// /// They are changed depending on the messages coming from method calls: /// "TextInput.setStyle", "TextInput.setEditableSizeAndTransform". + /// + /// In IOS, initial positionig of input element is not done in this method. + /// This method changes the location of the input element if it is already + /// positioned. + /// See [TextEditingElement._delayBeforePositining]. void _setDynamicStyleAttributes(html.HtmlElement domElement) { if (_editingLocationAndSize != null && - !(browserEngine == BrowserEngine.webkit && - operatingSystem == OperatingSystem.iOs)) { + (inputPositioned || + !(browserEngine == BrowserEngine.webkit && + operatingSystem == OperatingSystem.iOs))) { setStyle(domElement); } } @@ -741,6 +800,19 @@ class HybridTextEditing { ..transform = transformCss; } + // TODO(flutter_web): When 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 elements location somewhere outside of the screen. + /// + /// This is useful for not triggering a scroll when IOS virtual keyboard is + /// coming up. + /// + /// See [TextEditingElement._delayBeforePositining]. + void setStyleOutsideOfScreen(html.HtmlElement domElement) { + domElement.style.transform = 'translate(-9999px, -9999px)'; + } + html.InputElement createInputElement() { final html.InputElement input = html.InputElement(); _setStaticStyleAttributes(input); From 3d0194a4b87def245fdb56048014ede63529b34c Mon Sep 17 00:00:00 2001 From: Nurhan Turgut Date: Fri, 20 Sep 2019 13:09:21 -0700 Subject: [PATCH 3/5] Remove todo since this PR fixes the updated location issue for IOS too. All browsers are supported now. --- lib/web_ui/lib/src/engine/text_editing.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing.dart index ba9dc127df7e8..fedc069732d0b 100644 --- a/lib/web_ui/lib/src/engine/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing.dart @@ -857,8 +857,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, From f1126ba95fc3daf87759ee1a57073deda4c4cb78 Mon Sep 17 00:00:00 2001 From: Nurhan Turgut Date: Mon, 23 Sep 2019 07:55:20 -0700 Subject: [PATCH 4/5] Adding boolean flags to make conditions more readable. Fixing typos. --- lib/web_ui/lib/src/engine/text_editing.dart | 39 ++++++++++++--------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing.dart index fedc069732d0b..5e2f05a38f399 100644 --- a/lib/web_ui/lib/src/engine/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing.dart @@ -212,7 +212,7 @@ class TextEditingElement { /// /// 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 positining it offscreen. The location of the input + /// 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; @@ -292,8 +292,7 @@ class TextEditingElement { })); } - if (browserEngine == BrowserEngine.webkit && - operatingSystem == OperatingSystem.iOs) { + if (owner.doesKeyboardShiftInput) { /// Position the element outside of the page before focusing on it. /// /// See [_positionInputElementTimer]. @@ -625,16 +624,15 @@ class HybridTextEditing { /// Also used to define if a keyboard is needed. bool _isEditing = false; - /// Flag indication the input element needs to be positioned. + /// Flag indicating the input element needs to be positioned. /// /// See [TextEditingElement._delayBeforePositining]. bool get inputElementNeedsToBePositioned => !inputPositioned && _isEditing && - browserEngine == BrowserEngine.webkit && - operatingSystem == OperatingSystem.iOs; + doesKeyboardShiftInput; - /// Flag indication wheterher the input element's position is set. + /// Flag indicating whether the input element's position is set. /// /// See [inputElementNeedsToBePositioned]. bool inputPositioned = false; @@ -763,21 +761,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._delayBeforePositining]. + bool get _canPositionInput => inputPositioned || !doesKeyboardShiftInput; + + /// Flag indicating if 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". - /// - /// In IOS, initial positionig of input element is not done in this method. - /// This method changes the location of the input element if it is already - /// positioned. - /// See [TextEditingElement._delayBeforePositining]. void _setDynamicStyleAttributes(html.HtmlElement domElement) { - if (_editingLocationAndSize != null && - (inputPositioned || - !(browserEngine == BrowserEngine.webkit && - operatingSystem == OperatingSystem.iOs))) { + if (_editingLocationAndSize != null && _canPositionInput) { setStyle(domElement); } } From 19646a4522fbf315eb89bff68bc0882f4234628e Mon Sep 17 00:00:00 2001 From: Nurhan Turgut Date: Mon, 23 Sep 2019 11:02:30 -0700 Subject: [PATCH 5/5] Fixing unit tests. Addressing github PR comments. --- lib/web_ui/lib/src/engine/text_editing.dart | 89 ++++++++++++--------- 1 file changed, 51 insertions(+), 38 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing.dart index 5e2f05a38f399..116401de8c609 100644 --- a/lib/web_ui/lib/src/engine/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing.dart @@ -205,18 +205,18 @@ class TextEditingElement { /// 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. + /// 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 + /// 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 + /// in iOS is set to correct place, 100ms after focus. We use this timer for /// timing this delay. Timer _positionInputElementTimer; - static const Duration _delayBeforePositining = + static const Duration _delayBeforePositioning = const Duration(milliseconds: 100); final HybridTextEditing owner; @@ -247,7 +247,7 @@ class TextEditingElement { void configureInputElementForIOS() { if (browserEngine != BrowserEngine.webkit || operatingSystem != OperatingSystem.iOs) { - // Only relevant on Safari Mobile and Chrome on IOS. + // Only relevant on Safari-based on iOS. return; } @@ -293,30 +293,8 @@ class TextEditingElement { } if (owner.doesKeyboardShiftInput) { - /// 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(_delayBeforePositining, () { - 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(); - })); - })); + _preventShiftDuringFocus(); } - domElement.focus(); if (_lastEditingState != null) { @@ -343,6 +321,7 @@ class TextEditingElement { } _subscriptions.clear(); _positionInputElementTimer?.cancel(); + _positionInputElementTimer = null; owner.inputPositioned = false; _removeDomElement(); } @@ -373,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) { @@ -406,6 +411,14 @@ class TextEditingElement { ..addRange(_createRange(editingState)); break; } + + + if(owner.inputElementNeedsToBePositioned) { + _preventShiftDuringFocus(); + } + + // Re-focuses when setting editing state. + domElement.focus(); } /// Swap out the current DOM element and replace it with a new one of type @@ -624,9 +637,9 @@ class HybridTextEditing { /// Also used to define if a keyboard is needed. bool _isEditing = false; - /// Flag indicating the input element needs to be positioned. + /// Indicates whether the input element needs to be positioned. /// - /// See [TextEditingElement._delayBeforePositining]. + /// See [TextEditingElement._delayBeforePositioning]. bool get inputElementNeedsToBePositioned => !inputPositioned && _isEditing && @@ -765,14 +778,14 @@ class HybridTextEditing { /// 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._delayBeforePositining]. + /// See [TextEditingElement._delayBeforePositioning]. bool get _canPositionInput => inputPositioned || !doesKeyboardShiftInput; - /// Flag indicating if virtual keyboard shifts the location of input element. + /// 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 + /// 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 && @@ -807,15 +820,15 @@ class HybridTextEditing { ..transform = transformCss; } - // TODO(flutter_web): When the browser closes and re-opens the virtual - // shifts the page in IOS. Call this method from visibility change listener + // 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 elements location somewhere outside of the screen. + /// Set the dom element's location somewhere outside of the screen. /// - /// This is useful for not triggering a scroll when IOS virtual keyboard is + /// This is useful for not triggering a scroll when iOS virtual keyboard is /// coming up. /// - /// See [TextEditingElement._delayBeforePositining]. + /// See [TextEditingElement._delayBeforePositioning]. void setStyleOutsideOfScreen(html.HtmlElement domElement) { domElement.style.transform = 'translate(-9999px, -9999px)'; }