Skip to content

Commit f8f5953

Browse files
authored
obscureText and enableInteractiveSelection defaults (#24527)
* obscureText true defaults to disabling selection * Tests and comments for selectable text field * Improve selection docs * Refactor so that all enableInteractiveSelection params are null by default, delegate to selectionEnabled * Fix selection param macros
1 parent affcc38 commit f8f5953

4 files changed

Lines changed: 101 additions & 22 deletions

File tree

‎packages/flutter/lib/src/material/text_field.dart‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ class TextField extends StatefulWidget {
9292
/// switch to the [decoration.errorStyle] when the limit is exceeded.
9393
///
9494
/// The [textAlign], [autofocus], [obscureText], [autocorrect],
95-
/// [maxLengthEnforced], [scrollPadding], [maxLines], [maxLength],
96-
/// and [enableInteractiveSelection] arguments must not be null.
95+
/// [maxLengthEnforced], [scrollPadding], [maxLines], and [maxLength]
96+
/// arguments must not be null.
9797
///
9898
/// See also:
9999
///
@@ -126,7 +126,7 @@ class TextField extends StatefulWidget {
126126
this.cursorColor,
127127
this.keyboardAppearance,
128128
this.scrollPadding = const EdgeInsets.all(20.0),
129-
this.enableInteractiveSelection = true,
129+
this.enableInteractiveSelection,
130130
this.onTap,
131131
}) : assert(textAlign != null),
132132
assert(autofocus != null),
@@ -137,7 +137,6 @@ class TextField extends StatefulWidget {
137137
assert(maxLines == null || maxLines > 0),
138138
assert(maxLength == null || maxLength > 0),
139139
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
140-
assert(enableInteractiveSelection != null),
141140
super(key: key);
142141

143142
/// Controls the text being edited.
@@ -334,6 +333,11 @@ class TextField extends StatefulWidget {
334333
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
335334
final bool enableInteractiveSelection;
336335

336+
/// {@macro flutter.rendering.editable.selectionEnabled}
337+
bool get selectionEnabled {
338+
return enableInteractiveSelection ?? !obscureText;
339+
}
340+
337341
/// Called when the user taps on this textfield.
338342
///
339343
/// The textfield builds a [GestureDetector] to handle input events like tap,
@@ -508,7 +512,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
508512
}
509513

510514
void _handleTap() {
511-
if (widget.enableInteractiveSelection)
515+
if (widget.selectionEnabled)
512516
_renderEditable.handleTap();
513517
_requestKeyboard();
514518
_confirmCurrentSplash();
@@ -521,7 +525,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
521525
}
522526

523527
void _handleLongPress() {
524-
if (widget.enableInteractiveSelection)
528+
if (widget.selectionEnabled)
525529
_renderEditable.handleLongPress();
526530
_confirmCurrentSplash();
527531
}
@@ -599,7 +603,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
599603
autocorrect: widget.autocorrect,
600604
maxLines: widget.maxLines,
601605
selectionColor: themeData.textSelectionColor,
602-
selectionControls: widget.enableInteractiveSelection
606+
selectionControls: widget.selectionEnabled
603607
? (themeData.platform == TargetPlatform.iOS
604608
? cupertinoTextSelectionControls
605609
: materialTextSelectionControls)

‎packages/flutter/lib/src/rendering/editable.dart‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,6 @@ class RenderEditable extends RenderBox {
122122
///
123123
/// The [offset] is required and must not be null. You can use [new
124124
/// ViewportOffset.zero] if you have no need for scrolling.
125-
///
126-
/// The [enableInteractiveSelection] argument must not be null.
127125
RenderEditable({
128126
TextSpan text,
129127
@required TextDirection textDirection,
@@ -143,7 +141,7 @@ class RenderEditable extends RenderBox {
143141
Locale locale,
144142
double cursorWidth = 1.0,
145143
Radius cursorRadius,
146-
bool enableInteractiveSelection = true,
144+
bool enableInteractiveSelection,
147145
@required this.textSelectionDelegate,
148146
}) : assert(textAlign != null),
149147
assert(textDirection != null, 'RenderEditable created without a textDirection.'),
@@ -152,7 +150,6 @@ class RenderEditable extends RenderBox {
152150
assert(offset != null),
153151
assert(ignorePointer != null),
154152
assert(obscureText != null),
155-
assert(enableInteractiveSelection != null),
156153
assert(textSelectionDelegate != null),
157154
_textPainter = TextPainter(
158155
text: text,
@@ -715,6 +712,22 @@ class RenderEditable extends RenderBox {
715712
markNeedsSemanticsUpdate();
716713
}
717714

715+
/// {@template flutter.rendering.editable.selectionEnabled}
716+
/// True if interactive selection is enabled based on the values of
717+
/// [enableInteractiveSelection] and [obscureText].
718+
///
719+
/// By default [enableInteractiveSelection] is null, obscureText is false,
720+
/// and this method returns true.
721+
/// If [enableInteractiveSelection] is null and obscureText is true, then this
722+
/// method returns false. This is the common case for password fields.
723+
/// If [enableInteractiveSelection] is non-null then its value is returned. An
724+
/// app might set it to true to enable interactive selection for a password
725+
/// field, or to false to unconditionally disable interactive selection.
726+
/// {@endtemplate}
727+
bool get selectionEnabled {
728+
return enableInteractiveSelection ?? !obscureText;
729+
}
730+
718731
@override
719732
void describeSemanticsConfiguration(SemanticsConfiguration config) {
720733
super.describeSemanticsConfiguration(config);
@@ -728,10 +741,10 @@ class RenderEditable extends RenderBox {
728741
..isFocused = hasFocus
729742
..isTextField = true;
730743

731-
if (hasFocus && enableInteractiveSelection)
744+
if (hasFocus && selectionEnabled)
732745
config.onSetSelection = _handleSetSelection;
733746

734-
if (enableInteractiveSelection && _selection?.isValid == true) {
747+
if (selectionEnabled && _selection?.isValid == true) {
735748
config.textSelection = _selection;
736749
if (_textPainter.getOffsetBefore(_selection.extentOffset) != null) {
737750
config

‎packages/flutter/lib/src/widgets/editable_text.dart‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,7 @@ class EditableText extends StatefulWidget {
183183
/// default to [TextInputType.multiline].
184184
///
185185
/// The [controller], [focusNode], [style], [cursorColor], [textAlign],
186-
/// [rendererIgnoresPointer], and [enableInteractiveSelection] arguments must
187-
/// not be null.
186+
/// [rendererIgnoresPointer] arguments must not be null.
188187
EditableText({
189188
Key key,
190189
@required this.controller,
@@ -214,7 +213,7 @@ class EditableText extends StatefulWidget {
214213
this.cursorRadius,
215214
this.scrollPadding = const EdgeInsets.all(20.0),
216215
this.keyboardAppearance = Brightness.light,
217-
this.enableInteractiveSelection = true,
216+
this.enableInteractiveSelection,
218217
}) : assert(controller != null),
219218
assert(focusNode != null),
220219
assert(obscureText != null),
@@ -226,7 +225,6 @@ class EditableText extends StatefulWidget {
226225
assert(autofocus != null),
227226
assert(rendererIgnoresPointer != null),
228227
assert(scrollPadding != null),
229-
assert(enableInteractiveSelection != null),
230228
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
231229
inputFormatters = maxLines == 1
232230
? (
@@ -469,6 +467,11 @@ class EditableText extends StatefulWidget {
469467
/// Defaults to false, resulting in a typical blinking cursor.
470468
static bool debugDeterministicCursor = false;
471469

470+
/// {@macro flutter.rendering.editable.selectionEnabled}
471+
bool get selectionEnabled {
472+
return enableInteractiveSelection ?? !obscureText;
473+
}
474+
472475
@override
473476
EditableTextState createState() => EditableTextState();
474477

@@ -929,19 +932,19 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
929932
}
930933

931934
VoidCallback _semanticsOnCopy(TextSelectionControls controls) {
932-
return widget.enableInteractiveSelection && _hasFocus && controls?.canCopy(this) == true
935+
return widget.selectionEnabled && _hasFocus && controls?.canCopy(this) == true
933936
? () => controls.handleCopy(this)
934937
: null;
935938
}
936939

937940
VoidCallback _semanticsOnCut(TextSelectionControls controls) {
938-
return widget.enableInteractiveSelection && _hasFocus && controls?.canCut(this) == true
941+
return widget.selectionEnabled && _hasFocus && controls?.canCut(this) == true
939942
? () => controls.handleCut(this)
940943
: null;
941944
}
942945

943946
VoidCallback _semanticsOnPaste(TextSelectionControls controls) {
944-
return widget.enableInteractiveSelection &&_hasFocus && controls?.canPaste(this) == true
947+
return widget.selectionEnabled &&_hasFocus && controls?.canPaste(this) == true
945948
? () => controls.handlePaste(this)
946949
: null;
947950
}
@@ -1050,11 +1053,10 @@ class _Editable extends LeafRenderObjectWidget {
10501053
this.rendererIgnoresPointer = false,
10511054
this.cursorWidth,
10521055
this.cursorRadius,
1053-
this.enableInteractiveSelection = true,
1056+
this.enableInteractiveSelection,
10541057
this.textSelectionDelegate,
10551058
}) : assert(textDirection != null),
10561059
assert(rendererIgnoresPointer != null),
1057-
assert(enableInteractiveSelection != null),
10581060
super(key: key);
10591061

10601062
final TextSpan textSpan;

‎packages/flutter/test/material/text_field_test.dart‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,66 @@ void main() {
658658
// End the test here to ensure the animation is properly disposed of.
659659
});
660660

661+
testWidgets('An obscured TextField is not selectable by default', (WidgetTester tester) async {
662+
// This is a regression test for
663+
// https://github.com/flutter/flutter/issues/24100
664+
665+
final TextEditingController controller = TextEditingController();
666+
Widget buildFrame(bool obscureText, bool enableInteractiveSelection) {
667+
return overlay(
668+
child: TextField(
669+
controller: controller,
670+
obscureText: obscureText,
671+
enableInteractiveSelection: enableInteractiveSelection,
672+
),
673+
);
674+
}
675+
676+
// Obscure text and don't enable or disable selection
677+
await tester.pumpWidget(buildFrame(true, null));
678+
await tester.enterText(find.byType(TextField), 'abcdefghi');
679+
await skipPastScrollingAnimation(tester);
680+
expect(controller.selection.isCollapsed, true);
681+
682+
// Long press doesn't select anything
683+
final Offset ePos = textOffsetToPosition(tester, 1);
684+
final TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
685+
await tester.pump(const Duration(seconds: 2));
686+
await gesture.up();
687+
await tester.pump();
688+
expect(controller.selection.isCollapsed, true);
689+
});
690+
691+
testWidgets('An obscured TextField is selectable when enabled', (WidgetTester tester) async {
692+
// This is a regression test for
693+
// https://github.com/flutter/flutter/issues/24100
694+
695+
final TextEditingController controller = TextEditingController();
696+
Widget buildFrame(bool obscureText, bool enableInteractiveSelection) {
697+
return overlay(
698+
child: TextField(
699+
controller: controller,
700+
obscureText: obscureText,
701+
enableInteractiveSelection: enableInteractiveSelection,
702+
),
703+
);
704+
}
705+
706+
// Explicitly allow selection on obscured text
707+
await tester.pumpWidget(buildFrame(true, true));
708+
await tester.enterText(find.byType(TextField), 'abcdefghi');
709+
await skipPastScrollingAnimation(tester);
710+
expect(controller.selection.isCollapsed, true);
711+
712+
// Long press does select text
713+
final Offset ePos2 = textOffsetToPosition(tester, 1);
714+
final TestGesture gesture2 = await tester.startGesture(ePos2, pointer: 7);
715+
await tester.pump(const Duration(seconds: 2));
716+
await gesture2.up();
717+
await tester.pump();
718+
expect(controller.selection.isCollapsed, false);
719+
});
720+
661721
testWidgets('Multiline text will wrap up to maxLines', (WidgetTester tester) async {
662722
final Key textFieldKey = UniqueKey();
663723

0 commit comments

Comments
 (0)