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.
This commit is contained in:
Bruno Leroux 2025-11-06 22:45:20 +01:00 committed by GitHub
parent 767fac1b8d
commit 411566a2e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 130 additions and 15 deletions

View File

@ -596,6 +596,7 @@ class _Decoration {
required this.visualDensity,
required this.inputGap,
required this.maintainHintSize,
required this.maintainLabelSize,
this.icon,
this.input,
this.label,
@ -622,6 +623,7 @@ class _Decoration {
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 @@ class _Decoration {
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 @@ class _Decoration {
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 @@ class _RenderDecoration extends RenderBox
@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 @@ class _RenderDecoration extends RenderBox
@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 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
isEmpty: isEmpty,
visualDensity: visualDensity,
maintainHintSize: maintainHintSize,
maintainLabelSize: decoration.maintainLabelSize,
icon: icon,
input: input,
label: label,
@ -2787,6 +2797,7 @@ class InputDecoration {
)
this.maintainHintHeight = true,
this.maintainHintSize = true,
this.maintainLabelSize = false,
this.error,
this.errorText,
this.errorStyle,
@ -2884,6 +2895,7 @@ class InputDecoration {
)
this.maintainHintHeight = true,
this.maintainHintSize = true,
this.maintainLabelSize = false,
this.filled = false,
this.fillColor,
this.focusColor,
@ -3174,14 +3186,23 @@ class InputDecoration {
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 @@ class InputDecoration {
int? hintMaxLines,
bool? maintainHintHeight,
bool? maintainHintSize,
bool? maintainLabelSize,
Widget? error,
String? errorText,
TextStyle? errorStyle,
@ -3953,6 +3975,7 @@ class InputDecoration {
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 @@ class InputDecoration {
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 @@ class InputDecoration {
hintFadeDuration,
maintainHintHeight,
maintainHintSize,
maintainLabelSize,
error,
errorText,
errorStyle,
@ -4199,6 +4224,7 @@ class InputDecoration {
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"',

View File

@ -9411,26 +9411,115 @@ void main() {
);
// 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);
},
);
});