mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Reland "Improve the behavior of DropdownButton.disabledHint" (#71628)
* DropdownButton should display the value over hint and disabledHint if a DropdownButton.value is defined
This commit is contained in:
parent
95e444ce9a
commit
5f7f4f1aff
@ -793,9 +793,14 @@ class DropdownButton<T> extends StatefulWidget {
|
||||
/// The [items] must have distinct values. If [value] isn't null then it
|
||||
/// must be equal to one of the [DropdownMenuItem] values. If [items] or
|
||||
/// [onChanged] is null, the button will be disabled, the down arrow
|
||||
/// will be greyed out, and the [disabledHint] will be shown (if provided).
|
||||
/// If [disabledHint] is null and [hint] is non-null, [hint] will instead be
|
||||
/// shown.
|
||||
/// will be greyed out.
|
||||
///
|
||||
/// If [value] is null and the button is enabled, [hint] will be displayed
|
||||
/// if it is non-null.
|
||||
///
|
||||
/// If [value] is null and the button is disabled, [disabledHint] will be displayed
|
||||
/// if it is non-null. If [disabledHint] is null, then [hint] will be displayed
|
||||
/// if it is non-null.
|
||||
///
|
||||
/// The [elevation] and [iconSize] arguments must not be null (they both have
|
||||
/// defaults, so do not need to be specified). The boolean [isDense] and
|
||||
@ -813,7 +818,7 @@ class DropdownButton<T> extends StatefulWidget {
|
||||
this.value,
|
||||
this.hint,
|
||||
this.disabledHint,
|
||||
required this.onChanged,
|
||||
this.onChanged,
|
||||
this.onTap,
|
||||
this.elevation = 8,
|
||||
this.style,
|
||||
@ -852,29 +857,32 @@ class DropdownButton<T> extends StatefulWidget {
|
||||
///
|
||||
/// If the [onChanged] callback is null or the list of items is null
|
||||
/// then the dropdown button will be disabled, i.e. its arrow will be
|
||||
/// displayed in grey and it will not respond to input. A disabled button
|
||||
/// will display the [disabledHint] widget if it is non-null. If
|
||||
/// [disabledHint] is also null but [hint] is non-null, [hint] will instead
|
||||
/// be displayed.
|
||||
/// displayed in grey and it will not respond to input.
|
||||
final List<DropdownMenuItem<T>>? items;
|
||||
|
||||
/// The value of the currently selected [DropdownMenuItem].
|
||||
///
|
||||
/// If [value] is null and [hint] is non-null, the [hint] widget is
|
||||
/// displayed as a placeholder for the dropdown button's value.
|
||||
/// If [value] is null and the button is enabled, [hint] will be displayed
|
||||
/// if it is non-null.
|
||||
///
|
||||
/// If [value] is null and the button is disabled, [disabledHint] will be displayed
|
||||
/// if it is non-null. If [disabledHint] is null, then [hint] will be displayed
|
||||
/// if it is non-null.
|
||||
final T? value;
|
||||
|
||||
/// A placeholder widget that is displayed by the dropdown button.
|
||||
///
|
||||
/// If [value] is null, this widget is displayed as a placeholder for
|
||||
/// the dropdown button's value. This widget is also displayed if the button
|
||||
/// is disabled ([items] or [onChanged] is null) and [disabledHint] is null.
|
||||
/// If [value] is null and the dropdown is enabled ([items] and [onChanged] are non-null),
|
||||
/// this widget is displayed as a placeholder for the dropdown button's value.
|
||||
///
|
||||
/// If [value] is null and the dropdown is disabled and [disabledHint] is null,
|
||||
/// this widget is used as the placeholder.
|
||||
final Widget? hint;
|
||||
|
||||
/// A message to show when the dropdown is disabled.
|
||||
/// A preferred placeholder widget that is displayed when the dropdown is disabled.
|
||||
///
|
||||
/// Displayed if [items] or [onChanged] is null. If [hint] is non-null and
|
||||
/// [disabledHint] is null, the [hint] widget will be displayed instead.
|
||||
/// If [value] is null, the dropdown is disabled ([items] or [onChanged] is null),
|
||||
/// this widget is displayed as a placeholder for the dropdown button's value.
|
||||
final Widget? disabledHint;
|
||||
|
||||
/// {@template flutter.material.dropdownButton.onChanged}
|
||||
@ -1160,13 +1168,12 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
||||
}
|
||||
|
||||
void _updateSelectedIndex() {
|
||||
if (!_enabled) {
|
||||
if (widget.value == null || widget.items == null || widget.items!.isEmpty) {
|
||||
_selectedIndex = null;
|
||||
return;
|
||||
}
|
||||
|
||||
assert(widget.value == null ||
|
||||
widget.items!.where((DropdownMenuItem<T> item) => item.value == widget.value).length == 1);
|
||||
_selectedIndex = null;
|
||||
assert(widget.items!.where((DropdownMenuItem<T> item) => item.value == widget.value).length == 1);
|
||||
for (int itemIndex = 0; itemIndex < widget.items!.length; itemIndex++) {
|
||||
if (widget.items![itemIndex].value == widget.value) {
|
||||
_selectedIndex = itemIndex;
|
||||
@ -1307,7 +1314,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
||||
// otherwise, no explicit type adding items maybe trigger a crash/failure
|
||||
// when hint and selectedItemBuilder are provided.
|
||||
final List<Widget> items = widget.selectedItemBuilder == null
|
||||
? (_enabled ? List<Widget>.from(widget.items!) : <Widget>[])
|
||||
? (widget.items != null ? List<Widget>.from(widget.items!) : <Widget>[])
|
||||
: List<Widget>.from(widget.selectedItemBuilder!(context));
|
||||
|
||||
int? hintIndex;
|
||||
@ -1330,15 +1337,14 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
||||
? _kAlignedButtonPadding
|
||||
: _kUnalignedButtonPadding;
|
||||
|
||||
// If value is null (then _selectedIndex is null) or if disabled then we
|
||||
// If value is null (then _selectedIndex is null) then we
|
||||
// display the hint or nothing at all.
|
||||
final int? index = _enabled ? (_selectedIndex ?? hintIndex) : hintIndex;
|
||||
final Widget innerItemsWidget;
|
||||
if (items.isEmpty) {
|
||||
innerItemsWidget = Container();
|
||||
} else {
|
||||
innerItemsWidget = IndexedStack(
|
||||
index: index,
|
||||
index: _selectedIndex ?? hintIndex,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
children: widget.isDense ? items : items.map((Widget item) {
|
||||
return widget.itemHeight != null
|
||||
@ -1351,7 +1357,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
||||
const Icon defaultIcon = Icon(Icons.arrow_drop_down);
|
||||
|
||||
Widget result = DefaultTextStyle(
|
||||
style: _textStyle!,
|
||||
style: _enabled ? _textStyle! : _textStyle!.copyWith(color: Theme.of(context).disabledColor),
|
||||
child: Container(
|
||||
decoration: _showHighlight
|
||||
? BoxDecoration(
|
||||
@ -1443,7 +1449,7 @@ class DropdownButtonFormField<T> extends FormField<T> {
|
||||
T? value,
|
||||
Widget? hint,
|
||||
Widget? disabledHint,
|
||||
required this.onChanged,
|
||||
this.onChanged,
|
||||
VoidCallback? onTap,
|
||||
int elevation = 8,
|
||||
TextStyle? style,
|
||||
|
||||
@ -1341,6 +1341,131 @@ void main() {
|
||||
expect(enabledHintBox.size, equals(disabledHintBox.size));
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/70177
|
||||
testWidgets('disabledHint behavior test', (WidgetTester tester) async {
|
||||
Widget build({ List<String>? items, ValueChanged<String?>? onChanged, String? value, Widget? hint, Widget? disabledHint }) => buildFrame(
|
||||
items: items,
|
||||
onChanged: onChanged,
|
||||
value: value,
|
||||
hint: hint,
|
||||
disabledHint: disabledHint,
|
||||
);
|
||||
|
||||
// The selected value should be displayed when the button is disabled.
|
||||
await tester.pumpWidget(build(items: menuItems, onChanged: null, value: 'two'));
|
||||
// The dropdown icon and the selected menu item are vertically aligned.
|
||||
expect(tester.getCenter(find.text('two')).dy, tester.getCenter(find.byType(Icon)).dy);
|
||||
|
||||
// If [value] is null, the button is enabled, hint is displayed.
|
||||
await tester.pumpWidget(build(
|
||||
items: menuItems,
|
||||
onChanged: onChanged,
|
||||
value: null,
|
||||
hint: const Text('hint'),
|
||||
disabledHint: const Text('disabledHint'),
|
||||
));
|
||||
expect(tester.getCenter(find.text('hint')).dy, tester.getCenter(find.byType(Icon)).dy);
|
||||
|
||||
// If [value] is null, the button is disabled, [disabledHint] is displayed when [disabledHint] is non-null.
|
||||
await tester.pumpWidget(build(
|
||||
items: menuItems,
|
||||
onChanged: null,
|
||||
value: null,
|
||||
hint: const Text('hint'),
|
||||
disabledHint: const Text('disabledHint'),
|
||||
));
|
||||
expect(tester.getCenter(find.text('disabledHint')).dy, tester.getCenter(find.byType(Icon)).dy);
|
||||
|
||||
// If [value] is null, the button is disabled, [hint] is displayed when [disabledHint] is null.
|
||||
await tester.pumpWidget(build(
|
||||
items: menuItems,
|
||||
onChanged: null,
|
||||
value: null,
|
||||
hint: const Text('hint'),
|
||||
disabledHint: null,
|
||||
));
|
||||
expect(tester.getCenter(find.text('hint')).dy, tester.getCenter(find.byType(Icon)).dy);
|
||||
|
||||
int? getIndex() {
|
||||
final IndexedStack stack = tester.element(find.byType(IndexedStack)).widget as IndexedStack;
|
||||
return stack.index;
|
||||
}
|
||||
|
||||
// If [value], [hint] and [disabledHint] are null, the button is disabled, nothing displayed.
|
||||
await tester.pumpWidget(build(
|
||||
items: menuItems,
|
||||
onChanged: null,
|
||||
value: null,
|
||||
hint: null,
|
||||
disabledHint: null,
|
||||
));
|
||||
expect(getIndex(), null);
|
||||
|
||||
// If [value], [hint] and [disabledHint] are null, the button is enabled, nothing displayed.
|
||||
await tester.pumpWidget(build(
|
||||
items: menuItems,
|
||||
onChanged: onChanged,
|
||||
value: null,
|
||||
hint: null,
|
||||
disabledHint: null,
|
||||
));
|
||||
expect(getIndex(), null);
|
||||
});
|
||||
|
||||
testWidgets('DropdownButton selected item color test', (WidgetTester tester) async {
|
||||
Widget build({ ValueChanged<String?>? onChanged, String? value, Widget? hint, Widget? disabledHint }) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData(
|
||||
disabledColor: Colors.pink,
|
||||
),
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: Column(children: <Widget>[
|
||||
DropdownButtonFormField<String>(
|
||||
style: const TextStyle(
|
||||
color: Colors.yellow
|
||||
),
|
||||
disabledHint: disabledHint,
|
||||
hint: hint,
|
||||
items: const <DropdownMenuItem<String>>[
|
||||
DropdownMenuItem<String>(
|
||||
child: Text('one'),
|
||||
value: 'one',
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
child: Text('two'),
|
||||
value: 'two',
|
||||
),
|
||||
],
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
)
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color textColor(String text) {
|
||||
return tester.renderObject<RenderParagraph>(find.text(text)).text.style!.color!;
|
||||
}
|
||||
|
||||
// The selected value should be displayed when the button is enabled.
|
||||
await tester.pumpWidget(build(onChanged: onChanged, value: 'two'));
|
||||
// The dropdown icon and the selected menu item are vertically aligned.
|
||||
expect(tester.getCenter(find.text('two')).dy, tester.getCenter(find.byType(Icon)).dy);
|
||||
// Selected item has a normal color from [DropdownButtonFormField.style]
|
||||
// when the button is enabled.
|
||||
expect(textColor('two'), Colors.yellow);
|
||||
|
||||
// The selected value should be displayed when the button is disabled.
|
||||
await tester.pumpWidget(build(onChanged: null, value: 'two'));
|
||||
expect(tester.getCenter(find.text('two')).dy, tester.getCenter(find.byType(Icon)).dy);
|
||||
// Selected item has a disabled color from [theme.disabledColor]
|
||||
// when the button is disable.
|
||||
expect(textColor('two'), Colors.pink);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'DropdowwnButton hint displays when the items list is empty, '
|
||||
'items is null, and disabledHint is null',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user