diff --git a/dev/integration_tests/android_semantics_testing/test_driver/main_test.dart b/dev/integration_tests/android_semantics_testing/test_driver/main_test.dart index 2670eb03d2..ed651cdfbf 100644 --- a/dev/integration_tests/android_semantics_testing/test_driver/main_test.dart +++ b/dev/integration_tests/android_semantics_testing/test_driver/main_test.dart @@ -126,8 +126,6 @@ void main() { actions: [ AndroidSemanticsAction.click, AndroidSemanticsAction.accessibilityFocus, - AndroidSemanticsAction.setSelection, - AndroidSemanticsAction.copy, ], )); @@ -143,8 +141,6 @@ void main() { actions: [ AndroidSemanticsAction.click, AndroidSemanticsAction.accessibilityFocus, - AndroidSemanticsAction.setSelection, - AndroidSemanticsAction.copy, ], )); }); diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 5a111216df..bb2e470243 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -93,8 +93,8 @@ class TextField extends StatefulWidget { /// switch to the [decoration.errorStyle] when the limit is exceeded. /// /// The [textAlign], [autofocus], [obscureText], [autocorrect], - /// [maxLengthEnforced], [scrollPadding], [maxLines], [maxLength], - /// and [enableInteractiveSelection] arguments must not be null. + /// [maxLengthEnforced], [scrollPadding], [maxLines], and [maxLength] + /// arguments must not be null. /// /// See also: /// @@ -127,7 +127,7 @@ class TextField extends StatefulWidget { this.cursorColor, this.keyboardAppearance, this.scrollPadding = const EdgeInsets.all(20.0), - this.enableInteractiveSelection = true, + this.enableInteractiveSelection, this.onTap, }) : assert(textAlign != null), assert(autofocus != null), @@ -138,7 +138,6 @@ class TextField extends StatefulWidget { assert(maxLines == null || maxLines > 0), assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0), keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), - assert(enableInteractiveSelection != null), super(key: key); /// Controls the text being edited. @@ -340,6 +339,11 @@ class TextField extends StatefulWidget { /// {@macro flutter.widgets.editableText.enableInteractiveSelection} final bool enableInteractiveSelection; + /// {@macro flutter.rendering.editable.selectionEnabled} + bool get selectionEnabled { + return enableInteractiveSelection ?? !obscureText; + } + /// Called when the user taps on this textfield. /// /// The textfield builds a [GestureDetector] to handle input events like tap, @@ -517,7 +521,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi } void _handleTap() { - if (widget.enableInteractiveSelection) + if (widget.selectionEnabled) _renderEditable.handleTap(); _requestKeyboard(); _confirmCurrentSplash(); @@ -530,7 +534,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi } void _handleLongPress() { - if (widget.enableInteractiveSelection) + if (widget.selectionEnabled) _renderEditable.handleLongPress(); _confirmCurrentSplash(); } @@ -608,7 +612,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi autocorrect: widget.autocorrect, maxLines: widget.maxLines, selectionColor: themeData.textSelectionColor, - selectionControls: widget.enableInteractiveSelection + selectionControls: widget.selectionEnabled ? (themeData.platform == TargetPlatform.iOS ? cupertinoTextSelectionControls : materialTextSelectionControls) diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index cb55f705e8..579885a0fb 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -129,8 +129,6 @@ class RenderEditable extends RenderBox { /// /// The [offset] is required and must not be null. You can use [new /// ViewportOffset.zero] if you have no need for scrolling. - /// - /// The [enableInteractiveSelection] argument must not be null. RenderEditable({ TextSpan text, @required TextDirection textDirection, @@ -151,8 +149,8 @@ class RenderEditable extends RenderBox { Locale locale, double cursorWidth = 1.0, Radius cursorRadius, + bool enableInteractiveSelection, EdgeInsets floatingCursorAddedMargin = const EdgeInsets.fromLTRB(3, 6, 3, 6), - bool enableInteractiveSelection = true, @required this.textSelectionDelegate, }) : assert(textAlign != null), assert(textDirection != null, 'RenderEditable created without a textDirection.'), @@ -161,7 +159,6 @@ class RenderEditable extends RenderBox { assert(offset != null), assert(ignorePointer != null), assert(obscureText != null), - assert(enableInteractiveSelection != null), assert(textSelectionDelegate != null), _textPainter = TextPainter( text: text, @@ -756,6 +753,22 @@ class RenderEditable extends RenderBox { markNeedsSemanticsUpdate(); } + /// {@template flutter.rendering.editable.selectionEnabled} + /// True if interactive selection is enabled based on the values of + /// [enableInteractiveSelection] and [obscureText]. + /// + /// By default [enableInteractiveSelection] is null, obscureText is false, + /// and this method returns true. + /// If [enableInteractiveSelection] is null and obscureText is true, then this + /// method returns false. This is the common case for password fields. + /// If [enableInteractiveSelection] is non-null then its value is returned. An + /// app might set it to true to enable interactive selection for a password + /// field, or to false to unconditionally disable interactive selection. + /// {@endtemplate} + bool get selectionEnabled { + return enableInteractiveSelection ?? !obscureText; + } + @override void describeSemanticsConfiguration(SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); @@ -769,10 +782,10 @@ class RenderEditable extends RenderBox { ..isFocused = hasFocus ..isTextField = true; - if (hasFocus && enableInteractiveSelection) + if (hasFocus && selectionEnabled) config.onSetSelection = _handleSetSelection; - if (enableInteractiveSelection && _selection?.isValid == true) { + if (selectionEnabled && _selection?.isValid == true) { config.textSelection = _selection; if (_textPainter.getOffsetBefore(_selection.extentOffset) != null) { config diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index b5dade4e48..fb5c01ccf1 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -184,8 +184,7 @@ class EditableText extends StatefulWidget { /// default to [TextInputType.multiline]. /// /// The [controller], [focusNode], [style], [cursorColor], [backgroundCursorColor], - /// [textAlign], [rendererIgnoresPointer], and [enableInteractiveSelection] - /// arguments must not be null. + /// [textAlign], and [rendererIgnoresPointer] arguments must not be null. EditableText({ Key key, @required this.controller, @@ -216,7 +215,7 @@ class EditableText extends StatefulWidget { this.cursorRadius, this.scrollPadding = const EdgeInsets.all(20.0), this.keyboardAppearance = Brightness.light, - this.enableInteractiveSelection = true, + this.enableInteractiveSelection, }) : assert(controller != null), assert(focusNode != null), assert(obscureText != null), @@ -229,7 +228,6 @@ class EditableText extends StatefulWidget { assert(autofocus != null), assert(rendererIgnoresPointer != null), assert(scrollPadding != null), - assert(enableInteractiveSelection != null), keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), inputFormatters = maxLines == 1 ? ( @@ -479,6 +477,11 @@ class EditableText extends StatefulWidget { /// Defaults to false, resulting in a typical blinking cursor. static bool debugDeterministicCursor = false; + /// {@macro flutter.rendering.editable.selectionEnabled} + bool get selectionEnabled { + return enableInteractiveSelection ?? !obscureText; + } + @override EditableTextState createState() => EditableTextState(); @@ -1013,19 +1016,19 @@ class EditableTextState extends State with AutomaticKeepAliveClien } VoidCallback _semanticsOnCopy(TextSelectionControls controls) { - return widget.enableInteractiveSelection && _hasFocus && controls?.canCopy(this) == true + return widget.selectionEnabled && _hasFocus && controls?.canCopy(this) == true ? () => controls.handleCopy(this) : null; } VoidCallback _semanticsOnCut(TextSelectionControls controls) { - return widget.enableInteractiveSelection && _hasFocus && controls?.canCut(this) == true + return widget.selectionEnabled && _hasFocus && controls?.canCut(this) == true ? () => controls.handleCut(this) : null; } VoidCallback _semanticsOnPaste(TextSelectionControls controls) { - return widget.enableInteractiveSelection &&_hasFocus && controls?.canPaste(this) == true + return widget.selectionEnabled &&_hasFocus && controls?.canPaste(this) == true ? () => controls.handlePaste(this) : null; } @@ -1136,11 +1139,10 @@ class _Editable extends LeafRenderObjectWidget { this.rendererIgnoresPointer = false, this.cursorWidth, this.cursorRadius, - this.enableInteractiveSelection = true, + this.enableInteractiveSelection, this.textSelectionDelegate, }) : assert(textDirection != null), assert(rendererIgnoresPointer != null), - assert(enableInteractiveSelection != null), super(key: key); final TextSpan textSpan; diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 64ff26242c..3cd4937990 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -658,6 +658,66 @@ void main() { // End the test here to ensure the animation is properly disposed of. }); + testWidgets('An obscured TextField is not selectable by default', (WidgetTester tester) async { + // This is a regression test for + // https://github.com/flutter/flutter/issues/24100 + + final TextEditingController controller = TextEditingController(); + Widget buildFrame(bool obscureText, bool enableInteractiveSelection) { + return overlay( + child: TextField( + controller: controller, + obscureText: obscureText, + enableInteractiveSelection: enableInteractiveSelection, + ), + ); + } + + // Obscure text and don't enable or disable selection + await tester.pumpWidget(buildFrame(true, null)); + await tester.enterText(find.byType(TextField), 'abcdefghi'); + await skipPastScrollingAnimation(tester); + expect(controller.selection.isCollapsed, true); + + // Long press doesn't select anything + final Offset ePos = textOffsetToPosition(tester, 1); + final TestGesture gesture = await tester.startGesture(ePos, pointer: 7); + await tester.pump(const Duration(seconds: 2)); + await gesture.up(); + await tester.pump(); + expect(controller.selection.isCollapsed, true); + }); + + testWidgets('An obscured TextField is selectable when enabled', (WidgetTester tester) async { + // This is a regression test for + // https://github.com/flutter/flutter/issues/24100 + + final TextEditingController controller = TextEditingController(); + Widget buildFrame(bool obscureText, bool enableInteractiveSelection) { + return overlay( + child: TextField( + controller: controller, + obscureText: obscureText, + enableInteractiveSelection: enableInteractiveSelection, + ), + ); + } + + // Explicitly allow selection on obscured text + await tester.pumpWidget(buildFrame(true, true)); + await tester.enterText(find.byType(TextField), 'abcdefghi'); + await skipPastScrollingAnimation(tester); + expect(controller.selection.isCollapsed, true); + + // Long press does select text + final Offset ePos2 = textOffsetToPosition(tester, 1); + final TestGesture gesture2 = await tester.startGesture(ePos2, pointer: 7); + await tester.pump(const Duration(seconds: 2)); + await gesture2.up(); + await tester.pump(); + expect(controller.selection.isCollapsed, false); + }); + testWidgets('Multiline text will wrap up to maxLines', (WidgetTester tester) async { final Key textFieldKey = UniqueKey();