diff --git a/packages/flutter/lib/src/material/dropdown.dart b/packages/flutter/lib/src/material/dropdown.dart index d6c34efe80..e9b32eb037 100644 --- a/packages/flutter/lib/src/material/dropdown.dart +++ b/packages/flutter/lib/src/material/dropdown.dart @@ -1722,6 +1722,7 @@ class DropdownButtonFormField extends FormField { InputDecoration? decoration, super.onSaved, super.validator, + super.errorBuilder, AutovalidateMode? autovalidateMode, double? menuMaxHeight, bool? enableFeedback, @@ -1747,10 +1748,8 @@ class DropdownButtonFormField extends FormField { autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled, builder: (FormFieldState field) { final _DropdownButtonFormFieldState state = field as _DropdownButtonFormFieldState; - final InputDecoration decorationArg = decoration ?? const InputDecoration(); - final InputDecoration effectiveDecoration = decorationArg.applyDefaults( - Theme.of(field.context).inputDecorationTheme, - ); + InputDecoration effectiveDecoration = (decoration ?? const InputDecoration()) + .applyDefaults(Theme.of(field.context).inputDecorationTheme); final bool showSelectedItem = items != null && @@ -1767,6 +1766,22 @@ class DropdownButtonFormField extends FormField { : effectiveHint != null || effectiveDisabledHint != null; final bool isEmpty = !showSelectedItem && !isHintOrDisabledHintAvailable; + if (field.errorText != null || effectiveDecoration.hintText != null) { + final Widget? error = + field.errorText != null && errorBuilder != null + ? errorBuilder(state.context, field.errorText!) + : null; + final String? errorText = error == null ? field.errorText : null; + // Clear the decoration hintText because DropdownButton has its own hint logic. + final String? hintText = effectiveDecoration.hintText != null ? '' : null; + + effectiveDecoration = effectiveDecoration.copyWith( + error: error, + errorText: errorText, + hintText: hintText, + ); + } + // An unfocusable Focus widget so that this widget can detect if its // descendants have focus or not. return Focus( @@ -1800,11 +1815,7 @@ class DropdownButtonFormField extends FormField { enableFeedback: enableFeedback, alignment: alignment, borderRadius: borderRadius, - // Clear the decoration hintText because DropdownButton has its own hint logic. - inputDecoration: effectiveDecoration.copyWith( - errorText: field.errorText, - hintText: effectiveDecoration.hintText != null ? '' : null, - ), + inputDecoration: effectiveDecoration, isEmpty: isEmpty, padding: padding, ), diff --git a/packages/flutter/lib/src/material/text_form_field.dart b/packages/flutter/lib/src/material/text_form_field.dart index 81cddddc47..564543f306 100644 --- a/packages/flutter/lib/src/material/text_form_field.dart +++ b/packages/flutter/lib/src/material/text_form_field.dart @@ -150,6 +150,7 @@ class TextFormField extends FormField { ValueChanged? onFieldSubmitted, super.onSaved, super.validator, + super.errorBuilder, List? inputFormatters, bool? enabled, bool? ignorePointers, @@ -209,8 +210,17 @@ class TextFormField extends FormField { autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled, builder: (FormFieldState field) { final _TextFormFieldState state = field as _TextFormFieldState; - final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration()) + InputDecoration effectiveDecoration = (decoration ?? const InputDecoration()) .applyDefaults(Theme.of(field.context).inputDecorationTheme); + + final String? errorText = field.errorText; + if (errorText != null) { + effectiveDecoration = + errorBuilder != null + ? effectiveDecoration.copyWith(error: errorBuilder(state.context, errorText)) + : effectiveDecoration.copyWith(errorText: errorText); + } + void onChangedHandler(String value) { field.didChange(value); onChanged?.call(value); @@ -223,7 +233,7 @@ class TextFormField extends FormField { restorationId: restorationId, controller: state._effectiveController, focusNode: focusNode, - decoration: effectiveDecoration.copyWith(errorText: field.errorText), + decoration: effectiveDecoration, keyboardType: keyboardType, textInputAction: textInputAction, style: style, diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart index 147697d834..b915a9f8af 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -421,6 +421,14 @@ class _FormScope extends InheritedWidget { /// Used by [FormField.validator]. typedef FormFieldValidator = String? Function(T? value); +/// Signature for a callback that builds an error widget. +/// +/// See also: +/// +/// * [FormField.errorBuilder], which is of this type, and passes the result error +/// given by [TextFormField.validator]. +typedef FormFieldErrorBuilder = Widget Function(BuildContext context, String errorText); + /// Signature for being notified when a form field changes value. /// /// Used by [FormField.onSaved]. @@ -460,12 +468,19 @@ class FormField extends StatefulWidget { this.onSaved, this.forceErrorText, this.validator, + this.errorBuilder, this.initialValue, this.enabled = true, AutovalidateMode? autovalidateMode, this.restorationId, }) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled; + /// Function that returns the widget representing this form field. + /// + /// It is passed the form field state as input, containing the current value + /// and validation state of this field. + final FormFieldBuilder builder; + /// An optional method to call with the final value when the form is saved via /// [FormState.save]. final FormFieldSetter? onSaved; @@ -503,10 +518,14 @@ class FormField extends StatefulWidget { /// parameter to a space. final FormFieldValidator? validator; - /// Function that returns the widget representing this form field. It is - /// passed the form field state as input, containing the current value and - /// validation state of this field. - final FormFieldBuilder builder; + /// Function that returns the widget representing the error to display. + /// + /// It is passed the form field validator error string as input. + /// The resulting widget is passed to [InputDecoration.error]. + /// + /// If null, the validator error string is passed to + /// [InputDecoration.errorText]. + final FormFieldErrorBuilder? errorBuilder; /// An optional value to initialize the form field to, or null otherwise. /// diff --git a/packages/flutter/test/material/dropdown_form_field_test.dart b/packages/flutter/test/material/dropdown_form_field_test.dart index a10f9e5ccd..554e4f43ac 100644 --- a/packages/flutter/test/material/dropdown_form_field_test.dart +++ b/packages/flutter/test/material/dropdown_form_field_test.dart @@ -1229,4 +1229,28 @@ void main() { expect(inputDecorator.isFocused, true); expect(inputDecorator.decoration.errorText, 'Required'); }); + + // Regression test for https://github.com/flutter/flutter/issues/135292. + testWidgets('Widget returned by errorBuilder is shown', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: DropdownButtonFormField( + items: + menuItems.map((String value) { + return DropdownMenuItem(value: value, child: Text(value)); + }).toList(), + onChanged: onChanged, + autovalidateMode: AutovalidateMode.always, + validator: (String? v) => 'Required', + errorBuilder: (BuildContext context, String errorText) => Text('**$errorText**'), + ), + ), + ), + ); + + await tester.pump(); + + expect(find.text('**Required**'), findsOneWidget); + }); } diff --git a/packages/flutter/test/material/text_form_field_test.dart b/packages/flutter/test/material/text_form_field_test.dart index b6f15b2523..07d9174a62 100644 --- a/packages/flutter/test/material/text_form_field_test.dart +++ b/packages/flutter/test/material/text_form_field_test.dart @@ -1663,4 +1663,25 @@ void main() { expect(find.text(forceErrorText), findsOne); expect(find.text(decorationErrorText), findsNothing); }); + + // Regression test for https://github.com/flutter/flutter/issues/135292. + testWidgets('Widget returned by errorBuilder is shown', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: TextFormField( + autovalidateMode: AutovalidateMode.always, + validator: (String? value) => 'validation error', + errorBuilder: (BuildContext context, String errorText) => Text('**$errorText**'), + ), + ), + ), + ), + ); + + await tester.pump(); + + expect(find.text('**validation error**'), findsOneWidget); + }); }