diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index 656d48c625c12..e7c5ac47fb30d 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -49,9 +49,9 @@ const double _kInputExtraPadding = 4.0; // Padding between the character counter and helper/error text to prevent overlap. // Based on Material 3 specification for text fields. -const double _kSubtextCounterPadding = 16.0; +const double _kSupportingTextCounterPadding = 16.0; -typedef _SubtextSize = ({double ascent, double bottomHeight, double subtextHeight}); +typedef _SupportingTextSize = ({double ascent, double bottomHeight, double supportingTextHeight}); typedef _ChildBaselineGetter = double Function(RenderBox child, BoxConstraints constraints); // The default duration for hint fade in/out transitions. @@ -316,7 +316,7 @@ class _HelperError extends StatefulWidget { class _HelperErrorState extends State<_HelperError> with SingleTickerProviderStateMixin { // If the height of this widget and the counter are zero ("empty") at - // layout time, no space is allocated for the subtext. + // layout time, no space is allocated for the supportingText. static const Widget empty = SizedBox.shrink(); late AnimationController _controller; @@ -610,6 +610,7 @@ class _Decoration { this.helperError, this.counter, this.container, + this.supportingTextPadding, }); final EdgeInsetsDirectional contentPadding; @@ -637,6 +638,7 @@ class _Decoration { final Widget? helperError; final Widget? counter; final Widget? container; + final EdgeInsetsDirectional? supportingTextPadding; @override bool operator ==(Object other) { @@ -671,7 +673,8 @@ class _Decoration { other.suffixIcon == suffixIcon && other.helperError == helperError && other.counter == counter && - other.container == container; + other.container == container && + other.supportingTextPadding == supportingTextPadding; } @override @@ -695,7 +698,7 @@ class _Decoration { hint, prefix, suffix, - Object.hash(prefixIcon, suffixIcon, helperError, counter, container), + Object.hash(prefixIcon, suffixIcon, helperError, counter, container, supportingTextPadding), ); } @@ -707,14 +710,14 @@ class _RenderDecorationLayout { required this.inputConstraints, required this.baseline, required this.containerHeight, - required this.subtextSize, + required this.supportingTextSize, required this.size, }); final BoxConstraints inputConstraints; final double baseline; final double containerHeight; - final _SubtextSize? subtextSize; + final _SupportingTextSize? supportingTextSize; final Size size; } @@ -739,7 +742,7 @@ class _RenderDecoration extends RenderBox // TODO(bleroux): consider defining this value as a Material token and making it // configurable by InputDecorationThemeData. - double get subtextGap => material3 ? 4.0 : 8.0; + double get supportingTextGap => material3 ? 4.0 : 8.0; double get prefixToInputGap => material3 ? 4.0 : 0.0; double get inputToSuffixGap => material3 ? 4.0 : 0.0; @@ -921,7 +924,9 @@ class _RenderDecoration extends RenderBox EdgeInsetsDirectional get contentPadding => decoration.contentPadding; - _SubtextSize? _computeSubtextSizes({ + EdgeInsetsDirectional? get supportingTextPadding => decoration.supportingTextPadding; + + _SupportingTextSize? _computeSupportingTextSizes({ required BoxConstraints constraints, required ChildLayouter layoutChild, required _ChildBaselineGetter getBaseline, @@ -932,7 +937,7 @@ class _RenderDecoration extends RenderBox }; // Only add padding when counter is present (maxLength is used). - final double counterPadding = counter != null ? _kSubtextCounterPadding : 0.0; + final double counterPadding = counter != null ? _kSupportingTextCounterPadding : 0.0; final BoxConstraints helperErrorConstraints = constraints.deflate( EdgeInsets.only(left: counterSize.width + counterPadding), ); @@ -943,13 +948,20 @@ class _RenderDecoration extends RenderBox } // TODO(LongCatIsLooong): the bottomHeight expression doesn't make much sense. - // Use the real descent and make sure the subtext line box is tall enough for both children. + // Use the real descent and make sure the supportingText line box is tall enough for both children. // See https://github.com/flutter/flutter/issues/13715 + + // The vertical padding around the row containing the helper, error, and counter widgets. + // Defaults to `supportingTextGap` at the top and 0.0 at the bottom when `supportingTextPadding` is null. + final double topPadding = supportingTextPadding?.top ?? supportingTextGap; + final double bottomPadding = supportingTextPadding?.bottom ?? 0.0; final double ascent = - math.max(counterAscent, getBaseline(helperError, helperErrorConstraints)) + subtextGap; - final double bottomHeight = math.max(counterAscent, helperErrorHeight) + subtextGap; - final double subtextHeight = math.max(counterSize.height, helperErrorHeight) + subtextGap; - return (ascent: ascent, bottomHeight: bottomHeight, subtextHeight: subtextHeight); + math.max(counterAscent, getBaseline(helperError, helperErrorConstraints)) + topPadding; + final double bottomHeight = + math.max(counterAscent, helperErrorHeight) + (topPadding + bottomPadding); + final double supportingTextHeight = + math.max(counterSize.height, helperErrorHeight) + (topPadding + bottomPadding); + return (ascent: ascent, bottomHeight: bottomHeight, supportingTextHeight: supportingTextHeight); } // Returns a value used by performLayout to position all of the renderers. @@ -985,11 +997,17 @@ class _RenderDecoration extends RenderBox end: contentPadding.end + decoration.inputGap, ), ); + final BoxConstraints supportingTextConstraints = containerConstraints.deflate( + EdgeInsetsDirectional.only( + start: (supportingTextPadding?.start ?? contentPadding.start) + decoration.inputGap, + end: (supportingTextPadding?.end ?? contentPadding.end) + decoration.inputGap, + ), + ); // The helper or error text can occupy the full width less the space // occupied by the icon and counter. - final _SubtextSize? subtextSize = _computeSubtextSizes( - constraints: contentConstraints, + final _SupportingTextSize? supportingTextSize = _computeSupportingTextSizes( + constraints: supportingTextConstraints, layoutChild: layoutChild, getBaseline: getBaseline, ); @@ -1061,7 +1079,7 @@ class _RenderDecoration extends RenderBox // The height of the input needs to accommodate label above and counter and // helperError below, when they exist. - final double bottomHeight = subtextSize?.bottomHeight ?? 0.0; + final double bottomHeight = supportingTextSize?.bottomHeight ?? 0.0; final BoxConstraints inputConstraints = boxConstraints .deflate( EdgeInsets.only( @@ -1189,8 +1207,11 @@ class _RenderDecoration extends RenderBox inputConstraints: inputConstraints, containerHeight: containerHeight, baseline: baseline, - subtextSize: subtextSize, - size: Size(constraints.maxWidth, containerHeight + (subtextSize?.subtextHeight ?? 0.0)), + supportingTextSize: supportingTextSize, + size: Size( + constraints.maxWidth, + containerHeight + (supportingTextSize?.supportingTextHeight ?? 0.0), + ), ); } @@ -1271,6 +1292,12 @@ class _RenderDecoration extends RenderBox final double iconWidth = _minWidth(icon, iconHeight); width = math.max(width - iconWidth, 0.0); + final double supportingTextWidth = math.max( + width - + (supportingTextPadding?.horizontal ?? contentPadding.horizontal) - + decoration.inputGap * 2, + 0.0, + ); final double prefixIconHeight = _minHeight(prefixIcon, width); final double prefixIconWidth = _minWidth(prefixIcon, prefixIconHeight); @@ -1280,18 +1307,23 @@ class _RenderDecoration extends RenderBox width = math.max(width - contentPadding.horizontal - decoration.inputGap * 2, 0.0); - // TODO(LongCatIsLooong): use _computeSubtextSizes for subtext intrinsic sizes. + // TODO(LongCatIsLooong): use _computeSupportingTextSizes for supportingText intrinsic sizes. // See https://github.com/flutter/flutter/issues/13715. - final double counterHeight = _minHeight(counter, width); + final double counterHeight = _minHeight(counter, supportingTextWidth); final double counterWidth = _minWidth(counter, counterHeight); // Only add padding when counter is present (maxLength is used). - final double counterPadding = counter != null ? _kSubtextCounterPadding : 0.0; - final double helperErrorAvailableWidth = math.max(width - counterWidth - counterPadding, 0.0); + final double counterPadding = counter != null ? _kSupportingTextCounterPadding : 0.0; + final double helperErrorAvailableWidth = math.max( + supportingTextWidth - counterWidth - counterPadding, + 0.0, + ); final double helperErrorHeight = _minHeight(helperError, helperErrorAvailableWidth); - double subtextHeight = math.max(counterHeight, helperErrorHeight); - if (subtextHeight > 0.0) { - subtextHeight += subtextGap; + double supportingTextHeight = math.max(counterHeight, helperErrorHeight); + if (supportingTextHeight > 0.0) { + final double topPadding = supportingTextPadding?.top ?? supportingTextGap; + final double bottomPadding = supportingTextPadding?.bottom ?? 0.0; + supportingTextHeight += topPadding + bottomPadding; } final double prefixHeight = _minHeight(prefix, width); @@ -1330,7 +1362,7 @@ class _RenderDecoration extends RenderBox ? 0.0 : kMinInteractiveDimension; - return math.max(containerHeight, minContainerHeight) + subtextHeight; + return math.max(containerHeight, minContainerHeight) + supportingTextHeight; } @override @@ -1426,37 +1458,46 @@ class _RenderDecoration extends RenderBox centerLayout(icon!, x); } - final double subtextBaseline = (layout.subtextSize?.ascent ?? 0.0) + layout.containerHeight; + final double supportingTextBaseline = + (layout.supportingTextSize?.ascent ?? 0.0) + layout.containerHeight; final RenderBox? counter = this.counter; final double helperErrorBaseline = helperError.getDistanceToBaseline(TextBaseline.alphabetic)!; final double counterBaseline = counter?.getDistanceToBaseline(TextBaseline.alphabetic)! ?? 0.0; - double start, end; + double start, end, startSupporting, endSupporting; switch (textDirection) { case TextDirection.ltr: start = contentPadding.start + _boxSize(icon).width; end = overallWidth - contentPadding.end; + startSupporting = + (supportingTextPadding?.start ?? contentPadding.start) + _boxSize(icon).width; + endSupporting = overallWidth - (supportingTextPadding?.end ?? contentPadding.end); _boxParentData(helperError).offset = Offset( - start + decoration.inputGap, - subtextBaseline - helperErrorBaseline, + startSupporting + decoration.inputGap, + supportingTextBaseline - helperErrorBaseline, ); if (counter != null) { _boxParentData(counter).offset = Offset( - end - counter.size.width - decoration.inputGap, - subtextBaseline - counterBaseline, + endSupporting - counter.size.width - decoration.inputGap, + supportingTextBaseline - counterBaseline, ); } case TextDirection.rtl: start = overallWidth - contentPadding.start - _boxSize(icon).width; end = contentPadding.end; + startSupporting = + overallWidth - + (supportingTextPadding?.start ?? contentPadding.start) - + _boxSize(icon).width; + endSupporting = supportingTextPadding?.end ?? contentPadding.end; _boxParentData(helperError).offset = Offset( - start - helperError.size.width - decoration.inputGap, - subtextBaseline - helperErrorBaseline, + startSupporting - helperError.size.width - decoration.inputGap, + supportingTextBaseline - helperErrorBaseline, ); if (counter != null) { _boxParentData(counter).offset = Offset( - end + decoration.inputGap, - subtextBaseline - counterBaseline, + endSupporting + decoration.inputGap, + supportingTextBaseline - counterBaseline, ); } } @@ -2588,6 +2629,23 @@ class _InputDecoratorState extends State with TickerProviderStat resolvedPadding.bottom, ); + final EdgeInsets? resolvedSupportingTextPadding = decoration.supportingTextPadding?.resolve( + textDirection, + ); + final EdgeInsetsDirectional? decorationSupportingTextPadding = + resolvedSupportingTextPadding == null + ? null + : EdgeInsetsDirectional.fromSTEB( + flipHorizontal + ? resolvedSupportingTextPadding.right + : resolvedSupportingTextPadding.left, + resolvedSupportingTextPadding.top, + flipHorizontal + ? resolvedSupportingTextPadding.left + : resolvedSupportingTextPadding.right, + resolvedSupportingTextPadding.bottom, + ); + final EdgeInsetsDirectional contentPadding; final double floatingLabelHeight; @@ -2672,6 +2730,7 @@ class _InputDecoratorState extends State with TickerProviderStat helperError: helperError, counter: counter, container: container, + supportingTextPadding: decorationSupportingTextPadding, ), textDirection: textDirection, textBaseline: textBaseline, @@ -2850,6 +2909,7 @@ class InputDecoration { this.alignLabelWithHint, this.constraints, this.visualDensity, + this.supportingTextPadding, }) : assert( !(label != null && labelText != null), 'Declaring both label and labelText is not supported.', @@ -2959,7 +3019,8 @@ class InputDecoration { // ignore: prefer_initializing_formals, (can't use initializing formals for a deprecated parameter). floatingLabelAlignment = floatingLabelAlignment, alignLabelWithHint = false, - visualDensity = null; + visualDensity = null, + supportingTextPadding = null; /// An icon to show before the input field and outside of the decoration's /// container. @@ -3906,6 +3967,18 @@ class InputDecoration { /// given decorator. final VisualDensity? visualDensity; + /// {@template flutter.material.inputDecoration.supportingTextPadding} + /// The padding applied to the supporting text row. + /// + /// This padding is applied specifically to supporting text and is independent of [contentPadding]. + /// If [supportingTextPadding] is null, the value of [contentPadding] will be used + /// for all supporting text widgets including [InputDecoration.helper], [InputDecoration.counter] + /// and [InputDecoration.error]. + /// When non-null, it completely overrides the default behavior, including the default + /// vertical gap between the input container and the supporting text row. + /// {@endtemplate} + final EdgeInsetsGeometry? supportingTextPadding; + /// Creates a copy of this input decoration with the given fields replaced /// by the new values. InputDecoration copyWith({ @@ -3968,6 +4041,7 @@ class InputDecoration { BoxConstraints? constraints, VisualDensity? visualDensity, SemanticsService? semanticsService, + EdgeInsetsGeometry? supportingTextPadding, }) { return InputDecoration( icon: icon ?? this.icon, @@ -4028,6 +4102,7 @@ class InputDecoration { alignLabelWithHint: alignLabelWithHint ?? this.alignLabelWithHint, constraints: constraints ?? this.constraints, visualDensity: visualDensity ?? this.visualDensity, + supportingTextPadding: supportingTextPadding ?? this.supportingTextPadding, ); } @@ -4083,6 +4158,7 @@ class InputDecoration { alignLabelWithHint: alignLabelWithHint ?? theme.alignLabelWithHint, constraints: constraints ?? theme.constraints, visualDensity: visualDensity ?? theme.visualDensity, + supportingTextPadding: supportingTextPadding ?? theme.supportingTextPadding, ); } @@ -4152,7 +4228,8 @@ class InputDecoration { other.semanticCounterText == semanticCounterText && other.alignLabelWithHint == alignLabelWithHint && other.constraints == constraints && - other.visualDensity == visualDensity; + other.visualDensity == visualDensity && + other.supportingTextPadding == supportingTextPadding; } @override @@ -4216,6 +4293,7 @@ class InputDecoration { alignLabelWithHint, constraints, visualDensity, + supportingTextPadding, ]; return Object.hashAll(values); } @@ -4277,6 +4355,7 @@ class InputDecoration { if (alignLabelWithHint != null) 'alignLabelWithHint: $alignLabelWithHint', if (constraints != null) 'constraints: $constraints', if (visualDensity != null) 'visualDensity: $visualDensity', + if (supportingTextPadding != null) 'supportingTextPadding: $supportingTextPadding', ]; return 'InputDecoration(${description.join(', ')})'; } @@ -4336,6 +4415,7 @@ class InputDecorationTheme extends InheritedTheme with Diagnosticable { bool? alignLabelWithHint, BoxConstraints? constraints, VisualDensity? visualDensity, + EdgeInsetsGeometry? supportingTextPadding, InputDecorationThemeData? data, Widget? child, }) : assert( @@ -4376,7 +4456,8 @@ class InputDecorationTheme extends InheritedTheme with Diagnosticable { border ?? alignLabelWithHint ?? constraints ?? - visualDensity) == + visualDensity ?? + supportingTextPadding) == null, ), _labelStyle = labelStyle, @@ -4416,6 +4497,7 @@ class InputDecorationTheme extends InheritedTheme with Diagnosticable { _alignLabelWithHint = alignLabelWithHint ?? false, _constraints = constraints, _visualDensity = visualDensity, + _supportingTextPadding = supportingTextPadding, _data = data, super(child: child ?? const SizedBox.shrink()); @@ -4457,6 +4539,7 @@ class InputDecorationTheme extends InheritedTheme with Diagnosticable { final bool _alignLabelWithHint; final BoxConstraints? _constraints; final VisualDensity? _visualDensity; + final EdgeInsetsGeometry? _supportingTextPadding; /// Overrides the default value for [InputDecoration.labelStyle]. /// @@ -4687,6 +4770,13 @@ class InputDecorationTheme extends InheritedTheme with Diagnosticable { /// please use the [InputDecorationThemeData.visualDensity] property in [data] instead. VisualDensity? get visualDensity => _data != null ? _data.visualDensity : _visualDensity; + /// Overrides the default value for [InputDecoration.supportingTextPadding]. + /// + /// This property is obsolete and will be deprecated in a future release: + /// please use the [InputDecorationThemeData.supportingTextPadding] property in [data] instead. + EdgeInsetsGeometry? get supportingTextPadding => + _data != null ? _data.supportingTextPadding : _supportingTextPadding; + /// The properties used for all descendant [TabBar] widgets. InputDecorationThemeData get data => _data ?? @@ -4728,6 +4818,7 @@ class InputDecorationTheme extends InheritedTheme with Diagnosticable { alignLabelWithHint: _alignLabelWithHint, constraints: _constraints, visualDensity: _visualDensity, + supportingTextPadding: _supportingTextPadding, ); /// Returns the closest [InputDecorationThemeData] instance given the build context. @@ -4789,6 +4880,7 @@ class InputDecorationTheme extends InheritedTheme with Diagnosticable { bool? alignLabelWithHint, BoxConstraints? constraints, VisualDensity? visualDensity, + EdgeInsetsGeometry? supportingTextPadding, }) { return InputDecorationTheme( labelStyle: labelStyle ?? this.labelStyle, @@ -4828,6 +4920,7 @@ class InputDecorationTheme extends InheritedTheme with Diagnosticable { alignLabelWithHint: alignLabelWithHint ?? this.alignLabelWithHint, constraints: constraints ?? this.constraints, visualDensity: visualDensity ?? this.visualDensity, + supportingTextPadding: supportingTextPadding ?? this.supportingTextPadding, ); } @@ -4876,6 +4969,7 @@ class InputDecorationTheme extends InheritedTheme with Diagnosticable { border: border ?? other.border, constraints: constraints ?? other.constraints, visualDensity: visualDensity ?? other.visualDensity, + supportingTextPadding: supportingTextPadding ?? other.supportingTextPadding, ); } @@ -4943,6 +5037,7 @@ class InputDecorationThemeData with Diagnosticable { this.alignLabelWithHint = false, this.constraints, this.visualDensity, + this.supportingTextPadding, }); /// {@macro flutter.material.inputDecoration.labelStyle} @@ -5381,6 +5476,9 @@ class InputDecorationThemeData with Diagnosticable { /// given decorator. final VisualDensity? visualDensity; + /// {@macro flutter.material.inputDecoration.supportingTextPadding} + final EdgeInsetsGeometry? supportingTextPadding; + /// Creates a copy of this object but with the given fields replaced with the /// new values. InputDecorationThemeData copyWith({ @@ -5421,6 +5519,7 @@ class InputDecorationThemeData with Diagnosticable { bool? alignLabelWithHint, BoxConstraints? constraints, VisualDensity? visualDensity, + EdgeInsetsGeometry? supportingTextPadding, }) { return InputDecorationThemeData( labelStyle: labelStyle ?? this.labelStyle, @@ -5460,6 +5559,7 @@ class InputDecorationThemeData with Diagnosticable { alignLabelWithHint: alignLabelWithHint ?? this.alignLabelWithHint, constraints: constraints ?? this.constraints, visualDensity: visualDensity ?? this.visualDensity, + supportingTextPadding: supportingTextPadding ?? this.supportingTextPadding, ); } @@ -5510,6 +5610,7 @@ class InputDecorationThemeData with Diagnosticable { border: border ?? other.border, constraints: constraints ?? other.constraints, visualDensity: visualDensity ?? other.visualDensity, + supportingTextPadding: supportingTextPadding ?? other.supportingTextPadding, ); } @@ -5553,6 +5654,7 @@ class InputDecorationThemeData with Diagnosticable { constraints, hintFadeDuration, visualDensity, + supportingTextPadding, ), ); @@ -5602,7 +5704,8 @@ class InputDecorationThemeData with Diagnosticable { other.alignLabelWithHint == alignLabelWithHint && other.constraints == constraints && other.disabledBorder == disabledBorder && - other.visualDensity == visualDensity; + other.visualDensity == visualDensity && + other.supportingTextPadding == supportingTextPadding; } @override @@ -5680,6 +5783,13 @@ class InputDecorationThemeData with Diagnosticable { defaultValue: defaultTheme.contentPadding, ), ); + properties.add( + DiagnosticsProperty( + 'supportingTextPadding', + supportingTextPadding, + defaultValue: defaultTheme.supportingTextPadding, + ), + ); properties.add( DiagnosticsProperty('isCollapsed', isCollapsed, defaultValue: defaultTheme.isCollapsed), ); diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart index 6977ff0771ceb..e0d55d47f2117 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -571,7 +571,7 @@ class FormField extends StatefulWidget { /// value. /// /// Alternating between error and normal state can cause the height of the - /// [TextFormField] to change if no other subtext decoration is set on the + /// [TextFormField] to change if no other supportingText decoration is set on the /// field. To create a field whose height is fixed regardless of whether or /// not an error is displayed, either wrap the [TextFormField] in a fixed /// height parent like [SizedBox], or set the [InputDecoration.helperText] diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index 6db201346f2fb..8394a957affa3 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -11734,7 +11734,7 @@ void main() { // 12 - help/error/counter text (font size 12dps) // // When the label is not floating, it's vertically centered in the space - // above the subtext: + // above the supportingText: // // 20 - top padding // 16 - label (font size 16dps) @@ -11793,14 +11793,14 @@ void main() { // 12 - help/error/counter text (font size 12dps) // // When the label is not floating, it's vertically centered in the space - // above the subtext: + // above the supportingText: // // 16 - top padding // 16 - label (font size 16dps) // 16 - bottom padding (empty input text still appears here) // 8 - below the border padding // 12 - help/error/counter text (font size 12dps) - // The layout of the error/helper/counter subtext doesn't change for dense layout. + // The layout of the error/helper/counter supportingText doesn't change for dense layout. await tester.pumpWidget( buildInputDecoratorM2( // isEmpty: false (default) @@ -15770,4 +15770,191 @@ void main() { ); expect(tester.getSize(find.byType(InputDecorator)), Size.zero); }); + + testWidgets('supportingTextPadding defined in InputDecoration is used for supporting text', ( + WidgetTester tester, + ) async { + const customPaddingStart = 32.0; + const customPaddingEnd = 24.0; + const customPaddingTop = 16.0; + const customPaddingBottom = 12.0; + const inputWidth = 300.0; + const errorText = 'error'; + const helperText = 'helper'; + const counterText = 'counter'; + + Future buildDecorator({ + required TextDirection direction, + EdgeInsetsGeometry? supportingTextPadding, + String? errorText, + String? helperText, + }) async { + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Scaffold( + body: Directionality( + textDirection: direction, + child: SizedBox( + width: inputWidth, + child: InputDecorator( + decoration: InputDecoration( + filled: true, + errorText: errorText, + helperText: helperText, + counterText: counterText, + supportingTextPadding: supportingTextPadding, + ), + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + } + + final Finder errorFinder = find.text(errorText); + final Finder helperFinder = find.text(helperText); + final Finder counterFinder = find.text(counterText); + + const inputGap = 4.0; // _kInputExtraPadding in Material 3 filled field. + + // Calculate default vertical bounds first to ensure new vertical padding shifts correctly. + await buildDecorator(direction: TextDirection.ltr, errorText: errorText); + final double defaultErrorDy = tester.getTopLeft(errorFinder).dy; + final double defaultTotalHeight = tester.getSize(find.byType(InputDecorator)).height; + + // LTR with custom supportingTextPadding (error). + await buildDecorator( + direction: TextDirection.ltr, + errorText: errorText, + supportingTextPadding: const EdgeInsetsDirectional.only( + start: customPaddingStart, + end: customPaddingEnd, + top: customPaddingTop, + bottom: customPaddingBottom, + ), + ); + expect(tester.getTopLeft(errorFinder).dx, customPaddingStart + inputGap); + expect(tester.getTopRight(counterFinder).dx, inputWidth - customPaddingEnd - inputGap); + + // Verify vertical padding shift. + final double customErrorDy = tester.getTopLeft(errorFinder).dy; + final double customTotalHeight = tester.getSize(find.byType(InputDecorator)).height; + expect(customErrorDy > defaultErrorDy, true); + expect(customTotalHeight > defaultTotalHeight, true); + + // RTL with custom supportingTextPadding (error). + await buildDecorator( + direction: TextDirection.rtl, + errorText: errorText, + supportingTextPadding: const EdgeInsetsDirectional.only( + start: customPaddingStart, + end: customPaddingEnd, + ), + ); + // In RTL, "start" is from the right, so errorText is placed on the right. + expect(tester.getTopRight(errorFinder).dx, inputWidth - customPaddingStart - inputGap); + // In RTL, "end" is from the left, so counterText is placed on the left. + expect(tester.getTopLeft(counterFinder).dx, customPaddingEnd + inputGap); + + // LTR with custom supportingTextPadding (helper). + await buildDecorator( + direction: TextDirection.ltr, + helperText: helperText, + supportingTextPadding: const EdgeInsetsDirectional.only( + start: customPaddingStart, + end: customPaddingEnd, + ), + ); + expect(tester.getTopLeft(helperFinder).dx, customPaddingStart + inputGap); + expect(tester.getTopRight(counterFinder).dx, inputWidth - customPaddingEnd - inputGap); + + // RTL with custom supportingTextPadding (helper). + await buildDecorator( + direction: TextDirection.rtl, + helperText: helperText, + supportingTextPadding: const EdgeInsetsDirectional.only( + start: customPaddingStart, + end: customPaddingEnd, + ), + ); + expect(tester.getTopRight(helperFinder).dx, inputWidth - customPaddingStart - inputGap); + expect(tester.getTopLeft(counterFinder).dx, customPaddingEnd + inputGap); + }); + + testWidgets('supportingTextPadding defined in InputDecorationTheme is used for supporting text', ( + WidgetTester tester, + ) async { + const themePaddingStart = 40.0; + const themePaddingEnd = 20.0; + const inputWidth = 300.0; + const errorText = 'error'; + const helperText = 'helper'; + const counterText = 'counter'; + + Future buildDecorator({ + required TextDirection direction, + String? errorText, + String? helperText, + }) async { + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + useMaterial3: true, + inputDecorationTheme: const InputDecorationThemeData( + supportingTextPadding: EdgeInsetsDirectional.only( + start: themePaddingStart, + end: themePaddingEnd, + ), + ), + ), + home: Scaffold( + body: Directionality( + textDirection: direction, + child: SizedBox( + width: inputWidth, + child: InputDecorator( + decoration: InputDecoration( + filled: true, + errorText: errorText, + helperText: helperText, + counterText: counterText, + ), + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + } + + final Finder errorFinder = find.text(errorText); + final Finder helperFinder = find.text(helperText); + final Finder counterFinder = find.text(counterText); + const inputGap = 4.0; + + // LTR with theme supportingTextPadding (error). + await buildDecorator(direction: TextDirection.ltr, errorText: errorText); + expect(tester.getTopLeft(errorFinder).dx, themePaddingStart + inputGap); + expect(tester.getTopRight(counterFinder).dx, inputWidth - themePaddingEnd - inputGap); + + // RTL with theme supportingTextPadding (error). + await buildDecorator(direction: TextDirection.rtl, errorText: errorText); + // In RTL, "start" is from the right, "end" is from the left. + expect(tester.getTopRight(errorFinder).dx, inputWidth - themePaddingStart - inputGap); + expect(tester.getTopLeft(counterFinder).dx, themePaddingEnd + inputGap); + + // LTR with theme supportingTextPadding (helper). + await buildDecorator(direction: TextDirection.ltr, helperText: helperText); + expect(tester.getTopLeft(helperFinder).dx, themePaddingStart + inputGap); + expect(tester.getTopRight(counterFinder).dx, inputWidth - themePaddingEnd - inputGap); + + // RTL with theme supportingTextPadding (helper). + await buildDecorator(direction: TextDirection.rtl, helperText: helperText); + expect(tester.getTopRight(helperFinder).dx, inputWidth - themePaddingStart - inputGap); + expect(tester.getTopLeft(counterFinder).dx, themePaddingEnd + inputGap); + }); } diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 1dcb14898e26a..2a399175bb23f 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -5162,8 +5162,8 @@ void main() { // Adding a counter causes the EditableText to shrink to fit the counter // inside the parent as well. const counterHeight = 40.0; - const subtextGap = 8.0; - const double counterSpace = counterHeight + subtextGap; + const supportingTextGap = 8.0; + const double counterSpace = counterHeight + supportingTextGap; await tester.pumpWidget(containedTextFieldBuilder(counter: Container(height: counterHeight))); expect(findEditableText(), equals(inputBox)); expect(inputBox.size.height, height - padding - counterSpace); @@ -5173,7 +5173,7 @@ void main() { await tester.pumpWidget(containedTextFieldBuilder(helperText: 'I am helperText')); expect(findEditableText(), equals(inputBox)); const helperTextSpace = 12.0; - expect(inputBox.size.height, height - padding - helperTextSpace - subtextGap); + expect(inputBox.size.height, height - padding - helperTextSpace - supportingTextGap); // When both helperText and counter are present, EditableText shrinks by the // height of the taller of the two in order to fit both within the parent.