Kostia Sokolovskyi 3ee7f14390
[web] Fix unresponsive input above SelectionArea in Safari and Firefox. (#167275)
Fixes https://github.com/flutter/flutter/issues/157579

### Description

When the `TextField` is placed above the `HtmlElementView`, it becomes
unresponsive on Safari and Firefox. After the investigation, I found
that this happens because the underlying `input`/`textarea` loses focus,
leading to not listening to the keyboard input.

After some investigation, I found out that calling `preventDefault` on
`mousedown` events on SelectionArea's `div` element prevents the
`input/textarea` from losing focus.

This PR focuses on `SelectionArea`, but there is the same issue
happening in the `pointer_interceptor ` package
https://github.com/flutter/flutter/issues/157920. If this solution is
accepted, then I could file a separate PR for `pointer_interceptor`
package with the same fix.

| Before | After |
| :---: | :---: |
| https://input-above-selection-area-bug.web.app |
https://input-above-selection-area-fix.web.app |
| <video
src="https://github.com/user-attachments/assets/be73a5e9-84e4-44f9-96b3-f8d24f44e0b8"
/> | <video
src="https://github.com/user-attachments/assets/87746058-df6e-4caf-8f85-c240de32c630"
/> |

<details>
<summary>Old description</summary>

The fix I am proposing is to delay the `moveFocusToActiveDomElement` by
using `Timer`. I am not sure whether this is a proper fix, as it looks
like the issues may be in the way pointer events are handled. I tried
adding `event.preventDefault()` after `_callback(event, pointerData)` in
`pointer_binding.dart` and the issue was fixed, but then text selection
in `SelectionRegion` became broken.


aef4718b39/engine/src/flutter/lib/web_ui/lib/src/engine/pointer_binding.dart (L942-L974)

The application with the bug reproduction is hosted at:
https://input-above-element-view-bug.web.app
The application with the fix is hosted at:
https://input-above-element-view-fix.web.app
<details>
<summary>Application Source Code</summary>

```dart
import 'package:flutter/material.dart';
import 'package:web/web.dart' as web;

void main() async {
  runApp(
    MaterialApp(
      home: Screen(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Expanded(
            child: ColoredBox(
              color: Colors.green.withAlpha(50),
              child: SelectionArea(
                child: Center(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Text('SelectionArea below'),
                      OneLineTextField(),
                      MultilineTextField(),
                    ],
                  ),
                ),
              ),
            ),
          ),
          Builder(
            builder: (context) {
              return Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Expanded(
                    child: OneLineTextField(),
                  ),
                  ElevatedButton(
                    child: const Text('Show dialog'),
                    onPressed: () {
                      showDialog(
                        context: context,
                        builder: (BuildContext context) {
                          return SimpleDialog(
                            children: <Widget>[
                              Column(
                                mainAxisSize: MainAxisSize.min,
                                children: [
                                  OneLineTextField(),
                                  OneLineTextField(),
                                  OneLineTextField(),
                                ],
                              ),
                            ],
                          );
                        },
                      );
                    },
                  ),
                ],
              );
            },
          ),
          Expanded(
            child: ColoredBox(
              color: Colors.orange.withAlpha(50),
              child: SimpleDiv(
                child: Center(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Text('Simple div below'),
                      OneLineTextField(),
                      MultilineTextField(),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return TextField(
      decoration: InputDecoration(
        labelText: 'One-line',
        floatingLabelBehavior: FloatingLabelBehavior.always,
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return TextField(
      decoration: InputDecoration(
        labelText: 'Multiline',
        floatingLabelBehavior: FloatingLabelBehavior.always,
      ),
      minLines: 1,
      maxLines: null,
    );
  }
}

class SimpleDiv extends StatelessWidget {
  const SimpleDiv({
    required this.child,
    super.key,
  });

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned.fill(
          child: HtmlElementView.fromTagName(
            tagName: 'div',
            isVisible: false,
            onElementCreated: (element) {
              (element as web.HTMLElement)
                ..style.width = '100%'
                ..style.height = '100%';
            },
          ),
        ),
        child,
      ],
    );
  }
}
```

</details>

#### Firefox

On `TextField` tap, the focus moves to the `input`, and then back to the
`flutter-view`. You can take a look at the "Before" recording.

| Before | After |
| :---: | :---: |
| `flutter-view -> input -> flutter-view` | `flutter-view -> input ->
flutter-view -> input` |
| <video
src="https://github.com/user-attachments/assets/dbbbbca7-500c-4682-a2e8-b49751a27b5c"
/> | <video
src="https://github.com/user-attachments/assets/3886592f-c4e0-4c92-9e24-9cc2cb5a4763"
/> |

#### Safari

Like in Firefox, on `TextField` tap, the focus moves to the `input`, and
then back to the `flutter-view`. You can take a look at the "Before"
recording.

| Before | After |
| :---: | :---: |
| `flutter-view -> input -> flutter-view` | `flutter-view -> input ->
flutter-view -> input` |
| <video
src="https://github.com/user-attachments/assets/10c3b7e5-cf64-4858-8874-98c1e1aae74f"
/> | <video
src="https://github.com/user-attachments/assets/5c74ea51-494a-410c-ae02-be0dccf1b344"
/> |

#### Chrome

The issue is not happening on Chrome. If you take a look at the
recording, you will notice that on a `TextField` tap, the focus moves
the following way: `flutter-view -> input -> input`. The reason why it
doesn't move to `flutter-view` is that we have a
`moveFocusToActiveDomElement` call in the `handleBlur` function. As far
as I understand, it prevents `input` from losing focus.
However, the same call in Firefox doesn't prevent input focus loss.
In Safari, it fixes the issue, but listening to `blur` events is not a
way to go, according to the following comment
https://github.com/flutter/flutter/blob/master/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart#L1385-L1391

<video
src="https://github.com/user-attachments/assets/6168effd-49ff-4064-9876-50ab3bfae9ac"
/>

</details>

## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-05-27 19:39:24 +00:00
..

Flutter

Flutter is a new way to build high-performance, cross-platform mobile, web, and desktop apps. Flutter is optimized for today's — and tomorrow's — mobile and desktop devices. We are focused on low-latency input and high frame rates on all platforms.

See the getting started guide for information about using Flutter.