Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,9 @@ customize to your needs.
| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | --------------------- | :------: |
| [decoration](https://pub.dev/documentation/dropdown_button2/latest/dropdown_button2/DropdownButtonFormField2/decoration.html) | The decoration of the dropdown button form field | InputDecoration | No |
| [onSaved](https://api.flutter.dev/flutter/widgets/FormField/onSaved.html) | Called with the current selected item when the form is saved | FormFieldSetter<T> | No |
| [validator](https://api.flutter.dev/flutter/widgets/FormField/validator.html) | Called to validates if the input is invalid and display error text | FormFieldValidator<T> | No |
| [autovalidateMode](https://api.flutter.dev/flutter/widgets/AutovalidateMode.html) | Used to enable/disable auto validation | AutovalidateMode | No |
| [validator](https://api.flutter.dev/flutter/widgets/FormField/validator.html) | Called to validates if the input is invalid and display error text | FormFieldValidator<T> | No |
| [errorBuilder](https://api.flutter.dev/flutter/widgets/FormField/errorBuilder.html) | Called to display a custom error widget instead of the default error text | Widget Function(BuildContext, String) | No |
| [autovalidateMode](https://api.flutter.dev/flutter/widgets/AutovalidateMode.html) | Used to enable/disable auto validation | AutovalidateMode | No |

## Installation

Expand Down
3 changes: 2 additions & 1 deletion packages/dropdown_button2/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## UNRELEASED

- Upgrade minimum required Flutter SDK version to 3.32.0
- Upgrade minimum required Flutter SDK version to 3.32.0.
- Add `errorBuilder` support for DropdownButtonFormField2 [Flutter core].

## 3.0.0

Expand Down
28 changes: 19 additions & 9 deletions packages/dropdown_button2/lib/src/dropdown_button2.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,7 @@ class DropdownButtonFormField2<T> extends FormField<T> {
InputDecoration? decoration,
super.onSaved,
super.validator,
super.errorBuilder,
AutovalidateMode? autovalidateMode,
bool? enableFeedback,
AlignmentGeometry alignment = AlignmentDirectional.centerStart,
Expand Down Expand Up @@ -1096,10 +1097,8 @@ class DropdownButtonFormField2<T> extends FormField<T> {
builder: (FormFieldState<T> field) {
final _DropdownButtonFormField2State<T> state =
field as _DropdownButtonFormField2State<T>;
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 &&
Expand All @@ -1121,6 +1120,21 @@ class DropdownButtonFormField2<T> extends FormField<T> {
: 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,
);
}

final bool hasError =
field.hasError ||
effectiveDecoration.errorText != null ||
Expand Down Expand Up @@ -1161,11 +1175,7 @@ class DropdownButtonFormField2<T> extends FormField<T> {
barrierColor: barrierColor,
barrierLabel: barrierLabel,
openDropdownListenable: openDropdownListenable,
inputDecoration: effectiveDecoration.copyWith(
errorText: field.errorText,
// Clear the decoration hintText because DropdownButton has its own hint logic.
hintText: effectiveDecoration.hintText != null ? '' : null,
),
inputDecoration: effectiveDecoration,
isEmpty: isEmpty,
hasError: hasError,
),
Expand Down
266 changes: 265 additions & 1 deletion packages/dropdown_button2/test/dropdown_button2_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ void main() {
testWidgets('inkwell should not go beyond border and cover error message when pressed', (
WidgetTester tester,
) async {
// Regression test for https://github.com/AhmedLSayed9/dropdown_button2/issues/56
// Regression test for:
// https://github.com/AhmedLSayed9/dropdown_button2/issues/56
// https://github.com/AhmedLSayed9/dropdown_button2/issues/199

final GlobalKey<FormState> formKey = GlobalKey<FormState>();
const errorMessage = 'error_message';
Expand Down Expand Up @@ -145,4 +147,266 @@ void main() {
});
},
);

group(
'DropdownButtonFormField2 Error Properties',
() {
final List<int> menuItems = List<int>.generate(10, (int index) => index);
final valueListenable = ValueNotifier(menuItems.first);

final findDropdownButtonFormField = find.byType(DropdownButtonFormField2<int>);

List<DropdownItem<int>> buildItems() {
return menuItems.map<DropdownItem<int>>((int item) {
return DropdownItem<int>(
value: item,
child: Text(item.toString()),
);
}).toList();
}

InputDecorator getFormFieldInputDecorator(WidgetTester tester) {
final findInputDecorator = find.descendant(
of: findDropdownButtonFormField,
matching: find.byType(InputDecorator),
);
return tester.widget<InputDecorator>(findInputDecorator.first);
}

testWidgets('validator error text is displayed', (WidgetTester tester) async {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
const errorMessage = 'Please select a value';

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: Form(
key: formKey,
child: DropdownButtonFormField2<int>(
valueListenable: valueListenable,
items: buildItems(),
onChanged: (_) {},
validator: (int? v) => errorMessage,
),
),
),
),
),
);

formKey.currentState!.validate();
await tester.pumpAndSettle();

expect(find.text(errorMessage), findsOneWidget);
});

testWidgets('validator returning null shows no error', (WidgetTester tester) async {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: Form(
key: formKey,
child: DropdownButtonFormField2<int>(
valueListenable: valueListenable,
items: buildItems(),
onChanged: (_) {},
validator: (int? v) => null,
),
),
),
),
),
);

formKey.currentState!.validate();
await tester.pumpAndSettle();

final inputDecorator = getFormFieldInputDecorator(tester);
expect(inputDecorator.decoration.errorText, isNull);
expect(inputDecorator.decoration.error, isNull);
});

testWidgets('autovalidateMode.always validates on first build', (
WidgetTester tester,
) async {
int validateCalled = 0;

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: DropdownButtonFormField2<int>(
valueListenable: valueListenable,
items: buildItems(),
onChanged: (_) {},
autovalidateMode: AutovalidateMode.always,
validator: (int? value) {
validateCalled++;
return 'Error';
},
),
),
),
),
);

expect(validateCalled, 1);
expect(find.text('Error'), findsOneWidget);
});

