mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Refactoring text editing. Strategy pattern is used to handle different browser/operating system and a11y behavior. (flutter/engine#14131)
* adding the default text editing strategy * [DRAFT] Refactoring text editing. Strategy pattern is used to handle different browser/operating system and a11y behaviour. Unit tests are missing. Documentation needs updating. * addressing PR comments * addressing PR comments. Fixing documentation * fixing persistenttextediting element which is used in a11y mode * removing texteditingelement and using texteditingstrategy from hybridtextediting. fixing the unit tests. fixing comments * fix unit tests * add todos for firefox tests * fixing chrome/android a11y issue
This commit is contained in:
parent
45cb4d962c
commit
35a0c81b85
@ -29,10 +29,23 @@ enum BrowserEngine {
|
||||
/// Lazily initialized current browser engine.
|
||||
BrowserEngine _browserEngine;
|
||||
|
||||
/// Override the value of [browserEngine].
|
||||
///
|
||||
/// Setting this to `null` lets [browserEngine] detect the browser that the
|
||||
/// app is running on.
|
||||
///
|
||||
/// This is intended to be used for testing and debugging only.
|
||||
BrowserEngine debugBrowserEngineOverride;
|
||||
|
||||
/// Returns the [BrowserEngine] used by the current browser.
|
||||
///
|
||||
/// This is used to implement browser-specific behavior.
|
||||
BrowserEngine get browserEngine => _browserEngine ??= _detectBrowserEngine();
|
||||
BrowserEngine get browserEngine {
|
||||
if (debugBrowserEngineOverride != null) {
|
||||
return debugBrowserEngineOverride;
|
||||
}
|
||||
return _browserEngine ??= _detectBrowserEngine();
|
||||
}
|
||||
|
||||
BrowserEngine _detectBrowserEngine() {
|
||||
final String vendor = html.window.navigator.vendor;
|
||||
|
||||
@ -66,7 +66,7 @@ abstract class EngineInputType {
|
||||
html.HtmlElement createDomElement() => html.InputElement();
|
||||
|
||||
/// Given a [domElement], set attributes that are specific to this input type.
|
||||
void configureDomElement(html.HtmlElement domElement) {
|
||||
void configureInputMode(html.HtmlElement domElement) {
|
||||
if (inputmodeAttribute == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -12,16 +12,6 @@ const int _kReturnKeyCode = 13;
|
||||
|
||||
void _emptyCallback(dynamic _) {}
|
||||
|
||||
/// 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 constant throughout the life time of an input
|
||||
/// element.
|
||||
///
|
||||
@ -222,37 +212,78 @@ class InputConfiguration {
|
||||
typedef _OnChangeCallback = void Function(EditingState editingState);
|
||||
typedef _OnActionCallback = void Function(String inputAction);
|
||||
|
||||
/// Wraps the DOM element used to provide text editing capabilities.
|
||||
/// Interface defining the template for text editing strategies.
|
||||
///
|
||||
/// The backing DOM element could be one of:
|
||||
/// 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.
|
||||
abstract class TextEditingStrategy {
|
||||
void initializeTextEditing(
|
||||
InputConfiguration inputConfig, {
|
||||
@required _OnChangeCallback onChange,
|
||||
@required _OnActionCallback onAction,
|
||||
});
|
||||
|
||||
/// Place the DOM element to its initial position.
|
||||
///
|
||||
/// It will be located exactly in the same place with the editable widgets,
|
||||
/// however it's 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();
|
||||
|
||||
/// Register event listeners to the DOM element.
|
||||
///
|
||||
/// These event listener will be removed in [disable].
|
||||
void addEventHandlers();
|
||||
|
||||
/// Update the element's position.
|
||||
///
|
||||
/// The position will be updated everytime Flutter Framework sends
|
||||
/// 'TextInput.setEditableSizeAndTransform' message.
|
||||
void updateElementPosition(_GeometricInfo geometricInfo);
|
||||
|
||||
/// Set editing state of the element.
|
||||
///
|
||||
/// This includes text and selection relelated states. The editing state will
|
||||
/// be updated everytime Flutter Framework sends 'TextInput.setEditingState'
|
||||
/// message.
|
||||
void setEditingState(EditingState editingState);
|
||||
|
||||
/// Set style to the native DOM element used for text editing.
|
||||
void updateElementStyle(_EditingStyle style);
|
||||
|
||||
/// Disables the element so it's no longer used for text editing.
|
||||
///
|
||||
/// Calling [disable] also removes any registered event listeners.
|
||||
void disable();
|
||||
}
|
||||
|
||||
/// 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:
|
||||
///
|
||||
/// 1. `<input>`.
|
||||
/// 2. `<textarea>`.
|
||||
/// 3. `<span contenteditable="true">`.
|
||||
class TextEditingElement {
|
||||
/// Creates a non-persistent [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);
|
||||
|
||||
///
|
||||
/// This class includes all the default behaviour for an editing element as
|
||||
/// well as the common properties such as [domElement].
|
||||
///
|
||||
/// Strategies written for different formfactor's/browser's should extend this
|
||||
/// class instead of extending the interface [TextEditingStrategy].
|
||||
///
|
||||
/// Unless a formfactor/browser requires specific implementation for a specific
|
||||
/// strategy the methods in this class should be used.
|
||||
class DefaultTextEditingStrategy implements TextEditingStrategy {
|
||||
final HybridTextEditing owner;
|
||||
|
||||
DefaultTextEditingStrategy(this.owner);
|
||||
|
||||
@visibleForTesting
|
||||
bool isEnabled = false;
|
||||
|
||||
@ -272,103 +303,66 @@ class TextEditingElement {
|
||||
final List<StreamSubscription<html.Event>> _subscriptions =
|
||||
<StreamSubscription<html.Event>>[];
|
||||
|
||||
/// Whether or not the input element can be positioned at this point in time.
|
||||
///
|
||||
/// This is currently only used in iOS. It's set to false before focusing the
|
||||
/// input field, and set back to true after a short timer. We do this because
|
||||
/// if the input field is positioned before focus, it could be pushed to an
|
||||
/// incorrect position by the virtual keyboard.
|
||||
///
|
||||
/// See:
|
||||
///
|
||||
/// * [_delayBeforePositioning] which controls how long to wait before
|
||||
/// positioning the input field.
|
||||
bool _canPosition = true;
|
||||
|
||||
/// Enables the element so it can be used to edit text.
|
||||
///
|
||||
/// Register [callback] so that it gets invoked whenever any change occurs in
|
||||
/// the text editing element.
|
||||
///
|
||||
/// Changes could be:
|
||||
/// - Text changes, or
|
||||
/// - Selection changes.
|
||||
void enable(
|
||||
@override
|
||||
void initializeTextEditing(
|
||||
InputConfiguration inputConfig, {
|
||||
@required _OnChangeCallback onChange,
|
||||
@required _OnActionCallback onAction,
|
||||
}) {
|
||||
assert(!isEnabled);
|
||||
|
||||
_initDomElement(inputConfig);
|
||||
domElement = inputConfig.inputType.createDomElement();
|
||||
if (inputConfig.obscureText) {
|
||||
domElement.setAttribute('type', 'password');
|
||||
}
|
||||
|
||||
final String autocorrectValue = inputConfig.autocorrect ? 'on' : 'off';
|
||||
domElement.setAttribute('autocorrect', autocorrectValue);
|
||||
|
||||
_setStaticStyleAttributes(domElement);
|
||||
_style?.applyToDomElement(domElement);
|
||||
initializeElementPosition();
|
||||
domRenderer.glassPaneElement.append(domElement);
|
||||
|
||||
isEnabled = true;
|
||||
_inputConfiguration = inputConfig;
|
||||
_onChange = onChange;
|
||||
_onAction = onAction;
|
||||
}
|
||||
|
||||
// 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`.
|
||||
//
|
||||
// Safari on iOS does not hide the keyboard as a side-effect of tapping
|
||||
// outside the editable box. Instead it provides an explicit "done" button,
|
||||
// which is reported as "blur", so we must not reacquire focus when we see
|
||||
// a "blur" event and let the keyboard disappear.
|
||||
if (browserEngine == BrowserEngine.blink ||
|
||||
browserEngine == BrowserEngine.unknown) {
|
||||
_subscriptions.add(domElement.onBlur.listen((_) {
|
||||
if (isEnabled) {
|
||||
_refocus();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (_doesKeyboardShiftInput) {
|
||||
_preventShiftDuringFocus();
|
||||
}
|
||||
domElement.focus();
|
||||
|
||||
if (_lastEditingState != null) {
|
||||
setEditingState(_lastEditingState);
|
||||
}
|
||||
@override
|
||||
void initializeElementPosition() {
|
||||
positionElement();
|
||||
}
|
||||
|
||||
@override
|
||||
void addEventHandlers() {
|
||||
// Subscribe to text and selection changes.
|
||||
_subscriptions.add(domElement.onInput.listen(_handleChange));
|
||||
|
||||
_subscriptions.add(domElement.onKeyDown.listen(_maybeSendAction));
|
||||
|
||||
/// Detects changes in text selection.
|
||||
///
|
||||
/// Currently only used in Firefox.
|
||||
///
|
||||
/// In Firefox, when cursor moves, neither selectionChange nor onInput
|
||||
/// events are triggered. We are listening to keyup event. Selection start,
|
||||
/// end values are used to decide if the text cursor moved.
|
||||
///
|
||||
/// Specific keycodes are not checked since users/applications can bind
|
||||
/// their own keys to move the text cursor.
|
||||
/// Decides if the selection has changed (cursor moved) compared to the
|
||||
/// previous values.
|
||||
///
|
||||
/// After each keyup, the start/end values of the selection is compared to the
|
||||
/// previously saved editing state.
|
||||
if (browserEngine == BrowserEngine.firefox) {
|
||||
_subscriptions.add(domElement.onKeyUp.listen((event) {
|
||||
_handleChange(event);
|
||||
}));
|
||||
_subscriptions.add(html.document.onSelectionChange.listen(_handleChange));
|
||||
}
|
||||
|
||||
/// 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));
|
||||
} else {
|
||||
_subscriptions.add(html.document.onSelectionChange.listen(_handleChange));
|
||||
@override
|
||||
void updateElementPosition(_GeometricInfo geometricInfo) {
|
||||
_geometricInfo = geometricInfo;
|
||||
if (isEnabled) {
|
||||
positionElement();
|
||||
}
|
||||
}
|
||||
|
||||
/// Disables the element so it's no longer used for text editing.
|
||||
///
|
||||
/// Calling [disable] also removes any registered event listeners.
|
||||
@mustCallSuper
|
||||
@override
|
||||
void updateElementStyle(_EditingStyle style) {
|
||||
_style = style;
|
||||
if (isEnabled) {
|
||||
_style.applyToDomElement(domElement);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void disable() {
|
||||
assert(isEnabled);
|
||||
|
||||
@ -381,106 +375,25 @@ class TextEditingElement {
|
||||
_subscriptions[i].cancel();
|
||||
}
|
||||
_subscriptions.clear();
|
||||
_positionInputElementTimer?.cancel();
|
||||
_positionInputElementTimer = null;
|
||||
_removeDomElement();
|
||||
}
|
||||
|
||||
void _initDomElement(InputConfiguration inputConfig) {
|
||||
domElement = inputConfig.inputType.createDomElement();
|
||||
inputConfig.inputType.configureDomElement(domElement);
|
||||
if (inputConfig.obscureText) {
|
||||
domElement.setAttribute('type', 'password');
|
||||
}
|
||||
|
||||
final String autocorrectValue = inputConfig.autocorrect ? 'on' : 'off';
|
||||
domElement.setAttribute('autocorrect', autocorrectValue);
|
||||
|
||||
_setStaticStyleAttributes(domElement);
|
||||
applyAllStyles();
|
||||
domRenderer.glassPaneElement.append(domElement);
|
||||
}
|
||||
|
||||
void _removeDomElement() {
|
||||
domElement.remove();
|
||||
domElement = null;
|
||||
}
|
||||
|
||||
void _refocus() {
|
||||
domElement.focus();
|
||||
}
|
||||
|
||||
/// Set style to the native DOM element used for text editing.
|
||||
///
|
||||
/// It will be located exactly in the same place with the editable widgets,
|
||||
/// however it's 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 applyAllStyles() {
|
||||
_style?.applyToDomElement(domElement);
|
||||
_positionElement();
|
||||
}
|
||||
|
||||
void _positionElement() {
|
||||
if (_canPosition && _geometricInfo != null) {
|
||||
_geometricInfo.applyToDomElement(domElement);
|
||||
}
|
||||
}
|
||||
|
||||
void _preventShiftDuringFocus() {
|
||||
// Position the element outside of the page before focusing on it.
|
||||
//
|
||||
// See [_positionInputElementTimer].
|
||||
owner.setStyleOutsideOfScreen(domElement);
|
||||
_canPosition = false;
|
||||
|
||||
// TODO(mdebbar): Should we remove this listener after the first invocation?
|
||||
_subscriptions.add(domElement.onFocus.listen((_) {
|
||||
// Cancel previous timer if exists.
|
||||
_positionInputElementTimer?.cancel();
|
||||
_positionInputElementTimer = Timer(_delayBeforePositioning, () {
|
||||
_canPosition = true;
|
||||
_positionElement();
|
||||
});
|
||||
|
||||
// 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;
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
@mustCallSuper
|
||||
@override
|
||||
void setEditingState(EditingState editingState) {
|
||||
_lastEditingState = editingState;
|
||||
if (!isEnabled || !editingState.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastEditingState.applyToDomElement(domElement);
|
||||
}
|
||||
|
||||
// Re-focuses when setting editing state.
|
||||
void positionElement() {
|
||||
_geometricInfo?.applyToDomElement(domElement);
|
||||
domElement.focus();
|
||||
}
|
||||
|
||||
void setGeometricInfo(_GeometricInfo geometricInfo) {
|
||||
_geometricInfo = geometricInfo;
|
||||
if (isEnabled) {
|
||||
_positionElement();
|
||||
}
|
||||
}
|
||||
|
||||
void setStyle(_EditingStyle style) {
|
||||
_style = style;
|
||||
if (isEnabled) {
|
||||
_style.applyToDomElement(domElement);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleChange(html.Event event) {
|
||||
assert(isEnabled);
|
||||
assert(domElement != null);
|
||||
@ -502,22 +415,222 @@ class TextEditingElement {
|
||||
_onAction(_inputConfiguration.inputAction);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables the element so it can be used to edit text.
|
||||
///
|
||||
/// Register [callback] so that it gets invoked whenever any change occurs in
|
||||
/// the text editing element.
|
||||
///
|
||||
/// Changes could be:
|
||||
/// - Text changes, or
|
||||
/// - Selection changes.
|
||||
void enable(
|
||||
InputConfiguration inputConfig, {
|
||||
@required _OnChangeCallback onChange,
|
||||
@required _OnActionCallback onAction,
|
||||
}) {
|
||||
assert(!isEnabled);
|
||||
|
||||
initializeTextEditing(inputConfig, onChange: onChange, onAction: onAction);
|
||||
|
||||
addEventHandlers();
|
||||
|
||||
if (_lastEditingState != null) {
|
||||
setEditingState(this._lastEditingState);
|
||||
}
|
||||
|
||||
// Re-focuses after setting editing state.
|
||||
domElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/// The implementation of a persistent mode for [TextEditingElement].
|
||||
/// IOS/Safari behaviour for text editing.
|
||||
///
|
||||
/// Persistent mode assumes the caller will own the creation, insertion and
|
||||
/// disposal of the DOM element.
|
||||
/// In iOS, the virtual keyboard might shifts the screen up to make input
|
||||
/// visible depending on the location of the focused input element.
|
||||
///
|
||||
/// Due to this [initializeElementPosition] and [updateElementPosition]
|
||||
/// strategies are different.
|
||||
///
|
||||
/// [disable] is also different since the [_positionInputElementTimer]
|
||||
/// also needs to be cleaned.
|
||||
///
|
||||
/// inputmodeAttribute needs to be set for mobile devices. Due to this
|
||||
/// [initializeTextEditing] is different.
|
||||
class IOSTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
IOSTextEditingStrategy(HybridTextEditing owner) : super(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);
|
||||
|
||||
/// Whether or not the input element can be positioned at this point in time.
|
||||
///
|
||||
/// This is currently only used in iOS. It's set to false before focusing the
|
||||
/// input field, and set back to true after a short timer. We do this because
|
||||
/// if the input field is positioned before focus, it could be pushed to an
|
||||
/// incorrect position by the virtual keyboard.
|
||||
///
|
||||
/// See:
|
||||
///
|
||||
/// * [_delayBeforePositioning] which controls how long to wait before
|
||||
/// positioning the input field.
|
||||
bool _canPosition = true;
|
||||
|
||||
@override
|
||||
void initializeTextEditing(
|
||||
InputConfiguration inputConfig, {
|
||||
@required _OnChangeCallback onChange,
|
||||
@required _OnActionCallback onAction,
|
||||
}) {
|
||||
super.initializeTextEditing(inputConfig,
|
||||
onChange: onChange, onAction: onAction);
|
||||
inputConfig.inputType.configureInputMode(domElement);
|
||||
}
|
||||
|
||||
@override
|
||||
void initializeElementPosition() {
|
||||
/// 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)';
|
||||
|
||||
_canPosition = false;
|
||||
|
||||
_subscriptions.add(domElement.onFocus.listen((_) {
|
||||
// Cancel previous timer if exists.
|
||||
_positionInputElementTimer?.cancel();
|
||||
_positionInputElementTimer = Timer(_delayBeforePositioning, () {
|
||||
_canPosition = true;
|
||||
positionElement();
|
||||
});
|
||||
|
||||
// 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;
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
@override
|
||||
void updateElementPosition(_GeometricInfo geometricInfo) {
|
||||
_geometricInfo = geometricInfo;
|
||||
if (isEnabled && _canPosition) {
|
||||
positionElement();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void disable() {
|
||||
super.disable();
|
||||
_positionInputElementTimer?.cancel();
|
||||
_positionInputElementTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Android behaviour for text editing.
|
||||
///
|
||||
/// inputmodeAttribute needs to be set for mobile devices. Due to this
|
||||
/// [initializeTextEditing] is different.
|
||||
///
|
||||
/// Keyboard acts differently than other devices. [addEventHandlers] handles
|
||||
/// this case as an extra.
|
||||
class AndroidTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
AndroidTextEditingStrategy(HybridTextEditing owner) : super(owner);
|
||||
|
||||
@override
|
||||
void initializeTextEditing(
|
||||
InputConfiguration inputConfig, {
|
||||
@required _OnChangeCallback onChange,
|
||||
@required _OnActionCallback onAction,
|
||||
}) {
|
||||
super.initializeTextEditing(inputConfig,
|
||||
onChange: onChange, onAction: onAction);
|
||||
inputConfig.inputType.configureInputMode(domElement);
|
||||
}
|
||||
|
||||
@override
|
||||
void addEventHandlers() {
|
||||
super.addEventHandlers();
|
||||
// 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`.
|
||||
if (browserEngine == BrowserEngine.blink ||
|
||||
browserEngine == BrowserEngine.unknown) {
|
||||
_subscriptions.add(domElement.onBlur.listen((_) {
|
||||
if (isEnabled) {
|
||||
// Refocus.
|
||||
domElement.focus();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Firefox behaviour for text editing.
|
||||
///
|
||||
/// Selections are different in Firefox. [addEventHandlers] strategy is
|
||||
/// impelemented diefferently in Firefox.
|
||||
class FirefoxTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
FirefoxTextEditingStrategy(HybridTextEditing owner) : super(owner);
|
||||
|
||||
@override
|
||||
void addEventHandlers() {
|
||||
// Subscribe to text and selection changes.
|
||||
_subscriptions.add(domElement.onInput.listen(_handleChange));
|
||||
|
||||
_subscriptions.add(domElement.onKeyDown.listen(_maybeSendAction));
|
||||
|
||||
/// Detects changes in text selection.
|
||||
///
|
||||
/// In Firefox, when cursor moves, neither selectionChange nor onInput
|
||||
/// events are triggered. We are listening to keyup event. Selection start,
|
||||
/// end values are used to decide if the text cursor moved.
|
||||
///
|
||||
/// Specific keycodes are not checked since users/applications can bind
|
||||
/// their own keys to move the text cursor.
|
||||
/// Decides if the selection has changed (cursor moved) compared to the
|
||||
/// previous values.
|
||||
///
|
||||
/// After each keyup, the start/end values of the selection is compared to
|
||||
/// the previously saved editing state.
|
||||
_subscriptions.add(domElement.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));
|
||||
}
|
||||
}
|
||||
|
||||
/// Text editing used by accesibilty mode.
|
||||
///
|
||||
/// [PersistentTextEditingElement] assumes the caller will own the creation,
|
||||
/// insertion and disposal of the DOM element. Due to this
|
||||
/// [initializeElementPosition], [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.
|
||||
///
|
||||
/// Persistent mode is useful for callers that want to have full control over
|
||||
/// the placement and lifecycle of the DOM element. An example of such a caller
|
||||
/// is Semantic's TextField that needs to put the DOM element inside the
|
||||
/// semantic tree. It also requires that the DOM element remains in the tree
|
||||
/// when the user isn't editing.
|
||||
class PersistentTextEditingElement extends TextEditingElement {
|
||||
class PersistentTextEditingElement extends DefaultTextEditingStrategy {
|
||||
/// Creates a [PersistentTextEditingElement] that eagerly instantiates
|
||||
/// [domElement] so the caller can insert it before calling
|
||||
/// [PersistentTextEditingElement.enable].
|
||||
@ -528,21 +641,13 @@ class PersistentTextEditingElement extends TextEditingElement {
|
||||
// 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));
|
||||
this.domElement = domElement;
|
||||
super.domElement = domElement;
|
||||
}
|
||||
|
||||
@override
|
||||
void _initDomElement(InputConfiguration inputConfig) {
|
||||
// In persistent 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));
|
||||
}
|
||||
|
||||
@override
|
||||
void _removeDomElement() {
|
||||
// In persistent mode, we don't want to remove the DOM element because the
|
||||
// caller is responsible for that.
|
||||
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
|
||||
@ -551,12 +656,34 @@ class PersistentTextEditingElement extends TextEditingElement {
|
||||
}
|
||||
|
||||
@override
|
||||
void _refocus() {
|
||||
// The semantic text field on Android listens to the focus event in order to
|
||||
// switch to a new text field. If we refocus here, we break that
|
||||
// functionality and the user can't switch from one text field to another in
|
||||
// accessibility mode.
|
||||
void initializeElementPosition() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Text editing singleton.
|
||||
@ -571,22 +698,38 @@ final HybridTextEditing textEditing = HybridTextEditing();
|
||||
/// - HTML's contentEditable feature handles typing and text changes.
|
||||
/// - HTML's selection API handles selection changes and cursor movements.
|
||||
class HybridTextEditing {
|
||||
/// The default HTML element used to manage editing state when a custom
|
||||
/// element is not provided via [useCustomEditableElement].
|
||||
TextEditingElement _defaultEditingElement;
|
||||
/// 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]
|
||||
DefaultTextEditingStrategy _defaultEditingElement;
|
||||
|
||||
/// Private constructor so this class can be a singleton.
|
||||
///
|
||||
/// The constructor also decides which text editing strategy to use depending
|
||||
/// on the operating system and browser engine.
|
||||
HybridTextEditing() {
|
||||
_defaultEditingElement = TextEditingElement(this);
|
||||
if (browserEngine == BrowserEngine.webkit &&
|
||||
operatingSystem == OperatingSystem.iOs) {
|
||||
this._defaultEditingElement = IOSTextEditingStrategy(this);
|
||||
} else if (browserEngine == BrowserEngine.blink &&
|
||||
operatingSystem == OperatingSystem.android) {
|
||||
this._defaultEditingElement = AndroidTextEditingStrategy(this);
|
||||
} else if (browserEngine == BrowserEngine.firefox) {
|
||||
this._defaultEditingElement = FirefoxTextEditingStrategy(this);
|
||||
} else {
|
||||
this._defaultEditingElement = DefaultTextEditingStrategy(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// The HTML element used to manage editing state.
|
||||
///
|
||||
/// This field is populated using [useCustomEditableElement]. If `null` the
|
||||
/// [_defaultEditableElement] is used instead.
|
||||
TextEditingElement _customEditingElement;
|
||||
/// [_defaultEditingElement] is used instead.
|
||||
DefaultTextEditingStrategy _customEditingElement;
|
||||
|
||||
TextEditingElement get editingElement {
|
||||
DefaultTextEditingStrategy get editingElement {
|
||||
if (_customEditingElement != null) {
|
||||
return _customEditingElement;
|
||||
}
|
||||
@ -605,7 +748,8 @@ class HybridTextEditing {
|
||||
/// instead of the hidden default element.
|
||||
///
|
||||
/// Use [stopUsingCustomEditableElement] to switch back to default element.
|
||||
void useCustomEditableElement(TextEditingElement customEditingElement) {
|
||||
void useCustomEditableElement(
|
||||
DefaultTextEditingStrategy customEditingElement) {
|
||||
if (isEditing && customEditingElement != _customEditingElement) {
|
||||
stopEditing();
|
||||
}
|
||||
@ -655,11 +799,12 @@ class HybridTextEditing {
|
||||
|
||||
case 'TextInput.setEditableSizeAndTransform':
|
||||
editingElement
|
||||
.setGeometricInfo(_GeometricInfo.fromFlutter(call.arguments));
|
||||
.updateElementPosition(_GeometricInfo.fromFlutter(call.arguments));
|
||||
break;
|
||||
|
||||
case 'TextInput.setStyle':
|
||||
editingElement.setStyle(_EditingStyle.fromFlutter(call.arguments));
|
||||
editingElement
|
||||
.updateElementStyle(_EditingStyle.fromFlutter(call.arguments));
|
||||
break;
|
||||
|
||||
case 'TextInput.clearClient':
|
||||
@ -712,19 +857,6 @@ class HybridTextEditing {
|
||||
_emptyCallback,
|
||||
);
|
||||
}
|
||||
|
||||
// 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)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Information on the font and alignment of a text editing element.
|
||||
|
||||
@ -18,7 +18,10 @@ const int _kReturnKeyCode = 13;
|
||||
|
||||
const MethodCodec codec = JSONMethodCodec();
|
||||
|
||||
TextEditingElement editingElement;
|
||||
/// Add unit tests for [FirefoxTextEditingStrategy].
|
||||
/// TODO(nurhan): https://github.com/flutter/flutter/issues/46891
|
||||
|
||||
DefaultTextEditingStrategy editingElement;
|
||||
EditingState lastEditingState;
|
||||
String lastInputAction;
|
||||
|
||||
@ -54,16 +57,13 @@ void main() {
|
||||
lastInputAction = null;
|
||||
});
|
||||
|
||||
group('$TextEditingElement', () {
|
||||
group('$DefaultTextEditingStrategy', () {
|
||||
setUp(() {
|
||||
editingElement = TextEditingElement(HybridTextEditing());
|
||||
editingElement = DefaultTextEditingStrategy(HybridTextEditing());
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
if (editingElement.isEnabled) {
|
||||
// Clean up all the DOM elements and event listeners.
|
||||
editingElement.disable();
|
||||
}
|
||||
cleanTextEditingElement();
|
||||
});
|
||||
|
||||
test('Creates element when enabled and removes it when disabled', () {
|
||||
@ -204,23 +204,6 @@ void main() {
|
||||
expect(lastInputAction, isNull);
|
||||
});
|
||||
|
||||
test('Re-acquires focus', () async {
|
||||
editingElement.enable(
|
||||
singlelineConfig,
|
||||
onChange: trackEditingState,
|
||||
onAction: trackInputAction,
|
||||
);
|
||||
expect(document.activeElement, editingElement.domElement);
|
||||
|
||||
editingElement.domElement.blur();
|
||||
// The focus remains on [editingElement.domElement].
|
||||
expect(document.activeElement, editingElement.domElement);
|
||||
|
||||
// There should be no input action.
|
||||
expect(lastInputAction, isNull);
|
||||
}, // TODO(nurhan): https://github.com/flutter/flutter/issues/46638
|
||||
skip: (browserEngine == BrowserEngine.firefox));
|
||||
|
||||
test('Multi-line mode also works', () {
|
||||
// The textarea element is created lazily.
|
||||
expect(document.getElementsByTagName('textarea'), hasLength(0));
|
||||
@ -252,10 +235,6 @@ void main() {
|
||||
EditingState(text: 'bar\nbaz', baseOffset: 2, extentOffset: 7));
|
||||
checkTextAreaEditingState(textarea, 'bar\nbaz', 2, 7);
|
||||
|
||||
// Re-acquires focus.
|
||||
textarea.blur();
|
||||
expect(document.activeElement, textarea);
|
||||
|
||||
editingElement.disable();
|
||||
// The textarea should be cleaned up.
|
||||
expect(document.getElementsByTagName('textarea'), hasLength(0));
|
||||
@ -355,145 +334,150 @@ void main() {
|
||||
// And default behavior of keyboard event shouldn't have been prevented.
|
||||
expect(event.defaultPrevented, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('[persistent mode]', () {
|
||||
test('Does not accept dom elements of a wrong type', () {
|
||||
// A regular <span> shouldn't be accepted.
|
||||
final HtmlElement span = SpanElement();
|
||||
expect(
|
||||
() => PersistentTextEditingElement(HybridTextEditing(), span),
|
||||
throwsAssertionError,
|
||||
);
|
||||
});
|
||||
group('$PersistentTextEditingElement', () {
|
||||
InputElement testInputElement;
|
||||
|
||||
test('Does not re-acquire focus', () {
|
||||
// See [PersistentTextEditingElement._refocus] for an explanation of why
|
||||
// re-acquiring focus shouldn't happen in persistent mode.
|
||||
final InputElement input = InputElement();
|
||||
final PersistentTextEditingElement persistentEditingElement =
|
||||
PersistentTextEditingElement(HybridTextEditing(), input);
|
||||
expect(document.activeElement, document.body);
|
||||
setUp(() {
|
||||
testInputElement = InputElement();
|
||||
});
|
||||
|
||||
document.body.append(input);
|
||||
persistentEditingElement.enable(
|
||||
tearDown(() {
|
||||
cleanTextEditingElement();
|
||||
testInputElement = null;
|
||||
});
|
||||
|
||||
test('Does not accept dom elements of a wrong type', () {
|
||||
// A regular <span> shouldn't be accepted.
|
||||
final HtmlElement span = SpanElement();
|
||||
expect(
|
||||
() => PersistentTextEditingElement(HybridTextEditing(), span),
|
||||
throwsAssertionError,
|
||||
);
|
||||
});
|
||||
|
||||
test('Does not re-acquire focus', () {
|
||||
editingElement =
|
||||
PersistentTextEditingElement(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 lose focus now.
|
||||
editingElement.domElement.blur();
|
||||
expect(document.activeElement, document.body);
|
||||
|
||||
editingElement.disable();
|
||||
});
|
||||
|
||||
test('Does not dispose and recreate dom elements in persistent mode', () {
|
||||
editingElement =
|
||||
PersistentTextEditingElement(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,
|
||||
);
|
||||
expect(document.activeElement, input);
|
||||
),
|
||||
throwsAssertionError,
|
||||
);
|
||||
|
||||
// The input should lose focus now.
|
||||
persistentEditingElement.domElement.blur();
|
||||
expect(document.activeElement, document.body);
|
||||
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);
|
||||
|
||||
persistentEditingElement.disable();
|
||||
});
|
||||
editingElement.disable();
|
||||
// It doesn't remove the DOM element.
|
||||
expect(editingElement.domElement, testInputElement);
|
||||
expect(document.body.contains(editingElement.domElement), isTrue);
|
||||
// But the DOM element loses focus.
|
||||
expect(document.activeElement, document.body);
|
||||
});
|
||||
|
||||
test('Does not dispose and recreate dom elements in persistent mode', () {
|
||||
final InputElement input = InputElement();
|
||||
final PersistentTextEditingElement persistentEditingElement =
|
||||
PersistentTextEditingElement(HybridTextEditing(), input);
|
||||
test('Refocuses when setting editing state', () {
|
||||
editingElement =
|
||||
PersistentTextEditingElement(HybridTextEditing(), testInputElement);
|
||||
|
||||
// The DOM element should've been eagerly created.
|
||||
expect(input, isNotNull);
|
||||
// But doesn't have focus.
|
||||
expect(document.activeElement, document.body);
|
||||
document.body.append(testInputElement);
|
||||
editingElement.enable(
|
||||
singlelineConfig,
|
||||
onChange: trackEditingState,
|
||||
onAction: trackInputAction,
|
||||
);
|
||||
expect(document.activeElement, testInputElement);
|
||||
|
||||
// Can't enable before the input element is inserted into the DOM.
|
||||
expect(
|
||||
() => persistentEditingElement.enable(
|
||||
singlelineConfig,
|
||||
onChange: trackEditingState,
|
||||
onAction: trackInputAction,
|
||||
),
|
||||
throwsAssertionError,
|
||||
);
|
||||
editingElement.domElement.blur();
|
||||
expect(document.activeElement, document.body);
|
||||
|
||||
document.body.append(input);
|
||||
persistentEditingElement.enable(
|
||||
// The input should regain focus now.
|
||||
editingElement.setEditingState(EditingState(text: 'foo'));
|
||||
expect(document.activeElement, testInputElement);
|
||||
|
||||
editingElement.disable();
|
||||
});
|
||||
|
||||
test('Works in multi-line mode', () {
|
||||
final TextAreaElement textarea = TextAreaElement();
|
||||
editingElement =
|
||||
PersistentTextEditingElement(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,
|
||||
);
|
||||
expect(document.activeElement, persistentEditingElement.domElement);
|
||||
// It doesn't create a new DOM element.
|
||||
expect(persistentEditingElement.domElement, input);
|
||||
),
|
||||
throwsAssertionError,
|
||||
);
|
||||
|
||||
persistentEditingElement.disable();
|
||||
// It doesn't remove the DOM element.
|
||||
expect(persistentEditingElement.domElement, input);
|
||||
expect(document.body.contains(persistentEditingElement.domElement),
|
||||
isTrue);
|
||||
// But the DOM element loses focus.
|
||||
expect(document.activeElement, document.body);
|
||||
});
|
||||
document.body.append(textarea);
|
||||
editingElement.enable(
|
||||
multilineConfig,
|
||||
onChange: trackEditingState,
|
||||
onAction: trackInputAction,
|
||||
);
|
||||
// Focuses the textarea.
|
||||
expect(document.activeElement, textarea);
|
||||
|
||||
test('Refocuses when setting editing state', () {
|
||||
final InputElement input = InputElement();
|
||||
final PersistentTextEditingElement persistentEditingElement =
|
||||
PersistentTextEditingElement(HybridTextEditing(), input);
|
||||
// Doesn't re-acquire focus.
|
||||
textarea.blur();
|
||||
expect(document.activeElement, document.body);
|
||||
|
||||
document.body.append(input);
|
||||
persistentEditingElement.enable(
|
||||
singlelineConfig,
|
||||
onChange: trackEditingState,
|
||||
onAction: trackInputAction,
|
||||
);
|
||||
expect(document.activeElement, input);
|
||||
// Re-focuses when setting editing state
|
||||
editingElement.setEditingState(EditingState(text: 'foo'));
|
||||
expect(document.activeElement, textarea);
|
||||
|
||||
persistentEditingElement.domElement.blur();
|
||||
expect(document.activeElement, document.body);
|
||||
|
||||
// The input should regain focus now.
|
||||
persistentEditingElement.setEditingState(EditingState(text: 'foo'));
|
||||
expect(document.activeElement, input);
|
||||
|
||||
persistentEditingElement.disable();
|
||||
});
|
||||
|
||||
test('Works in multi-line mode', () {
|
||||
final TextAreaElement textarea = TextAreaElement();
|
||||
final PersistentTextEditingElement persistentEditingElement =
|
||||
PersistentTextEditingElement(HybridTextEditing(), textarea);
|
||||
|
||||
expect(persistentEditingElement.domElement, textarea);
|
||||
expect(document.activeElement, document.body);
|
||||
|
||||
// Can't enable before the textarea is inserted into the DOM.
|
||||
expect(
|
||||
() => persistentEditingElement.enable(
|
||||
singlelineConfig,
|
||||
onChange: trackEditingState,
|
||||
onAction: trackInputAction,
|
||||
),
|
||||
throwsAssertionError,
|
||||
);
|
||||
|
||||
document.body.append(textarea);
|
||||
persistentEditingElement.enable(
|
||||
multilineConfig,
|
||||
onChange: trackEditingState,
|
||||
onAction: trackInputAction,
|
||||
);
|
||||
// Focuses the textarea.
|
||||
expect(document.activeElement, textarea);
|
||||
|
||||
// Doesn't re-acquire focus.
|
||||
textarea.blur();
|
||||
expect(document.activeElement, document.body);
|
||||
|
||||
// Re-focuses when setting editing state
|
||||
persistentEditingElement.setEditingState(EditingState(text: 'foo'));
|
||||
expect(document.activeElement, textarea);
|
||||
|
||||
persistentEditingElement.disable();
|
||||
// It doesn't remove the textarea from the DOM.
|
||||
expect(persistentEditingElement.domElement, textarea);
|
||||
expect(document.body.contains(persistentEditingElement.domElement),
|
||||
isTrue);
|
||||
// But the textarea loses focus.
|
||||
expect(document.activeElement, document.body);
|
||||
});
|
||||
editingElement.disable();
|
||||
// It doesn't remove the textarea from the DOM.
|
||||
expect(editingElement.domElement, textarea);
|
||||
expect(document.body.contains(editingElement.domElement), isTrue);
|
||||
// But the textarea loses focus.
|
||||
expect(document.activeElement, document.body);
|
||||
});
|
||||
});
|
||||
|
||||
@ -542,6 +526,8 @@ void main() {
|
||||
|
||||
tearDown(() {
|
||||
spy.deactivate();
|
||||
debugBrowserEngineOverride = null;
|
||||
debugOperatingSystemOverride = null;
|
||||
if (textEditing.isEditing) {
|
||||
textEditing.stopEditing();
|
||||
}
|
||||
@ -990,6 +976,10 @@ void main() {
|
||||
test('sets correct input type in Android', () {
|
||||
debugOperatingSystemOverride = OperatingSystem.android;
|
||||
|
||||
/// During initialization [HybridTextEditing] will pick the correct
|
||||
/// text editing strategy for [OperatingSystem.android].
|
||||
textEditing = HybridTextEditing();
|
||||
|
||||
showKeyboard(inputType: 'text');
|
||||
expect(getEditingInputMode(), 'text');
|
||||
|
||||
@ -1008,10 +998,17 @@ void main() {
|
||||
hideKeyboard();
|
||||
|
||||
debugOperatingSystemOverride = null;
|
||||
});
|
||||
},
|
||||
// TODO(nurhan): https://github.com/flutter/flutter/issues/46638
|
||||
skip: (browserEngine == BrowserEngine.firefox));
|
||||
|
||||
test('sets correct input type in iOS', () {
|
||||
debugOperatingSystemOverride = OperatingSystem.iOs;
|
||||
debugBrowserEngineOverride = BrowserEngine.webkit;
|
||||
|
||||
/// During initialization [HybridTextEditing] will pick the correct
|
||||
/// text editing strategy for [OperatingSystem.iOs].
|
||||
textEditing = HybridTextEditing();
|
||||
|
||||
showKeyboard(inputType: 'text');
|
||||
expect(getEditingInputMode(), 'text');
|
||||
@ -1031,7 +1028,9 @@ void main() {
|
||||
hideKeyboard();
|
||||
|
||||
debugOperatingSystemOverride = null;
|
||||
});
|
||||
},
|
||||
// TODO(nurhan): https://github.com/flutter/flutter/issues/46638
|
||||
skip: (browserEngine == BrowserEngine.firefox));
|
||||
|
||||
test('sends the correct input action as a platform message', () {
|
||||
final int clientId = showKeyboard(
|
||||
@ -1079,6 +1078,19 @@ void main() {
|
||||
group('EditingState', () {
|
||||
EditingState _editingState;
|
||||
|
||||
setUp(() {
|
||||
editingElement = DefaultTextEditingStrategy(HybridTextEditing());
|
||||
editingElement.enable(
|
||||
singlelineConfig,
|
||||
onChange: trackEditingState,
|
||||
onAction: trackInputAction,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
cleanTextEditingElement();
|
||||
});
|
||||
|
||||
test('Configure input element from the editing state', () {
|
||||
final InputElement input = document.getElementsByTagName('input')[0];
|
||||
_editingState =
|
||||
@ -1092,6 +1104,13 @@ void main() {
|
||||
});
|
||||
|
||||
test('Configure text area element from the editing state', () {
|
||||
cleanTextEditingElement();
|
||||
editingElement.enable(
|
||||
multilineConfig,
|
||||
onChange: trackEditingState,
|
||||
onAction: trackInputAction,
|
||||
);
|
||||
|
||||
final TextAreaElement textArea =
|
||||
document.getElementsByTagName('textarea')[0];
|
||||
_editingState =
|
||||
@ -1118,6 +1137,13 @@ void main() {
|
||||
});
|
||||
|
||||
test('Get Editing State from text area element', () {
|
||||
cleanTextEditingElement();
|
||||
editingElement.enable(
|
||||
multilineConfig,
|
||||
onChange: trackEditingState,
|
||||
onAction: trackInputAction,
|
||||
);
|
||||
|
||||
final TextAreaElement input =
|
||||
document.getElementsByTagName('textarea')[0];
|
||||
input.value = 'Test';
|
||||
@ -1190,6 +1216,15 @@ MethodCall configureSetSizeAndTransformMethodCall(
|
||||
});
|
||||
}
|
||||
|
||||
/// Will disable editing element which will also clean the backup DOM
|
||||
/// element from the page.
|
||||
void cleanTextEditingElement() {
|
||||
if (editingElement.isEnabled) {
|
||||
// Clean up all the DOM elements and event listeners.
|
||||
editingElement.disable();
|
||||
}
|
||||
}
|
||||
|
||||
void checkInputEditingState(
|
||||
InputElement input, String text, int start, int end) {
|
||||
expect(document.activeElement, input);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user