Unverified Commit 581df52a authored by sandrasandeep's avatar sandrasandeep Committed by GitHub

Make EditableText cursor configurable (#18888)

* fixed segmented control golden test

* fixed segmented control golden test

* added cursorWidth, cursorRadius

* added default value for cursorWidth based on Apple specs

* test default cursorWidth

* removed cursorHeight stuff

* added functionality to keep cursor from blinking

* cursor width and radius is configurable + tests

* changed goldens repo version in goldens.version

* working version of configurable cursor (erased debugKeepCursorOn)

* minor changes

* docs

* changed textfield test that was failing due to new default cursorwidth

* added default value of cursorwidth in RenderEditable

* only run golden file tests on Mac

* cursor tests

* the tests are actually there now

* weak warning fixed

* switching to Linux

* changed default cursorWidth: 2.0 -> 1.0

* assorted changes, including changing text field test

* re-paint -> re-layout when changing cursorWidth
parent 989f5741
......@@ -16,7 +16,6 @@ import 'viewport_offset.dart';
const double _kCaretGap = 1.0; // pixels
const double _kCaretHeightOffset = 2.0; // pixels
const double _kCaretWidth = 1.0; // pixels
/// Signature for the callback that reports when the user changes the selection
/// (including the cursor location).
......@@ -134,6 +133,8 @@ class RenderEditable extends RenderBox {
this.ignorePointer = false,
bool obscureText = false,
Locale locale,
double cursorWidth = 1.0,
Radius cursorRadius,
}) : assert(textAlign != null),
assert(textDirection != null, 'RenderEditable created without a textDirection.'),
assert(maxLines == null || maxLines > 0),
......@@ -155,6 +156,8 @@ class RenderEditable extends RenderBox {
_selectionColor = selectionColor,
_selection = selection,
_offset = offset,
_cursorWidth = cursorWidth,
_cursorRadius = cursorRadius,
_obscureText = obscureText {
assert(_showCursor != null);
assert(!_showCursor.value || cursorColor != null);
......@@ -382,6 +385,26 @@ class RenderEditable extends RenderBox {
markNeedsLayout();
}
/// How thick the cursor will be.
double get cursorWidth => _cursorWidth;
double _cursorWidth = 1.0;
set cursorWidth(double value) {
if (_cursorWidth == value)
return;
_cursorWidth = value;
markNeedsLayout();
}
/// How rounded the corners of the cursor should be.
Radius get cursorRadius => _cursorRadius;
Radius _cursorRadius;
set cursorRadius(Radius value) {
if (_cursorRadius == value)
return;
_cursorRadius = value;
markNeedsPaint();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
......@@ -546,7 +569,7 @@ class RenderEditable extends RenderBox {
_layoutText(constraints.maxWidth);
final Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, _caretPrototype);
// This rect is the same as _caretPrototype but without the vertical padding.
return new Rect.fromLTWH(0.0, 0.0, _kCaretWidth, preferredLineHeight).shift(caretOffset + _paintOffset);
return new Rect.fromLTWH(0.0, 0.0, cursorWidth, preferredLineHeight).shift(caretOffset + _paintOffset);
}
@override
......@@ -683,7 +706,7 @@ class RenderEditable extends RenderBox {
assert(constraintWidth != null);
if (_textLayoutLastWidth == constraintWidth)
return;
const double caretMargin = _kCaretGap + _kCaretWidth;
final double caretMargin = _kCaretGap + cursorWidth;
final double availableWidth = math.max(0.0, constraintWidth - caretMargin);
final double maxWidth = _isMultiline ? availableWidth : double.infinity;
_textPainter.layout(minWidth: availableWidth, maxWidth: maxWidth);
......@@ -693,7 +716,7 @@ class RenderEditable extends RenderBox {
@override
void performLayout() {
_layoutText(constraints.maxWidth);
_caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, preferredLineHeight - 2.0 * _kCaretHeightOffset);
_caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, cursorWidth, preferredLineHeight - 2.0 * _kCaretHeightOffset);
_selectionRects = null;
// We grab _textPainter.size here because assigning to `size` on the next
// line will trigger us to validate our intrinsic sizes, which will change
......@@ -705,7 +728,7 @@ class RenderEditable extends RenderBox {
// See also RenderParagraph which has a similar issue.
final Size textPainterSize = _textPainter.size;
size = new Size(constraints.maxWidth, constraints.constrainHeight(_preferredHeight(constraints.maxWidth)));
final Size contentSize = new Size(textPainterSize.width + _kCaretGap + _kCaretWidth, textPainterSize.height);
final Size contentSize = new Size(textPainterSize.width + _kCaretGap + cursorWidth, textPainterSize.height);
final double _maxScrollExtent = _getMaxScrollExtent(contentSize);
_hasVisualOverflow = _maxScrollExtent > 0.0;
offset.applyViewportDimension(_viewportExtent);
......@@ -715,9 +738,18 @@ class RenderEditable extends RenderBox {
void _paintCaret(Canvas canvas, Offset effectiveOffset) {
assert(_textLayoutLastWidth == constraints.maxWidth);
final Offset caretOffset = _textPainter.getOffsetForCaret(_selection.extent, _caretPrototype);
final Paint paint = new Paint()..color = _cursorColor;
final Paint paint = new Paint()
..color = _cursorColor;
final Rect caretRect = _caretPrototype.shift(caretOffset + effectiveOffset);
if (cursorRadius == null) {
canvas.drawRect(caretRect, paint);
} else {
final RRect caretRRect = RRect.fromRectAndRadius(caretRect, cursorRadius);
canvas.drawRRect(caretRRect, paint);
}
if (caretRect != _lastCaretRect) {
_lastCaretRect = caretRect;
if (onCaretChanged != null)
......
......@@ -208,6 +208,8 @@ class EditableText extends StatefulWidget {
this.onSelectionChanged,
List<TextInputFormatter> inputFormatters,
this.rendererIgnoresPointer = false,
this.cursorWidth = 1.0,
this.cursorRadius,
}) : assert(controller != null),
assert(focusNode != null),
assert(obscureText != null),
......@@ -353,6 +355,16 @@ class EditableText extends StatefulWidget {
/// This property is false by default.
final bool rendererIgnoresPointer;
/// How thick the cursor will be.
///
/// Defaults to 1.0
final double cursorWidth;
/// How rounded the corners of the cursor should be.
///
/// By default, the cursor has a Radius of zero.
final Radius cursorRadius;
@override
EditableTextState createState() => new EditableTextState();
......@@ -810,6 +822,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
FocusScope.of(context).reparentIfNeeded(widget.focusNode);
super.build(context); // See AutomaticKeepAliveClientMixin.
final TextSelectionControls controls = widget.selectionControls;
return new Scrollable(
excludeFromSemantics: true,
axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
......@@ -841,6 +854,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
onSelectionChanged: _handleSelectionChanged,
onCaretChanged: _handleCaretChanged,
rendererIgnoresPointer: widget.rendererIgnoresPointer,
cursorWidth: widget.cursorWidth,
cursorRadius: widget.cursorRadius,
),
),
);
......@@ -902,6 +917,8 @@ class _Editable extends LeafRenderObjectWidget {
this.onSelectionChanged,
this.onCaretChanged,
this.rendererIgnoresPointer = false,
this.cursorWidth,
this.cursorRadius,
}) : assert(textDirection != null),
assert(rendererIgnoresPointer != null),
super(key: key);
......@@ -923,6 +940,8 @@ class _Editable extends LeafRenderObjectWidget {
final SelectionChangedHandler onSelectionChanged;
final CaretChangedHandler onCaretChanged;
final bool rendererIgnoresPointer;
final double cursorWidth;
final Radius cursorRadius;
@override
RenderEditable createRenderObject(BuildContext context) {
......@@ -943,6 +962,8 @@ class _Editable extends LeafRenderObjectWidget {
onCaretChanged: onCaretChanged,
ignorePointer: rendererIgnoresPointer,
obscureText: obscureText,
cursorWidth: cursorWidth,
cursorRadius: cursorRadius,
);
}
......@@ -964,6 +985,8 @@ class _Editable extends LeafRenderObjectWidget {
..onSelectionChanged = onSelectionChanged
..onCaretChanged = onCaretChanged
..ignorePointer = rendererIgnoresPointer
..obscureText = obscureText;
..obscureText = obscureText
..cursorWidth = cursorWidth
..cursorRadius = cursorRadius;
}
}
......@@ -967,6 +967,7 @@ void main() {
final Offset center = tester.getCenter(find.text('B'));
await tester.startGesture(center);
await tester.pumpAndSettle();
await expectLater(
find.byType(RepaintBoundary),
matchesGoldenFile('segmented_control_test.1.0.png'),
......
......@@ -1208,7 +1208,7 @@ void main() {
editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft,
);
expect(topLeft.dx, equals(399.0));
expect(topLeft.dx, equals(399));
await tester.enterText(find.byType(TextField), 'abcd');
await tester.pump();
......@@ -1217,7 +1217,7 @@ void main() {
editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
);
expect(topLeft.dx, equals(399.0));
expect(topLeft.dx, equals(399));
});
testWidgets('Can align to center within center', (WidgetTester tester) async {
......@@ -1240,7 +1240,7 @@ void main() {
editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft,
);
expect(topLeft.dx, equals(399.0));
expect(topLeft.dx, equals(399));
await tester.enterText(find.byType(TextField), 'abcd');
await tester.pump();
......@@ -1249,7 +1249,7 @@ void main() {
editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
);
expect(topLeft.dx, equals(399.0));
expect(topLeft.dx, equals(399));
});
testWidgets('Controller can update server', (WidgetTester tester) async {
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -56,7 +57,8 @@ void main() {
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputAction'], equals(serializedActionName));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals(serializedActionName));
}
testWidgets('has expected defaults', (WidgetTester tester) async {
......@@ -77,6 +79,26 @@ void main() {
expect(editableText.maxLines, equals(1));
expect(editableText.obscureText, isFalse);
expect(editableText.autocorrect, isTrue);
expect(editableText.cursorWidth, 1.0);
});
testWidgets('cursor has expected width and radius',
(WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
cursorWidth: 10.0,
cursorRadius: const Radius.circular(2.0),
)));
final EditableText editableText =
tester.firstWidget(find.byType(EditableText));
expect(editableText.cursorWidth, 10.0);
expect(editableText.cursorRadius.x, 2.0);
});
testWidgets('text keyboard is requested when maxLines is default',
......@@ -104,12 +126,14 @@ void main() {
tester.firstWidget(find.byType(EditableText));
expect(editableText.maxLines, equals(1));
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'], equals('TextInputType.text'));
expect(tester.testTextInput.setClientArgs['inputType']['name'],
equals('TextInputType.text'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.done'));
});
testWidgets('Keyboard is configured for "unspecified" action when explicitly requested',
testWidgets(
'Keyboard is configured for "unspecified" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
......@@ -118,7 +142,8 @@ void main() {
);
});
testWidgets('Keyboard is configured for "none" action when explicitly requested',
testWidgets(
'Keyboard is configured for "none" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
......@@ -127,7 +152,8 @@ void main() {
);
});
testWidgets('Keyboard is configured for "done" action when explicitly requested',
testWidgets(
'Keyboard is configured for "done" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
......@@ -136,7 +162,8 @@ void main() {
);
});
testWidgets('Keyboard is configured for "send" action when explicitly requested',
testWidgets(
'Keyboard is configured for "send" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
......@@ -145,7 +172,8 @@ void main() {
);
});
testWidgets('Keyboard is configured for "go" action when explicitly requested',
testWidgets(
'Keyboard is configured for "go" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
......@@ -154,7 +182,8 @@ void main() {
);
});
testWidgets('Keyboard is configured for "search" action when explicitly requested',
testWidgets(
'Keyboard is configured for "search" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
......@@ -163,7 +192,8 @@ void main() {
);
});
testWidgets('Keyboard is configured for "send" action when explicitly requested',
testWidgets(
'Keyboard is configured for "send" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
......@@ -172,7 +202,8 @@ void main() {
);
});
testWidgets('Keyboard is configured for "next" action when explicitly requested',
testWidgets(
'Keyboard is configured for "next" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
......@@ -181,7 +212,8 @@ void main() {
);
});
testWidgets('Keyboard is configured for "previous" action when explicitly requested',
testWidgets(
'Keyboard is configured for "previous" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
......@@ -190,7 +222,8 @@ void main() {
);
});
testWidgets('Keyboard is configured for "continue" action when explicitly requested',
testWidgets(
'Keyboard is configured for "continue" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
......@@ -199,7 +232,8 @@ void main() {
);
});
testWidgets('Keyboard is configured for "join" action when explicitly requested',
testWidgets(
'Keyboard is configured for "join" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
......@@ -208,7 +242,8 @@ void main() {
);
});
testWidgets('Keyboard is configured for "route" action when explicitly requested',
testWidgets(
'Keyboard is configured for "route" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
......@@ -217,7 +252,8 @@ void main() {
);
});
testWidgets('Keyboard is configured for "emergencyCall" action when explicitly requested',
testWidgets(
'Keyboard is configured for "emergencyCall" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
......@@ -250,11 +286,14 @@ void main() {
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'], equals('TextInputType.multiline'));
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.newline'));
expect(tester.testTextInput.setClientArgs['inputType']['name'],
equals('TextInputType.multiline'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.newline'));
});
testWidgets('Correct keyboard is requested when set explicitly and maxLines > 1',
testWidgets(
'Correct keyboard is requested when set explicitly and maxLines > 1',
(WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
......@@ -279,8 +318,10 @@ void main() {
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'], equals('TextInputType.phone'));
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.done'));
expect(tester.testTextInput.setClientArgs['inputType']['name'],
equals('TextInputType.phone'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.done'));
});
testWidgets('multiline keyboard is requested when set implicitly',
......@@ -307,8 +348,10 @@ void main() {
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'], equals('TextInputType.multiline'));
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.newline'));
expect(tester.testTextInput.setClientArgs['inputType']['name'],
equals('TextInputType.multiline'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.newline'));
});
testWidgets('single line inputs have correct default keyboard',
......@@ -335,12 +378,16 @@ void main() {
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'], equals('TextInputType.text'));
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.done'));
expect(tester.testTextInput.setClientArgs['inputType']['name'],
equals('TextInputType.text'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.done'));
});
testWidgets('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>();
testWidgets('Fires onChanged when text changes via TextSelectionOverlay',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
new GlobalKey<EditableTextState>();
String changedValue;
final Widget widget = new MaterialApp(
......@@ -361,9 +408,10 @@ void main() {
// Populate a fake clipboard.
const String clipboardContent = 'Dobunezumi mitai ni utsukushiku naritai';
SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async {
SystemChannels.platform
.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.getData')
return const <String, dynamic>{ 'text': clipboardContent };
return const <String, dynamic>{'text': clipboardContent};
return null;
});
......@@ -378,39 +426,111 @@ void main() {
expect(changedValue, clipboardContent);
});
testWidgets('Loses focus by default when "done" action is pressed', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>();
final FocusNode focusNode = new FocusNode();
testWidgets('cursor layout has correct width', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
new GlobalKey<EditableTextState>();
String changedValue;
final Widget widget = new MaterialApp(
home: new EditableText(
home: new RepaintBoundary(
key: const ValueKey<int>(1),
child: new EditableText(
key: editableTextKey,
controller: new TextEditingController(),
focusNode: focusNode,
focusNode: new FocusNode(),
style: new Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) {
changedValue = value;
},
cursorWidth: 15.0,
),
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
// Populate a fake clipboard.
const String clipboardContent = ' ';
SystemChannels.platform
.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.getData')
return const <String, dynamic>{'text': clipboardContent};
return null;
});
// Long-press to bring up the text editing controls.
final Finder textFinder = find.byKey(editableTextKey);
await tester.tap(textFinder);
await tester.longPress(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.tap(find.text('PASTE'));
await tester.pump();
// Lost focus because "done" was pressed.
expect(focusNode.hasFocus, false);
expect(changedValue, clipboardContent);
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('editable_text_test.0.0.png'),
);
}, skip: !Platform.isLinux);
testWidgets('cursor layout has correct radius', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
new GlobalKey<EditableTextState>();
String changedValue;
final Widget widget = new MaterialApp(
home: new RepaintBoundary(
key: const ValueKey<int>(1),
child: new EditableText(
key: editableTextKey,
controller: new TextEditingController(),
focusNode: new FocusNode(),
style: new Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) {
changedValue = value;
},
cursorWidth: 15.0,
cursorRadius: const Radius.circular(3.0),
),
),
);
await tester.pumpWidget(widget);
// Populate a fake clipboard.
const String clipboardContent = ' ';
SystemChannels.platform
.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.getData')
return const <String, dynamic>{'text': clipboardContent};
return null;
});
testWidgets('Does not lose focus by default when "next" action is pressed', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>();
// Long-press to bring up the text editing controls.
final Finder textFinder = find.byKey(editableTextKey);
await tester.longPress(textFinder);
await tester.pump();
await tester.tap(find.text('PASTE'));
await tester.pump();
expect(changedValue, clipboardContent);
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('editable_text_test.1.0.png'),
);
}, skip: !Platform.isLinux);
testWidgets('Does not lose focus by default when "next" action is pressed',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
new GlobalKey<EditableTextState>();
final FocusNode focusNode = new FocusNode();
final Widget widget = new MaterialApp(
......@@ -440,9 +560,11 @@ void main() {
expect(focusNode.hasFocus, true);
});
testWidgets('Does not lose focus by default when "done" action is pressed and onEditingComplete is provided',
testWidgets(
'Does not lose focus by default when "done" action is pressed and onEditingComplete is provided',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>();
final GlobalKey<EditableTextState> editableTextKey =
new GlobalKey<EditableTextState>();
final FocusNode focusNode = new FocusNode();
final Widget widget = new MaterialApp(
......@@ -476,9 +598,11 @@ void main() {
expect(focusNode.hasFocus, true);
});
testWidgets('When "done" is pressed callbacks are invoked: onEditingComplete > onSubmitted',
testWidgets(
'When "done" is pressed callbacks are invoked: onEditingComplete > onSubmitted',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>();
final GlobalKey<EditableTextState> editableTextKey =
new GlobalKey<EditableTextState>();
final FocusNode focusNode = new FocusNode();
bool onEditingCompleteCalled = false;
......@@ -518,9 +642,11 @@ void main() {
// and onSubmission callbacks.
});
testWidgets('When "next" is pressed callbacks are invoked: onEditingComplete > onSubmitted',
testWidgets(
'When "next" is pressed callbacks are invoked: onEditingComplete > onSubmitted',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>();
final GlobalKey<EditableTextState> editableTextKey =
new GlobalKey<EditableTextState>();
final FocusNode focusNode = new FocusNode();
bool onEditingCompleteCalled = false;
......@@ -560,10 +686,14 @@ void main() {
// and onSubmission callbacks.
});
testWidgets('Changing controller updates EditableText', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>();
final TextEditingController controller1 = new TextEditingController(text: 'Wibble');
final TextEditingController controller2 = new TextEditingController(text: 'Wobble');
testWidgets('Changing controller updates EditableText',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
new GlobalKey<EditableTextState>();
final TextEditingController controller1 =
new TextEditingController(text: 'Wibble');
final TextEditingController controller2 =
new TextEditingController(text: 'Wobble');
TextEditingController currentController = controller1;
StateSetter setState;
......@@ -579,11 +709,13 @@ void main() {
key: editableTextKey,
controller: currentController,
focusNode: new FocusNode(),
style: new Typography(platform: TargetPlatform.android).black.subhead,
style: new Typography(platform: TargetPlatform.android)
.black
.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) { },
onChanged: (String value) {},
),
),
),
......@@ -591,6 +723,7 @@ void main() {
},
);
}
await tester.pumpWidget(builder());
await tester.showKeyboard(find.byType(EditableText));
......@@ -605,7 +738,9 @@ void main() {
await tester.pump();
expect(log, hasLength(1));
expect(log.single, isMethodCall(
expect(
log.single,
isMethodCall(
'TextInput.setEditingState',
arguments: const <String, dynamic>{
'text': 'Wobble',
......@@ -619,7 +754,8 @@ void main() {
));
});
testWidgets('EditableText identifies as text field (w/ focus) in semantics', (WidgetTester tester) async {
testWidgets('EditableText identifies as text field (w/ focus) in semantics',
(WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
......@@ -638,18 +774,25 @@ void main() {
),
);
expect(semantics, includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField]));
expect(semantics,
includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField]));
await tester.tap(find.byType(EditableText));
await tester.idle();
await tester.pump();
expect(semantics, includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isFocused]));
expect(
semantics,
includesNodeWith(flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused
]));
semantics.dispose();
});
testWidgets('EditableText includes text as value in semantics', (WidgetTester tester) async {
testWidgets('EditableText includes text as value in semantics',
(WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
const String value1 = 'EditableText content';
......@@ -671,7 +814,9 @@ void main() {
),
);
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
flags: <SemanticsFlag>[SemanticsFlag.isTextField],
value: value1,
));
......@@ -681,7 +826,9 @@ void main() {
await tester.idle();
await tester.pump();
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
flags: <SemanticsFlag>[SemanticsFlag.isTextField],
value: value2,
));
......@@ -689,12 +836,14 @@ void main() {
semantics.dispose();
});
testWidgets('changing selection with keyboard does not show handles', (WidgetTester tester) async {
testWidgets('changing selection with keyboard does not show handles',
(WidgetTester tester) async {
const String value1 = 'Hello World';
controller.text = value1;
await tester.pumpWidget(new MaterialApp(
await tester.pumpWidget(
new MaterialApp(
home: new EditableText(
controller: controller,
selectionControls: materialTextSelectionControls,
......@@ -702,27 +851,36 @@ void main() {
style: textStyle,
cursorColor: cursorColor,
),
));
),
);
// Simulate selection change via tap to show handles.
final RenderEditable render = tester.allRenderObjects.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
render.onSelectionChanged(const TextSelection.collapsed(offset: 4), render, SelectionChangedCause.tap);
final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
render.onSelectionChanged(const TextSelection.collapsed(offset: 4), render,
SelectionChangedCause.tap);
await tester.pumpAndSettle();
final EditableTextState textState = tester.state(find.byType(EditableText));
expect(textState.selectionOverlay.handlesAreVisible, isTrue);
expect(textState.selectionOverlay.selectionDelegate.textEditingValue.selection, const TextSelection.collapsed(offset: 4));
expect(
textState.selectionOverlay.selectionDelegate.textEditingValue.selection,
const TextSelection.collapsed(offset: 4));
// Simulate selection change via keyboard and expect handles to disappear.
render.onSelectionChanged(const TextSelection.collapsed(offset: 10), render, SelectionChangedCause.keyboard);
render.onSelectionChanged(const TextSelection.collapsed(offset: 10), render,
SelectionChangedCause.keyboard);
await tester.pumpAndSettle();
expect(textState.selectionOverlay.handlesAreVisible, isFalse);
expect(textState.selectionOverlay.selectionDelegate.textEditingValue.selection, const TextSelection.collapsed(offset: 10));
expect(
textState.selectionOverlay.selectionDelegate.textEditingValue.selection,
const TextSelection.collapsed(offset: 10));
});
testWidgets('exposes correct cursor movement semantics', (WidgetTester tester) async {
testWidgets('exposes correct cursor movement semantics',
(WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
controller.text = 'test';
......@@ -736,15 +894,20 @@ void main() {
),
));
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
value: 'test',
));
controller.selection = new TextSelection.collapsed(offset: controller.text.length);
controller.selection =
new TextSelection.collapsed(offset: controller.text.length);
await tester.pumpAndSettle();
// At end, can only go backwards.
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
......@@ -752,11 +915,14 @@ void main() {
],
));
controller.selection = new TextSelection.collapsed(offset: controller.text.length - 2);
controller.selection =
new TextSelection.collapsed(offset: controller.text.length - 2);
await tester.pumpAndSettle();
// Somewhere in the middle, can go in both directions.
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
......@@ -769,7 +935,9 @@ void main() {
await tester.pumpAndSettle();
// At beginning, can only go forward.
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
......@@ -785,7 +953,8 @@ void main() {
const bool doNotExtendSelection = false;
controller.text = 'test';
controller.selection = new TextSelection.collapsed(offset: controller.text.length);
controller.selection =
new TextSelection.collapsed(offset: controller.text.length);
await tester.pumpWidget(new MaterialApp(
home: new EditableText(
......@@ -796,26 +965,32 @@ void main() {
),
));
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
],
));
final RenderEditable render = tester.allRenderObjects.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
final int semanticsId = render.debugSemantics.id;
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 4);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 3);
expect(controller.selection.extentOffset, 3);
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
......@@ -824,18 +999,23 @@ void main() {
],
));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 0);
await tester.pumpAndSettle();
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
......@@ -843,7 +1023,8 @@ void main() {
],
));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 1);
......@@ -852,13 +1033,15 @@ void main() {
semantics.dispose();
});
testWidgets('can extend selection with a11y means', (WidgetTester tester) async {
testWidgets('can extend selection with a11y means',
(WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
const bool extendSelection = true;
const bool doNotExtendSelection = false;
controller.text = 'test';
controller.selection = new TextSelection.collapsed(offset: controller.text.length);
controller.selection =
new TextSelection.collapsed(offset: controller.text.length);
await tester.pumpWidget(new MaterialApp(
home: new EditableText(
......@@ -869,26 +1052,32 @@ void main() {
),
));
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
],
));
final RenderEditable render = tester.allRenderObjects.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
final int semanticsId = render.debugSemantics.id;
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 4);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 3);
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
......@@ -897,18 +1086,23 @@ void main() {
],
));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 0);
await tester.pumpAndSettle();
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
......@@ -916,13 +1110,15 @@ void main() {
],
));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 1);
expect(controller.selection.extentOffset, 1);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByCharacter, extendSelection);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId,
SemanticsAction.moveCursorForwardByCharacter, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 1);
......@@ -931,7 +1127,8 @@ void main() {
semantics.dispose();
});
testWidgets('password fields have correct semantics', (WidgetTester tester) async {
testWidgets('password fields have correct semantics',
(WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
controller.text = 'super-secret-password!!1';
......@@ -948,7 +1145,10 @@ void main() {
final String expectedValue = '•' * controller.text.length;
expect(semantics, hasSemantics(new TestSemantics(
expect(
semantics,
hasSemantics(
new TestSemantics(
children: <TestSemantics>[
new TestSemantics.rootChild(
children: <TestSemantics>[
......@@ -956,7 +1156,10 @@ void main() {
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
new TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isObscured],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isObscured
],
value: expectedValue,
textDirection: TextDirection.ltr,
),
......@@ -965,13 +1168,17 @@ void main() {
],
),
],
), ignoreTransform: true, ignoreRect: true, ignoreId: true));
),
ignoreTransform: true,
ignoreRect: true,
ignoreId: true));
semantics.dispose();
});
group('a11y copy/cut/paste', () {
Future<Null> _buildApp(MockTextSelectionControls controls, WidgetTester tester) {
Future<Null> _buildApp(
MockTextSelectionControls controls, WidgetTester tester) {
return tester.pumpWidget(new MaterialApp(
home: new EditableText(
controller: controller,
......@@ -987,11 +1194,13 @@ void main() {
setUp(() {
controller.text = 'test';
controller.selection = new TextSelection.collapsed(offset: controller.text.length);
controller.selection =
new TextSelection.collapsed(offset: controller.text.length);
controls = new MockTextSelectionControls();
when(controls.buildHandle(any, any, any)).thenReturn(new Container());
when(controls.buildToolbar(any, any, any, any)).thenReturn(new Container());
when(controls.buildToolbar(any, any, any, any))
.thenReturn(new Container());
});
testWidgets('are exposed', (WidgetTester tester) async {
......@@ -1005,7 +1214,9 @@ void main() {
await tester.tap(find.byType(EditableText));
await tester.pump();
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
......@@ -1015,7 +1226,9 @@ void main() {
when(controls.canCopy(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
......@@ -1027,7 +1240,9 @@ void main() {
when(controls.canCopy(any)).thenReturn(false);
when(controls.canPaste(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
......@@ -1039,7 +1254,9 @@ void main() {
when(controls.canPaste(any)).thenReturn(false);
when(controls.canCut(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
......@@ -1052,7 +1269,9 @@ void main() {
when(controls.canCut(any)).thenReturn(true);
when(controls.canPaste(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(semantics, includesNodeWith(
expect(
semantics,
includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
......@@ -1079,7 +1298,10 @@ void main() {
final SemanticsOwner owner = tester.binding.pipelineOwner.semanticsOwner;
const int expectedNodeId = 4;
expect(semantics, hasSemantics(new TestSemantics.root(
expect(
semantics,
hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
......@@ -1102,7 +1324,8 @@ void main() {
SemanticsAction.paste
],
value: 'test',
textSelection: new TextSelection.collapsed(offset: controller.text.length),
textSelection: new TextSelection.collapsed(
offset: controller.text.length),
textDirection: TextDirection.ltr,
),
],
......@@ -1110,7 +1333,9 @@ void main() {
],
),
],
), ignoreRect: true, ignoreTransform: true));
),
ignoreRect: true,
ignoreTransform: true));
owner.performAction(expectedNodeId, SemanticsAction.copy);
verify(controls.handleCopy(any)).called(1);
......@@ -1125,7 +1350,8 @@ void main() {
});
});
testWidgets('allows customizing text style in subclasses', (WidgetTester tester) async {
testWidgets('allows customizing text style in subclasses',
(WidgetTester tester) async {
controller.text = 'Hello World';
await tester.pumpWidget(new MaterialApp(
......@@ -1138,11 +1364,13 @@ void main() {
));
// Simulate selection change via tap to show handles.
final RenderEditable render = tester.allRenderObjects.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
final RenderEditable render = tester.allRenderObjects
.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
expect(render.text.style.fontStyle, FontStyle.italic);
});
testWidgets('autofocus sets cursor to the end of text', (WidgetTester tester) async {
testWidgets('autofocus sets cursor to the end of text',
(WidgetTester tester) async {
const String text = 'hello world';
final FocusScopeNode focusScopeNode = new FocusScopeNode();
final FocusNode focusNode = new FocusNode();
......@@ -1177,8 +1405,8 @@ class CustomStyleEditableText extends EditableText {
Color cursorColor,
FocusNode focusNode,
TextStyle style,
}): super(
controller:controller,
}) : super(
controller: controller,
cursorColor: cursorColor,
focusNode: focusNode,
style: style,
......
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