From 2bc7939a9ca35839becc34b144df720dffa7a103 Mon Sep 17 00:00:00 2001 From: Viren Manojkumar Khatri Date: Fri, 16 Apr 2021 03:18:55 +0530 Subject: [PATCH] Add disable argument in DropdownMenuItem (#76968) Co-authored-by: Shi-Hao Hong Adds a disable argument in DropdownMenuItem widget and added tests. Design Doc: docs.google.com/document/d/13W6PupVZUt6TenoE3NaTP9OCYsKBIYg9YgdurKe1XKs --- .../flutter/lib/src/material/dropdown.dart | 31 +++++--- .../flutter/test/material/dropdown_test.dart | 77 +++++++++++++++++++ 2 files changed, 97 insertions(+), 11 deletions(-) diff --git a/packages/flutter/lib/src/material/dropdown.dart b/packages/flutter/lib/src/material/dropdown.dart index 671f3a5720..d948bdb0f0 100644 --- a/packages/flutter/lib/src/material/dropdown.dart +++ b/packages/flutter/lib/src/material/dropdown.dart @@ -155,6 +155,7 @@ class _DropdownMenuItemButtonState extends State<_DropdownMenuItemButton> @override Widget build(BuildContext context) { + final DropdownMenuItem dropdownMenuItem = widget.route.items[widget.itemIndex].item!; final CurvedAnimation opacity; final double unit = 0.5 / (widget.route.items.length + 1.5); if (widget.itemIndex == widget.route.selectedIndex) { @@ -164,19 +165,21 @@ class _DropdownMenuItemButtonState extends State<_DropdownMenuItemButton> final double end = (start + 1.5 * unit).clamp(0.0, 1.0); opacity = CurvedAnimation(parent: widget.route.animation!, curve: Interval(start, end)); } - Widget child = FadeTransition( - opacity: opacity, - child: InkWell( + Widget child = Container( + padding: widget.padding, + child: widget.route.items[widget.itemIndex], + ); + // An [InkWell] is added to the item only if it is enabled + if (dropdownMenuItem.enabled) { + child = InkWell( autofocus: widget.itemIndex == widget.route.selectedIndex, - child: Container( - padding: widget.padding, - child: widget.route.items[widget.itemIndex], - ), + child: child, onTap: _handleOnTap, onFocusChange: _handleFocusChange, - ), - ); - if (kIsWeb) { + ); + } + child = FadeTransition(opacity: opacity, child: child); + if (kIsWeb && dropdownMenuItem.enabled) { child = Shortcuts( shortcuts: _webShortcuts, child: child, @@ -685,6 +688,7 @@ class DropdownMenuItem extends _DropdownMenuItemContainer { Key? key, this.onTap, this.value, + this.enabled = true, required Widget child, }) : assert(child != null), super(key: key, child: child); @@ -696,6 +700,11 @@ class DropdownMenuItem extends _DropdownMenuItemContainer { /// /// Eventually returned in a call to [DropdownButton.onChanged]. final T? value; + + /// Whether or not a user can select this menu item. + /// + /// Defaults to `true`. + final bool enabled; } /// An inherited widget that causes any descendant [DropdownButton] @@ -1197,7 +1206,7 @@ class _DropdownButtonState extends State> with WidgetsBindi || widget.items!.isEmpty || (widget.value == null && widget.items! - .where((DropdownMenuItem item) => item.value == widget.value) + .where((DropdownMenuItem item) => item.enabled && item.value == widget.value) .isEmpty)) { _selectedIndex = null; return; diff --git a/packages/flutter/test/material/dropdown_test.dart b/packages/flutter/test/material/dropdown_test.dart index b4fe8f72fa..a1c148e29f 100644 --- a/packages/flutter/test/material/dropdown_test.dart +++ b/packages/flutter/test/material/dropdown_test.dart @@ -3165,4 +3165,81 @@ void main() { ..rrect(rrect: const RRect.fromLTRBXY(0.0, 0.0, 112.0, 47.0, 2.0, 2.0), color: Colors.grey[50], hasMaskFilter: false) ); }); + + testWidgets('Tapping a disabled item should not close DropdownButton', (WidgetTester tester) async { + String? value = 'first'; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) => DropdownButton( + value: value, + items: const >[ + DropdownMenuItem( + enabled: false, + child: Text('disabled'), + ), + DropdownMenuItem( + value: 'first', + child: Text('first'), + ), + DropdownMenuItem( + value: 'second', + child: Text('second'), + ), + ], + onChanged: (String? newValue) { + setState(() { + value = newValue; + }); + }, + ), + ), + ), + ), + ); + + // Open dropdown. + await tester.tap(find.text('first').hitTestable()); + await tester.pumpAndSettle(); + + // Tap on a disabled item. + await tester.tap(find.text('disabled').hitTestable()); + await tester.pumpAndSettle(); + + // The dropdown should still be open, i.e., there should be one widget with 'second' text. + expect(find.text('second').hitTestable(), findsOneWidget); + }); + + testWidgets('Disabled item should not be focusable', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DropdownButton( + value: 'enabled', + onChanged: onChanged, + items: const >[ + DropdownMenuItem( + enabled: false, + child: Text('disabled'), + ), + DropdownMenuItem( + value: 'enabled', + child: Text('enabled'), + ) + ], + ), + ), + ), + ); + + // Open dropdown. + await tester.tap(find.text('enabled').hitTestable()); + await tester.pumpAndSettle(); + + // The `FocusNode` of [disabledItem] should be `null` as enabled is false. + final Element disabledItem = tester.element(find.text('disabled').hitTestable()); + expect(Focus.maybeOf(disabledItem), null, reason: 'Disabled menu item should not be able to request focus'); + }); }