Fix TextField intrinsic width when hint is not visible (#161235)
## Description This PR changes the TextField intrinsic width computation for non-empty TextField. Before: When a TextField is non-empty, the intrinsic width can't be smaller than the hint width:  After: The width does not depend on the hint when the TextField is non empty.  <details><summary>Code sample</summary> ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( title: 'Material App', home: Scaffold( body: Center( child: IntrinsicWidth( child: TextField( decoration: InputDecoration( filled: true, hintText: 'Moderately long hint text', ), ), ), ), ), ); } } ``` </details> ## Related Issue Fixes [[TextField] The computation of the intrinsic width of a TextField should include the hint width only when visible](https://github.com/flutter/flutter/issues/93337) ## Tests Adds 2 tests.
This commit is contained in:
parent
f8b9bdceef
commit
a2ec056b82
@ -563,7 +563,9 @@ class _Decoration {
|
||||
required this.borderGap,
|
||||
required this.alignLabelWithHint,
|
||||
required this.isDense,
|
||||
required this.isEmpty,
|
||||
required this.visualDensity,
|
||||
required this.maintainHintSize,
|
||||
this.icon,
|
||||
this.input,
|
||||
this.label,
|
||||
@ -586,7 +588,9 @@ class _Decoration {
|
||||
final _InputBorderGap borderGap;
|
||||
final bool alignLabelWithHint;
|
||||
final bool? isDense;
|
||||
final bool isEmpty;
|
||||
final VisualDensity visualDensity;
|
||||
final bool maintainHintSize;
|
||||
final Widget? icon;
|
||||
final Widget? input;
|
||||
final Widget? label;
|
||||
@ -617,7 +621,9 @@ class _Decoration {
|
||||
other.borderGap == borderGap &&
|
||||
other.alignLabelWithHint == alignLabelWithHint &&
|
||||
other.isDense == isDense &&
|
||||
other.isEmpty == isEmpty &&
|
||||
other.visualDensity == visualDensity &&
|
||||
other.maintainHintSize == maintainHintSize &&
|
||||
other.icon == icon &&
|
||||
other.input == input &&
|
||||
other.label == label &&
|
||||
@ -641,7 +647,9 @@ class _Decoration {
|
||||
borderGap,
|
||||
alignLabelWithHint,
|
||||
isDense,
|
||||
isEmpty,
|
||||
visualDensity,
|
||||
maintainHintSize,
|
||||
icon,
|
||||
input,
|
||||
label,
|
||||
@ -650,9 +658,7 @@ class _Decoration {
|
||||
suffix,
|
||||
prefixIcon,
|
||||
suffixIcon,
|
||||
helperError,
|
||||
counter,
|
||||
container,
|
||||
Object.hash(helperError, counter, container),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1021,8 +1027,11 @@ class _RenderDecoration extends RenderBox
|
||||
final double hintBaseline =
|
||||
hint == null ? 0.0 : getBaseline(hint, boxConstraints.tighten(width: inputWidth));
|
||||
|
||||
// The field can be occupied by a hint or by the input itself
|
||||
final double inputHeight = math.max(hintSize.height, inputSize.height);
|
||||
// The field can be occupied by a hint or by the input itself.
|
||||
final double inputHeight = math.max(
|
||||
decoration.isEmpty || decoration.maintainHintSize ? hintSize.height : 0.0,
|
||||
inputSize.height,
|
||||
);
|
||||
final double inputInternalBaseline = math.max(inputBaseline, hintBaseline);
|
||||
|
||||
final double prefixBaseline = prefix == null ? 0.0 : getBaseline(prefix, contentConstraints);
|
||||
@ -1154,11 +1163,15 @@ class _RenderDecoration extends RenderBox
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
final double contentWidth =
|
||||
decoration.isEmpty || decoration.maintainHintSize
|
||||
? math.max(_minWidth(input, height), _minWidth(hint, height))
|
||||
: _minWidth(input, height);
|
||||
return _minWidth(icon, height) +
|
||||
(prefixIcon != null ? prefixToInputGap : contentPadding.start) +
|
||||
_minWidth(prefixIcon, height) +
|
||||
_minWidth(prefix, height) +
|
||||
math.max(_minWidth(input, height), _minWidth(hint, height)) +
|
||||
contentWidth +
|
||||
_minWidth(suffix, height) +
|
||||
_minWidth(suffixIcon, height) +
|
||||
(suffixIcon != null ? inputToSuffixGap : contentPadding.end);
|
||||
@ -1166,11 +1179,15 @@ class _RenderDecoration extends RenderBox
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
final double contentWidth =
|
||||
decoration.isEmpty || decoration.maintainHintSize
|
||||
? math.max(_maxWidth(input, height), _maxWidth(hint, height))
|
||||
: _maxWidth(input, height);
|
||||
return _maxWidth(icon, height) +
|
||||
(prefixIcon != null ? prefixToInputGap : contentPadding.start) +
|
||||
_maxWidth(prefixIcon, height) +
|
||||
_maxWidth(prefix, height) +
|
||||
math.max(_maxWidth(input, height), _maxWidth(hint, height)) +
|
||||
contentWidth +
|
||||
_maxWidth(suffix, height) +
|
||||
_maxWidth(suffixIcon, height) +
|
||||
(suffixIcon != null ? inputToSuffixGap : contentPadding.end);
|
||||
@ -1227,7 +1244,10 @@ class _RenderDecoration extends RenderBox
|
||||
width - prefixWidth - suffixWidth - prefixIconWidth - suffixIconWidth,
|
||||
0.0,
|
||||
);
|
||||
final double inputHeight = _lineHeight(availableInputWidth, <RenderBox?>[input, hint]);
|
||||
final double inputHeight = _lineHeight(availableInputWidth, <RenderBox?>[
|
||||
input,
|
||||
if (decoration.isEmpty) hint,
|
||||
]);
|
||||
final double inputMaxHeight = <double>[
|
||||
inputHeight,
|
||||
prefixHeight,
|
||||
@ -1567,7 +1587,9 @@ class _RenderDecoration extends RenderBox
|
||||
doPaint(suffix);
|
||||
doPaint(prefixIcon);
|
||||
doPaint(suffixIcon);
|
||||
doPaint(hint);
|
||||
if (decoration.isEmpty) {
|
||||
doPaint(hint);
|
||||
}
|
||||
doPaint(input);
|
||||
doPaint(helperError);
|
||||
doPaint(counter);
|
||||
@ -2247,7 +2269,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
|
||||
|
||||
final TextStyle hintStyle = _getInlineHintStyle(themeData, defaults);
|
||||
final String? hintText = decoration.hintText;
|
||||
final bool maintainHintHeight = decoration.maintainHintHeight;
|
||||
final bool maintainHintSize = decoration.maintainHintSize;
|
||||
Widget? hint;
|
||||
if (decoration.hint != null || hintText != null) {
|
||||
final Widget hintWidget =
|
||||
@ -2264,7 +2286,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
|
||||
);
|
||||
final bool showHint = isEmpty && !_hasInlineLabel;
|
||||
hint =
|
||||
maintainHintHeight
|
||||
maintainHintSize
|
||||
? AnimatedOpacity(
|
||||
opacity: showHint ? 1.0 : 0.0,
|
||||
duration: decoration.hintFadeDuration ?? _kHintFadeTransitionDuration,
|
||||
@ -2573,7 +2595,9 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
|
||||
borderGap: _borderGap,
|
||||
alignLabelWithHint: decoration.alignLabelWithHint ?? false,
|
||||
isDense: decoration.isDense,
|
||||
isEmpty: isEmpty,
|
||||
visualDensity: themeData.visualDensity,
|
||||
maintainHintSize: maintainHintSize,
|
||||
icon: icon,
|
||||
input: input,
|
||||
label: label,
|
||||
@ -2704,7 +2728,13 @@ class InputDecoration {
|
||||
this.hintTextDirection,
|
||||
this.hintMaxLines,
|
||||
this.hintFadeDuration,
|
||||
@Deprecated(
|
||||
'Use maintainHintSize instead. '
|
||||
'This will maintain both hint height and hint width. '
|
||||
'This feature was deprecated after v3.28.0-2.0.pre.',
|
||||
)
|
||||
this.maintainHintHeight = true,
|
||||
this.maintainHintSize = true,
|
||||
this.error,
|
||||
this.errorText,
|
||||
this.errorStyle,
|
||||
@ -2794,7 +2824,13 @@ class InputDecoration {
|
||||
this.hintTextDirection,
|
||||
this.hintMaxLines,
|
||||
this.hintFadeDuration,
|
||||
@Deprecated(
|
||||
'Use maintainHintSize instead. '
|
||||
'This will maintain both hint height and hint width. '
|
||||
'This feature was deprecated after v3.28.0-2.0.pre.',
|
||||
)
|
||||
this.maintainHintHeight = true,
|
||||
this.maintainHintSize = true,
|
||||
this.filled = false,
|
||||
this.fillColor,
|
||||
this.focusColor,
|
||||
@ -3076,8 +3112,22 @@ class InputDecoration {
|
||||
/// it's not visible, if this flag is set to false.
|
||||
///
|
||||
/// Defaults to true.
|
||||
@Deprecated(
|
||||
'Use maintainHintSize instead. '
|
||||
'This will maintain both hint height and hint width. '
|
||||
'This feature was deprecated after v3.28.0-2.0.pre.',
|
||||
)
|
||||
final bool maintainHintHeight;
|
||||
|
||||
/// Whether the input field's size should always be greater than or equal to
|
||||
/// the size of the [hintText], even if the [hintText] is not visible.
|
||||
///
|
||||
/// The [InputDecorator] widget ignores [hintText] during layout when
|
||||
/// it's not visible, if this flag is set to false.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool maintainHintSize;
|
||||
|
||||
/// Optional widget that appears below the [InputDecorator.child] and the border.
|
||||
///
|
||||
/// If non-null, the border's color animates to red and the [helperText] is not shown.
|
||||
@ -3764,6 +3814,7 @@ class InputDecoration {
|
||||
Duration? hintFadeDuration,
|
||||
int? hintMaxLines,
|
||||
bool? maintainHintHeight,
|
||||
bool? maintainHintSize,
|
||||
Widget? error,
|
||||
String? errorText,
|
||||
TextStyle? errorStyle,
|
||||
@ -3821,6 +3872,7 @@ class InputDecoration {
|
||||
hintMaxLines: hintMaxLines ?? this.hintMaxLines,
|
||||
hintFadeDuration: hintFadeDuration ?? this.hintFadeDuration,
|
||||
maintainHintHeight: maintainHintHeight ?? this.maintainHintHeight,
|
||||
maintainHintSize: maintainHintSize ?? this.maintainHintSize,
|
||||
error: error ?? this.error,
|
||||
errorText: errorText ?? this.errorText,
|
||||
errorStyle: errorStyle ?? this.errorStyle,
|
||||
@ -3931,6 +3983,7 @@ class InputDecoration {
|
||||
other.hintMaxLines == hintMaxLines &&
|
||||
other.hintFadeDuration == hintFadeDuration &&
|
||||
other.maintainHintHeight == maintainHintHeight &&
|
||||
other.maintainHintSize == maintainHintSize &&
|
||||
other.error == error &&
|
||||
other.errorText == errorText &&
|
||||
other.errorStyle == errorStyle &&
|
||||
@ -3991,6 +4044,7 @@ class InputDecoration {
|
||||
hintMaxLines,
|
||||
hintFadeDuration,
|
||||
maintainHintHeight,
|
||||
maintainHintSize,
|
||||
error,
|
||||
errorText,
|
||||
errorStyle,
|
||||
@ -4049,6 +4103,7 @@ class InputDecoration {
|
||||
if (hintMaxLines != null) 'hintMaxLines: "$hintMaxLines"',
|
||||
if (hintFadeDuration != null) 'hintFadeDuration: "$hintFadeDuration"',
|
||||
if (!maintainHintHeight) 'maintainHintHeight: false',
|
||||
if (!maintainHintSize) 'maintainHintSize: false',
|
||||
if (error != null) 'error: "$error"',
|
||||
if (errorText != null) 'errorText: "$errorText"',
|
||||
if (errorStyle != null) 'errorStyle: "$errorStyle"',
|
||||
|
@ -4717,14 +4717,14 @@ void main() {
|
||||
expect(hintTextWidget.style!.overflow, decoration.hintStyle!.overflow);
|
||||
});
|
||||
|
||||
testWidgets('Widget height collapses from hint height when maintainHintHeight is false', (
|
||||
testWidgets('Widget height collapses from hint height when maintainHintSize is false', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final String hintText = 'hint' * 20;
|
||||
final InputDecoration decoration = InputDecoration(
|
||||
hintText: hintText,
|
||||
hintMaxLines: 3,
|
||||
maintainHintHeight: false,
|
||||
maintainHintSize: false,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(buildInputDecorator(decoration: decoration));
|
||||
@ -4741,14 +4741,14 @@ void main() {
|
||||
expect(inputHeight, hintHeight + 16.0);
|
||||
});
|
||||
|
||||
testWidgets('hintFadeDuration applies to hint fade-in when maintainHintHeight is false', (
|
||||
testWidgets('hintFadeDuration applies to hint fade-in when maintainHintSize is false', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
const InputDecoration decoration = InputDecoration(
|
||||
hintText: hintText,
|
||||
hintMaxLines: 3,
|
||||
hintFadeDuration: Duration(milliseconds: 120),
|
||||
maintainHintHeight: false,
|
||||
maintainHintSize: false,
|
||||
);
|
||||
|
||||
// Build once with empty content.
|
||||
@ -4773,14 +4773,14 @@ void main() {
|
||||
expect(hintOpacity120ms, 1.0);
|
||||
});
|
||||
|
||||
testWidgets('hintFadeDuration applies to hint fade-out when maintainHintHeight is false', (
|
||||
testWidgets('hintFadeDuration applies to hint fade-out when maintainHintSize is false', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
const InputDecoration decoration = InputDecoration(
|
||||
hintText: hintText,
|
||||
hintMaxLines: 3,
|
||||
hintFadeDuration: Duration(milliseconds: 120),
|
||||
maintainHintHeight: false,
|
||||
maintainHintSize: false,
|
||||
);
|
||||
|
||||
// Build once with empty content.
|
||||
@ -8352,6 +8352,90 @@ void main() {
|
||||
|
||||
expect(textSizeWithIcons.width, equals(textSizeWithoutIcon.width));
|
||||
});
|
||||
|
||||
testWidgets('depends on hint width and content width when decorator is empty', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
const InputDecoration decorationWithHint = InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: 'Hint',
|
||||
);
|
||||
const double hintTextWidth = 66.0;
|
||||
const double smallContentWidth = 20.0;
|
||||
const double largeContentWidth = 80.0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
buildInputDecorator(
|
||||
decoration: decorationWithHint,
|
||||
useIntrinsicWidth: true,
|
||||
isEmpty: true,
|
||||
child: const SizedBox(width: smallContentWidth),
|
||||
),
|
||||
);
|
||||
|
||||
// Decorator width depends on the hint because the hint is larger than the content.
|
||||
expect(getDecoratorRect(tester).width, hintTextWidth);
|
||||
|
||||
await tester.pumpWidget(
|
||||
buildInputDecorator(
|
||||
decoration: decorationWithHint,
|
||||
useIntrinsicWidth: true,
|
||||
isEmpty: true,
|
||||
child: const SizedBox(width: largeContentWidth),
|
||||
),
|
||||
);
|
||||
|
||||
// Decorator width depends on the content because the content is larger than the hint.
|
||||
expect(getDecoratorRect(tester).width, largeContentWidth);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/93337.
|
||||
testWidgets(
|
||||
'depends on content width when decorator is not empty and maintainHintSize is false',
|
||||
(WidgetTester tester) async {
|
||||
const InputDecoration decorationWithHint = InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: 'Hint',
|
||||
maintainHintSize: false,
|
||||
);
|
||||
const double contentWidth = 20.0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
buildInputDecorator(
|
||||
decoration: decorationWithHint,
|
||||
useIntrinsicWidth: true,
|
||||
child: const SizedBox(width: contentWidth),
|
||||
),
|
||||
);
|
||||
|
||||
// The hint width is ignored even if larger than the content width.
|
||||
expect(getDecoratorRect(tester).width, contentWidth);
|
||||
},
|
||||
);
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/93337.
|
||||
testWidgets(
|
||||
'depends on content width when decorator is not empty and maintainHintSize is true',
|
||||
(WidgetTester tester) async {
|
||||
const InputDecoration decorationWithHint = InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: 'Hint',
|
||||
);
|
||||
const double contentWidth = 20.0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
buildInputDecorator(
|
||||
decoration: decorationWithHint,
|
||||
useIntrinsicWidth: true,
|
||||
child: const SizedBox(width: contentWidth),
|
||||
),
|
||||
);
|
||||
|
||||
// The hint width is ignored even if larger than the content width.
|
||||
const double hintTextWidth = 66.0;
|
||||
expect(getDecoratorRect(tester).width, hintTextWidth);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Ensure the height of labelStyle remains unchanged when TextField is focused', (
|
||||
|
Loading…
x
Reference in New Issue
Block a user