mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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