Unverified Commit a21a1f41 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

iOS selection handles are invisible (#31332)

Fix a bug where text selection handles were invisible in iOS
parent 6b191841
...@@ -654,4 +654,4 @@ class TextPainter { ...@@ -654,4 +654,4 @@ class TextPainter {
final List<int> indices = _paragraph.getWordBoundary(position.offset); final List<int> indices = _paragraph.getWordBoundary(position.offset);
return TextRange(start: indices[0], end: indices[1]); return TextRange(start: indices[0], end: indices[1]);
} }
} }
\ No newline at end of file
...@@ -302,15 +302,25 @@ class RenderEditable extends RenderBox { ...@@ -302,15 +302,25 @@ class RenderEditable extends RenderBox {
TextPosition(offset: _selection.start, affinity: _selection.affinity), TextPosition(offset: _selection.start, affinity: _selection.affinity),
Rect.zero, Rect.zero,
); );
// TODO(justinmc): https://github.com/flutter/flutter/issues/31495
_selectionStartInViewport.value = visibleRegion.contains(startOffset + effectiveOffset); // Check if the selection is visible with an approximation because a
// difference between rounded and unrounded values causes the caret to be
// reported as having a slightly (< 0.5) negative y offset. This rounding
// happens in paragraph.cc's layout and TextPainer's
// _applyFloatingPointHack. Ideally, the rounding mismatch will be fixed and
// this can be changed to be a strict check instead of an approximation.
const double visibleRegionSlop = 0.5;
_selectionStartInViewport.value = visibleRegion
.inflate(visibleRegionSlop)
.contains(startOffset + effectiveOffset);
final Offset endOffset = _textPainter.getOffsetForCaret( final Offset endOffset = _textPainter.getOffsetForCaret(
TextPosition(offset: _selection.end, affinity: _selection.affinity), TextPosition(offset: _selection.end, affinity: _selection.affinity),
Rect.zero, Rect.zero,
); );
_selectionEndInViewport.value = visibleRegion
_selectionEndInViewport.value = visibleRegion.contains(endOffset + effectiveOffset); .inflate(visibleRegionSlop)
.contains(endOffset + effectiveOffset);
} }
static const int _kLeftArrowCode = 21; static const int _kLeftArrowCode = 21;
......
...@@ -612,7 +612,6 @@ class _TextSelectionHandleOverlayState ...@@ -612,7 +612,6 @@ class _TextSelectionHandleOverlayState
point.dy.clamp(0.0, viewport.height), point.dy.clamp(0.0, viewport.height),
); );
return CompositedTransformFollower( return CompositedTransformFollower(
link: widget.layerLink, link: widget.layerLink,
showWhenUnlinked: false, showWhenUnlinked: false,
......
...@@ -2091,4 +2091,39 @@ void main() { ...@@ -2091,4 +2091,39 @@ void main() {
final EditableText editableText = tester.firstWidget(find.byType(EditableText)); final EditableText editableText = tester.firstWidget(find.byType(EditableText));
expect(editableText.cursorColor, const Color(0xFFF44336)); expect(editableText.cursorColor, const Color(0xFFF44336));
}); });
testWidgets('iOS shows selection handles', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const String testText = 'lorem ipsum';
final TextEditingController controller = TextEditingController(text: testText);
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(),
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
final RenderEditable renderEditable =
tester.state<EditableTextState>(find.byType(EditableText)).renderEditable;
await tester.tapAt(textOffsetToPosition(tester, 5));
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
await tester.pumpAndSettle();
final List<Widget> transitions =
find.byType(FadeTransition).evaluate().map((Element e) => e.widget).toList();
expect(transitions.length, 2);
final FadeTransition left = transitions[0];
final FadeTransition right = transitions[1];
expect(left.opacity.value, equals(1.0));
expect(right.opacity.value, equals(1.0));
debugDefaultTargetPlatformOverride = null;
});
} }
...@@ -5676,4 +5676,71 @@ void main() { ...@@ -5676,4 +5676,71 @@ void main() {
); );
expect(topLeft.dx, equals(383)); // Should be same as equivalent in 'Caret center position' expect(topLeft.dx, equals(383)); // Should be same as equivalent in 'Caret center position'
}); });
testWidgets('selection handles are rendered and not faded away', (WidgetTester tester) async {
const String testText = 'lorem ipsum';
final TextEditingController controller = TextEditingController(text: testText);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
),
),
),
);
final RenderEditable renderEditable =
tester.state<EditableTextState>(find.byType(EditableText)).renderEditable;
await tester.tapAt(const Offset(20, 10));
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
await tester.pumpAndSettle();
final List<Widget> transitions =
find.byType(FadeTransition).evaluate().map((Element e) => e.widget).toList();
// On Android, an empty app contains a single FadeTransition. The following
// two are the left and right text selection handles, respectively.
expect(transitions.length, 3);
final FadeTransition left = transitions[1];
final FadeTransition right = transitions[2];
expect(left.opacity.value, equals(1.0));
expect(right.opacity.value, equals(1.0));
});
testWidgets('iOS selection handles are rendered and not faded away', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const String testText = 'lorem ipsum';
final TextEditingController controller = TextEditingController(text: testText);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
),
),
),
);
final RenderEditable renderEditable =
tester.state<EditableTextState>(find.byType(EditableText)).renderEditable;
await tester.tapAt(const Offset(20, 10));
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
await tester.pumpAndSettle();
final List<Widget> transitions =
find.byType(FadeTransition).evaluate().map((Element e) => e.widget).toList();
expect(transitions.length, 2);
final FadeTransition left = transitions[0];
final FadeTransition right = transitions[1];
expect(left.opacity.value, equals(1.0));
expect(right.opacity.value, equals(1.0));
debugDefaultTargetPlatformOverride = null;
});
} }
...@@ -7,6 +7,7 @@ import 'dart:async'; ...@@ -7,6 +7,7 @@ import 'dart:async';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
...@@ -21,6 +22,10 @@ final FocusScopeNode focusScopeNode = FocusScopeNode(); ...@@ -21,6 +22,10 @@ final FocusScopeNode focusScopeNode = FocusScopeNode();
const TextStyle textStyle = TextStyle(); const TextStyle textStyle = TextStyle();
const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00); const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
enum HandlePositionInViewport {
leftEdge, rightEdge, within,
}
void main() { void main() {
setUp(() { setUp(() {
debugResetSemanticsIdCounter(); debugResetSemanticsIdCounter();
...@@ -469,14 +474,10 @@ void main() { ...@@ -469,14 +474,10 @@ void main() {
}); });
testWidgets('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async { testWidgets('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
String changedValue; String changedValue;
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: FocusNode(), focusNode: FocusNode(),
style: Typography(platform: TargetPlatform.android).black.subhead, style: Typography(platform: TargetPlatform.android).black.subhead,
...@@ -500,7 +501,7 @@ void main() { ...@@ -500,7 +501,7 @@ void main() {
}); });
// Long-press to bring up the text editing controls. // Long-press to bring up the text editing controls.
final Finder textFinder = find.byKey(editableTextKey); final Finder textFinder = find.byType(EditableText);
await tester.longPress(textFinder); await tester.longPress(textFinder);
tester.state<EditableTextState>(textFinder).showToolbar(); tester.state<EditableTextState>(textFinder).showToolbar();
await tester.pump(); await tester.pump();
...@@ -512,14 +513,11 @@ void main() { ...@@ -512,14 +513,11 @@ void main() {
}); });
testWidgets('Does not lose focus by default when "next" action is pressed', (WidgetTester tester) async { testWidgets('Does not lose focus by default when "next" action is pressed', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: focusNode, focusNode: focusNode,
style: Typography(platform: TargetPlatform.android).black.subhead, style: Typography(platform: TargetPlatform.android).black.subhead,
...@@ -531,7 +529,7 @@ void main() { ...@@ -531,7 +529,7 @@ void main() {
await tester.pumpWidget(widget); await tester.pumpWidget(widget);
// Select EditableText to give it focus. // Select EditableText to give it focus.
final Finder textFinder = find.byKey(editableTextKey); final Finder textFinder = find.byType(EditableText);
await tester.tap(textFinder); await tester.tap(textFinder);
await tester.pump(); await tester.pump();
...@@ -545,14 +543,11 @@ void main() { ...@@ -545,14 +543,11 @@ void main() {
}); });
testWidgets('Does not lose focus by default when "done" action is pressed and onEditingComplete is provided', (WidgetTester tester) async { testWidgets('Does not lose focus by default when "done" action is pressed and onEditingComplete is provided', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: focusNode, focusNode: focusNode,
style: Typography(platform: TargetPlatform.android).black.subhead, style: Typography(platform: TargetPlatform.android).black.subhead,
...@@ -567,7 +562,7 @@ void main() { ...@@ -567,7 +562,7 @@ void main() {
await tester.pumpWidget(widget); await tester.pumpWidget(widget);
// Select EditableText to give it focus. // Select EditableText to give it focus.
final Finder textFinder = find.byKey(editableTextKey); final Finder textFinder = find.byType(EditableText);
await tester.tap(textFinder); await tester.tap(textFinder);
await tester.pump(); await tester.pump();
...@@ -582,8 +577,6 @@ void main() { ...@@ -582,8 +577,6 @@ void main() {
}); });
testWidgets('When "done" is pressed callbacks are invoked: onEditingComplete > onSubmitted', (WidgetTester tester) async { testWidgets('When "done" is pressed callbacks are invoked: onEditingComplete > onSubmitted', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
bool onEditingCompleteCalled = false; bool onEditingCompleteCalled = false;
...@@ -592,7 +585,6 @@ void main() { ...@@ -592,7 +585,6 @@ void main() {
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: focusNode, focusNode: focusNode,
style: Typography(platform: TargetPlatform.android).black.subhead, style: Typography(platform: TargetPlatform.android).black.subhead,
...@@ -610,7 +602,7 @@ void main() { ...@@ -610,7 +602,7 @@ void main() {
await tester.pumpWidget(widget); await tester.pumpWidget(widget);
// Select EditableText to give it focus. // Select EditableText to give it focus.
final Finder textFinder = find.byKey(editableTextKey); final Finder textFinder = find.byType(EditableText);
await tester.tap(textFinder); await tester.tap(textFinder);
await tester.pump(); await tester.pump();
...@@ -625,8 +617,6 @@ void main() { ...@@ -625,8 +617,6 @@ void main() {
}); });
testWidgets('When "next" is pressed callbacks are invoked: onEditingComplete > onSubmitted', (WidgetTester tester) async { testWidgets('When "next" is pressed callbacks are invoked: onEditingComplete > onSubmitted', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
bool onEditingCompleteCalled = false; bool onEditingCompleteCalled = false;
...@@ -635,7 +625,6 @@ void main() { ...@@ -635,7 +625,6 @@ void main() {
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: focusNode, focusNode: focusNode,
style: Typography(platform: TargetPlatform.android).black.subhead, style: Typography(platform: TargetPlatform.android).black.subhead,
...@@ -653,7 +642,7 @@ void main() { ...@@ -653,7 +642,7 @@ void main() {
await tester.pumpWidget(widget); await tester.pumpWidget(widget);
// Select EditableText to give it focus. // Select EditableText to give it focus.
final Finder textFinder = find.byKey(editableTextKey); final Finder textFinder = find.byType(EditableText);
await tester.tap(textFinder); await tester.tap(textFinder);
await tester.pump(); await tester.pump();
...@@ -668,8 +657,6 @@ void main() { ...@@ -668,8 +657,6 @@ void main() {
}); });
testWidgets('When "newline" action is called on a Editable text with maxLines == 1 callbacks are invoked: onEditingComplete > onSubmitted', (WidgetTester tester) async { testWidgets('When "newline" action is called on a Editable text with maxLines == 1 callbacks are invoked: onEditingComplete > onSubmitted', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
bool onEditingCompleteCalled = false; bool onEditingCompleteCalled = false;
...@@ -678,7 +665,6 @@ void main() { ...@@ -678,7 +665,6 @@ void main() {
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: focusNode, focusNode: focusNode,
style: Typography(platform: TargetPlatform.android).black.subhead, style: Typography(platform: TargetPlatform.android).black.subhead,
...@@ -697,7 +683,7 @@ void main() { ...@@ -697,7 +683,7 @@ void main() {
await tester.pumpWidget(widget); await tester.pumpWidget(widget);
// Select EditableText to give it focus. // Select EditableText to give it focus.
final Finder textFinder = find.byKey(editableTextKey); final Finder textFinder = find.byType(EditableText);
await tester.tap(textFinder); await tester.tap(textFinder);
await tester.pump(); await tester.pump();
...@@ -711,8 +697,6 @@ void main() { ...@@ -711,8 +697,6 @@ void main() {
}); });
testWidgets('When "newline" action is called on a Editable text with maxLines != 1, onEditingComplete and onSubmitted callbacks are not invoked.', (WidgetTester tester) async { testWidgets('When "newline" action is called on a Editable text with maxLines != 1, onEditingComplete and onSubmitted callbacks are not invoked.', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
bool onEditingCompleteCalled = false; bool onEditingCompleteCalled = false;
...@@ -721,7 +705,6 @@ void main() { ...@@ -721,7 +705,6 @@ void main() {
final Widget widget = MaterialApp( final Widget widget = MaterialApp(
home: EditableText( home: EditableText(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
key: editableTextKey,
controller: TextEditingController(), controller: TextEditingController(),
focusNode: focusNode, focusNode: focusNode,
style: Typography(platform: TargetPlatform.android).black.subhead, style: Typography(platform: TargetPlatform.android).black.subhead,
...@@ -738,7 +721,7 @@ void main() { ...@@ -738,7 +721,7 @@ void main() {
await tester.pumpWidget(widget); await tester.pumpWidget(widget);
// Select EditableText to give it focus. // Select EditableText to give it focus.
final Finder textFinder = find.byKey(editableTextKey); final Finder textFinder = find.byType(EditableText);
await tester.tap(textFinder); await tester.tap(textFinder);
await tester.pump(); await tester.pump();
...@@ -754,8 +737,6 @@ void main() { ...@@ -754,8 +737,6 @@ void main() {
}); });
testWidgets('Changing controller updates EditableText', (WidgetTester tester) async { testWidgets('Changing controller updates EditableText', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
final TextEditingController controller1 = final TextEditingController controller1 =
TextEditingController(text: 'Wibble'); TextEditingController(text: 'Wibble');
final TextEditingController controller2 = final TextEditingController controller2 =
...@@ -775,7 +756,6 @@ void main() { ...@@ -775,7 +756,6 @@ void main() {
child: Material( child: Material(
child: EditableText( child: EditableText(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
key: editableTextKey,
controller: currentController, controller: currentController,
focusNode: FocusNode(), focusNode: FocusNode(),
style: Typography(platform: TargetPlatform.android) style: Typography(platform: TargetPlatform.android)
...@@ -1904,15 +1884,12 @@ void main() { ...@@ -1904,15 +1884,12 @@ void main() {
composing: TextRange(start: 5, end: 14), composing: TextRange(start: 5, end: 14),
), ),
); );
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
await tester.pumpWidget(MaterialApp( // So we can show overlays. await tester.pumpWidget(MaterialApp( // So we can show overlays.
home: EditableText( home: EditableText(
autofocus: true, autofocus: true,
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
key: editableTextKey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
...@@ -1943,19 +1920,16 @@ void main() { ...@@ -1943,19 +1920,16 @@ void main() {
}); });
testWidgets('text selection handle visibility', (WidgetTester tester) async { testWidgets('text selection handle visibility', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = // Text with two separate words to select.
GlobalKey<EditableTextState>();
const String testText = 'XXXXX XXXXX'; const String testText = 'XXXXX XXXXX';
final TextEditingController controller = TextEditingController(text: testText); final TextEditingController controller = TextEditingController(text: testText);
final Widget widget = MaterialApp( await tester.pumpWidget(MaterialApp(
home: Align( home: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: SizedBox( child: SizedBox(
width: 100, width: 100,
child: EditableText( child: EditableText(
key: editableTextKey,
controller: controller, controller: controller,
focusNode: FocusNode(), focusNode: FocusNode(),
style: Typography(platform: TargetPlatform.android).black.subhead, style: Typography(platform: TargetPlatform.android).black.subhead,
...@@ -1966,77 +1940,83 @@ void main() { ...@@ -1966,77 +1940,83 @@ void main() {
), ),
), ),
), ),
); ));
await tester.pumpWidget(widget);
final EditableTextState state = final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText)); tester.state<EditableTextState>(find.byType(EditableText));
final RenderEditable renderEditable = state.renderEditable; final RenderEditable renderEditable = state.renderEditable;
final Scrollable scrollable = tester.widget<Scrollable>(find.byType(Scrollable)); final Scrollable scrollable = tester.widget<Scrollable>(find.byType(Scrollable));
bool leftVisibleBefore = false; bool expectedLeftVisibleBefore = false;
bool rightVisibleBefore = false; bool expectedRightVisibleBefore = false;
Future<void> verifyVisibility( Future<void> verifyVisibility(
bool leftVisible, HandlePositionInViewport leftPosition,
Symbol leftPosition, bool expectedLeftVisible,
bool rightVisible, HandlePositionInViewport rightPosition,
Symbol rightPosition, bool expectedRightVisible,
) async { ) async {
await tester.pump(); await tester.pump();
// Check the signal from RenderEditable about whether they're within the // Check the signal from RenderEditable about whether they're within the
// viewport. // viewport.
expect(renderEditable.selectionStartInViewport.value, equals(leftVisible)); expect(renderEditable.selectionStartInViewport.value, equals(expectedLeftVisible));
expect(renderEditable.selectionEndInViewport.value, equals(rightVisible)); expect(renderEditable.selectionEndInViewport.value, equals(expectedRightVisible));
// Check that the animations are functional and going in the right // Check that the animations are functional and going in the right
// direction. // direction.
final List<Widget> transitions = final List<Widget> transitions =
find.byType(FadeTransition).evaluate().map((Element e) => e.widget).toList(); find.byType(FadeTransition).evaluate().map((Element e) => e.widget).toList();
// On Android, an empty app contains a single FadeTransition. The following
// two are the left and right text selection handles, respectively.
final FadeTransition left = transitions[1]; final FadeTransition left = transitions[1];
final FadeTransition right = transitions[2]; final FadeTransition right = transitions[2];
if (leftVisibleBefore) if (expectedLeftVisibleBefore)
expect(left.opacity.value, equals(1.0)); expect(left.opacity.value, equals(1.0));
if (rightVisibleBefore) if (expectedRightVisibleBefore)
expect(right.opacity.value, equals(1.0)); expect(right.opacity.value, equals(1.0));
await tester.pump(TextSelectionOverlay.fadeDuration ~/ 2); await tester.pump(TextSelectionOverlay.fadeDuration ~/ 2);
if (leftVisible != leftVisibleBefore) if (expectedLeftVisible != expectedLeftVisibleBefore)
expect(left.opacity.value, equals(0.5)); expect(left.opacity.value, equals(0.5));
if (rightVisible != rightVisibleBefore) if (expectedRightVisible != expectedRightVisibleBefore)
expect(right.opacity.value, equals(0.5)); expect(right.opacity.value, equals(0.5));
await tester.pump(TextSelectionOverlay.fadeDuration ~/ 2); await tester.pump(TextSelectionOverlay.fadeDuration ~/ 2);
if (leftVisible) if (expectedLeftVisible)
expect(left.opacity.value, equals(1.0)); expect(left.opacity.value, equals(1.0));
if (rightVisible) if (expectedRightVisible)
expect(right.opacity.value, equals(1.0)); expect(right.opacity.value, equals(1.0));
leftVisibleBefore = leftVisible; expectedLeftVisibleBefore = expectedLeftVisible;
rightVisibleBefore = rightVisible; expectedRightVisibleBefore = expectedRightVisible;
// Check that the handles' positions are correct (clamped within the // Check that the handles' positions are correct.
// viewport but not stuck).
final List<Positioned> positioned = final List<Positioned> positioned =
find.byType(Positioned).evaluate().map((Element e) => e.widget).cast<Positioned>().toList(); find.byType(Positioned).evaluate().map((Element e) => e.widget).cast<Positioned>().toList();
final Size viewport = renderEditable.size; final Size viewport = renderEditable.size;
void testPosition(double pos, Symbol expected) { void testPosition(double pos, HandlePositionInViewport expected) {
if (expected == #left) switch (expected) {
expect(pos, equals(0.0)); case HandlePositionInViewport.leftEdge:
if (expected == #right) expect(pos, equals(0.0));
expect(pos, equals(viewport.width)); break;
if (expected == #middle) case HandlePositionInViewport.rightEdge:
expect(pos, inExclusiveRange(0.0, viewport.width)); expect(pos, equals(viewport.width));
break;
case HandlePositionInViewport.within:
expect(pos, inExclusiveRange(0.0, viewport.width));
break;
default:
throw TestFailure('HandlePositionInViewport can\'t be null.');
}
} }
testPosition(positioned[0].left, leftPosition); testPosition(positioned[0].left, leftPosition);
...@@ -2047,38 +2027,224 @@ void main() { ...@@ -2047,38 +2027,224 @@ void main() {
await tester.tapAt(const Offset(20, 10)); await tester.tapAt(const Offset(20, 10));
renderEditable.selectWord(cause: SelectionChangedCause.longPress); renderEditable.selectWord(cause: SelectionChangedCause.longPress);
await tester.pump(); await tester.pump();
await verifyVisibility(true, #left, true, #middle); await verifyVisibility(HandlePositionInViewport.leftEdge, true, HandlePositionInViewport.within, true);
// Drag the text slightly so the first word is partially visible. Only the
// right handle should be visible.
scrollable.controller.jumpTo(20.0);
await verifyVisibility(HandlePositionInViewport.leftEdge, false, HandlePositionInViewport.within, true);
// Drag the text all the way to the left so the first word is not visible at
// all (and the second word is fully visible). Both handles should be
// invisible now.
scrollable.controller.jumpTo(200.0);
await verifyVisibility(HandlePositionInViewport.leftEdge, false, HandlePositionInViewport.leftEdge, false);
// Tap to unselect.
await tester.tap(find.byType(EditableText));
await tester.pump();
// Now that the second word has been dragged fully into view, select it.
await tester.tapAt(const Offset(80, 10));
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
await tester.pump();
await verifyVisibility(HandlePositionInViewport.within, true, HandlePositionInViewport.within, true);
// Drag the text slightly to the right. Only the left handle should be
// visible.
scrollable.controller.jumpTo(150);
await verifyVisibility(HandlePositionInViewport.within, true, HandlePositionInViewport.rightEdge, false);
// Drag the text all the way to the right, so the second word is not visible
// at all. Again, both handles should be invisible.
scrollable.controller.jumpTo(0);
await verifyVisibility(HandlePositionInViewport.rightEdge, false, HandlePositionInViewport.rightEdge, false);
});
testWidgets('text selection handle visibility RTL', (WidgetTester tester) async {
// Text with two separate words to select.
const String testText = 'XXXXX XXXXX';
final TextEditingController controller = TextEditingController(text: testText);
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 100,
child: EditableText(
controller: controller,
focusNode: FocusNode(),
style: Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
textAlign: TextAlign.right,
),
),
),
));
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
// Select the first word. Both handles should be visible.
await tester.tapAt(const Offset(20, 10));
state.renderEditable.selectWord(cause: SelectionChangedCause.longPress);
await tester.pump();
final List<Positioned> positioned =
find.byType(Positioned).evaluate().map((Element e) => e.widget).cast<Positioned>().toList();
expect(positioned[0].left, 0.0);
expect(positioned[1].left, 70.0);
expect(controller.selection.base.offset, 0);
expect(controller.selection.extent.offset, 5);
});
// Regression test for https://github.com/flutter/flutter/issues/31287
testWidgets('iOS text selection handle visibility', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
// Text with two separate words to select.
const String testText = 'XXXXX XXXXX';
final TextEditingController controller = TextEditingController(text: testText);
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: Container(
child: SizedBox(
width: 100,
child: EditableText(
controller: controller,
focusNode: FocusNode(),
style: Typography(platform: TargetPlatform.iOS).black.subhead,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
selectionControls: cupertinoTextSelectionControls,
keyboardType: TextInputType.text,
),
),
),
),
));
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
final RenderEditable renderEditable = state.renderEditable;
final Scrollable scrollable = tester.widget<Scrollable>(find.byType(Scrollable));
bool expectedLeftVisibleBefore = false;
bool expectedRightVisibleBefore = false;
Future<void> verifyVisibility(
HandlePositionInViewport leftPosition,
bool expectedLeftVisible,
HandlePositionInViewport rightPosition,
bool expectedRightVisible,
) async {
await tester.pump();
// Check the signal from RenderEditable about whether they're within the
// viewport.
expect(renderEditable.selectionStartInViewport.value, equals(expectedLeftVisible));
expect(renderEditable.selectionEndInViewport.value, equals(expectedRightVisible));
// Check that the animations are functional and going in the right
// direction.
final List<Widget> transitions =
find.byType(FadeTransition).evaluate().map((Element e) => e.widget).toList();
final FadeTransition left = transitions[0];
final FadeTransition right = transitions[1];
if (expectedLeftVisibleBefore)
expect(left.opacity.value, equals(1.0));
if (expectedRightVisibleBefore)
expect(right.opacity.value, equals(1.0));
await tester.pump(TextSelectionOverlay.fadeDuration ~/ 2);
if (expectedLeftVisible != expectedLeftVisibleBefore)
expect(left.opacity.value, equals(0.5));
if (expectedRightVisible != expectedRightVisibleBefore)
expect(right.opacity.value, equals(0.5));
await tester.pump(TextSelectionOverlay.fadeDuration ~/ 2);
if (expectedLeftVisible)
expect(left.opacity.value, equals(1.0));
if (expectedRightVisible)
expect(right.opacity.value, equals(1.0));
expectedLeftVisibleBefore = expectedLeftVisible;
expectedRightVisibleBefore = expectedRightVisible;
// Check that the handles' positions are correct.
final List<Positioned> positioned =
find.byType(Positioned).evaluate().map((Element e) => e.widget).cast<Positioned>().toList();
final Size viewport = renderEditable.size;
void testPosition(double pos, HandlePositionInViewport expected) {
switch (expected) {
case HandlePositionInViewport.leftEdge:
expect(pos, equals(0.0));
break;
case HandlePositionInViewport.rightEdge:
expect(pos, equals(viewport.width));
break;
case HandlePositionInViewport.within:
expect(pos, inExclusiveRange(0.0, viewport.width));
break;
default:
throw TestFailure('HandlePositionInViewport can\'t be null.');
}
}
testPosition(positioned[1].left, leftPosition);
testPosition(positioned[2].left, rightPosition);
}
// Select the first word. Both handles should be visible.
await tester.tapAt(const Offset(20, 10));
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
await tester.pump();
await verifyVisibility(HandlePositionInViewport.leftEdge, true, HandlePositionInViewport.within, true);
// Drag the text slightly so the first word is partially visible. Only the // Drag the text slightly so the first word is partially visible. Only the
// right handle should be visible. // right handle should be visible.
scrollable.controller.jumpTo(20.0); scrollable.controller.jumpTo(20.0);
await verifyVisibility(false, #left, true, #middle); await verifyVisibility(HandlePositionInViewport.leftEdge, false, HandlePositionInViewport.within, true);
// Drag the text all the way to the left so the first word is not visible at // Drag the text all the way to the left so the first word is not visible at
// all (and the second word is fully visible). Both handles should be // all (and the second word is fully visible). Both handles should be
// invisible now. // invisible now.
scrollable.controller.jumpTo(200.0); scrollable.controller.jumpTo(200.0);
await verifyVisibility(false, #left, false, #left); await verifyVisibility(HandlePositionInViewport.leftEdge, false, HandlePositionInViewport.leftEdge, false);
// Tap to unselect. // Tap to unselect.
await tester.tap(find.byKey(editableTextKey)); await tester.tap(find.byType(EditableText));
await tester.pump(); await tester.pump();
// Now that the second word has been dragged fully into view, select it. // Now that the second word has been dragged fully into view, select it.
await tester.tapAt(const Offset(80, 10)); await tester.tapAt(const Offset(80, 10));
renderEditable.selectWord(cause: SelectionChangedCause.longPress); renderEditable.selectWord(cause: SelectionChangedCause.longPress);
await tester.pump(); await tester.pump();
await verifyVisibility(true, #middle, true, #middle); await verifyVisibility(HandlePositionInViewport.within, true, HandlePositionInViewport.within, true);
// Drag the text slightly to the right. Only the left handle should be // Drag the text slightly to the right. Only the left handle should be
// visible. // visible.
scrollable.controller.jumpTo(150); scrollable.controller.jumpTo(150);
await verifyVisibility(true, #middle, false, #right); await verifyVisibility(HandlePositionInViewport.within, true, HandlePositionInViewport.rightEdge, false);
// Drag the text all the way to the right, so the second word is not visible // Drag the text all the way to the right, so the second word is not visible
// at all. Again, both handles should be invisible. // at all. Again, both handles should be invisible.
scrollable.controller.jumpTo(0); scrollable.controller.jumpTo(0);
await verifyVisibility(false, #right, false, #right); await verifyVisibility(HandlePositionInViewport.rightEdge, false, HandlePositionInViewport.rightEdge, false);
debugDefaultTargetPlatformOverride = null;
}); });
} }
......
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