Allow label to be used to compute InputDecorator Intrinsic width (#178101)
## Description
This PR adds `InputDecorator.maintainLabelSize` (similar to
`InputDecorator.maintainHintSize`) to allow the label to be used in the
intrinsic width calculation (if could be used for the intrinsic height
calculation later if needed).
I opted for this flag (and defaulting to false) because changing the
default calculation would probably break various usages.
See
https://github.com/flutter/flutter/issues/178099#issuecomment-3496116095
for why this change will be helpful to simplify and fix DropdownMenu
implementation.
## Before
The label might be cut off:
<img width="126" height="71" alt="Screenshot 2025-11-05 at 20 16 43"
src="https://github.com/user-attachments/assets/61d9f817-5c58-43f9-9307-976f9c124ec7"
/>
## After
The label is entirely visible because it is part of the intrinsic width
calculation:
<img width="126" height="71" alt="Screenshot 2025-11-05 at 20 16 09"
src="https://github.com/user-attachments/assets/47360e17-3cde-4f05-8a6b-cc9e86644ffc"
/>
## Related Issue
Fixes [DropdownMenu menu panel does not close when pressing ESC and
requestFocusOnTap is
false](https://github.com/flutter/flutter/issues/177993)
Part of https://github.com/flutter/flutter/issues/123797
## Tests
- Adds 4 tests.
- Updates 1 non-related test where I spotted some nits.
diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart
index c3bd474..3a524ea 100644
--- a/packages/flutter/lib/src/material/input_decorator.dart
+++ b/packages/flutter/lib/src/material/input_decorator.dart
@@ -596,6 +596,7 @@
required this.visualDensity,
required this.inputGap,
required this.maintainHintSize,
+ required this.maintainLabelSize,
this.icon,
this.input,
this.label,
@@ -622,6 +623,7 @@
final VisualDensity visualDensity;
final double inputGap;
final bool maintainHintSize;
+ final bool maintainLabelSize;
final Widget? icon;
final Widget? input;
final Widget? label;
@@ -656,6 +658,7 @@
other.visualDensity == visualDensity &&
other.inputGap == inputGap &&
other.maintainHintSize == maintainHintSize &&
+ other.maintainLabelSize == maintainLabelSize &&
other.icon == icon &&
other.input == input &&
other.label == label &&
@@ -683,14 +686,14 @@
visualDensity,
inputGap,
maintainHintSize,
+ maintainLabelSize,
icon,
input,
label,
hint,
prefix,
suffix,
- prefixIcon,
- Object.hash(suffixIcon, helperError, counter, container),
+ Object.hash(prefixIcon, suffixIcon, helperError, counter, container),
);
}
@@ -1212,9 +1215,12 @@
@override
double computeMinIntrinsicWidth(double height) {
- final double contentWidth = decoration.isEmpty || decoration.maintainHintSize
+ final double inputWidth = decoration.isEmpty || decoration.maintainHintSize
? math.max(_minWidth(input, height), _minWidth(hint, height))
: _minWidth(input, height);
+ final double contentWidth = decoration.maintainLabelSize
+ ? math.max(inputWidth, _minWidth(label, height))
+ : inputWidth;
return _minWidth(icon, height) +
(prefixIcon != null ? prefixToInputGap : contentPadding.start + decoration.inputGap) +
_minWidth(prefixIcon, height) +
@@ -1227,9 +1233,12 @@
@override
double computeMaxIntrinsicWidth(double height) {
- final double contentWidth = decoration.isEmpty || decoration.maintainHintSize
+ final double inputWidth = decoration.isEmpty || decoration.maintainHintSize
? math.max(_maxWidth(input, height), _maxWidth(hint, height))
: _maxWidth(input, height);
+ final double contentWidth = decoration.maintainLabelSize
+ ? math.max(inputWidth, _maxWidth(label, height))
+ : inputWidth;
return _maxWidth(icon, height) +
(prefixIcon != null ? prefixToInputGap : contentPadding.start + decoration.inputGap) +
_maxWidth(prefixIcon, height) +
@@ -2651,6 +2660,7 @@
isEmpty: isEmpty,
visualDensity: visualDensity,
maintainHintSize: maintainHintSize,
+ maintainLabelSize: decoration.maintainLabelSize,
icon: icon,
input: input,
label: label,
@@ -2787,6 +2797,7 @@
)
this.maintainHintHeight = true,
this.maintainHintSize = true,
+ this.maintainLabelSize = false,
this.error,
this.errorText,
this.errorStyle,
@@ -2884,6 +2895,7 @@
)
this.maintainHintHeight = true,
this.maintainHintSize = true,
+ this.maintainLabelSize = false,
this.filled = false,
this.fillColor,
this.focusColor,
@@ -3174,14 +3186,23 @@
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 size of the [hint] or [hintText], even if the [hint] or [hintText] are not visible.
///
- /// The [InputDecorator] widget ignores [hintText] during layout when
- /// it's not visible, if this flag is set to false.
+ /// The [InputDecorator] widget ignores [hint] and [hintText] during layout when
+ /// they are not visible, if this flag is set to false.
///
/// Defaults to true.
final bool maintainHintSize;
+ /// Whether the input field's size should always be greater than or equal to
+ /// the size of the [label] or [labelText], even if the [label] or [labelText] are not visible.
+ ///
+ /// The [InputDecorator] widget ignores [label] and [labelText] during layout when
+ /// this flag is set to false.
+ ///
+ /// Defaults to false for compatibility reason.
+ final bool maintainLabelSize;
+
/// 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.
@@ -3893,6 +3914,7 @@
int? hintMaxLines,
bool? maintainHintHeight,
bool? maintainHintSize,
+ bool? maintainLabelSize,
Widget? error,
String? errorText,
TextStyle? errorStyle,
@@ -3953,6 +3975,7 @@
hintFadeDuration: hintFadeDuration ?? this.hintFadeDuration,
maintainHintHeight: maintainHintHeight ?? this.maintainHintHeight,
maintainHintSize: maintainHintSize ?? this.maintainHintSize,
+ maintainLabelSize: maintainLabelSize ?? this.maintainLabelSize,
error: error ?? this.error,
errorText: errorText ?? this.errorText,
errorStyle: errorStyle ?? this.errorStyle,
@@ -4077,6 +4100,7 @@
other.hintFadeDuration == hintFadeDuration &&
other.maintainHintHeight == maintainHintHeight &&
other.maintainHintSize == maintainHintSize &&
+ other.maintainLabelSize == maintainLabelSize &&
other.error == error &&
other.errorText == errorText &&
other.errorStyle == errorStyle &&
@@ -4139,6 +4163,7 @@
hintFadeDuration,
maintainHintHeight,
maintainHintSize,
+ maintainLabelSize,
error,
errorText,
errorStyle,
@@ -4199,6 +4224,7 @@
if (hintFadeDuration != null) 'hintFadeDuration: "$hintFadeDuration"',
if (!maintainHintHeight) 'maintainHintHeight: false',
if (!maintainHintSize) 'maintainHintSize: false',
+ if (maintainLabelSize) 'maintainLabelSize: true',
if (error != null) 'error: "$error"',
if (errorText != null) 'errorText: "$errorText"',
if (errorStyle != null) 'errorStyle: "$errorStyle"',
diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart
index deac899..0e89267 100644
--- a/packages/flutter/test/material/input_decorator_test.dart
+++ b/packages/flutter/test/material/input_decorator_test.dart
@@ -9411,26 +9411,115 @@
);
// Regression test for https://github.com/flutter/flutter/issues/93337.
+ testWidgets('depends on hint 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),
+ ),
+ );
+
+ const double hintTextWidth = 66.0;
+ expect(getDecoratorRect(tester).width, hintTextWidth);
+ });
+
testWidgets(
- 'depends on content width when decorator is not empty and maintainHintSize is true',
+ 'does not depend on label width when decorator is empty and maintainLabelSize is false',
(WidgetTester tester) async {
- const InputDecoration decorationWithHint = InputDecoration(
+ const double labelWidth = 30;
+ const InputDecoration decorationWithLabel = InputDecoration(
contentPadding: EdgeInsets.zero,
- hintText: 'Hint',
+ label: SizedBox(width: labelWidth),
);
- const double contentWidth = 20.0;
await tester.pumpWidget(
buildInputDecorator(
- decoration: decorationWithHint,
+ decoration: decorationWithLabel,
+ useIntrinsicWidth: true,
+ isEmpty: true,
+ child: const SizedBox.shrink(),
+ ),
+ );
+
+ // The label width is ignored even if larger than the content width.
+ expect(getDecoratorRect(tester).width, 0);
+ },
+ );
+
+ testWidgets('depends on label width when decorator is empty and maintainLabelSize is true', (
+ WidgetTester tester,
+ ) async {
+ const double labelWidth = 30;
+ const InputDecoration decorationWithLabel = InputDecoration(
+ contentPadding: EdgeInsets.zero,
+ label: SizedBox(width: labelWidth),
+ maintainLabelSize: true,
+ );
+
+ await tester.pumpWidget(
+ buildInputDecorator(
+ decoration: decorationWithLabel,
+ useIntrinsicWidth: true,
+ isEmpty: true,
+ child: const SizedBox.shrink(),
+ ),
+ );
+
+ expect(getDecoratorRect(tester).width, labelWidth);
+ });
+
+ testWidgets(
+ 'does not depend on label width when decorator is not empty and maintainLabelSize is false',
+ (WidgetTester tester) async {
+ const double contentWidth = 20.0;
+ const double labelWidth = 30;
+ const InputDecoration decorationWithLabel = InputDecoration(
+ contentPadding: EdgeInsets.zero,
+ label: SizedBox(width: labelWidth),
+ );
+
+ await tester.pumpWidget(
+ buildInputDecorator(
+ decoration: decorationWithLabel,
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);
+ // The label width is ignored even if larger than the content width.
+ expect(getDecoratorRect(tester).width, contentWidth);
+ },
+ );
+
+ testWidgets(
+ 'depends on label width when decorator is not empty and maintainLabelSize is true',
+ (WidgetTester tester) async {
+ const double contentWidth = 20.0;
+ const double labelWidth = 30;
+ const InputDecoration decorationWithLabel = InputDecoration(
+ contentPadding: EdgeInsets.zero,
+ label: SizedBox(width: labelWidth),
+ maintainLabelSize: true,
+ );
+
+ await tester.pumpWidget(
+ buildInputDecorator(
+ decoration: decorationWithLabel,
+ useIntrinsicWidth: true,
+ child: const SizedBox(width: contentWidth),
+ ),
+ );
+
+ expect(getDecoratorRect(tester).width, labelWidth);
},
);
});