diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index b0067f3405d..0584c69c9a3 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -186,6 +186,7 @@ class CupertinoTextField extends StatefulWidget { this.keyboardAppearance, this.scrollPadding = const EdgeInsets.all(20.0), this.dragStartBehavior = DragStartBehavior.start, + this.enableInteractiveSelection, this.scrollPhysics, }) : assert(textAlign != null), assert(autofocus != null), @@ -423,12 +424,20 @@ class CupertinoTextField extends StatefulWidget { /// {@macro flutter.widgets.editableText.scrollPadding} final EdgeInsets scrollPadding; + /// {@macro flutter.widgets.editableText.enableInteractiveSelection} + final bool enableInteractiveSelection; + /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// {@macro flutter.widgets.edtiableText.scrollPhysics} final ScrollPhysics scrollPhysics; + /// {@macro flutter.rendering.editable.selectionEnabled} + bool get selectionEnabled { + return enableInteractiveSelection ?? !obscureText; + } + @override _CupertinoTextFieldState createState() => _CupertinoTextFieldState(); @@ -456,6 +465,7 @@ class CupertinoTextField extends StatefulWidget { properties.add(IntProperty('maxLength', maxLength, defaultValue: null)); properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, ifTrue: 'max length enforced')); properties.add(DiagnosticsProperty('cursorColor', cursorColor, defaultValue: null)); + properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled')); properties.add(DiagnosticsProperty('scrollPhysics', scrollPhysics, defaultValue: null)); } } @@ -530,10 +540,12 @@ class _CupertinoTextFieldState extends State with AutomaticK } void _handleForcePressStarted(ForcePressDetails details) { - _renderEditable.selectWordsInRange( - from: details.globalPosition, - cause: SelectionChangedCause.forcePress, - ); + if (widget.selectionEnabled) { + _renderEditable.selectWordsInRange( + from: details.globalPosition, + cause: SelectionChangedCause.forcePress, + ); + } } void _handleForcePressEnded(ForcePressDetails details) { @@ -546,22 +558,28 @@ class _CupertinoTextFieldState extends State with AutomaticK } void _handleSingleTapUp(TapUpDetails details) { - _renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); + if (widget.selectionEnabled) { + _renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); + } _requestKeyboard(); } void _handleSingleLongTapStart(LongPressStartDetails details) { - _renderEditable.selectPositionAt( - from: details.globalPosition, - cause: SelectionChangedCause.longPress, - ); + if (widget.selectionEnabled) { + _renderEditable.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.longPress, + ); + } } void _handleSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { - _renderEditable.selectPositionAt( - from: details.globalPosition, - cause: SelectionChangedCause.longPress, - ); + if (widget.selectionEnabled) { + _renderEditable.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.longPress, + ); + } } void _handleSingleLongTapEnd(LongPressEndDetails details) { @@ -570,9 +588,11 @@ class _CupertinoTextFieldState extends State with AutomaticK } void _handleDoubleTapDown(TapDownDetails details) { - _renderEditable.selectWord(cause: SelectionChangedCause.tap); - if (_shouldShowSelectionToolbar) - _editableText.showToolbar(); + if (widget.selectionEnabled) { + _renderEditable.selectWord(cause: SelectionChangedCause.tap); + if (_shouldShowSelectionToolbar) + _editableText.showToolbar(); + } } bool _shouldShowSelectionHandles(SelectionChangedCause cause) { @@ -782,7 +802,8 @@ class _CupertinoTextFieldState extends State with AutomaticK minLines: widget.minLines, expands: widget.expands, selectionColor: _kSelectionHighlightColor, - selectionControls: cupertinoTextSelectionControls, + selectionControls: widget.selectionEnabled + ? cupertinoTextSelectionControls : null, onChanged: widget.onChanged, onSelectionChanged: _handleSelectionChanged, onEditingComplete: widget.onEditingComplete, @@ -800,6 +821,7 @@ class _CupertinoTextFieldState extends State with AutomaticK keyboardAppearance: keyboardAppearance, dragStartBehavior: widget.dragStartBehavior, scrollPhysics: widget.scrollPhysics, + enableInteractiveSelection: widget.enableInteractiveSelection, ), ), ); diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index 3308eccdde5..6c8c9883618 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -1399,6 +1399,102 @@ void main() { }, ); + testWidgets( + 'An obscured CupertinoTextField is not selectable by default', + (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: 'Atwater Peel Sherbrooke Bonaventure', + ); + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: controller, + obscureText: true, + ), + ), + ), + ); + + final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField)); + + await tester.tapAt(textfieldStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 50)); + final TestGesture gesture = + await tester.startGesture(textfieldStart + const Offset(150.0, 5.0)); + // Hold the press. + await tester.pump(const Duration(milliseconds: 500)); + + // Nothing is selected despite the double tap long press gesture. + expect( + controller.selection, + const TextSelection(baseOffset: 35, extentOffset: 35), + ); + + // The selection menu is not present. + expect(find.byType(CupertinoButton), findsNWidgets(0)); + + await gesture.up(); + await tester.pump(); + + // Still nothing selected and no selection menu. + expect( + controller.selection, + const TextSelection(baseOffset: 35, extentOffset: 35), + ); + expect(find.byType(CupertinoButton), findsNWidgets(0)); + }, + ); + + testWidgets( + 'An obscured CupertinoTextField is selectable when enabled', + (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: 'Atwater Peel Sherbrooke Bonaventure', + ); + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: controller, + obscureText: true, + enableInteractiveSelection: true, + ), + ), + ), + ); + + final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField)); + + await tester.tapAt(textfieldStart + const Offset(150.0, 5.0)); + await tester.pump(const Duration(milliseconds: 50)); + final TestGesture gesture = + await tester.startGesture(textfieldStart + const Offset(150.0, 5.0)); + // Hold the press. + await tester.pump(const Duration(milliseconds: 500)); + + // The obscured text is not broken into words, so only one letter is + // selected at a time. + expect( + controller.selection, + const TextSelection(baseOffset: 9, extentOffset: 10), + ); + + // Selected text shows 3 toolbar buttons. + expect(find.byType(CupertinoButton), findsNWidgets(3)); + + await gesture.up(); + await tester.pump(); + + // Still selected. + expect( + controller.selection, + const TextSelection(baseOffset: 9, extentOffset: 10), + ); + expect(find.byType(CupertinoButton), findsNWidgets(3)); + }, + ); + testWidgets( 'long press moves cursor to the exact long press position and shows toolbar', (WidgetTester tester) async {