Skip to content

Commit

Permalink
Web - Fix selection jump on Chrome for Android (#41202)
Browse files Browse the repository at this point in the history
## Description

This PR fixes cursor jump on Chrome for Android when the user taps in a multiline `TextField`.

Using the following code sample:

<details><summary>Code sample</summary>

```dart

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @OverRide
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Text Field Focus',
      home: MyCustomForm(),
    );
  }
}

// Define a custom Form widget.
class MyCustomForm extends StatelessWidget {
  const MyCustomForm({super.key});

  @OverRide
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Text Field Focus'),
      ),
      backgroundColor: Colors.amber,
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: TextField(
          decoration: const InputDecoration(
            fillColor: Colors.white,
            filled: true
          ),
          autofocus: true,
          maxLines: 3,
          controller: TextEditingController(text: '1\n2\n3\n4\n'),
        ),
      ),// This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

```
</details>

On a mobile browser, once the page is loaded, tap after the number 3: 

- Before this PR: the TextField content is automaticaly scrolled and the selection is set after number 1.

https://user-images.githubusercontent.com/840911/232051413-b913f890-6cb1-4c60-92d0-7a3bf74cc688.mov

## Implementation

A multiline `TextField` relies on an HTML `<textarea>` elements. When a tap occurs the selection should be updated from Flutter not by the HTML element itself. 
This PR prevents mouse events on Chrome for Android. Those events conflicts with Flutter selection changes.
Previously, mouse events were only prevented on desktop but they are also emitted on mobile, see https://bugs.chromium.org/p/chromium/issues/detail?id=119216#c11.

## Related Issue

Related to flutter/flutter#124483 (partial fix because the issue is also reproducible on iOS/Safari).

## Tests

Adds 1 test.
  • Loading branch information
bleroux authored May 2, 2023
1 parent 4f84cac commit c4a2712
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 2 deletions.
9 changes: 7 additions & 2 deletions lib/web_ui/lib/src/engine/text_editing/text_editing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1415,9 +1415,12 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements

/// Prevent default behavior for mouse down, up and move.
///
/// When normal mouse events are not prevented, in desktop browsers, mouse
/// selection conflicts with selection sent from the framework, which creates
/// When normal mouse events are not prevented, mouse selection
/// conflicts with selection sent from the framework, which creates
/// flickering during selection by mouse.
///
/// On mobile browsers, mouse events are sent after a touch event,
/// see: https://bugs.chromium.org/p/chromium/issues/detail?id=119216#c11.
void preventDefaultForMouseEvents() {
subscriptions.add(
DomSubscription(activeDomElement, 'mousedown', (_) {
Expand Down Expand Up @@ -1704,6 +1707,8 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
owner.sendTextConnectionClosedToFrameworkIfAny();
}
}));

preventDefaultForMouseEvents();
}

@override
Expand Down
54 changes: 54 additions & 0 deletions lib/web_ui/test/engine/text_editing_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1896,6 +1896,60 @@ Future<void> testMain() async {
hideKeyboard();
});

test('prevent mouse events on Android', () {
// Regression test for https://github.com/flutter/flutter/issues/124483.
debugOperatingSystemOverride = OperatingSystem.android;
debugBrowserEngineOverride = BrowserEngine.blink;

/// During initialization [HybridTextEditing] will pick the correct
/// text editing strategy for [OperatingSystem.android].
textEditing = HybridTextEditing();

final MethodCall setClient = MethodCall(
'TextInput.setClient',
<dynamic>[123, flutterMultilineConfig],
);
sendFrameworkMessage(codec.encodeMethodCall(setClient));

// Editing shouldn't have started yet.
expect(defaultTextEditingRoot.ownerDocument?.activeElement, domDocument.body);

const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));

// The "setSizeAndTransform" message has to be here before we call
// checkInputEditingState, since on some platforms (e.g. Desktop Safari)
// we don't put the input element into the DOM until we get its correct
// dimensions from the framework.
final List<double> transform = Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList();
final MethodCall setSizeAndTransform = configureSetSizeAndTransformMethodCall(150, 50, transform);
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

final DomHTMLTextAreaElement textarea = textEditing!.strategy.domElement! as DomHTMLTextAreaElement;
checkTextAreaEditingState(textarea, '', 0, 0);

// Can set editing state and preserve new lines.
const MethodCall setEditingState = MethodCall(
'TextInput.setEditingState',
<String, dynamic>{
'text': '1\n2\n3\n4\n',
'selectionBase': 8,
'selectionExtent': 8,
'composingBase': null,
'composingExtent': null,
},
);
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
checkTextAreaEditingState(textarea, '1\n2\n3\n4\n', 8, 8);

// 'mousedown' event should be prevented.
final DomEvent event = createDomEvent('Event', 'mousedown');
textarea.dispatchEvent(event);
expect(event.defaultPrevented, isTrue);

hideKeyboard();
});

test('sets correct input type in iOS', () {
// Test on ios-safari only.
if (browserEngine == BrowserEngine.webkit &&
Expand Down

0 comments on commit c4a2712

Please sign in to comment.