Unverified Commit 804a7b28 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Make `CupertinoTextField` at least as tall as its first line of placeholder (#134198)

Fixes https://github.com/flutter/flutter/issues/133241
and some CupertinoTextField cleanup.
parent fc671188
...@@ -444,7 +444,7 @@ class _CupertinoSearchTextFieldState extends State<CupertinoSearchTextField> ...@@ -444,7 +444,7 @@ class _CupertinoSearchTextFieldState extends State<CupertinoSearchTextField>
suffix: suffix, suffix: suffix,
keyboardType: widget.keyboardType, keyboardType: widget.keyboardType,
onTap: widget.onTap, onTap: widget.onTap,
enabled: widget.enabled, enabled: widget.enabled ?? true,
suffixMode: widget.suffixMode, suffixMode: widget.suffixMode,
placeholder: placeholder, placeholder: placeholder,
placeholderStyle: placeholderStyle, placeholderStyle: placeholderStyle,
......
...@@ -261,7 +261,7 @@ class CupertinoTextField extends StatefulWidget { ...@@ -261,7 +261,7 @@ class CupertinoTextField extends StatefulWidget {
this.onSubmitted, this.onSubmitted,
this.onTapOutside, this.onTapOutside,
this.inputFormatters, this.inputFormatters,
this.enabled, this.enabled = true,
this.cursorWidth = 2.0, this.cursorWidth = 2.0,
this.cursorHeight, this.cursorHeight,
this.cursorRadius = const Radius.circular(2.0), this.cursorRadius = const Radius.circular(2.0),
...@@ -393,7 +393,7 @@ class CupertinoTextField extends StatefulWidget { ...@@ -393,7 +393,7 @@ class CupertinoTextField extends StatefulWidget {
this.onSubmitted, this.onSubmitted,
this.onTapOutside, this.onTapOutside,
this.inputFormatters, this.inputFormatters,
this.enabled, this.enabled = true,
this.cursorWidth = 2.0, this.cursorWidth = 2.0,
this.cursorHeight, this.cursorHeight,
this.cursorRadius = const Radius.circular(2.0), this.cursorRadius = const Radius.circular(2.0),
...@@ -653,7 +653,9 @@ class CupertinoTextField extends StatefulWidget { ...@@ -653,7 +653,9 @@ class CupertinoTextField extends StatefulWidget {
/// Text fields in disabled states have a light grey background and don't /// Text fields in disabled states have a light grey background and don't
/// respond to touch events including the [prefix], [suffix] and the clear /// respond to touch events including the [prefix], [suffix] and the clear
/// button. /// button.
final bool? enabled; ///
/// Defaults to true.
final bool enabled;
/// {@macro flutter.widgets.editableText.cursorWidth} /// {@macro flutter.widgets.editableText.cursorWidth}
final double cursorWidth; final double cursorWidth;
...@@ -946,7 +948,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -946,7 +948,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
if (widget.controller == null) { if (widget.controller == null) {
_createLocalController(); _createLocalController();
} }
_effectiveFocusNode.canRequestFocus = widget.enabled ?? true; _effectiveFocusNode.canRequestFocus = widget.enabled;
_effectiveFocusNode.addListener(_handleFocusChanged); _effectiveFocusNode.addListener(_handleFocusChanged);
} }
...@@ -965,7 +967,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -965,7 +967,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
(oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged); (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged);
(widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged); (widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged);
} }
_effectiveFocusNode.canRequestFocus = widget.enabled ?? true; _effectiveFocusNode.canRequestFocus = widget.enabled;
} }
@override @override
...@@ -1079,41 +1081,16 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1079,41 +1081,16 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
@override @override
bool get wantKeepAlive => _controller?.value.text.isNotEmpty ?? false; bool get wantKeepAlive => _controller?.value.text.isNotEmpty ?? false;
bool _shouldShowAttachment({ static bool _shouldShowAttachment({
required OverlayVisibilityMode attachment, required OverlayVisibilityMode attachment,
required bool hasText, required bool hasText,
}) { }) {
switch (attachment) { return switch (attachment) {
case OverlayVisibilityMode.never: OverlayVisibilityMode.never => false,
return false; OverlayVisibilityMode.always => true,
case OverlayVisibilityMode.always: OverlayVisibilityMode.editing => hasText,
return true; OverlayVisibilityMode.notEditing => !hasText,
case OverlayVisibilityMode.editing: };
return hasText;
case OverlayVisibilityMode.notEditing:
return !hasText;
}
}
bool _showPrefixWidget(TextEditingValue text) {
return widget.prefix != null && _shouldShowAttachment(
attachment: widget.prefixMode,
hasText: text.text.isNotEmpty,
);
}
bool _showSuffixWidget(TextEditingValue text) {
return widget.suffix != null && _shouldShowAttachment(
attachment: widget.suffixMode,
hasText: text.text.isNotEmpty,
);
}
bool _showClearButton(TextEditingValue text) {
return _shouldShowAttachment(
attachment: widget.clearButtonMode,
hasText: text.text.isNotEmpty,
);
} }
// True if any surrounding decoration widgets will be shown. // True if any surrounding decoration widgets will be shown.
...@@ -1134,6 +1111,32 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1134,6 +1111,32 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
return _hasDecoration ? TextAlignVertical.center : TextAlignVertical.top; return _hasDecoration ? TextAlignVertical.center : TextAlignVertical.top;
} }
void _onClearButtonTapped() {
final bool hadText = _effectiveController.text.isNotEmpty;
_effectiveController.clear();
if (hadText) {
// Tapping the clear button is also considered a "user initiated" change
// (instead of a programmatical one), so call `onChanged` if the text
// changed as a result.
widget.onChanged?.call(_effectiveController.text);
}
}
Widget _buildClearButton() {
return GestureDetector(
key: _clearGlobalKey,
onTap: widget.enabled ? _onClearButtonTapped : null,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: Icon(
CupertinoIcons.clear_thick_circled,
size: 18.0,
color: CupertinoDynamicColor.resolve(_kClearButtonColor, context),
),
),
);
}
Widget _addTextDependentAttachments(Widget editableText, TextStyle textStyle, TextStyle placeholderStyle) { Widget _addTextDependentAttachments(Widget editableText, TextStyle textStyle, TextStyle placeholderStyle) {
// If there are no surrounding widgets, just return the core editable text // If there are no surrounding widgets, just return the core editable text
// part. // part.
...@@ -1145,59 +1148,69 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1145,59 +1148,69 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
return ValueListenableBuilder<TextEditingValue>( return ValueListenableBuilder<TextEditingValue>(
valueListenable: _effectiveController, valueListenable: _effectiveController,
child: editableText, child: editableText,
builder: (BuildContext context, TextEditingValue? text, Widget? child) { builder: (BuildContext context, TextEditingValue text, Widget? child) {
return Row(children: <Widget>[ final bool hasText = text.text.isNotEmpty;
// Insert a prefix at the front if the prefix visibility mode matches final String? placeholderText = widget.placeholder;
// the current text state. final Widget? placeholder = placeholderText == null
if (_showPrefixWidget(text!)) widget.prefix!, ? null
// In the middle part, stack the placeholder on top of the main EditableText // Make the placeholder invisible when hasText is true.
// if needed. : Visibility(
Expanded( maintainAnimation: true,
child: Stack( maintainSize: true,
children: <Widget>[ maintainState: true,
if (widget.placeholder != null && text.text.isEmpty) visible: !hasText,
SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: Padding( child: Padding(
padding: widget.padding, padding: widget.padding,
child: Text( child: Text(
widget.placeholder!, placeholderText,
maxLines: widget.maxLines, // This is to make sure the text field is always tall enough
overflow: placeholderStyle.overflow ?? TextOverflow.ellipsis, // to accommodate the first line of the placeholder, so the
// text does not shrink vertically as you type (however in
// rare circumstances, the height may still change when
// there's no placeholder text).
maxLines: hasText ? 1 : widget.maxLines,
overflow: placeholderStyle.overflow,
style: placeholderStyle, style: placeholderStyle,
textAlign: widget.textAlign, textAlign: widget.textAlign,
), ),
), ),
), ),
child!, );
final Widget? prefixWidget = _shouldShowAttachment(attachment: widget.prefixMode, hasText: hasText) ? widget.prefix : null;
// Show user specified suffix if applicable and fall back to clear button.
final bool showUserSuffix = _shouldShowAttachment(attachment: widget.suffixMode, hasText: hasText);
final bool showClearButton = _shouldShowAttachment(attachment: widget.clearButtonMode, hasText: hasText);
final Widget? suffixWidget = switch ((showUserSuffix, showClearButton)) {
(false, false) => null,
(true, false) => widget.suffix,
(true, true) => widget.suffix ?? _buildClearButton(),
(false, true) => _buildClearButton(),
};
return Row(children: <Widget>[
// Insert a prefix at the front if the prefix visibility mode matches
// the current text state.
if (prefixWidget != null) prefixWidget,
// In the middle part, stack the placeholder on top of the main EditableText
// if needed.
Expanded(
child: Stack(
// Ideally this should be baseline aligned. However that comes at
// the cost of the ability to compute the intrinsic dimensions of
// this widget.
// See also https://github.com/flutter/flutter/issues/13715.
alignment: AlignmentDirectional.center,
textDirection: widget.textDirection,
children: <Widget>[
if (placeholder != null) placeholder,
editableText,
], ],
), ),
), ),
// First add the explicit suffix if the suffix visibility mode matches. if (suffixWidget != null) suffixWidget
if (_showSuffixWidget(text))
widget.suffix!
// Otherwise, try to show a clear button if its visibility mode matches.
else if (_showClearButton(text))
GestureDetector(
key: _clearGlobalKey,
onTap: widget.enabled ?? true ? () {
// Special handle onChanged for ClearButton
// Also call onChanged when the clear button is tapped.
final bool textChanged = _effectiveController.text.isNotEmpty;
_effectiveController.clear();
if (widget.onChanged != null && textChanged) {
widget.onChanged!(_effectiveController.text);
}
} : null,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: Icon(
CupertinoIcons.clear_thick_circled,
size: 18.0,
color: CupertinoDynamicColor.resolve(_kClearButtonColor, context),
),
),
),
]); ]);
}, },
); );
...@@ -1251,7 +1264,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1251,7 +1264,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
}; };
} }
final bool enabled = widget.enabled ?? true; final bool enabled = widget.enabled;
final Offset cursorOffset = Offset(_iOSHorizontalCursorOffsetPixels / MediaQuery.devicePixelRatioOf(context), 0); final Offset cursorOffset = Offset(_iOSHorizontalCursorOffsetPixels / MediaQuery.devicePixelRatioOf(context), 0);
final List<TextInputFormatter> formatters = <TextInputFormatter>[ final List<TextInputFormatter> formatters = <TextInputFormatter>[
...?widget.inputFormatters, ...?widget.inputFormatters,
......
...@@ -219,7 +219,7 @@ class CupertinoTextFormFieldRow extends FormField<String> { ...@@ -219,7 +219,7 @@ class CupertinoTextFormFieldRow extends FormField<String> {
onEditingComplete: onEditingComplete, onEditingComplete: onEditingComplete,
onSubmitted: onFieldSubmitted, onSubmitted: onFieldSubmitted,
inputFormatters: inputFormatters, inputFormatters: inputFormatters,
enabled: enabled, enabled: enabled ?? true,
cursorWidth: cursorWidth, cursorWidth: cursorWidth,
cursorHeight: cursorHeight, cursorHeight: cursorHeight,
cursorColor: cursorColor, cursorColor: cursorColor,
......
...@@ -569,15 +569,11 @@ class RenderStack extends RenderBox ...@@ -569,15 +569,11 @@ class RenderStack extends RenderBox
double width = constraints.minWidth; double width = constraints.minWidth;
double height = constraints.minHeight; double height = constraints.minHeight;
final BoxConstraints nonPositionedConstraints; final BoxConstraints nonPositionedConstraints = switch (fit) {
switch (fit) { StackFit.loose => constraints.loosen(),
case StackFit.loose: StackFit.expand => BoxConstraints.tight(constraints.biggest),
nonPositionedConstraints = constraints.loosen(); StackFit.passthrough => constraints,
case StackFit.expand: };
nonPositionedConstraints = BoxConstraints.tight(constraints.biggest);
case StackFit.passthrough:
nonPositionedConstraints = constraints;
}
RenderBox? child = firstChild; RenderBox? child = firstChild;
while (child != null) { while (child != null) {
......
...@@ -1071,7 +1071,8 @@ void main() { ...@@ -1071,7 +1071,8 @@ void main() {
await tester.enterText(find.byType(CupertinoTextField), 'input'); await tester.enterText(find.byType(CupertinoTextField), 'input');
await tester.pump(); await tester.pump();
expect(find.text('placeholder'), findsNothing); final Element element = tester.element(find.text('placeholder'));
expect(Visibility.of(element), false);
}, },
); );
...@@ -1964,7 +1965,9 @@ void main() { ...@@ -1964,7 +1965,9 @@ void main() {
expect(find.text('field 1'), findsOneWidget); expect(find.text('field 1'), findsOneWidget);
expect(find.text("j'aime la poutine"), findsOneWidget); expect(find.text("j'aime la poutine"), findsOneWidget);
expect(find.text('field 2'), findsNothing);
final Element placeholder2Element = tester.element(find.text('field 2'));
expect(Visibility.of(placeholder2Element), false);
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
testWidgets( testWidgets(
...@@ -8096,9 +8099,7 @@ void main() { ...@@ -8096,9 +8099,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
const CupertinoApp( const CupertinoApp(
home: Center( home: Center(
child: CupertinoTextField( child: CupertinoTextField(),
enabled: true,
),
), ),
), ),
); );
...@@ -9867,4 +9868,59 @@ void main() { ...@@ -9867,4 +9868,59 @@ void main() {
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }), variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }),
); );
testWidgets('Does not shrink in height when enters text when there is large single-line placeholder', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/133241.
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
CupertinoApp(
home: Align(
alignment: Alignment.topCenter,
child: CupertinoTextField(
placeholderStyle: const TextStyle(fontSize: 100),
placeholder: 'p',
controller: controller,
),
),
),
);
final Rect rectWithPlaceholder = tester.getRect(find.byType(CupertinoTextField));
controller.value = const TextEditingValue(text: 'input');
await tester.pump();
final Rect rectWithText = tester.getRect(find.byType(CupertinoTextField));
expect(rectWithPlaceholder, rectWithText);
});
testWidgets('Does not match the height of a multiline placeholder', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
CupertinoApp(
home: Align(
alignment: Alignment.topCenter,
child: CupertinoTextField(
placeholderStyle: const TextStyle(fontSize: 100),
placeholder: 'p' * 50,
maxLines: null,
controller: controller,
),
),
),
);
final Rect rectWithPlaceholder = tester.getRect(find.byType(CupertinoTextField));
controller.value = const TextEditingValue(text: 'input');
await tester.pump();
final Rect rectWithText = tester.getRect(find.byType(CupertinoTextField));
// The text field is still top aligned.
expect(rectWithPlaceholder.top, rectWithText.top);
// But after entering text the text field should shrink since the
// placeholder text is huge and multiline.
expect(rectWithPlaceholder.height, greaterThan(rectWithText.height));
// But still should be taller than or the same height of the first line of
// placeholder.
expect(rectWithText.height, greaterThan(100));
});
} }
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