Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Updating text field location in IOS as a pre-work for spellcheck #12192

Merged
merged 5 commits into from
Sep 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 2 additions & 13 deletions lib/web_ui/lib/src/engine/dom_renderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 0 additions & 5 deletions lib/web_ui/lib/src/engine/pointer_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 0 additions & 1 deletion lib/web_ui/lib/src/engine/semantics/text_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
116 changes: 103 additions & 13 deletions lib/web_ui/lib/src/engine/text_editing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}
}

Expand Down Expand Up @@ -274,6 +292,9 @@ class TextEditingElement {
}));
}

if (owner.doesKeyboardShiftInput) {
_preventShiftDuringFocus();
}
domElement.focus();

if (_lastEditingState != null) {
Expand All @@ -299,6 +320,9 @@ class TextEditingElement {
_subscriptions[i].cancel();
}
_subscriptions.clear();
_positionInputElementTimer?.cancel();
_positionInputElementTimer = null;
owner.inputPositioned = false;
_removeDomElement();
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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<String, dynamic> _configuration;

Expand Down Expand Up @@ -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);
}
}
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand Down