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:
chunhtai 2019-07-26 10:03:30 -07:00 committed by GitHub
parent 68fc7231b3
commit 0d0af31598
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 386 additions and 74 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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);
}

View File

@ -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:

View File

@ -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:
///

View File

@ -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 {

View File

@ -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(
await tester.pumpWidget(overlay(
child: TextField(
controller: controller,
obscureText: obscureText,
enableInteractiveSelection: enableInteractiveSelection,
obscureText: true,
),
);
}
// Explicitly allow selection on obscured text
await tester.pumpWidget(buildFrame(true, true));
await tester.enterText(find.byType(TextField), 'abcdefghi');
));
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 {

View File

@ -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),
),
],
),

View File

@ -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(