diff --git a/packages/flutter/lib/src/material/dropdown.dart b/packages/flutter/lib/src/material/dropdown.dart index 681cb3ec8c1..6fc0b8edd40 100644 --- a/packages/flutter/lib/src/material/dropdown.dart +++ b/packages/flutter/lib/src/material/dropdown.dart @@ -793,9 +793,14 @@ class DropdownButton 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 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 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>? 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 extends State> 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 item) => item.value == widget.value).length == 1); - _selectedIndex = null; + assert(widget.items!.where((DropdownMenuItem 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 extends State> with WidgetsBindi // otherwise, no explicit type adding items maybe trigger a crash/failure // when hint and selectedItemBuilder are provided. final List items = widget.selectedItemBuilder == null - ? (_enabled ? List.from(widget.items!) : []) + ? (widget.items != null ? List.from(widget.items!) : []) : List.from(widget.selectedItemBuilder!(context)); int? hintIndex; @@ -1330,15 +1337,14 @@ class _DropdownButtonState extends State> 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 extends State> 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 extends FormField { T? value, Widget? hint, Widget? disabledHint, - required this.onChanged, + this.onChanged, VoidCallback? onTap, int elevation = 8, TextStyle? style, diff --git a/packages/flutter/test/material/dropdown_test.dart b/packages/flutter/test/material/dropdown_test.dart index 7d77a1a6231..75decc2c977 100644 --- a/packages/flutter/test/material/dropdown_test.dart +++ b/packages/flutter/test/material/dropdown_test.dart @@ -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? items, ValueChanged? 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? onChanged, String? value, Widget? hint, Widget? disabledHint }) { + return MaterialApp( + theme: ThemeData( + disabledColor: Colors.pink, + ), + home: Scaffold( + body: Center( + child: Column(children: [ + DropdownButtonFormField( + style: const TextStyle( + color: Colors.yellow + ), + disabledHint: disabledHint, + hint: hint, + items: const >[ + DropdownMenuItem( + child: Text('one'), + value: 'one', + ), + DropdownMenuItem( + child: Text('two'), + value: 'two', + ), + ], + value: value, + onChanged: onChanged, + ) + ]), + ), + ), + ); + } + + Color textColor(String text) { + return tester.renderObject(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',