Unverified Commit eddcc6bb authored by chunhtai's avatar chunhtai Committed by GitHub

reland Enable selection by default for password text field and expose api to...

reland Enable selection by default for password text field and expose api to turn on and off context menu options (#37324)
parent 58e507ba
......@@ -126,6 +126,8 @@ void main() {
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
AndroidSemanticsAction.accessibilityFocus,
AndroidSemanticsAction.setSelection,
AndroidSemanticsAction.copy,
],
));
......@@ -141,6 +143,8 @@ void main() {
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
AndroidSemanticsAction.accessibilityFocus,
AndroidSemanticsAction.setSelection,
AndroidSemanticsAction.copy,
],
));
});
......
......@@ -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:
......
......@@ -871,7 +871,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
......@@ -936,7 +936,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.
///
......@@ -1021,7 +1021,7 @@ class TextSelectionGestureDetectorBuilder {
/// Handler for [TextSelectionGestureDetector.onSingleLongTapEnd].
///
/// By default, it shows tool bar if necessary.
/// By default, it shows toolbar if necessary.
///
/// See also:
///
......@@ -1036,7 +1036,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 {
......
......@@ -741,9 +741,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.
......@@ -866,7 +864,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),
......@@ -888,7 +886,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(
......@@ -1085,9 +1083,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);
......@@ -1568,39 +1564,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, bool enableInteractiveSelection) {
Widget buildFrame(bool obscureText) {
return overlay(
child: TextField(
controller: controller,
obscureText: obscureText,
enableInteractiveSelection: enableInteractiveSelection,
),
);
}
// Obscure text and don't enable or disable selection
await tester.pumpWidget(buildFrame(true, null));
// 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 doesn't select anything
// Long press does select text.
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();
await tester.longPressAt(ePos, pointer: 7);
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.isCollapsed, false);
});
testWidgets('An obscured TextField is selectable when enabled', (WidgetTester tester) async {
testWidgets('An obscured TextField is not selectable when disabled', (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, bool enableInteractiveSelection) {
......@@ -1613,19 +1606,75 @@ void main() {
);
}
// Explicitly allow selection on obscured text
await tester.pumpWidget(buildFrame(true, true));
// 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 does select text
// Long press doesn't 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();
await tester.longPressAt(ePos2, pointer: 7);
await tester.pump();
expect(controller.selection.isCollapsed, false);
expect(controller.selection.isCollapsed, true);
});
testWidgets('An obscured TextField is selected as one word', (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 does select text.
final Offset bPos = textOffsetToPosition(tester, 1);
await tester.longPressAt(bPos, pointer: 7);
await tester.pump();
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 {
......
......@@ -619,6 +619,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(
......@@ -1714,8 +1787,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(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment