diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 1c23e12a32..d1cf8af57d 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:math' as math; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; @@ -1120,10 +1121,11 @@ class EditableTextState extends State with AutomaticKeepAliveClien double scrollOffset = _scrollController.offset; final double viewportExtent = _scrollController.position.viewportDimension; - if (caretStart < 0.0) // cursor before start of bounds + if (caretStart < 0.0) { // cursor before start of bounds scrollOffset += caretStart; - else if (caretEnd >= viewportExtent) // cursor after end of bounds + } else if (caretEnd >= viewportExtent) { // cursor after end of bounds scrollOffset += caretEnd - viewportExtent; + } return scrollOffset; } @@ -1271,12 +1273,32 @@ class EditableTextState extends State with AutomaticKeepAliveClien curve: _caretAnimationCurve, ); final Rect newCaretRect = _getCaretRectAtScrollOffset(_currentCaretRect, scrollOffsetForCaret); - // Enlarge newCaretRect by scrollPadding to ensure that caret is not positioned directly at the edge after scrolling. + // Enlarge newCaretRect by scrollPadding to ensure that caret is not + // positioned directly at the edge after scrolling. + double bottomSpacing = widget.scrollPadding.bottom; + if (_selectionOverlay?.selectionControls != null) { + final double handleHeight = _selectionOverlay.selectionControls + .getHandleSize(renderEditable.preferredLineHeight).height; + final double interactiveHandleHeight = math.max( + handleHeight, + kMinInteractiveSize, + ); + final Offset anchor = _selectionOverlay.selectionControls + .getHandleAnchor( + TextSelectionHandleType.collapsed, + renderEditable.preferredLineHeight, + ); + final double handleCenter = handleHeight / 2 - anchor.dy; + bottomSpacing = math.max( + handleCenter + interactiveHandleHeight / 2, + bottomSpacing, + ); + } final Rect inflatedRect = Rect.fromLTRB( newCaretRect.left - widget.scrollPadding.left, newCaretRect.top - widget.scrollPadding.top, newCaretRect.right + widget.scrollPadding.right, - newCaretRect.bottom + widget.scrollPadding.bottom, + newCaretRect.bottom + bottomSpacing, ); _editableKey.currentContext.findRenderObject().showOnScreen( rect: inflatedRect, diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index 81bf06e831..2989b48ade 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -2626,6 +2626,34 @@ void main() { debugDefaultTargetPlatformOverride = null; }); + testWidgets('when CupertinoTextField would be blocked by keyboard, it is shown with enough space for the selection handle', (WidgetTester tester) async { + final ScrollController scrollController = ScrollController(); + final TextEditingController controller = TextEditingController(); + + await tester.pumpWidget(CupertinoApp( + theme: const CupertinoThemeData(), + home: Center( + child: ListView( + controller: scrollController, + children: [ + Container(height: 585), // Push field almost off screen. + CupertinoTextField(controller: controller), + Container(height: 1000), + ], + ), + ), + )); + + // Tap the TextField to put the cursor into it and bring it into view. + expect(scrollController.offset, 0.0); + await tester.tap(find.byType(CupertinoTextField)); + await tester.pumpAndSettle(); + + // The ListView has scrolled to keep the TextField and cursor handle + // visible. + expect(scrollController.offset, 26.0); + }); + testWidgets('disabled state golden', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 8931b4edbb..22fede4503 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -6581,4 +6581,34 @@ void main() { await tester.tapAt(handlePos, pointer: 7); expect(editableText.selectionOverlay.toolbarIsVisible, isFalse); }); + + testWidgets('when TextField would be blocked by keyboard, it is shown with enough space for the selection handle', (WidgetTester tester) async { + final ScrollController scrollController = ScrollController(); + final TextEditingController controller = TextEditingController(); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(), + home: Scaffold( + body: Center( + child: ListView( + controller: scrollController, + children: [ + Container(height: 579), // Push field almost off screen. + TextField(controller: controller), + Container(height: 1000), + ], + ), + ), + ), + )); + + // Tap the TextField to put the cursor into it and bring it into view. + expect(scrollController.offset, 0.0); + await tester.tap(find.byType(TextField)); + await tester.pumpAndSettle(); + + // The ListView has scrolled to keep the TextField and cursor handle + // visible. + expect(scrollController.offset, 44.0); + }); }