diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index e3f4e1077a..c54ba8a05a 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -302,6 +302,7 @@ class _HelperError extends StatefulWidget { this.helperText, this.helperStyle, this.helperMaxLines, + this.error, this.errorText, this.errorStyle, this.errorMaxLines, @@ -311,6 +312,7 @@ class _HelperError extends StatefulWidget { final String? helperText; final TextStyle? helperStyle; final int? helperMaxLines; + final Widget? error; final String? errorText; final TextStyle? errorStyle; final int? errorMaxLines; @@ -328,6 +330,8 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta Widget? _helper; Widget? _error; + bool get _hasError => widget.errorText != null || widget.error != null; + @override void initState() { super.initState(); @@ -335,7 +339,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta duration: _kTransitionDuration, vsync: this, ); - if (widget.errorText != null) { + if (_hasError) { _error = _buildError(); _controller.value = 1.0; } else if (widget.helperText != null) { @@ -360,16 +364,19 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta void didUpdateWidget(_HelperError old) { super.didUpdateWidget(old); + final Widget? newError = widget.error; final String? newErrorText = widget.errorText; final String? newHelperText = widget.helperText; + final Widget? oldError = old.error; final String? oldErrorText = old.errorText; final String? oldHelperText = old.helperText; + final bool errorStateChanged = (newError != null) != (oldError != null); final bool errorTextStateChanged = (newErrorText != null) != (oldErrorText != null); final bool helperTextStateChanged = newErrorText == null && (newHelperText != null) != (oldHelperText != null); - if (errorTextStateChanged || helperTextStateChanged) { - if (newErrorText != null) { + if (errorStateChanged || errorTextStateChanged || helperTextStateChanged) { + if (newError != null || newErrorText != null) { _error = _buildError(); _controller.forward(); } else if (newHelperText != null) { @@ -399,7 +406,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta } Widget _buildError() { - assert(widget.errorText != null); + assert(widget.error != null || widget.errorText != null); return Semantics( container: true, child: FadeTransition( @@ -409,7 +416,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta begin: const Offset(0.0, -0.25), end: Offset.zero, ).evaluate(_controller.view), - child: Text( + child: widget.error ?? Text( widget.errorText!, style: widget.errorStyle, textAlign: widget.textAlign, @@ -435,7 +442,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta if (_controller.isCompleted) { _helper = null; - if (widget.errorText != null) { + if (_hasError) { return _error = _buildError(); } else { _error = null; @@ -443,7 +450,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta } } - if (_helper == null && widget.errorText != null) { + if (_helper == null && _hasError) { return _buildError(); } @@ -451,7 +458,7 @@ class _HelperErrorState extends State<_HelperError> with SingleTickerProviderSta return _buildHelper(); } - if (widget.errorText != null) { + if (_hasError) { return Stack( children: [ FadeTransition( @@ -2365,6 +2372,7 @@ class _InputDecoratorState extends State with TickerProviderStat helperText: decoration.helperText, helperStyle: _getHelperStyle(themeData, defaults), helperMaxLines: decoration.helperMaxLines, + error: decoration.error, errorText: decoration.errorText, errorStyle: _getErrorStyle(themeData, defaults), errorMaxLines: decoration.errorMaxLines, @@ -2562,6 +2570,7 @@ class InputDecoration { this.hintStyle, this.hintTextDirection, this.hintMaxLines, + this.error, this.errorText, this.errorStyle, this.errorMaxLines, @@ -2601,7 +2610,8 @@ class InputDecoration { this.constraints, }) : assert(!(label != null && labelText != null), 'Declaring both label and labelText is not supported.'), 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.'); + assert(!(suffix != null && suffixText != null), 'Declaring both suffix and suffixText is not supported.'), + assert(!(error != null && errorText != null), 'Declaring both error and errorText is not supported.'); /// Defines an [InputDecorator] that is the same size as the input field. /// @@ -2630,6 +2640,7 @@ class InputDecoration { helperStyle = null, helperMaxLines = null, hintMaxLines = null, + error = null, errorText = null, errorStyle = null, errorMaxLines = null, @@ -2842,6 +2853,13 @@ class InputDecoration { /// used to handle the overflow when it is limited to single line. final int? hintMaxLines; + /// 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. + /// + /// Only one of [error] and [errorText] can be specified. + final Widget? error; + /// Text that appears below the [InputDecorator.child] and the border. /// /// If non-null, the border's color animates to red and the [helperText] is @@ -2849,6 +2867,10 @@ class InputDecoration { /// /// In a [TextFormField], this is overridden by the value returned from /// [TextFormField.validator], if that is not null. + /// + /// If a more elaborate error is required, consider using [error] instead. + /// + /// Only one of [error] and [errorText] can be specified. final String? errorText; /// {@template flutter.material.inputDecoration.errorStyle} @@ -3485,6 +3507,7 @@ class InputDecoration { TextStyle? hintStyle, TextDirection? hintTextDirection, int? hintMaxLines, + Widget? error, String? errorText, TextStyle? errorStyle, int? errorMaxLines, @@ -3537,6 +3560,7 @@ class InputDecoration { hintStyle: hintStyle ?? this.hintStyle, hintTextDirection: hintTextDirection ?? this.hintTextDirection, hintMaxLines: hintMaxLines ?? this.hintMaxLines, + error: error ?? this.error, errorText: errorText ?? this.errorText, errorStyle: errorStyle ?? this.errorStyle, errorMaxLines: errorMaxLines ?? this.errorMaxLines, @@ -3639,6 +3663,7 @@ class InputDecoration { && other.hintStyle == hintStyle && other.hintTextDirection == hintTextDirection && other.hintMaxLines == hintMaxLines + && other.error == error && other.errorText == errorText && other.errorStyle == errorStyle && other.errorMaxLines == errorMaxLines @@ -3694,6 +3719,7 @@ class InputDecoration { hintStyle, hintTextDirection, hintMaxLines, + error, errorText, errorStyle, errorMaxLines, @@ -3747,6 +3773,7 @@ class InputDecoration { if (helperMaxLines != null) 'helperMaxLines: "$helperMaxLines"', if (hintText != null) 'hintText: "$hintText"', if (hintMaxLines != null) 'hintMaxLines: "$hintMaxLines"', + if (error != null) 'error: "$error"', if (errorText != null) 'errorText: "$errorText"', if (errorStyle != null) 'errorStyle: "$errorStyle"', if (errorMaxLines != null) 'errorMaxLines: "$errorMaxLines"', diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index 9810c3baad..4d543a8d98 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -1643,6 +1643,47 @@ void main() { expect(tester.getBottomLeft(find.text(kHelper1)), const Offset(12.0, 76.0)); }); + testWidgets('InputDecorator shows error text', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + decoration: const InputDecoration( + errorText: 'errorText', + ), + ), + ); + + expect(find.text('errorText'), findsOneWidget); + }); + + testWidgets('InputDecorator shows error widget', (WidgetTester tester) async { + await tester.pumpWidget( + buildInputDecorator( + useMaterial3: useMaterial3, + decoration: const InputDecoration( + error: Text('error', style: TextStyle(fontSize: 20.0)), + ), + ), + ); + + expect(find.text('error'), findsOneWidget); + }); + + testWidgets('InputDecorator throws when error text and error widget are provided', (WidgetTester tester) async { + expect( + () { + buildInputDecorator( + useMaterial3: useMaterial3, + decoration: InputDecoration( + errorText: 'errorText', + error: const Text('error', style: TextStyle(fontSize: 20.0)), + ), + ); + }, + throwsAssertionError, + ); + }); + testWidgets('InputDecorator prefix/suffix texts', (WidgetTester tester) async { await tester.pumpWidget( buildInputDecorator( @@ -5217,7 +5258,6 @@ void main() { useMaterial3: useMaterial3, // isFocused: false (default) decoration: const InputDecoration( - // errorText: false (default) enabled: false, errorBorder: errorBorder, focusedBorder: focusedBorder,