-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
Currently TextInputClient on Flutter Web always receives a TextEditingValue with an empty composing range while the composing text is mixed inside TextEditingValue.text with no hint indicating their nature (The composing range is erased). This behavior is off spec.
This is due to current web engine only listens to onInput event and lacks ability to infer composing range https://github.com/flutter/engine/blob/7d927dd4a4cb1d5a4dc713b5349dfc3578921b97/lib/web_ui/lib/src/engine/text_editing/text_editing.dart#L232
Demo
Details
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
home: Scaffold(
appBar: AppBar(
title: Text('Material App Bar'),
),
body: CustomInput(),
),
);
}
}
class CustomInput extends StatefulWidget {
@override
_CustomInputState createState() => _CustomInputState();
}
class _CustomInputState extends State<CustomInput>
with AutomaticKeepAliveClientMixin {
TextInputConnection _textInputConnection;
FocusNode _focusNode;
FocusAttachment _focusAttachment;
InputConnectionController _input;
void requestKeyboard() {
if (_focusNode.hasFocus) {
_input.openConnection(Brightness.dark);
} else {
FocusScope.of(context).requestFocus(_focusNode);
}
}
void focusOrUnfocusIfNeeded() {
if (_focusNode.hasFocus) {
_focusNode.unfocus();
}
}
@override
bool get wantKeepAlive => _focusNode.hasFocus;
void _handleFocusChange() {
if (_focusNode.hasFocus && _focusNode.consumeKeyboardToken()) {
_input.openConnection(Brightness.dark);
} else if (!_focusNode.hasFocus) {
_input.closeConnection();
}
updateKeepAlive();
}
bool get _hasFocus => _focusNode.hasFocus;
bool get _hasInputConnection =>
_textInputConnection != null && _textInputConnection.attached;
@override
void initState() {
_focusNode = FocusNode();
super.initState();
_focusAttachment = _focusNode.attach(context);
_input = InputConnectionController();
_focusNode.addListener(_handleFocusChange);
}
@override
void dispose() {
_focusAttachment.detach();
_focusNode.removeListener(_handleFocusChange);
_input.closeConnection();
super.dispose();
}
@override
Widget build(BuildContext context) {
_focusAttachment.reparent();
super.build(context); // See AutomaticKeepAliveState.
return GestureDetector(
onTap: () => _focusNode.requestFocus(),
child: Container(
width: 200,
height: 200,
color: Colors.red,
),
);
}
}
class InputConnectionController implements TextInputClient {
TextInputConnection _textInputConnection;
bool get hasConnection =>
_textInputConnection != null && _textInputConnection.attached;
void openOrCloseConnection(
FocusNode focusNode, Brightness keyboardAppearance) {
if (focusNode.hasFocus && focusNode.consumeKeyboardToken()) {
openConnection(keyboardAppearance);
} else if (!focusNode.hasFocus) {
closeConnection();
}
}
void openConnection(Brightness keyboardAppearance) {
if (!hasConnection) {
_textInputConnection = TextInput.attach(
this,
TextInputConfiguration(
inputType: TextInputType.multiline,
obscureText: false,
autocorrect: false,
inputAction: TextInputAction.newline,
keyboardAppearance: keyboardAppearance,
textCapitalization: TextCapitalization.sentences,
),
);
}
_textInputConnection.show();
}
void closeConnection() {
if (hasConnection) {
_textInputConnection.close();
_textInputConnection = null;
}
}
@override
void connectionClosed() {
_textInputConnection.connectionClosedReceived();
_textInputConnection = null;
}
@override
AutofillScope get currentAutofillScope => null;
@override
TextEditingValue get currentTextEditingValue => null;
@override
void performAction(TextInputAction action) { }
@override
void showAutocorrectionPromptRect(int start, int end) {}
@override
void updateEditingValue(TextEditingValue value) {
print('Composing range: ${value.composing.start} to ${value.composing.end}');
}
@override
void updateFloatingCursor(RawFloatingCursorPoint point) {}
@override
void performPrivateCommand(String action, Map<String, dynamic> data) {}
}
When testing on Web, using any IME with composing text feature, the printed composing range will always be -1 to -1.
flutter doctor -v
Details
[√] Flutter (Channel master, 1.22.0-10.0.pre.87, on Microsoft Windows [Version 10.0.19041.450], locale zh-CN)
• Flutter version 1.22.0-10.0.pre.87 at C:\Users\Zhennan Wu\fvm\versions\master
• Framework revision 4732a214a7 (3 days ago), 2020-09-04 21:05:02 -0400
• Engine revision ac8b9c4c52
• Dart version 2.10.0 (build 2.10.0-86.0.dev)
[√] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
• Android SDK at C:\Users\Zhennan Wu\AppData\Local\Android\sdk
• Platform android-29, build-tools 29.0.2
• Java binary at: C:\Program Files\Android\Android Studio\jre\bin\java
• Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b01)
• All Android licenses accepted.
[√] Chrome - develop for the web
• Chrome at C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
[√] Visual Studio - develop for Windows (Visual Studio Community 2019 16.7.1)
• Visual Studio at C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
• Visual Studio Community 2019 version 16.7.30406.217
• Windows 10 SDK version 10.0.18362.0
[√] Android Studio (version 4.0)
• Android Studio at C:\Program Files\Android\Android Studio
• Flutter plugin version 48.1.2
• Dart plugin version 193.7361
• Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b01)
[√] IntelliJ IDEA Community Edition (version 2019.3)
• IntelliJ at C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2019.1.3
• Flutter plugin version 46.0.2
• Dart plugin version 193.7361
[√] VS Code (version 1.48.2)
• VS Code at C:\Users\Zhennan Wu\AppData\Local\Programs\Microsoft VS Code
• Flutter extension version 3.14.0
[√] Connected device (4 available)
• Windows (desktop) • windows • windows-x64 • Microsoft Windows [Version 10.0.19041.450]
• Web Server (web) • web-server • web-javascript • Flutter Tools
• Chrome (web) • chrome • web-javascript • Google Chrome 84.0.4147.135
• Edge (web) • edge • web-javascript • Microsoft Edge 84.0.522.59
• No issues found!
Use case
An erased composing range will cause:
- We can no longer infer the Unicode characters that users actually wanted to type from
TextEditingValue.text. The composing string itself cannot be filtered and can trigger unwanted logic while the user is actually trying to type something else via their IME. - Make text diffing strategy completely unusable when IME is activated. These strategies are the only solution for problems like Detect when delete is typed into a TextField #14809.
Proposal
Subscribe to compositionstart, compositionend, compositionupdate event in HTML element to either calculate a correct composing range or ignore composing text completely (like current Flutter Windows is doing).