Unverified Commit a9e0dd40 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Disallow copy and cut when text field is obscured. (#96309)

Before this change, it was possible to select and copy obscured text from a text field.

This  changes things so that:
- Obscured text fields don't allow copy or cut.
- If a field is both obscured and read-only, then selection is disabled as well (if you can't modify it, and can't copy it, there's no point in selecting it).
parent e25e1f90
...@@ -289,7 +289,7 @@ class CupertinoTextField extends StatefulWidget { ...@@ -289,7 +289,7 @@ class CupertinoTextField extends StatefulWidget {
this.keyboardAppearance, this.keyboardAppearance,
this.scrollPadding = const EdgeInsets.all(20.0), this.scrollPadding = const EdgeInsets.all(20.0),
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true, bool? enableInteractiveSelection,
this.selectionControls, this.selectionControls,
this.onTap, this.onTap,
this.scrollController, this.scrollController,
...@@ -341,17 +341,31 @@ class CupertinoTextField extends StatefulWidget { ...@@ -341,17 +341,31 @@ class CupertinoTextField extends StatefulWidget {
), ),
assert(enableIMEPersonalizedLearning != null), assert(enableIMEPersonalizedLearning != null),
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
toolbarOptions = toolbarOptions ?? (obscureText ? enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
const ToolbarOptions( toolbarOptions = toolbarOptions ??
(obscureText
? (readOnly
// No point in even offering "Select All" in a read-only obscured
// field.
? const ToolbarOptions()
// Writable, but obscured.
: const ToolbarOptions(
selectAll: true, selectAll: true,
paste: true, paste: true,
) : ))
const ToolbarOptions( : (readOnly
// Read-only, not obscured.
? const ToolbarOptions(
selectAll: true,
copy: true,
)
// Writable, not obscured.
: const ToolbarOptions(
copy: true, copy: true,
cut: true, cut: true,
selectAll: true, selectAll: true,
paste: true, paste: true,
)), ))),
super(key: key); super(key: key);
/// Creates a borderless iOS-style text field. /// Creates a borderless iOS-style text field.
...@@ -446,7 +460,7 @@ class CupertinoTextField extends StatefulWidget { ...@@ -446,7 +460,7 @@ class CupertinoTextField extends StatefulWidget {
this.keyboardAppearance, this.keyboardAppearance,
this.scrollPadding = const EdgeInsets.all(20.0), this.scrollPadding = const EdgeInsets.all(20.0),
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true, bool? enableInteractiveSelection,
this.selectionControls, this.selectionControls,
this.onTap, this.onTap,
this.scrollController, this.scrollController,
...@@ -499,17 +513,31 @@ class CupertinoTextField extends StatefulWidget { ...@@ -499,17 +513,31 @@ class CupertinoTextField extends StatefulWidget {
assert(clipBehavior != null), assert(clipBehavior != null),
assert(enableIMEPersonalizedLearning != null), assert(enableIMEPersonalizedLearning != null),
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
toolbarOptions = toolbarOptions ?? (obscureText ? enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
const ToolbarOptions( toolbarOptions = toolbarOptions ??
(obscureText
? (readOnly
// No point in even offering "Select All" in a read-only obscured
// field.
? const ToolbarOptions()
// Writable, but obscured.
: const ToolbarOptions(
selectAll: true, selectAll: true,
paste: true, paste: true,
) : ))
const ToolbarOptions( : (readOnly
// Read-only, not obscured.
? const ToolbarOptions(
selectAll: true,
copy: true,
)
// Writable, not obscured.
: const ToolbarOptions(
copy: true, copy: true,
cut: true, cut: true,
selectAll: true, selectAll: true,
paste: true, paste: true,
)), ))),
super(key: key); super(key: key);
/// Controls the text being edited. /// Controls the text being edited.
......
...@@ -319,7 +319,7 @@ class TextField extends StatefulWidget { ...@@ -319,7 +319,7 @@ class TextField extends StatefulWidget {
this.keyboardAppearance, this.keyboardAppearance,
this.scrollPadding = const EdgeInsets.all(20.0), this.scrollPadding = const EdgeInsets.all(20.0),
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true, bool? enableInteractiveSelection,
this.selectionControls, this.selectionControls,
this.onTap, this.onTap,
this.mouseCursor, this.mouseCursor,
...@@ -339,7 +339,6 @@ class TextField extends StatefulWidget { ...@@ -339,7 +339,6 @@ class TextField extends StatefulWidget {
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
assert(enableSuggestions != null), assert(enableSuggestions != null),
assert(enableInteractiveSelection != null),
assert(maxLengthEnforced != null), assert(maxLengthEnforced != null),
assert( assert(
maxLengthEnforced || maxLengthEnforcement == null, maxLengthEnforced || maxLengthEnforcement == null,
...@@ -372,17 +371,31 @@ class TextField extends StatefulWidget { ...@@ -372,17 +371,31 @@ class TextField extends StatefulWidget {
assert(clipBehavior != null), assert(clipBehavior != null),
assert(enableIMEPersonalizedLearning != null), assert(enableIMEPersonalizedLearning != null),
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
toolbarOptions = toolbarOptions ?? (obscureText ? enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
const ToolbarOptions( toolbarOptions = toolbarOptions ??
(obscureText
? (readOnly
// No point in even offering "Select All" in a read-only obscured
// field.
? const ToolbarOptions()
// Writable, but obscured.
: const ToolbarOptions(
selectAll: true, selectAll: true,
paste: true, paste: true,
) : ))
const ToolbarOptions( : (readOnly
// Read-only, not obscured.
? const ToolbarOptions(
selectAll: true,
copy: true,
)
// Writable, not obscured.
: const ToolbarOptions(
copy: true, copy: true,
cut: true, cut: true,
selectAll: true, selectAll: true,
paste: true, paste: true,
)), ))),
super(key: key); super(key: key);
/// Controls the text being edited. /// Controls the text being edited.
......
...@@ -143,7 +143,7 @@ class TextFormField extends FormField<String> { ...@@ -143,7 +143,7 @@ class TextFormField extends FormField<String> {
Color? cursorColor, Color? cursorColor,
Brightness? keyboardAppearance, Brightness? keyboardAppearance,
EdgeInsets scrollPadding = const EdgeInsets.all(20.0), EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
bool enableInteractiveSelection = true, bool? enableInteractiveSelection,
TextSelectionControls? selectionControls, TextSelectionControls? selectionControls,
InputCounterWidgetBuilder? buildCounter, InputCounterWidgetBuilder? buildCounter,
ScrollPhysics? scrollPhysics, ScrollPhysics? scrollPhysics,
...@@ -179,7 +179,6 @@ class TextFormField extends FormField<String> { ...@@ -179,7 +179,6 @@ class TextFormField extends FormField<String> {
), ),
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'), assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0), assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
assert(enableInteractiveSelection != null),
assert(enableIMEPersonalizedLearning != null), assert(enableIMEPersonalizedLearning != null),
super( super(
key: key, key: key,
...@@ -243,7 +242,7 @@ class TextFormField extends FormField<String> { ...@@ -243,7 +242,7 @@ class TextFormField extends FormField<String> {
scrollPadding: scrollPadding, scrollPadding: scrollPadding,
scrollPhysics: scrollPhysics, scrollPhysics: scrollPhysics,
keyboardAppearance: keyboardAppearance, keyboardAppearance: keyboardAppearance,
enableInteractiveSelection: enableInteractiveSelection, enableInteractiveSelection: enableInteractiveSelection ?? (!obscureText || !readOnly),
selectionControls: selectionControls, selectionControls: selectionControls,
buildCounter: buildCounter, buildCounter: buildCounter,
autofillHints: autofillHints, autofillHints: autofillHints,
......
...@@ -508,16 +508,11 @@ class EditableText extends StatefulWidget { ...@@ -508,16 +508,11 @@ class EditableText extends StatefulWidget {
this.scrollPadding = const EdgeInsets.all(20.0), this.scrollPadding = const EdgeInsets.all(20.0),
this.keyboardAppearance = Brightness.light, this.keyboardAppearance = Brightness.light,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true, bool? enableInteractiveSelection,
this.scrollController, this.scrollController,
this.scrollPhysics, this.scrollPhysics,
this.autocorrectionTextRectColor, this.autocorrectionTextRectColor,
this.toolbarOptions = const ToolbarOptions( ToolbarOptions? toolbarOptions,
copy: true,
cut: true,
paste: true,
selectAll: true,
),
this.autofillHints = const <String>[], this.autofillHints = const <String>[],
this.autofillClient, this.autofillClient,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
...@@ -533,7 +528,6 @@ class EditableText extends StatefulWidget { ...@@ -533,7 +528,6 @@ class EditableText extends StatefulWidget {
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
assert(enableSuggestions != null), assert(enableSuggestions != null),
assert(showSelectionHandles != null), assert(showSelectionHandles != null),
assert(enableInteractiveSelection != null),
assert(readOnly != null), assert(readOnly != null),
assert(forceLine != null), assert(forceLine != null),
assert(style != null), assert(style != null),
...@@ -560,7 +554,31 @@ class EditableText extends StatefulWidget { ...@@ -560,7 +554,31 @@ class EditableText extends StatefulWidget {
assert(rendererIgnoresPointer != null), assert(rendererIgnoresPointer != null),
assert(scrollPadding != null), assert(scrollPadding != null),
assert(dragStartBehavior != null), assert(dragStartBehavior != null),
assert(toolbarOptions != null), enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
toolbarOptions = toolbarOptions ??
(obscureText
? (readOnly
// No point in even offering "Select All" in a read-only obscured
// field.
? const ToolbarOptions()
// Writable, but obscured.
: const ToolbarOptions(
selectAll: true,
paste: true,
))
: (readOnly
// Read-only, not obscured.
? const ToolbarOptions(
selectAll: true,
copy: true,
)
// Writable, not obscured.
: const ToolbarOptions(
copy: true,
cut: true,
selectAll: true,
paste: true,
))),
assert(clipBehavior != null), assert(clipBehavior != null),
assert(enableIMEPersonalizedLearning != null), assert(enableIMEPersonalizedLearning != null),
_strutStyle = strutStyle, _strutStyle = strutStyle,
...@@ -593,7 +611,9 @@ class EditableText extends StatefulWidget { ...@@ -593,7 +611,9 @@ class EditableText extends StatefulWidget {
/// Whether to hide the text being edited (e.g., for passwords). /// Whether to hide the text being edited (e.g., for passwords).
/// ///
/// When this is set to true, all the characters in the text field are /// When this is set to true, all the characters in the text field are
/// replaced by [obscuringCharacter]. /// replaced by [obscuringCharacter], and the text in the field cannot be
/// copied with copy or cut. If [readOnly] is also true, then the text cannot
/// be selected.
/// ///
/// Defaults to false. Cannot be null. /// Defaults to false. Cannot be null.
/// {@endtemplate} /// {@endtemplate}
...@@ -629,8 +649,10 @@ class EditableText extends StatefulWidget { ...@@ -629,8 +649,10 @@ class EditableText extends StatefulWidget {
/// Configuration of toolbar options. /// Configuration of toolbar options.
/// ///
/// By default, all options are enabled. If [readOnly] is true, /// By default, all options are enabled. If [readOnly] is true, paste and cut
/// paste and cut will be disabled regardless. /// will be disabled regardless. If [obscureText] is true, cut and copy will
/// be disabled regardless. If [readOnly] and [obscureText] are both true,
/// select all will also be disabled.
final ToolbarOptions toolbarOptions; final ToolbarOptions toolbarOptions;
/// Whether to show selection handles. /// Whether to show selection handles.
...@@ -1492,6 +1514,7 @@ class EditableText extends StatefulWidget { ...@@ -1492,6 +1514,7 @@ class EditableText extends StatefulWidget {
properties.add(DiagnosticsProperty<TextEditingController>('controller', controller)); properties.add(DiagnosticsProperty<TextEditingController>('controller', controller));
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode)); properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode));
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false)); properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
properties.add(DiagnosticsProperty<bool>('readOnly', readOnly, defaultValue: false));
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true)); properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled)); properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled)); properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
...@@ -1511,6 +1534,7 @@ class EditableText extends StatefulWidget { ...@@ -1511,6 +1534,7 @@ class EditableText extends StatefulWidget {
properties.add(DiagnosticsProperty<Iterable<String>>('autofillHints', autofillHints, defaultValue: null)); properties.add(DiagnosticsProperty<Iterable<String>>('autofillHints', autofillHints, defaultValue: null));
properties.add(DiagnosticsProperty<TextHeightBehavior>('textHeightBehavior', textHeightBehavior, defaultValue: null)); properties.add(DiagnosticsProperty<TextHeightBehavior>('textHeightBehavior', textHeightBehavior, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true)); properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
properties.add(DiagnosticsProperty<bool>('enableInteractiveSelection', enableInteractiveSelection, defaultValue: true));
} }
} }
...@@ -1573,16 +1597,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1573,16 +1597,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value); Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
@override @override
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly; bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly && !widget.obscureText;
@override @override
bool get copyEnabled => widget.toolbarOptions.copy; bool get copyEnabled => widget.toolbarOptions.copy && !widget.obscureText;
@override @override
bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly; bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly;
@override @override
bool get selectAllEnabled => widget.toolbarOptions.selectAll; bool get selectAllEnabled => widget.toolbarOptions.selectAll && (!widget.readOnly || !widget.obscureText) && widget.enableInteractiveSelection;
void _onChangedClipboardStatus() { void _onChangedClipboardStatus() {
setState(() { setState(() {
...@@ -1602,11 +1626,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1602,11 +1626,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
void copySelection(SelectionChangedCause cause) { void copySelection(SelectionChangedCause cause) {
final TextSelection selection = textEditingValue.selection; final TextSelection selection = textEditingValue.selection;
final String text = textEditingValue.text;
assert(selection != null); assert(selection != null);
if (selection.isCollapsed) { if (selection.isCollapsed || widget.obscureText) {
return; return;
} }
final String text = textEditingValue.text;
Clipboard.setData(ClipboardData(text: selection.textInside(text))); Clipboard.setData(ClipboardData(text: selection.textInside(text)));
if (cause == SelectionChangedCause.toolbar) { if (cause == SelectionChangedCause.toolbar) {
bringIntoView(textEditingValue.selection.extent); bringIntoView(textEditingValue.selection.extent);
...@@ -1636,7 +1660,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1636,7 +1660,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
/// Cut current selection to [Clipboard]. /// Cut current selection to [Clipboard].
@override @override
void cutSelection(SelectionChangedCause cause) { void cutSelection(SelectionChangedCause cause) {
if (widget.readOnly) { if (widget.readOnly || widget.obscureText) {
return; return;
} }
final TextSelection selection = textEditingValue.selection; final TextSelection selection = textEditingValue.selection;
...@@ -1681,6 +1705,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1681,6 +1705,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
/// Select the entire text value. /// Select the entire text value.
@override @override
void selectAll(SelectionChangedCause cause) { void selectAll(SelectionChangedCause cause) {
if (widget.readOnly && widget.obscureText) {
// If we can't modify it, and we can't copy it, there's no point in
// selecting it.
return;
}
userUpdateTextEditingValue( userUpdateTextEditingValue(
textEditingValue.copyWith( textEditingValue.copyWith(
selection: TextSelection(baseOffset: 0, extentOffset: textEditingValue.text.length), selection: TextSelection(baseOffset: 0, extentOffset: textEditingValue.text.length),
...@@ -3057,7 +3086,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3057,7 +3086,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
selectionHeightStyle: widget.selectionHeightStyle, selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle, selectionWidthStyle: widget.selectionWidthStyle,
paintCursorAboveText: widget.paintCursorAboveText, paintCursorAboveText: widget.paintCursorAboveText,
enableInteractiveSelection: widget.enableInteractiveSelection, enableInteractiveSelection: widget.enableInteractiveSelection && (!widget.readOnly || !widget.obscureText),
textSelectionDelegate: this, textSelectionDelegate: this,
devicePixelRatio: _devicePixelRatio, devicePixelRatio: _devicePixelRatio,
promptRectRange: _currentPromptRectRange, promptRectRange: _currentPromptRectRange,
...@@ -3287,6 +3316,7 @@ class _Editable extends MultiChildRenderObjectWidget { ...@@ -3287,6 +3316,7 @@ class _Editable extends MultiChildRenderObjectWidget {
..cursorOffset = cursorOffset ..cursorOffset = cursorOffset
..selectionHeightStyle = selectionHeightStyle ..selectionHeightStyle = selectionHeightStyle
..selectionWidthStyle = selectionWidthStyle ..selectionWidthStyle = selectionWidthStyle
..enableInteractiveSelection = enableInteractiveSelection
..textSelectionDelegate = textSelectionDelegate ..textSelectionDelegate = textSelectionDelegate
..devicePixelRatio = devicePixelRatio ..devicePixelRatio = devicePixelRatio
..paintCursorAboveText = paintCursorAboveText ..paintCursorAboveText = paintCursorAboveText
......
...@@ -1747,6 +1747,44 @@ void main() { ...@@ -1747,6 +1747,44 @@ void main() {
}, },
); );
testWidgets(
'double tap does not select word on read-only obscured field',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
readOnly: true,
obscureText: true,
controller: controller,
),
),
),
);
// Long press to put the cursor after the "w".
const int index = 3;
await tester.longPressAt(textOffsetToPosition(tester, index));
await tester.pump();
// Second tap doesn't select anything.
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, index));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 35),
);
// Selected text shows nothing.
expect(find.byType(CupertinoButton), findsNothing);
},
);
testWidgets('Readonly text field does not have tap action', (WidgetTester tester) async { testWidgets('Readonly text field does not have tap action', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
...@@ -2132,6 +2170,54 @@ void main() { ...@@ -2132,6 +2170,54 @@ void main() {
}, },
); );
testWidgets(
'A read-only obscured CupertinoTextField is not selectable',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
obscureText: true,
readOnly: 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.collapsed(offset: 35),
);
expect(find.byType(CupertinoButton), findsNWidgets(0));
},
);
testWidgets( testWidgets(
'An obscured CupertinoTextField is selectable by default', 'An obscured CupertinoTextField is selectable by default',
(WidgetTester tester) async { (WidgetTester tester) async {
......
...@@ -2454,6 +2454,34 @@ void main() { ...@@ -2454,6 +2454,34 @@ void main() {
expect(controller.selection.isCollapsed, true); expect(controller.selection.isCollapsed, true);
}); });
testWidgets('An obscured TextField is not selectable when read-only', (WidgetTester tester) async {
// This is a regression test for
// https://github.com/flutter/flutter/issues/32845
final TextEditingController controller = TextEditingController();
Widget buildFrame(bool obscureText, bool readOnly) {
return overlay(
child: TextField(
controller: controller,
obscureText: obscureText,
readOnly: readOnly,
),
);
}
// Explicitly disabled selection on obscured text that is read-only.
await tester.pumpWidget(buildFrame(true, true));
await tester.enterText(find.byType(TextField), 'abcdefghi');
await skipPastScrollingAnimation(tester);
expect(controller.selection.isCollapsed, true);
// Long press doesn't select text.
final Offset ePos2 = textOffsetToPosition(tester, 1);
await tester.longPressAt(ePos2, pointer: 7);
await tester.pump();
expect(controller.selection.isCollapsed, true);
});
testWidgets('An obscured TextField is selected as one word', (WidgetTester tester) async { testWidgets('An obscured TextField is selected as one word', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
...@@ -4970,82 +4998,6 @@ void main() { ...@@ -4970,82 +4998,6 @@ void main() {
variant: KeySimulatorTransitModeVariant.all() variant: KeySimulatorTransitModeVariant.all()
); );
testWidgets('Copy paste obscured text test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController();
final TextField textField =
TextField(
controller: controller,
obscureText: true,
);
String clipboardContent = '';
tester.binding.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.setData')
// ignore: avoid_dynamic_calls
clipboardContent = methodCall.arguments['text'] as String;
else if (methodCall.method == 'Clipboard.getData')
return <String, dynamic>{'text': clipboardContent};
return null;
});
await tester.pumpWidget(
MaterialApp(
home: Material(
child: RawKeyboardListener(
focusNode: focusNode,
child: textField,
),
),
),
);
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house jumped over a mouse';
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
// Select the first 5 characters
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
for (int i = 0; i < 5; i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
}
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
// Copy them
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
await tester.sendKeyEvent(LogicalKeyboardKey.keyC);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
await tester.pumpAndSettle();
expect(clipboardContent, 'a big');
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
// Paste them
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200));
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
await tester.pumpAndSettle();
const String expected = 'a biga big house jumped over a mouse';
expect(find.text(expected), findsOneWidget, reason: 'Because text contains ${controller.text}');
},
skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events.
variant: KeySimulatorTransitModeVariant.all()
);
// Regressing test for https://github.com/flutter/flutter/issues/78219 // Regressing test for https://github.com/flutter/flutter/issues/78219
testWidgets('Paste does not crash when the section is inValid', (WidgetTester tester) async { testWidgets('Paste does not crash when the section is inValid', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
...@@ -5174,83 +5126,6 @@ void main() { ...@@ -5174,83 +5126,6 @@ void main() {
variant: KeySimulatorTransitModeVariant.all() variant: KeySimulatorTransitModeVariant.all()
); );
testWidgets('Cut obscured text test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController();
final TextField textField = TextField(
controller: controller,
obscureText: true,
);
String clipboardContent = '';
tester.binding.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.setData')
// ignore: avoid_dynamic_calls
clipboardContent = methodCall.arguments['text'] as String;
else if (methodCall.method == 'Clipboard.getData')
return <String, dynamic>{'text': clipboardContent};
return null;
});
await tester.pumpWidget(
MaterialApp(
home: Material(
child: RawKeyboardListener(
focusNode: focusNode,
child: textField,
),
),
),
);
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house jumped over a mouse';
await tester.enterText(find.byType(TextField), testValue);
await tester.idle();
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
// Select the first 5 characters
for (int i = 0; i < 5; i += 1) {
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
await tester.pumpAndSettle();
}
// Cut them
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
await tester.sendKeyEvent(LogicalKeyboardKey.keyX);
await tester.pumpAndSettle();
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
await tester.pumpAndSettle();
expect(clipboardContent, 'a big');
for (int i = 0; i < 5; i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
}
// Paste them
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 200));
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
await tester.pumpAndSettle();
const String expected = ' housa bige jumped over a mouse';
expect(find.text(expected), findsOneWidget);
},
skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events.
variant: KeySimulatorTransitModeVariant.all()
);
testWidgets('Select all test', (WidgetTester tester) async { testWidgets('Select all test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
...@@ -7125,6 +7000,55 @@ void main() { ...@@ -7125,6 +7000,55 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
); );
testWidgets(
'double tap does not select word on read-only obscured field',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
obscureText: true,
readOnly: true,
controller: controller,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
// This tap just puts the cursor somewhere different than where the double
// tap will occur to test that the double tap moves the existing cursor first.
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump(const Duration(milliseconds: 500));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 35),
);
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pumpAndSettle();
// Second tap doesn't select anything.
expect(
controller.selection,
const TextSelection.collapsed(offset: 35),
);
// Selected text shows nothing.
expect(find.byType(CupertinoButton), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets( testWidgets(
'double tap selects word and first tap of double tap moves cursor and shows toolbar', 'double tap selects word and first tap of double tap moves cursor and shows toolbar',
(WidgetTester tester) async { (WidgetTester tester) async {
......
...@@ -102,6 +102,94 @@ void main() { ...@@ -102,6 +102,94 @@ void main() {
skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web. skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web.
); );
testWidgets('the desktop cut/copy/paste buttons are disabled for read-only obscured form fields', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'blah1 blah2',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextFormField(
readOnly: true,
obscureText: true,
controller: controller,
),
),
),
),
);
// Initially, the menu is not shown and there is no selection.
expect(find.byType(CupertinoButton), findsNothing);
const TextSelection invalidSelection = TextSelection(baseOffset: -1, extentOffset: -1);
expect(controller.selection, invalidSelection);
final Offset midBlah1 = textOffsetToPosition(tester, 2);
// Right clicking shows the menu.
final TestGesture gesture = await tester.startGesture(
midBlah1,
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection, invalidSelection);
expect(find.text('Copy'), findsNothing);
expect(find.text('Cut'), findsNothing);
expect(find.text('Paste'), findsNothing);
expect(find.byType(CupertinoButton), findsNothing);
},
variant: TargetPlatformVariant.desktop(),
skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web.
);
testWidgets('the desktop cut/copy buttons are disabled for obscured form fields', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'blah1 blah2',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextFormField(
obscureText: true,
controller: controller,
),
),
),
),
);
// Initially, the menu is not shown and there is no selection.
expect(find.byType(CupertinoButton), findsNothing);
const TextSelection invalidSelection = TextSelection(baseOffset: -1, extentOffset: -1);
expect(controller.selection, invalidSelection);
final Offset midBlah1 = textOffsetToPosition(tester, 2);
// Right clicking shows the menu.
final TestGesture gesture = await tester.startGesture(
midBlah1,
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton,
);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 11));
expect(find.text('Copy'), findsNothing);
expect(find.text('Cut'), findsNothing);
expect(find.text('Paste'), findsOneWidget);
},
variant: TargetPlatformVariant.desktop(),
skip: kIsWeb, // [intended] we don't supply the cut/copy/paste buttons on the web.
);
testWidgets('TextFormField accepts TextField.noMaxLength as value to maxLength parameter', (WidgetTester tester) async { testWidgets('TextFormField accepts TextField.noMaxLength as value to maxLength parameter', (WidgetTester tester) async {
bool asserted; bool asserted;
try { try {
......
...@@ -1506,7 +1506,7 @@ void main() { ...@@ -1506,7 +1506,7 @@ void main() {
expect(find.text('Cut'), findsNothing); expect(find.text('Cut'), findsNothing);
}); });
testWidgets('cut and paste are disabled in read only mode even if explicit set', (WidgetTester tester) async { testWidgets('cut and paste are disabled in read only mode even if explicitly set', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: EditableText( home: EditableText(
...@@ -1514,6 +1514,12 @@ void main() { ...@@ -1514,6 +1514,12 @@ void main() {
controller: TextEditingController(text: 'blah blah'), controller: TextEditingController(text: 'blah blah'),
focusNode: focusNode, focusNode: focusNode,
readOnly: true, readOnly: true,
toolbarOptions: const ToolbarOptions(
copy: true,
cut: true,
paste: true,
selectAll: true,
),
style: textStyle, style: textStyle,
cursorColor: cursorColor, cursorColor: cursorColor,
selectionControls: materialTextSelectionControls, selectionControls: materialTextSelectionControls,
...@@ -1539,6 +1545,113 @@ void main() { ...@@ -1539,6 +1545,113 @@ void main() {
expect(find.text('Cut'), findsNothing); expect(find.text('Cut'), findsNothing);
}); });
testWidgets('cut and copy are disabled in obscured mode even if explicitly set', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: TextEditingController(text: 'blah blah'),
focusNode: focusNode,
obscureText: true,
toolbarOptions: const ToolbarOptions(
copy: true,
cut: true,
paste: true,
selectAll: true,
),
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
),
),
);
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
await tester.tap(find.byType(EditableText));
await tester.pump();
// Select something, but not the whole thing.
state.renderEditable.selectWord(cause: SelectionChangedCause.tap);
await tester.pump();
expect(state.selectAllEnabled, isTrue);
expect(state.pasteEnabled, isTrue);
expect(state.cutEnabled, isFalse);
expect(state.copyEnabled, isFalse);
// On web, we don't let Flutter show the toolbar.
expect(state.showToolbar(), kIsWeb ? isFalse : isTrue);
await tester.pump();
expect(find.text('Select all'), kIsWeb ? findsNothing : findsOneWidget);
expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget);
expect(find.text('Cut'), findsNothing);
});
testWidgets('cut and copy do nothing in obscured mode even if explicitly called', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: TextEditingController(text: 'blah blah'),
focusNode: focusNode,
obscureText: true,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
),
),
);
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
expect(state.selectAllEnabled, isTrue);
expect(state.pasteEnabled, isTrue);
expect(state.cutEnabled, isFalse);
expect(state.copyEnabled, isFalse);
// Select all.
state.selectAll(SelectionChangedCause.toolbar);
await tester.pump();
await Clipboard.setData(const ClipboardData(text: ''));
state.cutSelection(SelectionChangedCause.toolbar);
ClipboardData? data = await Clipboard.getData('text/plain');
expect(data, isNotNull);
expect(data!.text, isEmpty);
state.selectAll(SelectionChangedCause.toolbar);
await tester.pump();
await Clipboard.setData(const ClipboardData(text: ''));
state.copySelection(SelectionChangedCause.toolbar);
data = await Clipboard.getData('text/plain');
expect(data, isNotNull);
expect(data!.text, isEmpty);
});
testWidgets('select all does nothing if obscured and read-only, even if explicitly called', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: TextEditingController(text: 'blah blah'),
focusNode: focusNode,
obscureText: true,
readOnly: true,
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
),
),
);
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
// Select all.
state.selectAll(SelectionChangedCause.toolbar);
expect(state.selectAllEnabled, isFalse);
expect(state.textEditingValue.selection.isCollapsed, isTrue);
});
testWidgets('Handles the read-only flag correctly', (WidgetTester tester) async { testWidgets('Handles the read-only flag correctly', (WidgetTester tester) async {
final TextEditingController controller = final TextEditingController controller =
TextEditingController(text: 'Lorem ipsum dolor sit amet'); TextEditingController(text: 'Lorem ipsum dolor sit amet');
......
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