diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index 37efddfc2423..c65b0430ae8e 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -509,6 +509,9 @@ class _CupertinoTextFieldState extends State with AutomaticK FocusNode _focusNode; FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); + // Keep track of whether the text has just been cleared. + bool _clearing = false; + // The selection overlay should only be shown when the user is interacting // through a touch screen (via either a finger or a stylus). A mouse shouldn't // trigger the selection overlay. @@ -590,6 +593,14 @@ class _CupertinoTextFieldState extends State with AutomaticK } void _handleSingleTapUp(TapUpDetails details) { + // Because TextSelectionGestureDetector listens to taps that happen on + // widgets in front of it, tapping the clear button will also trigger this + // handler here. If this is the case, return. + if (_clearing) { + _clearing = false; + return; + } + if (widget.selectionEnabled) { _renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); } @@ -796,6 +807,7 @@ class _CupertinoTextFieldState extends State with AutomaticK rowChildren.add( GestureDetector( onTap: widget.enabled ?? true ? () { + _clearing = true; // Special handle onChanged for ClearButton // Also call onChanged when the clear button is tapped. final bool textChanged = _effectiveController.text.isNotEmpty; @@ -844,64 +856,50 @@ class _CupertinoTextFieldState extends State with AutomaticK ? widget.decoration : widget.decoration?.copyWith(color: widget.decoration?.color ?? disabledColor); - final Widget paddedEditable = TextSelectionGestureDetector( - onTapDown: _handleTapDown, - onForcePressStart: _handleForcePressStarted, - onForcePressEnd: _handleForcePressEnded, - onSingleTapUp: _handleSingleTapUp, - onSingleLongTapStart: _handleSingleLongTapStart, - onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate, - onSingleLongTapEnd: _handleSingleLongTapEnd, - onDoubleTapDown: _handleDoubleTapDown, - onDragSelectionStart: _handleMouseDragSelectionStart, - onDragSelectionUpdate: _handleMouseDragSelectionUpdate, - onDragSelectionEnd: _handleMouseDragSelectionEnd, - behavior: HitTestBehavior.translucent, - child: Padding( - padding: widget.padding, - child: RepaintBoundary( - child: EditableText( - key: _editableTextKey, - controller: controller, - readOnly: widget.readOnly, - showCursor: widget.showCursor, - showSelectionHandles: _showSelectionHandles, - focusNode: _effectiveFocusNode, - keyboardType: widget.keyboardType, - textInputAction: widget.textInputAction, - textCapitalization: widget.textCapitalization, - style: textStyle, - strutStyle: widget.strutStyle, - textAlign: widget.textAlign, - autofocus: widget.autofocus, - obscureText: widget.obscureText, - autocorrect: widget.autocorrect, - maxLines: widget.maxLines, - minLines: widget.minLines, - expands: widget.expands, - selectionColor: _kSelectionHighlightColor, - selectionControls: widget.selectionEnabled - ? cupertinoTextSelectionControls : null, - onChanged: widget.onChanged, - onSelectionChanged: _handleSelectionChanged, - onEditingComplete: widget.onEditingComplete, - onSubmitted: widget.onSubmitted, - inputFormatters: formatters, - rendererIgnoresPointer: true, - cursorWidth: widget.cursorWidth, - cursorRadius: widget.cursorRadius, - cursorColor: cursorColor, - cursorOpacityAnimates: true, - cursorOffset: cursorOffset, - paintCursorAboveText: true, - backgroundCursorColor: CupertinoColors.inactiveGray, - scrollPadding: widget.scrollPadding, - keyboardAppearance: keyboardAppearance, - dragStartBehavior: widget.dragStartBehavior, - scrollController: widget.scrollController, - scrollPhysics: widget.scrollPhysics, - enableInteractiveSelection: widget.enableInteractiveSelection, - ), + final Widget paddedEditable = Padding( + padding: widget.padding, + child: RepaintBoundary( + child: EditableText( + key: _editableTextKey, + controller: controller, + readOnly: widget.readOnly, + showCursor: widget.showCursor, + showSelectionHandles: _showSelectionHandles, + focusNode: _effectiveFocusNode, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + textCapitalization: widget.textCapitalization, + style: textStyle, + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + autofocus: widget.autofocus, + obscureText: widget.obscureText, + autocorrect: widget.autocorrect, + maxLines: widget.maxLines, + minLines: widget.minLines, + expands: widget.expands, + selectionColor: _kSelectionHighlightColor, + selectionControls: widget.selectionEnabled + ? cupertinoTextSelectionControls : null, + onChanged: widget.onChanged, + onSelectionChanged: _handleSelectionChanged, + onEditingComplete: widget.onEditingComplete, + onSubmitted: widget.onSubmitted, + inputFormatters: formatters, + rendererIgnoresPointer: true, + cursorWidth: widget.cursorWidth, + cursorRadius: widget.cursorRadius, + cursorColor: cursorColor, + cursorOpacityAnimates: true, + cursorOffset: cursorOffset, + paintCursorAboveText: true, + backgroundCursorColor: CupertinoColors.inactiveGray, + scrollPadding: widget.scrollPadding, + keyboardAppearance: keyboardAppearance, + dragStartBehavior: widget.dragStartBehavior, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + enableInteractiveSelection: widget.enableInteractiveSelection, ), ), ); @@ -917,11 +915,25 @@ class _CupertinoTextFieldState extends State with AutomaticK ignoring: !enabled, child: Container( decoration: effectiveDecoration, - child: Align( - alignment: Alignment(-1.0, _textAlignVertical.y), - widthFactor: 1.0, - heightFactor: 1.0, - child: _addTextDependentAttachments(paddedEditable, textStyle, placeholderStyle), + child: TextSelectionGestureDetector( + onTapDown: _handleTapDown, + onForcePressStart: _handleForcePressStarted, + onForcePressEnd: _handleForcePressEnded, + onSingleTapUp: _handleSingleTapUp, + onSingleLongTapStart: _handleSingleLongTapStart, + onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate, + onSingleLongTapEnd: _handleSingleLongTapEnd, + onDoubleTapDown: _handleDoubleTapDown, + onDragSelectionStart: _handleMouseDragSelectionStart, + onDragSelectionUpdate: _handleMouseDragSelectionUpdate, + onDragSelectionEnd: _handleMouseDragSelectionEnd, + behavior: HitTestBehavior.translucent, + child: Align( + alignment: Alignment(-1.0, _textAlignVertical.y), + widthFactor: 1.0, + heightFactor: 1.0, + child: _addTextDependentAttachments(paddedEditable, textStyle, placeholderStyle), + ), ), ), ), diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index 904b3bfc0e95..25497aad3dc3 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -1252,11 +1252,6 @@ class RenderEditable extends RenderBox { double get preferredLineHeight => _textPainter.preferredLineHeight; double _preferredHeight(double width) { - // Use the full available height if expands is true. - if (expands) { - return constraints.maxHeight; - } - // Lock height to maxLines if needed. final bool lockedMax = maxLines != null && minLines == null; final bool lockedBoth = minLines != null && minLines == maxLines; diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index 30e512ae9c79..af8a1939312b 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -173,7 +173,6 @@ void main() { EditableText.debugDeterministicCursor = false; }); - testWidgets( 'takes available space horizontally and takes intrinsic space vertically no-strut', (WidgetTester tester) async { @@ -409,7 +408,7 @@ void main() { ); final Finder textFinder = find.byType(CupertinoTextField); - await tester.tapAt(tester.getTopLeft(textFinder)); + await tester.tap(textFinder); await tester.pump(); final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); @@ -2760,7 +2759,7 @@ void main() { ); final Finder textFinder = find.byType(CupertinoTextField); - await tester.tapAt(tester.getTopLeft(textFinder)); + await tester.tap(textFinder); await tester.pump(); final EditableTextState editableTextState = @@ -3087,6 +3086,9 @@ void main() { ), ), ); + + tester.binding.window.physicalSizeTestValue = null; + tester.binding.window.devicePixelRatioTestValue = null; }); testWidgets('selecting multiple words works', (WidgetTester tester) async { @@ -3155,6 +3157,9 @@ void main() { ), ), ); + + tester.binding.window.physicalSizeTestValue = null; + tester.binding.window.devicePixelRatioTestValue = null; }); testWidgets('selecting multiline works', (WidgetTester tester) async { @@ -3227,6 +3232,411 @@ void main() { ), ), ); + + tester.binding.window.physicalSizeTestValue = null; + tester.binding.window.devicePixelRatioTestValue = null; + }); + }); + + group('textAlignVertical position', () { + group('simple case', () { + testWidgets('align top (default)', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + const Size size = Size(200.0, 200.0); + await tester.pumpWidget( + CupertinoApp( + debugShowCheckedModeBanner: false, + home: CupertinoPageScaffold( + child: Align( + alignment: Alignment.center, + child: SizedBox( + width: size.width, + height: size.height, + child: CupertinoTextField( + focusNode: focusNode, + expands: true, + maxLines: null, + ), + ), + ), + ), + ), + ); + + // Fills the whole container since expands is true. + expect(tester.getSize(find.byType(CupertinoTextField)), size); + + // Tapping anywhere inside focuses it. + expect(focusNode.hasFocus, false); + await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField))); + await tester.pumpAndSettle(const Duration(milliseconds: 400)); // timer error if no duration + expect(focusNode.hasFocus, true); + focusNode.unfocus(); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, false); + final Offset justInside = tester + .getBottomLeft(find.byType(CupertinoTextField)) + .translate(0.0, -1.0); + await tester.tapAt(justInside); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, true); + + // The EditableText is at the top. + expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, closeTo(size.height, .0001)); + expect(tester.getTopLeft(find.byType(EditableText)).dy, closeTo(206.0, .0001)); + }); + + testWidgets('align center', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + const Size size = Size(200.0, 200.0); + await tester.pumpWidget( + CupertinoApp( + debugShowCheckedModeBanner: false, + home: CupertinoPageScaffold( + child: Align( + alignment: Alignment.center, + child: SizedBox( + width: size.width, + height: size.height, + child: CupertinoTextField( + textAlignVertical: TextAlignVertical.center, + focusNode: focusNode, + expands: true, + maxLines: null, + ), + ), + ), + ), + ), + ); + + // Fills the whole container since expands is true. + expect(tester.getSize(find.byType(CupertinoTextField)), size); + + // Tapping anywhere inside focuses it. + expect(focusNode.hasFocus, false); + await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField))); + await tester.pumpAndSettle(const Duration(milliseconds: 400)); // timer error if no duration + expect(focusNode.hasFocus, true); + focusNode.unfocus(); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, false); + final Offset justInside = tester + .getBottomLeft(find.byType(CupertinoTextField)) + .translate(0.0, -1.0); + await tester.tapAt(justInside); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, true); + + // The EditableText is at the center. + expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, closeTo(size.height, .0001)); + expect(tester.getTopLeft(find.byType(EditableText)).dy, closeTo(291.5, .0001)); + }); + + testWidgets('align bottom', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + const Size size = Size(200.0, 200.0); + await tester.pumpWidget( + CupertinoApp( + debugShowCheckedModeBanner: false, + home: CupertinoPageScaffold( + child: Align( + alignment: Alignment.center, + child: SizedBox( + width: size.width, + height: size.height, + child: CupertinoTextField( + textAlignVertical: TextAlignVertical.bottom, + focusNode: focusNode, + expands: true, + maxLines: null, + ), + ), + ), + ), + ), + ); + + // Fills the whole container since expands is true. + expect(tester.getSize(find.byType(CupertinoTextField)), size); + + // Tapping anywhere inside focuses it. + expect(focusNode.hasFocus, false); + await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField))); + await tester.pumpAndSettle(const Duration(milliseconds: 400)); // timer error if no duration + expect(focusNode.hasFocus, true); + focusNode.unfocus(); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, false); + final Offset justInside = tester + .getBottomLeft(find.byType(CupertinoTextField)) + .translate(0.0, -1.0); + await tester.tapAt(justInside); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, true); + + // The EditableText is at the bottom. + expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, closeTo(size.height, .0001)); + expect(tester.getTopLeft(find.byType(EditableText)).dy, closeTo(377.0, .0001)); + }); + + testWidgets('align as a double', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + const Size size = Size(200.0, 200.0); + await tester.pumpWidget( + CupertinoApp( + debugShowCheckedModeBanner: false, + home: CupertinoPageScaffold( + child: Align( + alignment: Alignment.center, + child: SizedBox( + width: size.width, + height: size.height, + child: CupertinoTextField( + textAlignVertical: const TextAlignVertical(y: 0.75), + focusNode: focusNode, + expands: true, + maxLines: null, + ), + ), + ), + ), + ), + ); + + // Fills the whole container since expands is true. + expect(tester.getSize(find.byType(CupertinoTextField)), size); + + // Tapping anywhere inside focuses it. + expect(focusNode.hasFocus, false); + await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField))); + await tester.pumpAndSettle(const Duration(milliseconds: 400)); // timer error if no duration + expect(focusNode.hasFocus, true); + focusNode.unfocus(); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, false); + final Offset justInside = tester + .getBottomLeft(find.byType(CupertinoTextField)) + .translate(0.0, -1.0); + await tester.tapAt(justInside); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, true); + + // The EditableText is near the bottom. + expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, closeTo(size.height, .0001)); + expect(tester.getTopLeft(find.byType(EditableText)).dy, closeTo(355.625, .0001)); + }); + }); + + group('tall prefix', () { + testWidgets('align center (default when prefix)', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + const Size size = Size(200.0, 200.0); + await tester.pumpWidget( + CupertinoApp( + debugShowCheckedModeBanner: false, + home: CupertinoPageScaffold( + child: Align( + alignment: Alignment.center, + child: SizedBox( + width: size.width, + height: size.height, + child: CupertinoTextField( + focusNode: focusNode, + expands: true, + maxLines: null, + prefix: Container( + //key: pKey, + height: 100, + width: 10, + ), + ), + ), + ), + ), + ), + ); + + // Fills the whole container since expands is true. + expect(tester.getSize(find.byType(CupertinoTextField)), size); + + // Tapping anywhere inside focuses it. + expect(focusNode.hasFocus, false); + await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField))); + await tester.pumpAndSettle(const Duration(milliseconds: 400)); // timer error if no duration + expect(focusNode.hasFocus, true); + focusNode.unfocus(); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, false); + final Offset justInside = tester + .getBottomLeft(find.byType(CupertinoTextField)) + .translate(0.0, -1.0); + await tester.tapAt(justInside); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, true); + + // The EditableText is at the center. Same as without prefix. + expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, closeTo(size.height, .0001)); + expect(tester.getTopLeft(find.byType(EditableText)).dy, closeTo(291.5, .0001)); + }); + + testWidgets('align top', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + const Size size = Size(200.0, 200.0); + await tester.pumpWidget( + CupertinoApp( + debugShowCheckedModeBanner: false, + home: CupertinoPageScaffold( + child: Align( + alignment: Alignment.center, + child: SizedBox( + width: size.width, + height: size.height, + child: CupertinoTextField( + textAlignVertical: TextAlignVertical.top, + focusNode: focusNode, + expands: true, + maxLines: null, + prefix: Container( + //key: pKey, + height: 100, + width: 10, + ), + ), + ), + ), + ), + ), + ); + + // Fills the whole container since expands is true. + expect(tester.getSize(find.byType(CupertinoTextField)), size); + + // Tapping anywhere inside focuses it. + expect(focusNode.hasFocus, false); + await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField))); + await tester.pumpAndSettle(const Duration(milliseconds: 400)); // timer error if no duration + expect(focusNode.hasFocus, true); + focusNode.unfocus(); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, false); + final Offset justInside = tester + .getBottomLeft(find.byType(CupertinoTextField)) + .translate(0.0, -1.0); + await tester.tapAt(justInside); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, true); + + // The prefix is at the top, and the EditableText is centered within its + // height. + expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, closeTo(size.height, .0001)); + expect(tester.getTopLeft(find.byType(EditableText)).dy, closeTo(241.5, .0001)); + }); + + testWidgets('align bottom', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + const Size size = Size(200.0, 200.0); + await tester.pumpWidget( + CupertinoApp( + debugShowCheckedModeBanner: false, + home: CupertinoPageScaffold( + child: Align( + alignment: Alignment.center, + child: SizedBox( + width: size.width, + height: size.height, + child: CupertinoTextField( + textAlignVertical: TextAlignVertical.bottom, + focusNode: focusNode, + expands: true, + maxLines: null, + prefix: Container( + //key: pKey, + height: 100, + width: 10, + ), + ), + ), + ), + ), + ), + ); + + // Fills the whole container since expands is true. + expect(tester.getSize(find.byType(CupertinoTextField)), size); + + // Tapping anywhere inside focuses it. + expect(focusNode.hasFocus, false); + await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField))); + await tester.pumpAndSettle(const Duration(milliseconds: 400)); // timer error if no duration + expect(focusNode.hasFocus, true); + focusNode.unfocus(); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, false); + final Offset justInside = tester + .getBottomLeft(find.byType(CupertinoTextField)) + .translate(0.0, -1.0); + await tester.tapAt(justInside); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, true); + + // The prefix is at the bottom, and the EditableText is centered within + // its height. + expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, closeTo(size.height, .0001)); + expect(tester.getTopLeft(find.byType(EditableText)).dy, closeTo(341.5, .0001)); + }); + + testWidgets('align as a double', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + const Size size = Size(200.0, 200.0); + await tester.pumpWidget( + CupertinoApp( + debugShowCheckedModeBanner: false, + home: CupertinoPageScaffold( + child: Align( + alignment: Alignment.center, + child: SizedBox( + width: size.width, + height: size.height, + child: CupertinoTextField( + textAlignVertical: const TextAlignVertical(y: 0.75), + focusNode: focusNode, + expands: true, + maxLines: null, + prefix: Container( + //key: pKey, + height: 100, + width: 10, + ), + ), + ), + ), + ), + ), + ); + + // Fills the whole container since expands is true. + expect(tester.getSize(find.byType(CupertinoTextField)), size); + + // Tapping anywhere inside focuses it. + expect(focusNode.hasFocus, false); + await tester.tapAt(tester.getTopLeft(find.byType(CupertinoTextField))); + await tester.pumpAndSettle(const Duration(milliseconds: 400)); // timer error if no duration + expect(focusNode.hasFocus, true); + focusNode.unfocus(); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, false); + final Offset justInside = tester + .getBottomLeft(find.byType(CupertinoTextField)) + .translate(0.0, -1.0); + await tester.tapAt(justInside); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, true); + + // The EditableText is near the bottom. + expect(tester.getTopLeft(find.byType(CupertinoTextField)).dy, closeTo(size.height, .0001)); + expect(tester.getTopLeft(find.byType(EditableText)).dy, closeTo(329.0, .0001)); + }); }); }); } diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index 33882edf2d11..f52b47959251 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -471,9 +471,6 @@ void main() { expect(tester.getBottomLeft(find.text('label')).dy, 308.0); // Entering text happens in the center as well. - // TODO(justinmc): This is the last failing test right now. Looking at - // the above app, I expect the text the center in the input. However, - // currently it happens at the top. await tester.enterText(find.byType(InputDecorator), text); expect(tester.getTopLeft(find.text(text)).dy, 291.0); controller.clear();