testWidgets('decoration errorStyle is applied to error text', (
WidgetTester tester,
) async {
const errorStyle = TextStyle(color: Colors.orange, fontSize: 20);

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: DropdownButtonFormField2<int>(
valueListenable: valueListenable,
items: buildItems(),
onChanged: (_) {},
autovalidateMode: AutovalidateMode.always,
validator: (int? v) => 'Styled error',
decoration: const InputDecoration(errorStyle: errorStyle),
),
),
),
),
);

await tester.pumpAndSettle();

final inputDecorator = getFormFieldInputDecorator(tester);
expect(inputDecorator.decoration.errorStyle, errorStyle);
expect(find.text('Styled error'), findsOneWidget);
});

testWidgets('decoration errorMaxLines is respected', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: DropdownButtonFormField2<int>(
valueListenable: valueListenable,
items: buildItems(),
onChanged: (_) {},
autovalidateMode: AutovalidateMode.always,
validator: (int? v) => 'A very long error message\nthat spans multiple lines',
decoration: const InputDecoration(errorMaxLines: 2),
),
),
),
),
);

await tester.pumpAndSettle();

final inputDecorator = getFormFieldInputDecorator(tester);
expect(inputDecorator.decoration.errorMaxLines, 2);
});

testWidgets('widget returned by errorBuilder is shown', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: DropdownButtonFormField2<int>(
valueListenable: valueListenable,
items: buildItems(),
onChanged: (_) {},
autovalidateMode: AutovalidateMode.always,
validator: (int? v) => 'Required',
errorBuilder: (BuildContext context, String errorText) {
return Text('Custom: $errorText');
},
),
),
),
),
);

await tester.pumpAndSettle();

expect(find.text('Custom: Required'), findsOneWidget);
expect(find.text('Required'), findsNothing);
});

testWidgets('errorBuilder widget is passed to InputDecorator as error', (
WidgetTester tester,
) async {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: Form(
key: formKey,
child: DropdownButtonFormField2<int>(
valueListenable: valueListenable,
items: buildItems(),
onChanged: (_) {},
validator: (int? v) => 'Required',
errorBuilder: (BuildContext context, String errorText) {
return Row(
children: [
const Icon(Icons.error, color: Colors.red),
Text(errorText),
],
);
},
),
),
),
),
),
);

formKey.currentState!.validate();
await tester.pumpAndSettle();

final inputDecorator = getFormFieldInputDecorator(tester);
// When errorBuilder is used, error widget is set instead of errorText.
expect(inputDecorator.decoration.error, isNotNull);
expect(inputDecorator.decoration.errorText, isNull);
expect(find.byIcon(Icons.error), findsOneWidget);
});

testWidgets('errorBuilder is not called when there is no error', (
WidgetTester tester,
) async {
bool errorBuilderCalled = false;

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: DropdownButtonFormField2<int>(
valueListenable: valueListenable,
items: buildItems(),
onChanged: (_) {},
autovalidateMode: AutovalidateMode.always,
validator: (int? v) => null,
errorBuilder: (BuildContext context, String errorText) {
errorBuilderCalled = true;
return Text(errorText);
},
),
),
),
),
);

await tester.pumpAndSettle();

expect(errorBuilderCalled, isFalse);
});
},
);
}
Loading