Unverified Commit 2b67846a authored by Mouad Debbar's avatar Mouad Debbar Committed by GitHub

Make SelectableText work better on web (#63786)

parent 414f8b59
...@@ -447,6 +447,7 @@ class TextInputConfiguration { ...@@ -447,6 +447,7 @@ class TextInputConfiguration {
/// [actionLabel] may be null. /// [actionLabel] may be null.
const TextInputConfiguration({ const TextInputConfiguration({
this.inputType = TextInputType.text, this.inputType = TextInputType.text,
this.readOnly = false,
this.obscureText = false, this.obscureText = false,
this.autocorrect = true, this.autocorrect = true,
SmartDashesType? smartDashesType, SmartDashesType? smartDashesType,
...@@ -470,6 +471,11 @@ class TextInputConfiguration { ...@@ -470,6 +471,11 @@ class TextInputConfiguration {
/// The type of information for which to optimize the text input control. /// The type of information for which to optimize the text input control.
final TextInputType inputType; final TextInputType inputType;
/// Whether the text field can be edited or not.
///
/// Defaults to false.
final bool readOnly;
/// Whether to hide the text being edited (e.g., for passwords). /// Whether to hide the text being edited (e.g., for passwords).
/// ///
/// Defaults to false. /// Defaults to false.
...@@ -580,6 +586,7 @@ class TextInputConfiguration { ...@@ -580,6 +586,7 @@ class TextInputConfiguration {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return <String, dynamic>{ return <String, dynamic>{
'inputType': inputType.toJson(), 'inputType': inputType.toJson(),
'readOnly': readOnly,
'obscureText': obscureText, 'obscureText': obscureText,
'autocorrect': autocorrect, 'autocorrect': autocorrect,
'smartDashesType': smartDashesType.index.toString(), 'smartDashesType': smartDashesType.index.toString(),
......
...@@ -484,6 +484,10 @@ class EditableText extends StatefulWidget { ...@@ -484,6 +484,10 @@ class EditableText extends StatefulWidget {
assert(dragStartBehavior != null), assert(dragStartBehavior != null),
assert(toolbarOptions != null), assert(toolbarOptions != null),
assert(clipBehavior != null), assert(clipBehavior != null),
assert(
!readOnly || autofillHints == null,
"Read-only fields can't have autofill hints.",
),
_strutStyle = strutStyle, _strutStyle = strutStyle,
keyboardType = keyboardType ?? _inferKeyboardType(autofillHints: autofillHints, maxLines: maxLines), keyboardType = keyboardType ?? _inferKeyboardType(autofillHints: autofillHints, maxLines: maxLines),
inputFormatters = maxLines == 1 inputFormatters = maxLines == 1
...@@ -1437,6 +1441,21 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1437,6 +1441,21 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// Is this field in the current autofill context. // Is this field in the current autofill context.
bool _isInAutofillContext = false; bool _isInAutofillContext = false;
/// Whether to create an input connection with the platform for text editing
/// or not.
///
/// Read-only input fields do not need a connection with the platform since
/// there's no need for text editing capabilities (e.g. virtual keyboard).
///
/// On the web, we always need a connection because we want some browser
/// functionalities to continue to work on read-only input fields like:
///
/// - Relevant context menu.
/// - cmd/ctrl+c shortcut to copy.
/// - cmd/ctrl+a to select all.
/// - Changing the selection using a physical keyboard.
bool get _shouldCreateInputConnection => kIsWeb || !widget.readOnly;
// This value is an eyeball estimation of the time it takes for the iOS cursor // This value is an eyeball estimation of the time it takes for the iOS cursor
// to ease in and out. // to ease in and out.
static const Duration _fadeDuration = Duration(milliseconds: 250); static const Duration _fadeDuration = Duration(milliseconds: 250);
...@@ -1531,7 +1550,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1531,7 +1550,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
widget.focusNode.addListener(_handleFocusChanged); widget.focusNode.addListener(_handleFocusChanged);
updateKeepAlive(); updateKeepAlive();
} }
if (widget.readOnly) { if (!_shouldCreateInputConnection) {
_closeInputConnectionIfNeeded(); _closeInputConnectionIfNeeded();
} else { } else {
if (oldWidget.readOnly && _hasFocus) if (oldWidget.readOnly && _hasFocus)
...@@ -1597,7 +1616,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1597,7 +1616,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
void updateEditingValue(TextEditingValue value) { void updateEditingValue(TextEditingValue value) {
// Since we still have to support keyboard select, this is the best place // Since we still have to support keyboard select, this is the best place
// to disable text updating. // to disable text updating.
if (widget.readOnly) { if (!_shouldCreateInputConnection) {
return; return;
} }
_receivedRemoteTextEditingValue = value; _receivedRemoteTextEditingValue = value;
...@@ -1846,7 +1865,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1846,7 +1865,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
bool get _shouldBeInAutofillContext => _needsAutofill && currentAutofillScope != null; bool get _shouldBeInAutofillContext => _needsAutofill && currentAutofillScope != null;
void _openInputConnection() { void _openInputConnection() {
if (widget.readOnly) { if (!_shouldCreateInputConnection) {
return; return;
} }
if (!_hasInputConnection) { if (!_hasInputConnection) {
...@@ -2305,6 +2324,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2305,6 +2324,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
assert(needsAutofillConfiguration != null); assert(needsAutofillConfiguration != null);
return TextInputConfiguration( return TextInputConfiguration(
inputType: widget.keyboardType, inputType: widget.keyboardType,
readOnly: widget.readOnly,
obscureText: widget.obscureText, obscureText: widget.obscureText,
autocorrect: widget.autocorrect, autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType ?? (widget.obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), smartDashesType: widget.smartDashesType ?? (widget.obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
......
...@@ -73,6 +73,7 @@ void main() { ...@@ -73,6 +73,7 @@ void main() {
test('sets expected defaults', () { test('sets expected defaults', () {
const TextInputConfiguration configuration = TextInputConfiguration(); const TextInputConfiguration configuration = TextInputConfiguration();
expect(configuration.inputType, TextInputType.text); expect(configuration.inputType, TextInputType.text);
expect(configuration.readOnly, false);
expect(configuration.obscureText, false); expect(configuration.obscureText, false);
expect(configuration.autocorrect, true); expect(configuration.autocorrect, true);
expect(configuration.actionLabel, null); expect(configuration.actionLabel, null);
...@@ -83,6 +84,7 @@ void main() { ...@@ -83,6 +84,7 @@ void main() {
test('text serializes to JSON', () async { test('text serializes to JSON', () async {
const TextInputConfiguration configuration = TextInputConfiguration( const TextInputConfiguration configuration = TextInputConfiguration(
inputType: TextInputType.text, inputType: TextInputType.text,
readOnly: true,
obscureText: true, obscureText: true,
autocorrect: false, autocorrect: false,
actionLabel: 'xyzzy', actionLabel: 'xyzzy',
...@@ -93,6 +95,7 @@ void main() { ...@@ -93,6 +95,7 @@ void main() {
'signed': null, 'signed': null,
'decimal': null, 'decimal': null,
}); });
expect(json['readOnly'], true);
expect(json['obscureText'], true); expect(json['obscureText'], true);
expect(json['autocorrect'], false); expect(json['autocorrect'], false);
expect(json['actionLabel'], 'xyzzy'); expect(json['actionLabel'], 'xyzzy');
...@@ -111,6 +114,7 @@ void main() { ...@@ -111,6 +114,7 @@ void main() {
'signed': false, 'signed': false,
'decimal': true, 'decimal': true,
}); });
expect(json['readOnly'], false);
expect(json['obscureText'], true); expect(json['obscureText'], true);
expect(json['autocorrect'], false); expect(json['autocorrect'], false);
expect(json['actionLabel'], 'xyzzy'); expect(json['actionLabel'], 'xyzzy');
......
...@@ -1302,6 +1302,77 @@ void main() { ...@@ -1302,6 +1302,77 @@ void main() {
expect(find.text('Cut'), findsNothing); expect(find.text('Cut'), findsNothing);
}); });
testWidgets('Handles the read-only flag correctly', (WidgetTester tester) async {
final TextEditingController controller =
TextEditingController(text: 'Lorem ipsum dolor sit amet');
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
);
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
// Select the first word "Lorem".
state.renderEditable.selectWordsInRange(
from: Offset.zero,
cause: SelectionChangedCause.tap,
);
if (kIsWeb) {
// On the web, a regular connection to the platform should've been made
// with the `readOnly` flag set to true.
expect(tester.testTextInput.hasAnyClients, isTrue);
expect(tester.testTextInput.setClientArgs['readOnly'], isTrue);
expect(tester.testTextInput.editingState['text'], equals('Lorem'));
} else {
// On non-web platforms, a read-only field doesn't need a connection with
// the platform.
expect(tester.testTextInput.hasAnyClients, isFalse);
}
});
testWidgets('Does not accept updates when read-only', (WidgetTester tester) async {
final TextEditingController controller =
TextEditingController(text: 'Lorem ipsum dolor sit amet');
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
);
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
// Select something.
state.renderEditable.selectWordsInRange(
from: Offset.zero,
cause: SelectionChangedCause.tap,
);
expect(tester.testTextInput.hasAnyClients, kIsWeb ? isTrue : isFalse);
if (kIsWeb) {
// On the web, the input connection exists, but text updates should be
// ignored.
tester.testTextInput.enterText('Foo bar');
// No change.
expect(controller.text, 'Lorem ipsum dolor sit amet');
}
});
testWidgets('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async { testWidgets('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async {
String changedValue; String changedValue;
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment