Enable selection by default for password text field and expose api to turn on and off context menu options (#34676)
This commit is contained in:
parent
68fc7231b3
commit
0d0af31598
@ -209,6 +209,7 @@ class CupertinoTextField extends StatefulWidget {
|
||||
this.textAlign = TextAlign.start,
|
||||
this.textAlignVertical,
|
||||
this.readOnly = false,
|
||||
ToolbarOptions toolbarOptions,
|
||||
this.showCursor,
|
||||
this.autofocus = false,
|
||||
this.obscureText = false,
|
||||
@ -229,7 +230,7 @@ class CupertinoTextField extends StatefulWidget {
|
||||
this.keyboardAppearance,
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.enableInteractiveSelection,
|
||||
this.enableInteractiveSelection = true,
|
||||
this.onTap,
|
||||
this.scrollController,
|
||||
this.scrollPhysics,
|
||||
@ -257,6 +258,17 @@ class CupertinoTextField extends StatefulWidget {
|
||||
assert(prefixMode != null),
|
||||
assert(suffixMode != 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,
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
/// Controls the text being edited.
|
||||
@ -358,6 +370,13 @@ class CupertinoTextField extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.editableText.textAlign}
|
||||
final TextAlign textAlign;
|
||||
|
||||
/// Configuration of toolbar options.
|
||||
///
|
||||
/// If not set, select all and paste will default to be enabled. Copy and cut
|
||||
/// will be disabled if [obscureText] is true. If [readOnly] is true,
|
||||
/// paste and cut will be disabled regardless.
|
||||
final ToolbarOptions toolbarOptions;
|
||||
|
||||
/// {@macro flutter.material.inputDecorator.textAlignVertical}
|
||||
final TextAlignVertical textAlignVertical;
|
||||
|
||||
@ -498,9 +517,7 @@ class CupertinoTextField extends StatefulWidget {
|
||||
final ScrollPhysics scrollPhysics;
|
||||
|
||||
/// {@macro flutter.rendering.editable.selectionEnabled}
|
||||
bool get selectionEnabled {
|
||||
return enableInteractiveSelection ?? !obscureText;
|
||||
}
|
||||
bool get selectionEnabled => enableInteractiveSelection;
|
||||
|
||||
/// {@macro flutter.material.textfield.onTap}
|
||||
final GestureTapCallback onTap;
|
||||
@ -804,6 +821,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
key: editableTextKey,
|
||||
controller: controller,
|
||||
readOnly: widget.readOnly,
|
||||
toolbarOptions: widget.toolbarOptions,
|
||||
showCursor: widget.showCursor,
|
||||
showSelectionHandles: _showSelectionHandles,
|
||||
focusNode: _effectiveFocusNode,
|
||||
|
@ -195,6 +195,7 @@ class SelectableText extends StatefulWidget {
|
||||
this.textDirection,
|
||||
this.showCursor = false,
|
||||
this.autofocus = false,
|
||||
ToolbarOptions toolbarOptions,
|
||||
this.maxLines,
|
||||
this.cursorWidth = 2.0,
|
||||
this.cursorRadius,
|
||||
@ -213,6 +214,11 @@ class SelectableText extends StatefulWidget {
|
||||
'A non-null String must be provided to a SelectableText widget.',
|
||||
),
|
||||
textSpan = null,
|
||||
toolbarOptions = toolbarOptions ??
|
||||
const ToolbarOptions(
|
||||
selectAll: true,
|
||||
copy: true,
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
/// Creates a selectable text widget with a [TextSpan].
|
||||
@ -229,6 +235,7 @@ class SelectableText extends StatefulWidget {
|
||||
this.textDirection,
|
||||
this.showCursor = false,
|
||||
this.autofocus = false,
|
||||
ToolbarOptions toolbarOptions,
|
||||
this.maxLines,
|
||||
this.cursorWidth = 2.0,
|
||||
this.cursorRadius,
|
||||
@ -247,6 +254,11 @@ class SelectableText extends StatefulWidget {
|
||||
'A non-null TextSpan must be provided to a SelectableText.rich widget.',
|
||||
),
|
||||
data = null,
|
||||
toolbarOptions = toolbarOptions ??
|
||||
const ToolbarOptions(
|
||||
selectAll: true,
|
||||
copy: true,
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
/// The text to display.
|
||||
@ -325,6 +337,13 @@ class SelectableText extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
|
||||
/// Configuration of toolbar options.
|
||||
///
|
||||
/// Paste and cut will be disabled regardless.
|
||||
///
|
||||
/// If not set, select all and copy will be enabled by default.
|
||||
final ToolbarOptions toolbarOptions;
|
||||
|
||||
/// {@macro flutter.rendering.editable.selectionEnabled}
|
||||
bool get selectionEnabled {
|
||||
return enableInteractiveSelection;
|
||||
@ -543,6 +562,7 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
|
||||
textDirection: widget.textDirection,
|
||||
autofocus: widget.autofocus,
|
||||
forceLine: false,
|
||||
toolbarOptions: widget.toolbarOptions,
|
||||
maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
|
||||
selectionColor: themeData.textSelectionColor,
|
||||
selectionControls: widget.selectionEnabled ? textSelectionControls : null,
|
||||
|
@ -256,6 +256,7 @@ class TextField extends StatefulWidget {
|
||||
this.textAlignVertical,
|
||||
this.textDirection,
|
||||
this.readOnly = false,
|
||||
ToolbarOptions toolbarOptions,
|
||||
this.showCursor,
|
||||
this.autofocus = false,
|
||||
this.obscureText = false,
|
||||
@ -276,7 +277,7 @@ class TextField extends StatefulWidget {
|
||||
this.keyboardAppearance,
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.enableInteractiveSelection,
|
||||
this.enableInteractiveSelection = true,
|
||||
this.onTap,
|
||||
this.buildCounter,
|
||||
this.scrollController,
|
||||
@ -286,6 +287,7 @@ class TextField extends StatefulWidget {
|
||||
assert(autofocus != null),
|
||||
assert(obscureText != null),
|
||||
assert(autocorrect != null),
|
||||
assert(enableInteractiveSelection != null),
|
||||
assert(maxLengthEnforced != null),
|
||||
assert(scrollPadding != null),
|
||||
assert(dragStartBehavior != null),
|
||||
@ -302,6 +304,17 @@ class TextField extends StatefulWidget {
|
||||
),
|
||||
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
|
||||
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,
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
/// Controls the text being edited.
|
||||
@ -410,6 +423,13 @@ class TextField extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.editableText.readOnly}
|
||||
final bool readOnly;
|
||||
|
||||
/// Configuration of toolbar options.
|
||||
///
|
||||
/// If not set, select all and paste will default to be enabled. Copy and cut
|
||||
/// will be disabled if [obscureText] is true. If [readOnly] is true,
|
||||
/// paste and cut will be disabled regardless.
|
||||
final ToolbarOptions toolbarOptions;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.showCursor}
|
||||
final bool showCursor;
|
||||
|
||||
@ -538,9 +558,7 @@ class TextField extends StatefulWidget {
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
|
||||
/// {@macro flutter.rendering.editable.selectionEnabled}
|
||||
bool get selectionEnabled {
|
||||
return enableInteractiveSelection ?? !obscureText;
|
||||
}
|
||||
bool get selectionEnabled => enableInteractiveSelection;
|
||||
|
||||
/// {@template flutter.material.textfield.onTap}
|
||||
/// Called for each distinct tap except for every second tap of a double tap.
|
||||
@ -952,6 +970,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
child: EditableText(
|
||||
key: editableTextKey,
|
||||
readOnly: widget.readOnly,
|
||||
toolbarOptions: widget.toolbarOptions,
|
||||
showCursor: widget.showCursor,
|
||||
showSelectionHandles: _showSelectionHandles,
|
||||
controller: controller,
|
||||
|
@ -89,6 +89,7 @@ class TextFormField extends FormField<String> {
|
||||
TextAlign textAlign = TextAlign.start,
|
||||
bool autofocus = false,
|
||||
bool readOnly = false,
|
||||
ToolbarOptions toolbarOptions,
|
||||
bool showCursor,
|
||||
bool obscureText = false,
|
||||
bool autocorrect = true,
|
||||
@ -163,6 +164,7 @@ class TextFormField extends FormField<String> {
|
||||
textDirection: textDirection,
|
||||
textCapitalization: textCapitalization,
|
||||
autofocus: autofocus,
|
||||
toolbarOptions: toolbarOptions,
|
||||
readOnly: readOnly,
|
||||
showCursor: showCursor,
|
||||
obscureText: obscureText,
|
||||
|
@ -1554,6 +1554,10 @@ class RenderEditable extends RenderBox {
|
||||
// When long-pressing past the end of the text, we want a collapsed cursor.
|
||||
if (position.offset >= word.end)
|
||||
return TextSelection.fromPosition(position);
|
||||
// If text is obscured, the entire sentence should be treated as one word.
|
||||
if (obscureText) {
|
||||
return TextSelection(baseOffset: 0, extentOffset: text.toPlainText().length);
|
||||
}
|
||||
return TextSelection(baseOffset: word.start, extentOffset: word.end);
|
||||
}
|
||||
|
||||
|
@ -221,6 +221,54 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Toolbar configuration for [EditableText].
|
||||
///
|
||||
/// Toolbar is a context menu that will show up when user right click or long
|
||||
/// press the [EditableText]. It includes several options: cut, copy, paste,
|
||||
/// and select all.
|
||||
///
|
||||
/// [EditableText] and its derived widgets have their own default [ToolbarOptions].
|
||||
/// Create a custom [ToolbarOptions] if you want explicit control over the toolbar
|
||||
/// option.
|
||||
class ToolbarOptions {
|
||||
/// Create a toolbar configuration with given options.
|
||||
///
|
||||
/// All options default to false if they are not explicitly set.
|
||||
const ToolbarOptions({
|
||||
this.copy = false,
|
||||
this.cut = false,
|
||||
this.paste = false,
|
||||
this.selectAll = false,
|
||||
}) : assert(copy != null),
|
||||
assert(cut != null),
|
||||
assert(paste != null),
|
||||
assert(selectAll != null);
|
||||
|
||||
/// Whether to show copy option in toolbar.
|
||||
///
|
||||
/// Defaults to false. Must not be null.
|
||||
final bool copy;
|
||||
|
||||
/// Whether to show cut option in toolbar.
|
||||
///
|
||||
/// If [EditableText.readOnly] is set to true, cut will be disabled regardless.
|
||||
///
|
||||
/// Defaults to false. Must not be null.
|
||||
final bool cut;
|
||||
|
||||
/// Whether to show paste option in toolbar.
|
||||
///
|
||||
/// If [EditableText.readOnly] is set to true, paste will be disabled regardless.
|
||||
///
|
||||
/// Defaults to false. Must not be null.
|
||||
final bool paste;
|
||||
|
||||
/// Whether to show select all option in toolbar.
|
||||
///
|
||||
/// Defaults to false. Must not be null.
|
||||
final bool selectAll;
|
||||
}
|
||||
|
||||
/// A basic text input field.
|
||||
///
|
||||
/// This widget interacts with the [TextInput] service to let the user edit the
|
||||
@ -336,14 +384,21 @@ class EditableText extends StatefulWidget {
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.keyboardAppearance = Brightness.light,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.enableInteractiveSelection,
|
||||
this.enableInteractiveSelection = true,
|
||||
this.scrollController,
|
||||
this.scrollPhysics,
|
||||
this.toolbarOptions = const ToolbarOptions(
|
||||
copy: true,
|
||||
cut: true,
|
||||
paste: true,
|
||||
selectAll: true
|
||||
)
|
||||
}) : assert(controller != null),
|
||||
assert(focusNode != null),
|
||||
assert(obscureText != null),
|
||||
assert(autocorrect != null),
|
||||
assert(showSelectionHandles != null),
|
||||
assert(enableInteractiveSelection != null),
|
||||
assert(readOnly != null),
|
||||
assert(forceLine != null),
|
||||
assert(style != null),
|
||||
@ -367,6 +422,7 @@ class EditableText extends StatefulWidget {
|
||||
assert(rendererIgnoresPointer != null),
|
||||
assert(scrollPadding != null),
|
||||
assert(dragStartBehavior != null),
|
||||
assert(toolbarOptions != null),
|
||||
_strutStyle = strutStyle,
|
||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||
inputFormatters = maxLines == 1
|
||||
@ -419,6 +475,12 @@ class EditableText extends StatefulWidget {
|
||||
/// * [textWidthBasis], which controls the calculation of text width.
|
||||
final bool forceLine;
|
||||
|
||||
/// Configuration of toolbar options.
|
||||
///
|
||||
/// By default, all options are enabled. If [readOnly] is true,
|
||||
/// paste and cut will be disabled regardless.
|
||||
final ToolbarOptions toolbarOptions;
|
||||
|
||||
/// Whether to show selection handles.
|
||||
///
|
||||
/// When a selection is active, there will be two handles at each side of
|
||||
@ -903,9 +965,7 @@ class EditableText extends StatefulWidget {
|
||||
final ScrollPhysics scrollPhysics;
|
||||
|
||||
/// {@macro flutter.rendering.editable.selectionEnabled}
|
||||
bool get selectionEnabled {
|
||||
return enableInteractiveSelection ?? !obscureText;
|
||||
}
|
||||
bool get selectionEnabled => enableInteractiveSelection;
|
||||
|
||||
@override
|
||||
EditableTextState createState() => EditableTextState();
|
||||
@ -969,16 +1029,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
|
||||
|
||||
@override
|
||||
bool get cutEnabled => !widget.readOnly;
|
||||
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly;
|
||||
|
||||
@override
|
||||
bool get copyEnabled => true;
|
||||
bool get copyEnabled => widget.toolbarOptions.copy;
|
||||
|
||||
@override
|
||||
bool get pasteEnabled => !widget.readOnly;
|
||||
bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly;
|
||||
|
||||
@override
|
||||
bool get selectAllEnabled => true;
|
||||
bool get selectAllEnabled => widget.toolbarOptions.selectAll;
|
||||
|
||||
// State lifecycle:
|
||||
|
||||
|
@ -872,7 +872,7 @@ class TextSelectionGestureDetectorBuilder {
|
||||
@protected
|
||||
final TextSelectionGestureDetectorBuilderDelegate delegate;
|
||||
|
||||
/// Whether to show the selection tool bar.
|
||||
/// Whether to show the selection toolbar.
|
||||
///
|
||||
/// It is based on the signal source when a [onTapDown] is called. This getter
|
||||
/// will return true if current [onTapDown] event is triggered by a touch or
|
||||
@ -937,7 +937,7 @@ class TextSelectionGestureDetectorBuilder {
|
||||
/// Handler for [TextSelectionGestureDetector.onForcePressEnd].
|
||||
///
|
||||
/// By default, it selects words in the range specified in [details] and shows
|
||||
/// tool bar if it is necessary.
|
||||
/// toolbar if it is necessary.
|
||||
///
|
||||
/// This callback is only applicable when force press is enabled.
|
||||
///
|
||||
@ -1022,7 +1022,7 @@ class TextSelectionGestureDetectorBuilder {
|
||||
|
||||
/// Handler for [TextSelectionGestureDetector.onSingleLongTapEnd].
|
||||
///
|
||||
/// By default, it shows tool bar if necessary.
|
||||
/// By default, it shows toolbar if necessary.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@ -1037,7 +1037,7 @@ class TextSelectionGestureDetectorBuilder {
|
||||
/// Handler for [TextSelectionGestureDetector.onDoubleTapDown].
|
||||
///
|
||||
/// By default, it selects a word through [renderEditable.selectWord] if
|
||||
/// selectionEnabled and shows tool bar if necessary.
|
||||
/// selectionEnabled and shows toolbar if necessary.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
|
@ -1461,10 +1461,7 @@ void main() {
|
||||
|
||||
// Long press to put the cursor after the "w".
|
||||
const int index = 3;
|
||||
final TestGesture gesture =
|
||||
await tester.startGesture(textOffsetToPosition(tester, index));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await gesture.up();
|
||||
await tester.longPressAt(textOffsetToPosition(tester, index));
|
||||
await tester.pump();
|
||||
expect(
|
||||
controller.selection,
|
||||
@ -1619,7 +1616,7 @@ void main() {
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'An obscured CupertinoTextField is not selectable by default',
|
||||
'An obscured CupertinoTextField is not selectable when disabled',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
@ -1630,6 +1627,7 @@ void main() {
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
obscureText: true,
|
||||
enableInteractiveSelection: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -1666,7 +1664,7 @@ void main() {
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'An obscured CupertinoTextField is selectable when enabled',
|
||||
'An obscured CupertinoTextField is selectable by default',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
@ -1677,7 +1675,6 @@ void main() {
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
obscureText: true,
|
||||
enableInteractiveSelection: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -1692,15 +1689,14 @@ void main() {
|
||||
// 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.
|
||||
// The obscured text is treated as one word, should select all
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection(baseOffset: 9, extentOffset: 10),
|
||||
const TextSelection(baseOffset: 0, extentOffset: 35),
|
||||
);
|
||||
|
||||
// Selected text shows 3 toolbar buttons.
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||
// Selected text shows paste toolbar buttons.
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(1));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
@ -1708,12 +1704,56 @@ void main() {
|
||||
// Still selected.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection(baseOffset: 9, extentOffset: 10),
|
||||
const TextSelection(baseOffset: 0, extentOffset: 35),
|
||||
);
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(1));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('An obscured TextField has correct default context menu', (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.getCenter(find.byType(CupertinoTextField));
|
||||
|
||||
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.longPressAt(textfieldStart + const Offset(150.0, 5.0));
|
||||
await tester.pump();
|
||||
|
||||
// Should only have paste option when whole obscure text is selected.
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
expect(find.text('Copy'), findsNothing);
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
expect(find.text('Select All'), findsNothing);
|
||||
|
||||
// Tap to cancel selection.
|
||||
final Offset textfieldEnd = tester.getTopRight(find.byType(CupertinoTextField));
|
||||
await tester.tapAt(textfieldEnd + const Offset(-10.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
// Long tap at the end.
|
||||
await tester.longPressAt(textfieldEnd + const Offset(-10.0, 5.0));
|
||||
await tester.pump();
|
||||
|
||||
// Should have paste and select all options when collapse.
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
expect(find.text('Select All'), findsOneWidget);
|
||||
expect(find.text('Copy'), findsNothing);
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'long press moves cursor to the exact long press position and shows toolbar',
|
||||
(WidgetTester tester) async {
|
||||
|
@ -742,9 +742,7 @@ void main() {
|
||||
|
||||
// Long press the 'e' to select 'def'.
|
||||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||||
final TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
await gesture.up();
|
||||
await tester.longPressAt(ePos, pointer: 7);
|
||||
await tester.pump();
|
||||
|
||||
// 'def' is selected.
|
||||
@ -867,7 +865,7 @@ void main() {
|
||||
expect(find.text('CUT'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('does not paint tool bar when no options available on ios', (WidgetTester tester) async {
|
||||
testWidgets('does not paint toolbar when no options available on ios', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(platform: TargetPlatform.iOS),
|
||||
@ -889,7 +887,7 @@ void main() {
|
||||
expect(find.byType(CupertinoTextSelectionToolbar), paintsNothing);
|
||||
});
|
||||
|
||||
testWidgets('text field build empty tool bar when no options available android', (WidgetTester tester) async {
|
||||
testWidgets('text field build empty toolbar when no options available android', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Material(
|
||||
@ -1086,9 +1084,7 @@ void main() {
|
||||
|
||||
// Long press the 'e' to select 'def'.
|
||||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||||
final TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
await gesture.up();
|
||||
await tester.longPressAt(ePos, pointer: 7);
|
||||
await tester.pump();
|
||||
|
||||
expect(controller.selection.isCollapsed, true);
|
||||
@ -1569,9 +1565,36 @@ void main() {
|
||||
// End the test here to ensure the animation is properly disposed of.
|
||||
});
|
||||
|
||||
testWidgets('An obscured TextField is not selectable by default', (WidgetTester tester) async {
|
||||
testWidgets('An obscured TextField is selectable by default', (WidgetTester tester) async {
|
||||
// This is a regression test for
|
||||
// https://github.com/flutter/flutter/issues/24100
|
||||
// https://github.com/flutter/flutter/issues/32845
|
||||
|
||||
final TextEditingController controller = TextEditingController();
|
||||
Widget buildFrame(bool obscureText) {
|
||||
return overlay(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
obscureText: obscureText,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Obscure text and don't enable or disable selection.
|
||||
await tester.pumpWidget(buildFrame(true));
|
||||
await tester.enterText(find.byType(TextField), 'abcdefghi');
|
||||
await skipPastScrollingAnimation(tester);
|
||||
expect(controller.selection.isCollapsed, true);
|
||||
|
||||
// Long press does select text.
|
||||
final Offset ePos = textOffsetToPosition(tester, 1);
|
||||
await tester.longPressAt(ePos, pointer: 7);
|
||||
await tester.pump();
|
||||
expect(controller.selection.isCollapsed, false);
|
||||
});
|
||||
|
||||
testWidgets('An obscured TextField is not selectable when disabled', (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 enableInteractiveSelection) {
|
||||
@ -1584,49 +1607,75 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
// Obscure text and don't enable or disable selection
|
||||
await tester.pumpWidget(buildFrame(true, null));
|
||||
// Explicitly disabled selection on obscured text.
|
||||
await tester.pumpWidget(buildFrame(true, false));
|
||||
await tester.enterText(find.byType(TextField), 'abcdefghi');
|
||||
await skipPastScrollingAnimation(tester);
|
||||
expect(controller.selection.isCollapsed, true);
|
||||
|
||||
// Long press doesn't select anything
|
||||
final Offset ePos = textOffsetToPosition(tester, 1);
|
||||
final TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
await gesture.up();
|
||||
// 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 selectable when enabled', (WidgetTester tester) async {
|
||||
// This is a regression test for
|
||||
// https://github.com/flutter/flutter/issues/24100
|
||||
|
||||
testWidgets('An obscured TextField is selected as one word', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
Widget buildFrame(bool obscureText, bool enableInteractiveSelection) {
|
||||
return overlay(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
obscureText: obscureText,
|
||||
enableInteractiveSelection: enableInteractiveSelection,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Explicitly allow selection on obscured text
|
||||
await tester.pumpWidget(buildFrame(true, true));
|
||||
await tester.enterText(find.byType(TextField), 'abcdefghi');
|
||||
await tester.pumpWidget(overlay(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
obscureText: true,
|
||||
),
|
||||
));
|
||||
await tester.enterText(find.byType(TextField), 'abcde fghi');
|
||||
await skipPastScrollingAnimation(tester);
|
||||
expect(controller.selection.isCollapsed, true);
|
||||
|
||||
// Long press does select text
|
||||
final Offset ePos2 = textOffsetToPosition(tester, 1);
|
||||
final TestGesture gesture2 = await tester.startGesture(ePos2, pointer: 7);
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
await gesture2.up();
|
||||
// Long press does select text.
|
||||
final Offset bPos = textOffsetToPosition(tester, 1);
|
||||
await tester.longPressAt(bPos, pointer: 7);
|
||||
await tester.pump();
|
||||
expect(controller.selection.isCollapsed, false);
|
||||
final TextSelection selection = controller.selection;
|
||||
expect(selection.isCollapsed, false);
|
||||
expect(selection.baseOffset, 0);
|
||||
expect(selection.extentOffset, 10);
|
||||
});
|
||||
|
||||
testWidgets('An obscured TextField has correct default context menu', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
|
||||
await tester.pumpWidget(overlay(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
obscureText: true,
|
||||
),
|
||||
));
|
||||
await tester.enterText(find.byType(TextField), 'abcde fghi');
|
||||
await skipPastScrollingAnimation(tester);
|
||||
|
||||
// Long press to select text.
|
||||
final Offset bPos = textOffsetToPosition(tester, 1);
|
||||
await tester.longPressAt(bPos, pointer: 7);
|
||||
await tester.pump();
|
||||
|
||||
// Should only have paste option when whole obscure text is selected.
|
||||
expect(find.text('PASTE'), findsOneWidget);
|
||||
expect(find.text('COPY'), findsNothing);
|
||||
expect(find.text('CUT'), findsNothing);
|
||||
expect(find.text('SELECT ALL'), findsNothing);
|
||||
|
||||
// Long press at the end
|
||||
final Offset iPos = textOffsetToPosition(tester, 10);
|
||||
final Offset slightRight = iPos + const Offset(30.0, 0.0);
|
||||
await tester.longPressAt(slightRight, pointer: 7);
|
||||
await tester.pump();
|
||||
|
||||
// Should have paste and select all options when collapse.
|
||||
expect(find.text('PASTE'), findsOneWidget);
|
||||
expect(find.text('SELECT ALL'), findsOneWidget);
|
||||
expect(find.text('COPY'), findsNothing);
|
||||
expect(find.text('CUT'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('TextField height with minLines unset', (WidgetTester tester) async {
|
||||
|
@ -533,6 +533,79 @@ void main() {
|
||||
expect(find.text('PASTE'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('can dynamically disable options in toolbar', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: EditableText(
|
||||
backgroundCursorColor: Colors.grey,
|
||||
controller: TextEditingController(text: 'blah blah'),
|
||||
focusNode: focusNode,
|
||||
toolbarOptions: const ToolbarOptions(
|
||||
copy: true,
|
||||
selectAll: true,
|
||||
),
|
||||
style: textStyle,
|
||||
cursorColor: cursorColor,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final EditableTextState state =
|
||||
tester.state<EditableTextState>(find.byType(EditableText));
|
||||
|
||||
// Select something. Doesn't really matter what.
|
||||
state.renderEditable.selectWordsInRange(
|
||||
from: const Offset(0, 0),
|
||||
cause: SelectionChangedCause.tap,
|
||||
);
|
||||
await tester.pump();
|
||||
expect(state.showToolbar(), true);
|
||||
await tester.pump();
|
||||
expect(find.text('SELECT ALL'), findsOneWidget);
|
||||
expect(find.text('COPY'), findsOneWidget);
|
||||
expect(find.text('PASTE'), findsNothing);
|
||||
expect(find.text('CUT'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('cut and paste are disabled in read only mode even if explicit set', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: EditableText(
|
||||
backgroundCursorColor: Colors.grey,
|
||||
controller: TextEditingController(text: 'blah blah'),
|
||||
focusNode: focusNode,
|
||||
readOnly: true,
|
||||
toolbarOptions: const ToolbarOptions(
|
||||
paste: true,
|
||||
cut: true,
|
||||
selectAll: true,
|
||||
copy: true,
|
||||
),
|
||||
style: textStyle,
|
||||
cursorColor: cursorColor,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final EditableTextState state =
|
||||
tester.state<EditableTextState>(find.byType(EditableText));
|
||||
|
||||
// Select something. Doesn't really matter what.
|
||||
state.renderEditable.selectWordsInRange(
|
||||
from: const Offset(0, 0),
|
||||
cause: SelectionChangedCause.tap,
|
||||
);
|
||||
await tester.pump();
|
||||
expect(state.showToolbar(), true);
|
||||
await tester.pump();
|
||||
expect(find.text('SELECT ALL'), findsOneWidget);
|
||||
expect(find.text('COPY'), findsOneWidget);
|
||||
expect(find.text('PASTE'), findsNothing);
|
||||
expect(find.text('CUT'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async {
|
||||
String changedValue;
|
||||
final Widget widget = MaterialApp(
|
||||
@ -1628,8 +1701,14 @@ void main() {
|
||||
SemanticsFlag.isObscured,
|
||||
SemanticsFlag.isFocused,
|
||||
],
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.moveCursorBackwardByCharacter,
|
||||
SemanticsAction.setSelection,
|
||||
SemanticsAction.moveCursorBackwardByWord
|
||||
],
|
||||
value: expectedValue,
|
||||
textDirection: TextDirection.ltr,
|
||||
textSelection: const TextSelection.collapsed(offset: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -592,6 +592,27 @@ void main() {
|
||||
expect(find.text('CUT'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('selectable text can disable toolbar options', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
overlay(
|
||||
child: const SelectableText(
|
||||
'a selectable text',
|
||||
toolbarOptions: ToolbarOptions(
|
||||
copy: false,
|
||||
selectAll: true,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
const int dIndex = 5;
|
||||
final Offset dPos = textOffsetToPosition(tester, dIndex);
|
||||
await tester.longPressAt(dPos);
|
||||
await tester.pump();
|
||||
// Context menu should not have copy.
|
||||
expect(find.text('COPY'), findsNothing);
|
||||
expect(find.text('SELECT ALL'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
|
Loading…
x
Reference in New Issue
Block a user