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);
       },
     );
   });