Use decoration hint text as the default value for dropdown button hints (#152474)

## Description

This PR makes `DropdownButtonFormField` hint defaults to the provided inputDecoration hintText.

Because `DropDownButtonFormField` accepts both a `hint` parameter and a `decoration`parameter, one can expect `InputDecoration.hintText` to be valid.

Before this PR, when `InputDecoration.hintText` was specified, it is shown but the vertical position is wrong.
After this PR, when `InputDecoration.hintText` is specified, it is used as the default value for `DropDownButtonFormField.hint` and `DropDownButtonFormField.disabledHint`.

| Before | After |
|--------|--------|
| ![image](https://github.com/user-attachments/assets/a08ff75c-edd4-4e16-9cfa-98ddb349d860) | ![image](https://github.com/user-attachments/assets/55f08bee-8f88-4125-8fae-68e2be724955) |

## Related Issue

Fixes https://github.com/flutter/flutter/issues/111958.

## Tests

Adds 5 tests.
This commit is contained in:
Bruno Leroux 2024-07-31 13:22:26 +02:00 committed by GitHub
parent 0c6b600e76
commit 1a8e57f42e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 197 additions and 129 deletions

View File

@ -1672,9 +1672,7 @@ class DropdownButtonFormField<T> extends FormField<T> {
// When adding new arguments, consider adding similar arguments to
// DropdownButton.
}) : assert(items == null || items.isEmpty || value == null ||
items.where((DropdownMenuItem<T> item) {
return item.value == value;
}).length == 1,
items.where((DropdownMenuItem<T> item) => item.value == value).length == 1,
"There should be exactly one item with [DropdownButton]'s value: "
'$value. \n'
'Either zero or 2 or more [DropdownMenuItem]s were detected '
@ -1693,15 +1691,15 @@ class DropdownButtonFormField<T> extends FormField<T> {
);
final bool showSelectedItem = items != null && items.where((DropdownMenuItem<T> item) => item.value == state.value).isNotEmpty;
bool isHintOrDisabledHintAvailable() {
final bool isDropdownDisabled = onChanged == null || (items == null || items.isEmpty);
if (isDropdownDisabled) {
return hint != null || disabledHint != null;
} else {
return hint != null;
}
}
final bool isEmpty = !showSelectedItem && !isHintOrDisabledHintAvailable();
final bool isDropdownEnabled = onChanged != null && items != null && items.isNotEmpty;
// If decoration hintText is provided, use it as the default value for both hint and disabledHint.
final Widget? decorationHint = effectiveDecoration.hintText != null ? Text(effectiveDecoration.hintText!) : null;
final Widget? effectiveHint = hint ?? decorationHint;
final Widget? effectiveDisabledHint = disabledHint ?? effectiveHint;
final bool isHintOrDisabledHintAvailable = isDropdownEnabled
? effectiveHint != null
: effectiveHint != null || effectiveDisabledHint != null;
final bool isEmpty = !showSelectedItem && !isHintOrDisabledHintAvailable;
final bool hasError = effectiveDecoration.errorText != null;
// An unfocusable Focus widget so that this widget can detect if its
@ -1742,8 +1740,8 @@ class DropdownButtonFormField<T> extends FormField<T> {
items: items,
selectedItemBuilder: selectedItemBuilder,
value: state.value,
hint: hint,
disabledHint: disabledHint,
hint: effectiveHint,
disabledHint: effectiveDisabledHint,
onChanged: onChanged == null ? null : state.didChange,
onTap: onTap,
elevation: elevation,
@ -1763,7 +1761,11 @@ class DropdownButtonFormField<T> extends FormField<T> {
enableFeedback: enableFeedback,
alignment: alignment,
borderRadius: borderRadius ?? effectiveBorderRadius(),
inputDecoration: effectiveDecoration.copyWith(errorText: field.errorText),
// Clear the decoration hintText because DropdownButton has its own hint logic.
inputDecoration: effectiveDecoration.copyWith(
errorText: field.errorText,
hintText: effectiveDecoration.hintText != null ? '' : null,
),
isEmpty: isEmpty,
isFocused: isFocused,
padding: padding,

View File

@ -69,6 +69,7 @@ Widget buildDropdown({
Color? dropdownColor,
double? menuMaxHeight,
EdgeInsetsGeometry? padding,
InputDecoration? decoration,
}) {
final List<DropdownMenuItem<String>>? listItems = items?.map<DropdownMenuItem<String>>((String item) {
return DropdownMenuItem<String>(
@ -104,6 +105,7 @@ Widget buildDropdown({
alignment: alignment,
menuMaxHeight: menuMaxHeight,
padding: padding,
decoration: decoration,
),
);
}
@ -165,6 +167,7 @@ Widget buildFrame({
EdgeInsetsGeometry? padding,
Alignment dropdownAlignment = Alignment.center,
bool? useMaterial3,
InputDecoration? decoration,
}) {
return Theme(
data: ThemeData(useMaterial3: useMaterial3),
@ -201,6 +204,7 @@ Widget buildFrame({
alignment: alignment,
menuMaxHeight: menuMaxHeight,
padding: padding,
decoration: decoration,
),
),
),
@ -3741,50 +3745,50 @@ void main() {
alignment: AlignmentDirectional.centerStart,
isExpanded: false,
));
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dx, 348.0);
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dy, 292.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 348.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 292.0);
// AlignmentDirectional.topStart
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.topStart,
isExpanded: false,
));
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dx, 348.0);
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dy, 250.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 348.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 250.0);
// AlignmentDirectional.bottomStart
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.bottomStart,
isExpanded: false,
));
expect(tester.getBottomLeft(find.text(hintText,skipOffstage: false)).dx, 348.0);
expect(tester.getBottomLeft(find.text(hintText,skipOffstage: false)).dy, 350.0);
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dx, 348.0);
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dy, 350.0);
// AlignmentDirectional.center
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.center,
isExpanded: false,
));
expect(tester.getCenter(find.text(hintText,skipOffstage: false)).dx, 388.0);
expect(tester.getCenter(find.text(hintText,skipOffstage: false)).dy, 300.0);
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dx, 388.0);
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dy, 300.0);
// AlignmentDirectional.topEnd
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.topEnd,
isExpanded: false,
));
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dx, 428.0);
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dy, 250.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 428.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 250.0);
// AlignmentDirectional.centerEnd
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.centerEnd,
isExpanded: false,
));
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dx, 428.0);
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dy, 292.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 428.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 292.0);
// AlignmentDirectional.bottomEnd
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.bottomEnd,
isExpanded: false,
));
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dx, 428.0);
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dy, 334.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 428.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 334.0);
// DropdownButton with `isExpanded: true`
// AlignmentDirectional.centerStart (default)
@ -3792,50 +3796,50 @@ void main() {
alignment: AlignmentDirectional.centerStart,
isExpanded: true,
));
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dx, 0.0);
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dy, 292.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 0.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 292.0);
// AlignmentDirectional.topStart
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.topStart,
isExpanded: true,
));
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dx, 0.0);
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dy, 250.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 0.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 250.0);
// AlignmentDirectional.bottomStart
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.bottomStart,
isExpanded: true,
));
expect(tester.getBottomLeft(find.text(hintText,skipOffstage: false)).dx, 0.0);
expect(tester.getBottomLeft(find.text(hintText,skipOffstage: false)).dy, 350.0);
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dx, 0.0);
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dy, 350.0);
// AlignmentDirectional.center
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.center,
isExpanded: true,
));
expect(tester.getCenter(find.text(hintText,skipOffstage: false)).dx, 388.0);
expect(tester.getCenter(find.text(hintText,skipOffstage: false)).dy, 300.0);
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dx, 388.0);
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dy, 300.0);
// AlignmentDirectional.topEnd
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.topEnd,
isExpanded: true,
));
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dx, 776.0);
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dy, 250.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 776.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 250.0);
// AlignmentDirectional.centerEnd
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.centerEnd,
isExpanded: true,
));
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dx, 776.0);
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dy, 292.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 776.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 292.0);
// AlignmentDirectional.bottomEnd
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.bottomEnd,
isExpanded: true,
));
expect(tester.getBottomRight(find.text(hintText,skipOffstage: false)).dx, 776.0);
expect(tester.getBottomRight(find.text(hintText,skipOffstage: false)).dy, 350.0);
expect(tester.getBottomRight(find.text(hintText, skipOffstage: false)).dx, 776.0);
expect(tester.getBottomRight(find.text(hintText, skipOffstage: false)).dy, 350.0);
});
testWidgets('DropdownButton hint alignment with selectedItemBuilder', (WidgetTester tester) async {
@ -3847,56 +3851,56 @@ void main() {
isExpanded: false,
enableSelectedItemBuilder: true,
));
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dx, 348.0);
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dy, 292.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 348.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 292.0);
// AlignmentDirectional.topStart
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.topStart,
isExpanded: false,
enableSelectedItemBuilder: true,
));
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dx, 348.0);
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dy, 250.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 348.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 250.0);
// AlignmentDirectional.bottomStart
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.bottomStart,
isExpanded: false,
enableSelectedItemBuilder: true,
));
expect(tester.getBottomLeft(find.text(hintText,skipOffstage: false)).dx, 348.0);
expect(tester.getBottomLeft(find.text(hintText,skipOffstage: false)).dy, 350.0);
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dx, 348.0);
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dy, 350.0);
// AlignmentDirectional.center
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.center,
isExpanded: false,
enableSelectedItemBuilder: true,
));
expect(tester.getCenter(find.text(hintText,skipOffstage: false)).dx, 388.0);
expect(tester.getCenter(find.text(hintText,skipOffstage: false)).dy, 300.0);
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dx, 388.0);
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dy, 300.0);
// AlignmentDirectional.topEnd
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.topEnd,
isExpanded: false,
enableSelectedItemBuilder: true,
));
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dx, 428.0);
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dy, 250.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 428.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 250.0);
// AlignmentDirectional.centerEnd
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.centerEnd,
isExpanded: false,
enableSelectedItemBuilder: true,
));
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dx, 428.0);
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dy, 292.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 428.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 292.0);
// AlignmentDirectional.bottomEnd
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.bottomEnd,
isExpanded: false,
enableSelectedItemBuilder: true,
));
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dx, 428.0);
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dy, 334.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 428.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 334.0);
// DropdownButton with `isExpanded: true`
// AlignmentDirectional.centerStart (default)
@ -3905,56 +3909,118 @@ void main() {
isExpanded: true,
enableSelectedItemBuilder: true,
));
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dx, 0.0);
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dy, 292.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 0.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 292.0);
// AlignmentDirectional.topStart
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.topStart,
isExpanded: true,
enableSelectedItemBuilder: true,
));
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dx, 0.0);
expect(tester.getTopLeft(find.text(hintText,skipOffstage: false)).dy, 250.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dx, 0.0);
expect(tester.getTopLeft(find.text(hintText, skipOffstage: false)).dy, 250.0);
// AlignmentDirectional.bottomStart
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.bottomStart,
isExpanded: true,
enableSelectedItemBuilder: true,
));
expect(tester.getBottomLeft(find.text(hintText,skipOffstage: false)).dx, 0.0);
expect(tester.getBottomLeft(find.text(hintText,skipOffstage: false)).dy, 350.0);
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dx, 0.0);
expect(tester.getBottomLeft(find.text(hintText, skipOffstage: false)).dy, 350.0);
// AlignmentDirectional.center
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.center,
isExpanded: true,
enableSelectedItemBuilder: true,
));
expect(tester.getCenter(find.text(hintText,skipOffstage: false)).dx, 388.0);
expect(tester.getCenter(find.text(hintText,skipOffstage: false)).dy, 300.0);
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dx, 388.0);
expect(tester.getCenter(find.text(hintText, skipOffstage: false)).dy, 300.0);
// AlignmentDirectional.topEnd
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.topEnd,
isExpanded: true,
enableSelectedItemBuilder: true,
));
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dx, 776.0);
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dy, 250.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 776.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 250.0);
// AlignmentDirectional.centerEnd
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.centerEnd,
isExpanded: true,
enableSelectedItemBuilder: true,
));
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dx, 776.0);
expect(tester.getTopRight(find.text(hintText,skipOffstage: false)).dy, 292.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dx, 776.0);
expect(tester.getTopRight(find.text(hintText, skipOffstage: false)).dy, 292.0);
// AlignmentDirectional.bottomEnd
await tester.pumpWidget(buildDropdownWithHint(
alignment: AlignmentDirectional.bottomEnd,
isExpanded: true,
enableSelectedItemBuilder: true,
));
expect(tester.getBottomRight(find.text(hintText,skipOffstage: false)).dx, 776.0);
expect(tester.getBottomRight(find.text(hintText,skipOffstage: false)).dy, 350.0);
expect(tester.getBottomRight(find.text(hintText, skipOffstage: false)).dx, 776.0);
expect(tester.getBottomRight(find.text(hintText, skipOffstage: false)).dy, 350.0);
});
group('DropdownButtonFormField decoration hintText', () {
const String decorationHintText = 'Decoration Hint text';
const String hintText = 'Hint text';
const String disabledHintText = 'Disabled Hint text';
testWidgets('is the fallback value for DropdownButtonFormField.hint', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(
isFormField: true,
onChanged: (String? newValue) {},
decoration: const InputDecoration(hintText: decorationHintText),
));
expect(find.text(decorationHintText, skipOffstage: false), findsOne);
});
testWidgets('does not override DropdownButtonFormField.hint', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(
hint: const Text(hintText),
isFormField: true,
onChanged: (String? newValue) {},
decoration: const InputDecoration(hintText: decorationHintText),
));
expect(find.text(hintText, skipOffstage: false), findsOne);
expect(find.text(decorationHintText, skipOffstage: false), findsNothing);
});
testWidgets('is the fallback value for DropdownButtonFormField.disabledHint', (WidgetTester tester) async {
// The Dropdown is disabled because onChanged is not defined.
await tester.pumpWidget(buildFrame(
isFormField: true,
decoration: const InputDecoration(hintText: decorationHintText),
));
expect(find.text(decorationHintText, skipOffstage: false), findsOne);
});
testWidgets('does not override DropdownButtonFormField.disabledHint', (WidgetTester tester) async {
// The Dropdown is disabled because onChanged is not defined.
await tester.pumpWidget(buildFrame(
disabledHint: const Text(disabledHintText),
isFormField: true,
decoration: const InputDecoration(hintText: decorationHintText),
));
expect(find.text(disabledHintText, skipOffstage: false), findsOne);
expect(find.text(decorationHintText, skipOffstage: false), findsNothing);
});
testWidgets('is not used for disabledHint if DropdownButtonFormField.hint is provided', (WidgetTester tester) async {
// The Dropdown is disabled because onChanged is not defined.
await tester.pumpWidget(buildFrame(
hint: const Text(hintText),
isFormField: true,
decoration: const InputDecoration(hintText: decorationHintText),
));
expect(find.text(hintText, skipOffstage: false), findsOne);
expect(find.text(decorationHintText, skipOffstage: false), findsNothing);
});
});
testWidgets('BorderRadius property clips dropdown button and dropdown menu', (WidgetTester tester) async {