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
147 lines
4.9 KiB
Dart
147 lines
4.9 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
// This code is copied from `package:web` which still needs its own
|
|
// documentation for public members. Since this is a shim that users should not
|
|
// use, we ignore this lint for this file.
|
|
// ignore_for_file: public_member_api_docs
|
|
|
|
/// A stripped down version of `package:web` to avoid pinning that repo in
|
|
/// Flutter as a dependency.
|
|
///
|
|
/// These are manually copied over from `package:web` as needed, and should stay
|
|
/// in sync with the latest package version as much as possible.
|
|
///
|
|
/// If missing members are needed, copy them over into the corresponding
|
|
/// extension or interface. If missing interfaces/types are needed, copy them
|
|
/// over while excluding unnecessary inheritance to make the copy minimal. These
|
|
/// types are erased at runtime, so excluding supertypes is safe. If a member is
|
|
/// needed that belongs to a supertype, then add the necessary `implements`
|
|
/// clause to the subtype when you add that supertype. Keep extensions next to
|
|
/// the interface they extend.
|
|
library;
|
|
|
|
import 'dart:js_interop';
|
|
|
|
typedef EventListener = JSFunction;
|
|
typedef XMLHttpRequestResponseType = String;
|
|
|
|
@JS()
|
|
external Document get document;
|
|
|
|
@JS()
|
|
external Window get window;
|
|
|
|
extension type CSSStyleDeclaration._(JSObject _) implements JSObject {
|
|
external set backgroundColor(String value);
|
|
external String get backgroundColor;
|
|
external set border(String value);
|
|
external String get border;
|
|
external set height(String value);
|
|
external String get height;
|
|
external set width(String value);
|
|
external String get width;
|
|
}
|
|
|
|
extension type CSSStyleSheet._(JSObject _) implements JSObject {
|
|
external int insertRule(String rule, [int index]);
|
|
}
|
|
|
|
extension type Document._(JSObject _) implements JSObject {
|
|
external Element createElement(String localName, [JSAny options]);
|
|
external Range createRange();
|
|
external HTMLHeadElement? get head;
|
|
}
|
|
|
|
extension type DOMTokenList._(JSObject _) implements JSObject {
|
|
external void add(String tokens);
|
|
}
|
|
|
|
extension type Element._(JSObject _) implements Node, JSObject {
|
|
external DOMTokenList get classList;
|
|
external void append(JSAny nodes);
|
|
external void remove();
|
|
}
|
|
|
|
extension type Event._(JSObject _) implements JSObject {}
|
|
|
|
extension type EventTarget._(JSObject _) implements JSObject {
|
|
external void addEventListener(String type, EventListener? callback, [JSAny options]);
|
|
}
|
|
|
|
extension type HTMLElement._(JSObject _) implements Element, JSObject {
|
|
external String get innerText;
|
|
external set innerText(String value);
|
|
external CSSStyleDeclaration get style;
|
|
external HTMLElement cloneNode(bool deep);
|
|
}
|
|
|
|
extension type HTMLHeadElement._(JSObject _) implements HTMLElement, JSObject {}
|
|
|
|
extension type HTMLStyleElement._(JSObject _) implements HTMLElement, JSObject {
|
|
external CSSStyleSheet? get sheet;
|
|
}
|
|
|
|
extension type HTMLImageElement._(JSObject _) implements HTMLElement, JSObject {
|
|
external String get src;
|
|
external set src(String value);
|
|
external num get naturalWidth;
|
|
external num get naturalHeight;
|
|
external JSPromise<JSAny?> decode();
|
|
}
|
|
|
|
extension type HTMLCanvasElement._(JSObject _) implements HTMLElement, JSObject {
|
|
external int get width;
|
|
external set width(int value);
|
|
external int get height;
|
|
external set height(int value);
|
|
external String toDataURL();
|
|
}
|
|
|
|
extension type MediaQueryList._(JSObject _) implements EventTarget, JSObject {
|
|
external bool get matches;
|
|
}
|
|
|
|
extension type MouseEvent._(JSObject _) implements JSObject {
|
|
external num get offsetX;
|
|
external num get offsetY;
|
|
external int get button;
|
|
external void preventDefault();
|
|
}
|
|
|
|
extension type Navigator._(JSObject _) implements JSObject {
|
|
external String get platform;
|
|
}
|
|
|
|
extension type Node._(JSObject _) implements EventTarget, JSObject {}
|
|
|
|
extension type Range._(JSObject _) implements JSObject {
|
|
external void selectNode(Node node);
|
|
}
|
|
|
|
extension type Selection._(JSObject _) implements JSObject {
|
|
external void addRange(Range range);
|
|
external void removeAllRanges();
|
|
}
|
|
|
|
extension type Window._(JSObject _) implements EventTarget, JSObject {
|
|
external Navigator get navigator;
|
|
external MediaQueryList matchMedia(String query);
|
|
external Selection? getSelection();
|
|
external String get origin;
|
|
}
|
|
|
|
extension type XMLHttpRequest._(JSObject _) implements XMLHttpRequestEventTarget, JSObject {
|
|
external factory XMLHttpRequest();
|
|
external void open(String method, String url, [bool async, String? username, String? password]);
|
|
external void setRequestHeader(String name, String value);
|
|
external void send([JSAny? body]);
|
|
external int get status;
|
|
external set responseType(XMLHttpRequestResponseType value);
|
|
external XMLHttpRequestResponseType get responseType;
|
|
external JSAny? get response;
|
|
}
|
|
|
|
extension type XMLHttpRequestEventTarget._(JSObject _) implements EventTarget, JSObject {}
|