diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index 2c957d8006..76cece9dca 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -2363,7 +2363,7 @@ class _InputDecoratorState extends State with TickerProviderStat : const EdgeInsets.fromLTRB(12.0, 24.0, 12.0, 16.0)); } - return _Decorator( + final _Decorator decorator = _Decorator( decoration: _Decoration( contentPadding: contentPadding, isCollapsed: decoration!.isCollapsed, @@ -2393,6 +2393,15 @@ class _InputDecoratorState extends State with TickerProviderStat isFocused: isFocused, expands: widget.expands, ); + + final BoxConstraints? constraints = decoration!.constraints ?? themeData.inputDecorationTheme.constraints; + if (constraints != null) { + return ConstrainedBox( + constraints: constraints, + child: decorator, + ); + } + return decorator; } } @@ -2563,6 +2572,7 @@ class InputDecoration { this.enabled = true, this.semanticCounterText, this.alignLabelWithHint, + this.constraints, }) : assert(enabled != null), assert(!(prefix != null && prefixText != null), 'Declaring both prefix and prefixText is not supported.'), assert(!(suffix != null && suffixText != null), 'Declaring both suffix and suffixText is not supported.'); @@ -2625,7 +2635,8 @@ class InputDecoration { disabledBorder = null, enabledBorder = null, semanticCounterText = null, - alignLabelWithHint = false; + alignLabelWithHint = false, + constraints = null; /// An icon to show before the input field and outside of the decoration's /// container. @@ -3320,6 +3331,20 @@ class InputDecoration { /// Defaults to false. final bool? alignLabelWithHint; + /// Defines minimum and maximum sizes for the [InputDecorator]. + /// + /// Typically the decorator will fill the horizontal space it is given. For + /// larger screens, it may be useful to have the maximum width clamped to + /// a given value so it doesn't fill the whole screen. This property + /// allows you to control how big the decorator will be in its available + /// space. + /// + /// If null, then the ambient [ThemeData.inputDecorationTheme]'s + /// [InputDecorationTheme.constraints] will be used. If that + /// is null then the decorator will fill the available width with + /// a default height based on text size. + final BoxConstraints? constraints; + /// Creates a copy of this input decoration with the given fields replaced /// by the new values. InputDecoration copyWith({ @@ -3367,6 +3392,7 @@ class InputDecoration { bool? enabled, String? semanticCounterText, bool? alignLabelWithHint, + BoxConstraints? constraints, }) { return InputDecoration( icon: icon ?? this.icon, @@ -3413,6 +3439,7 @@ class InputDecoration { enabled: enabled ?? this.enabled, semanticCounterText: semanticCounterText ?? this.semanticCounterText, alignLabelWithHint: alignLabelWithHint ?? this.alignLabelWithHint, + constraints: constraints ?? this.constraints, ); } @@ -3448,6 +3475,7 @@ class InputDecoration { enabledBorder: enabledBorder ?? theme.enabledBorder, border: border ?? theme.border, alignLabelWithHint: alignLabelWithHint ?? theme.alignLabelWithHint, + constraints: constraints ?? theme.constraints, ); } @@ -3501,7 +3529,8 @@ class InputDecoration { && other.border == border && other.enabled == enabled && other.semanticCounterText == semanticCounterText - && other.alignLabelWithHint == alignLabelWithHint; + && other.alignLabelWithHint == alignLabelWithHint + && other.constraints == constraints; } @override @@ -3553,6 +3582,7 @@ class InputDecoration { enabled, semanticCounterText, alignLabelWithHint, + constraints, ]; return hashList(values); } @@ -3600,6 +3630,7 @@ class InputDecoration { if (!enabled) 'enabled: false', if (semanticCounterText != null) 'semanticCounterText: $semanticCounterText', if (alignLabelWithHint != null) 'alignLabelWithHint: $alignLabelWithHint', + if (constraints != null) 'constraints: $constraints', ]; return 'InputDecoration(${description.join(', ')})'; } @@ -3651,6 +3682,7 @@ class InputDecorationTheme with Diagnosticable { this.enabledBorder, this.border, this.alignLabelWithHint = false, + this.constraints, }) : assert(isDense != null), assert(isCollapsed != null), assert(filled != null), @@ -3972,6 +4004,23 @@ class InputDecorationTheme with Diagnosticable { /// behavior of aligning the label with the center of the [TextField]. final bool alignLabelWithHint; + /// Defines minimum and maximum sizes for the [InputDecorator]. + /// + /// Typically the decorator will fill the horizontal space it is given. For + /// larger screens, it may be useful to have the maximum width clamped to + /// a given value so it doesn't fill the whole screen. This property + /// allows you to control how big the decorator will be in its available + /// space. + /// + /// If null, then the decorator will fill the available width with + /// a default height based on text size. + /// + /// See also: + /// + /// * [InputDecoration.constraints], which can override this setting for a + /// given decorator. + final BoxConstraints? constraints; + /// Creates a copy of this object but with the given fields replaced with the /// new values. InputDecorationTheme copyWith({ @@ -4004,6 +4053,7 @@ class InputDecorationTheme with Diagnosticable { InputBorder? enabledBorder, InputBorder? border, bool? alignLabelWithHint, + BoxConstraints? constraints, }) { return InputDecorationTheme( labelStyle: labelStyle ?? this.labelStyle, @@ -4031,6 +4081,7 @@ class InputDecorationTheme with Diagnosticable { enabledBorder: enabledBorder ?? this.enabledBorder, border: border ?? this.border, alignLabelWithHint: alignLabelWithHint ?? this.alignLabelWithHint, + constraints: constraints ?? this.constraints, ); } @@ -4062,6 +4113,7 @@ class InputDecorationTheme with Diagnosticable { enabledBorder, border, alignLabelWithHint, + constraints, ]); } @@ -4096,6 +4148,7 @@ class InputDecorationTheme with Diagnosticable { && other.enabledBorder == enabledBorder && other.border == border && other.alignLabelWithHint == alignLabelWithHint + && other.constraints == constraints && other.disabledBorder == disabledBorder; } @@ -4128,5 +4181,6 @@ class InputDecorationTheme with Diagnosticable { properties.add(DiagnosticsProperty('enabledBorder', enabledBorder, defaultValue: defaultTheme.enabledBorder)); properties.add(DiagnosticsProperty('border', border, defaultValue: defaultTheme.border)); properties.add(DiagnosticsProperty('alignLabelWithHint', alignLabelWithHint, defaultValue: defaultTheme.alignLabelWithHint)); + properties.add(DiagnosticsProperty('constraints', constraints, defaultValue: defaultTheme.constraints)); } } diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index 0b56a55e12..8ae3805dba 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -1728,6 +1728,49 @@ void main() { expect(tester.getTopLeft(find.byKey(prefixKey)).dy, 0.0); }); + group('constraints', () { + testWidgets('No InputDecorator constraints', (WidgetTester tester) async { + await tester.pumpWidget(buildInputDecorator()); + + // Should fill the screen width and be default height + expect(tester.getSize(find.byType(InputDecorator)), const Size(800, 48)); + }); + + testWidgets('InputDecoratorThemeData constraints', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + theme: ThemeData( + inputDecorationTheme: const InputDecorationTheme( + constraints: BoxConstraints(maxWidth: 300, maxHeight: 40), + ), + ), + ) + ); + + // Theme settings should make it 300x40 pixels + expect(tester.getSize(find.byType(InputDecorator)), const Size(300, 40)); + }); + + testWidgets('InputDecorator constraints', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + theme: ThemeData( + inputDecorationTheme: const InputDecorationTheme( + constraints: BoxConstraints(maxWidth: 300, maxHeight: 40), + ), + ), + decoration: const InputDecoration( + constraints: BoxConstraints(maxWidth: 200, maxHeight: 32), + ), + ) + ); + + // InputDecoration.constraints should override the theme. It should be + // only 200x32 pixels + expect(tester.getSize(find.byType(InputDecorator)), const Size(200, 32)); + }); + }); + group('textAlignVertical position', () { group('simple case', () { testWidgets('align top (default)', (WidgetTester tester) async { @@ -3139,6 +3182,7 @@ void main() { focusColor: Colors.blue, border: InputBorder.none, alignLabelWithHint: true, + constraints: BoxConstraints(minWidth: 10, maxWidth: 20, minHeight: 30, maxHeight: 40), ), ); @@ -3155,6 +3199,7 @@ void main() { expect(decoration.fillColor, Colors.red); expect(decoration.border, InputBorder.none); expect(decoration.alignLabelWithHint, true); + expect(decoration.constraints, const BoxConstraints(minWidth: 10, maxWidth: 20, minHeight: 30, maxHeight: 40)); // InputDecoration (baseDecoration) defines InputDecoration properties decoration = const InputDecoration( @@ -3171,6 +3216,7 @@ void main() { fillColor: Colors.blue, border: OutlineInputBorder(), alignLabelWithHint: false, + constraints: BoxConstraints(minWidth: 10, maxWidth: 20, minHeight: 30, maxHeight: 40), ).applyDefaults( const InputDecorationTheme( labelStyle: themeStyle, @@ -3189,6 +3235,7 @@ void main() { focusColor: Colors.blue, border: InputBorder.none, alignLabelWithHint: true, + constraints: BoxConstraints(minWidth: 40, maxWidth: 30, minHeight: 20, maxHeight: 10), ), ); @@ -3207,6 +3254,7 @@ void main() { expect(decoration.fillColor, Colors.blue); expect(decoration.border, const OutlineInputBorder()); expect(decoration.alignLabelWithHint, false); + expect(decoration.constraints, const BoxConstraints(minWidth: 10, maxWidth: 20, minHeight: 30, maxHeight: 40)); }); testWidgets('InputDecorator OutlineInputBorder fillColor is clipped by border', (WidgetTester tester) async {