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 {
/// [actionLabel] may be null.
const TextInputConfiguration({
this.inputType = TextInputType.text,
this.readOnly = false,
this.obscureText = false,
this.autocorrect = true,
SmartDashesType? smartDashesType,
......@@ -470,6 +471,11 @@ class TextInputConfiguration {
/// The type of information for which to optimize the text input control.
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).
///
/// Defaults to false.
......@@ -580,6 +586,7 @@ class TextInputConfiguration {
Map<String, dynamic> toJson() {
return <String, dynamic>{
'inputType': inputType.toJson(),
'readOnly': readOnly,
'obscureText': obscureText,
'autocorrect': autocorrect,
'smartDashesType': smartDashesType.index.toString(),
......
......@@ -484,6 +484,10 @@ class EditableText extends StatefulWidget {
assert(dragStartBehavior != null),
assert(toolbarOptions != null),
assert(clipBehavior != null),
assert(
!readOnly || autofillHints == null,
"Read-only fields can't have autofill hints.",
),
_strutStyle = strutStyle,
keyboardType = keyboardType ?? _inferKeyboardType(autofillHints: autofillHints, maxLines: maxLines),
inputFormatters = maxLines == 1
......@@ -1437,6 +1441,21 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// Is this field in the current autofill context.
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
// to ease in and out.
static const Duration _fadeDuration = Duration(milliseconds: 250);
......@@ -1531,7 +1550,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
widget.focusNode.addListener(_handleFocusChanged);
updateKeepAlive();
}
if (widget.readOnly) {
if (!_shouldCreateInputConnection) {
_closeInputConnectionIfNeeded();
} else {
if (oldWidget.readOnly && _hasFocus)
......@@ -1597,7 +1616,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
void updateEditingValue(TextEditingValue value) {
// Since we still have to support keyboard select, this is the best place
// to disable text updating.
if (widget.readOnly) {
if (!_shouldCreateInputConnection) {
return;
}
_receivedRemoteTextEditingValue = value;
......@@ -1846,7 +1865,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
bool get _shouldBeInAutofillContext => _needsAutofill && currentAutofillScope != null;
void _openInputConnection() {
if (widget.readOnly) {
if (!_shouldCreateInputConnection) {
return;
}
if (!_hasInputConnection) {
......@@ -2305,6 +2324,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
assert(needsAutofillConfiguration != null);
return TextInputConfiguration(
inputType: widget.keyboardType,
readOnly: widget.readOnly,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType ?? (widget.obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
......
......@@ -73,6 +73,7 @@ void main() {
test('sets expected defaults', () {
const TextInputConfiguration configuration = TextInputConfiguration();
expect(configuration.inputType, TextInputType.text);
expect(configuration.readOnly, false);
expect(configuration.obscureText, false);
expect(configuration.autocorrect, true);
expect(configuration.actionLabel, null);
......@@ -83,6 +84,7 @@ void main() {
test('text serializes to JSON', () async {
const TextInputConfiguration configuration = TextInputConfiguration(
inputType: TextInputType.text,
readOnly: true,
obscureText: true,
autocorrect: false,
actionLabel: 'xyzzy',
......@@ -93,6 +95,7 @@ void main() {
'signed': null,
'decimal': null,
});
expect(json['readOnly'], true);
expect(json['obscureText'], true);
expect(json['autocorrect'], false);
expect(json['actionLabel'], 'xyzzy');
......@@ -111,6 +114,7 @@ void main() {
'signed': false,
'decimal': true,
});
expect(json['readOnly'], false);
expect(json['obscureText'], true);
expect(json['autocorrect'], false);
expect(json['actionLabel'], 'xyzzy');
......
......@@ -1302,6 +1302,77 @@ void main() {
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 {
String changedValue;
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