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).
This commit is contained in:
parent
e25e1f9037
commit
a9e0dd40dc
@ -289,7 +289,7 @@ class CupertinoTextField extends StatefulWidget {
|
||||
this.keyboardAppearance,
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.enableInteractiveSelection = true,
|
||||
bool? enableInteractiveSelection,
|
||||
this.selectionControls,
|
||||
this.onTap,
|
||||
this.scrollController,
|
||||
@ -341,17 +341,31 @@ class CupertinoTextField extends StatefulWidget {
|
||||
),
|
||||
assert(enableIMEPersonalizedLearning != null),
|
||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||
toolbarOptions = toolbarOptions ?? (obscureText ?
|
||||
const ToolbarOptions(
|
||||
selectAll: true,
|
||||
paste: true,
|
||||
) :
|
||||
const ToolbarOptions(
|
||||
copy: true,
|
||||
cut: true,
|
||||
selectAll: true,
|
||||
paste: true,
|
||||
)),
|
||||
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,
|
||||
))),
|
||||
super(key: key);
|
||||
|
||||
/// Creates a borderless iOS-style text field.
|
||||
@ -446,7 +460,7 @@ class CupertinoTextField extends StatefulWidget {
|
||||
this.keyboardAppearance,
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.enableInteractiveSelection = true,
|
||||
bool? enableInteractiveSelection,
|
||||
this.selectionControls,
|
||||
this.onTap,
|
||||
this.scrollController,
|
||||
@ -499,17 +513,31 @@ class CupertinoTextField extends StatefulWidget {
|
||||
assert(clipBehavior != null),
|
||||
assert(enableIMEPersonalizedLearning != null),
|
||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||
toolbarOptions = toolbarOptions ?? (obscureText ?
|
||||
const ToolbarOptions(
|
||||
selectAll: true,
|
||||
paste: true,
|
||||
) :
|
||||
const ToolbarOptions(
|
||||
copy: true,
|
||||
cut: true,
|
||||
selectAll: true,
|
||||
paste: true,
|
||||
)),
|
||||
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,
|
||||
))),
|
||||
super(key: key);
|
||||
|
||||
/// Controls the text being edited.
|
||||
|
@ -319,7 +319,7 @@ class TextField extends StatefulWidget {
|
||||
this.keyboardAppearance,
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.enableInteractiveSelection = true,
|
||||
bool? enableInteractiveSelection,
|
||||
this.selectionControls,
|
||||
this.onTap,
|
||||
this.mouseCursor,
|
||||
@ -339,7 +339,6 @@ class TextField extends StatefulWidget {
|
||||
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
||||
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||
assert(enableSuggestions != null),
|
||||
assert(enableInteractiveSelection != null),
|
||||
assert(maxLengthEnforced != null),
|
||||
assert(
|
||||
maxLengthEnforced || maxLengthEnforcement == null,
|
||||
@ -372,17 +371,31 @@ class TextField extends StatefulWidget {
|
||||
assert(clipBehavior != null),
|
||||
assert(enableIMEPersonalizedLearning != null),
|
||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||
toolbarOptions = toolbarOptions ?? (obscureText ?
|
||||
const ToolbarOptions(
|
||||
selectAll: true,
|
||||
paste: true,
|
||||
) :
|
||||
const ToolbarOptions(
|
||||
copy: true,
|
||||
cut: true,
|
||||
selectAll: true,
|
||||
paste: true,
|
||||
)),
|
||||
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,
|
||||
))),
|
||||
super(key: key);
|
||||
|
||||
/// Controls the text being edited.
|
||||
|
@ -143,7 +143,7 @@ class TextFormField extends FormField<String> {
|
||||
Color? cursorColor,
|
||||
Brightness? keyboardAppearance,
|
||||
EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
|
||||
bool enableInteractiveSelection = true,
|
||||
bool? enableInteractiveSelection,
|
||||
TextSelectionControls? selectionControls,
|
||||
InputCounterWidgetBuilder? buildCounter,
|
||||
ScrollPhysics? scrollPhysics,
|
||||
@ -179,7 +179,6 @@ class TextFormField extends FormField<String> {
|
||||
),
|
||||
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
|
||||
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
|
||||
assert(enableInteractiveSelection != null),
|
||||
assert(enableIMEPersonalizedLearning != null),
|
||||
super(
|
||||
key: key,
|
||||
@ -243,7 +242,7 @@ class TextFormField extends FormField<String> {
|
||||
scrollPadding: scrollPadding,
|
||||
scrollPhysics: scrollPhysics,
|
||||
keyboardAppearance: keyboardAppearance,
|
||||
enableInteractiveSelection: enableInteractiveSelection,
|
||||
enableInteractiveSelection: enableInteractiveSelection ?? (!obscureText || !readOnly),
|
||||
selectionControls: selectionControls,
|
||||
buildCounter: buildCounter,
|
||||
autofillHints: autofillHints,
|
||||
|
@ -508,16 +508,11 @@ class EditableText extends StatefulWidget {
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.keyboardAppearance = Brightness.light,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.enableInteractiveSelection = true,
|
||||
bool? enableInteractiveSelection,
|
||||
this.scrollController,
|
||||
this.scrollPhysics,
|
||||
this.autocorrectionTextRectColor,
|
||||
this.toolbarOptions = const ToolbarOptions(
|
||||
copy: true,
|
||||
cut: true,
|
||||
paste: true,
|
||||
selectAll: true,
|
||||
),
|
||||
ToolbarOptions? toolbarOptions,
|
||||
this.autofillHints = const <String>[],
|
||||
this.autofillClient,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
@ -533,7 +528,6 @@ class EditableText extends StatefulWidget {
|
||||
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||
assert(enableSuggestions != null),
|
||||
assert(showSelectionHandles != null),
|
||||
assert(enableInteractiveSelection != null),
|
||||
assert(readOnly != null),
|
||||
assert(forceLine != null),
|
||||
assert(style != null),
|
||||
@ -560,7 +554,31 @@ class EditableText extends StatefulWidget {
|
||||
assert(rendererIgnoresPointer != null),
|
||||
assert(scrollPadding != 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(enableIMEPersonalizedLearning != null),
|
||||
_strutStyle = strutStyle,
|
||||
@ -593,7 +611,9 @@ class EditableText extends StatefulWidget {
|
||||
/// 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
|
||||
/// 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.
|
||||
/// {@endtemplate}
|
||||
@ -629,8 +649,10 @@ class EditableText extends StatefulWidget {
|
||||
|
||||
/// Configuration of toolbar options.
|
||||
///
|
||||
/// By default, all options are enabled. If [readOnly] is true,
|
||||
/// paste and cut will be disabled regardless.
|
||||
/// By default, all options are enabled. If [readOnly] is true, paste and cut
|
||||
/// 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;
|
||||
|
||||
/// Whether to show selection handles.
|
||||
@ -1492,6 +1514,7 @@ class EditableText extends StatefulWidget {
|
||||
properties.add(DiagnosticsProperty<TextEditingController>('controller', controller));
|
||||
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode));
|
||||
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(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
|
||||
properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
|
||||
@ -1511,6 +1534,7 @@ class EditableText extends StatefulWidget {
|
||||
properties.add(DiagnosticsProperty<Iterable<String>>('autofillHints', autofillHints, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<TextHeightBehavior>('textHeightBehavior', textHeightBehavior, defaultValue: null));
|
||||
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
|
||||
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
|
||||
|
||||
@override
|
||||
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly;
|
||||
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly && !widget.obscureText;
|
||||
|
||||
@override
|
||||
bool get copyEnabled => widget.toolbarOptions.copy;
|
||||
bool get copyEnabled => widget.toolbarOptions.copy && !widget.obscureText;
|
||||
|
||||
@override
|
||||
bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly;
|
||||
|
||||
@override
|
||||
bool get selectAllEnabled => widget.toolbarOptions.selectAll;
|
||||
bool get selectAllEnabled => widget.toolbarOptions.selectAll && (!widget.readOnly || !widget.obscureText) && widget.enableInteractiveSelection;
|
||||
|
||||
void _onChangedClipboardStatus() {
|
||||
setState(() {
|
||||
@ -1602,11 +1626,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
@override
|
||||
void copySelection(SelectionChangedCause cause) {
|
||||
final TextSelection selection = textEditingValue.selection;
|
||||
final String text = textEditingValue.text;
|
||||
assert(selection != null);
|
||||
if (selection.isCollapsed) {
|
||||
if (selection.isCollapsed || widget.obscureText) {
|
||||
return;
|
||||
}
|
||||
final String text = textEditingValue.text;
|
||||
Clipboard.setData(ClipboardData(text: selection.textInside(text)));
|
||||
if (cause == SelectionChangedCause.toolbar) {
|
||||
bringIntoView(textEditingValue.selection.extent);
|
||||
@ -1636,7 +1660,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
/// Cut current selection to [Clipboard].
|
||||
@override
|
||||
void cutSelection(SelectionChangedCause cause) {
|
||||
if (widget.readOnly) {
|
||||
if (widget.readOnly || widget.obscureText) {
|
||||
return;
|
||||
}
|
||||
final TextSelection selection = textEditingValue.selection;
|
||||
@ -1681,6 +1705,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
/// Select the entire text value.
|
||||
@override
|
||||
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(
|
||||
textEditingValue.copyWith(
|
||||
selection: TextSelection(baseOffset: 0, extentOffset: textEditingValue.text.length),
|
||||
@ -3057,7 +3086,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
selectionHeightStyle: widget.selectionHeightStyle,
|
||||
selectionWidthStyle: widget.selectionWidthStyle,
|
||||
paintCursorAboveText: widget.paintCursorAboveText,
|
||||
enableInteractiveSelection: widget.enableInteractiveSelection,
|
||||
enableInteractiveSelection: widget.enableInteractiveSelection && (!widget.readOnly || !widget.obscureText),
|
||||
textSelectionDelegate: this,
|
||||
devicePixelRatio: _devicePixelRatio,
|
||||
promptRectRange: _currentPromptRectRange,
|
||||
@ -3287,6 +3316,7 @@ class _Editable extends MultiChildRenderObjectWidget {
|
||||
..cursorOffset = cursorOffset
|
||||
..selectionHeightStyle = selectionHeightStyle
|
||||
..selectionWidthStyle = selectionWidthStyle
|
||||
..enableInteractiveSelection = enableInteractiveSelection
|
||||
..textSelectionDelegate = textSelectionDelegate
|
||||
..devicePixelRatio = devicePixelRatio
|
||||
..paintCursorAboveText = paintCursorAboveText
|
||||
|
@ -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 {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
|
||||
@ -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(
|
||||
'An obscured CupertinoTextField is selectable by default',
|
||||
(WidgetTester tester) async {
|
||||
|
@ -2454,6 +2454,34 @@ void main() {
|
||||
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 {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
|
||||
@ -4970,82 +4998,6 @@ void main() {
|
||||
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
|
||||
testWidgets('Paste does not crash when the section is inValid', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
@ -5174,83 +5126,6 @@ void main() {
|
||||
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 {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
final TextEditingController controller = TextEditingController();
|
||||
@ -7125,6 +7000,55 @@ void main() {
|
||||
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(
|
||||
'double tap selects word and first tap of double tap moves cursor and shows toolbar',
|
||||
(WidgetTester tester) async {
|
||||
|
@ -102,6 +102,94 @@ void main() {
|
||||
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 {
|
||||
bool asserted;
|
||||
try {
|
||||
|
@ -1506,7 +1506,7 @@ void main() {
|
||||
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(
|
||||
MaterialApp(
|
||||
home: EditableText(
|
||||
@ -1514,6 +1514,12 @@ void main() {
|
||||
controller: TextEditingController(text: 'blah blah'),
|
||||
focusNode: focusNode,
|
||||
readOnly: true,
|
||||
toolbarOptions: const ToolbarOptions(
|
||||
copy: true,
|
||||
cut: true,
|
||||
paste: true,
|
||||
selectAll: true,
|
||||
),
|
||||
style: textStyle,
|
||||
cursorColor: cursorColor,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
@ -1539,6 +1545,113 @@ void main() {
|
||||
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 {
|
||||
final TextEditingController controller =
|
||||
TextEditingController(text: 'Lorem ipsum dolor sit amet');
|
||||
|
Loading…
x
Reference in New Issue
Block a user