Add disable argument in DropdownMenuItem (#76968)
Co-authored-by: Shi-Hao Hong <shihaohong@google.com> Adds a disable argument in DropdownMenuItem widget and added tests. Design Doc: docs.google.com/document/d/13W6PupVZUt6TenoE3NaTP9OCYsKBIYg9YgdurKe1XKs
This commit is contained in:
parent
56c0002c47
commit
2bc7939a9c
@ -155,6 +155,7 @@ class _DropdownMenuItemButtonState<T> extends State<_DropdownMenuItemButton<T>>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final DropdownMenuItem<T> dropdownMenuItem = widget.route.items[widget.itemIndex].item!;
|
||||||
final CurvedAnimation opacity;
|
final CurvedAnimation opacity;
|
||||||
final double unit = 0.5 / (widget.route.items.length + 1.5);
|
final double unit = 0.5 / (widget.route.items.length + 1.5);
|
||||||
if (widget.itemIndex == widget.route.selectedIndex) {
|
if (widget.itemIndex == widget.route.selectedIndex) {
|
||||||
@ -164,19 +165,21 @@ class _DropdownMenuItemButtonState<T> extends State<_DropdownMenuItemButton<T>>
|
|||||||
final double end = (start + 1.5 * unit).clamp(0.0, 1.0);
|
final double end = (start + 1.5 * unit).clamp(0.0, 1.0);
|
||||||
opacity = CurvedAnimation(parent: widget.route.animation!, curve: Interval(start, end));
|
opacity = CurvedAnimation(parent: widget.route.animation!, curve: Interval(start, end));
|
||||||
}
|
}
|
||||||
Widget child = FadeTransition(
|
Widget child = Container(
|
||||||
opacity: opacity,
|
|
||||||
child: InkWell(
|
|
||||||
autofocus: widget.itemIndex == widget.route.selectedIndex,
|
|
||||||
child: Container(
|
|
||||||
padding: widget.padding,
|
padding: widget.padding,
|
||||||
child: widget.route.items[widget.itemIndex],
|
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: child,
|
||||||
onTap: _handleOnTap,
|
onTap: _handleOnTap,
|
||||||
onFocusChange: _handleFocusChange,
|
onFocusChange: _handleFocusChange,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (kIsWeb) {
|
}
|
||||||
|
child = FadeTransition(opacity: opacity, child: child);
|
||||||
|
if (kIsWeb && dropdownMenuItem.enabled) {
|
||||||
child = Shortcuts(
|
child = Shortcuts(
|
||||||
shortcuts: _webShortcuts,
|
shortcuts: _webShortcuts,
|
||||||
child: child,
|
child: child,
|
||||||
@ -685,6 +688,7 @@ class DropdownMenuItem<T> extends _DropdownMenuItemContainer {
|
|||||||
Key? key,
|
Key? key,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.value,
|
this.value,
|
||||||
|
this.enabled = true,
|
||||||
required Widget child,
|
required Widget child,
|
||||||
}) : assert(child != null),
|
}) : assert(child != null),
|
||||||
super(key: key, child: child);
|
super(key: key, child: child);
|
||||||
@ -696,6 +700,11 @@ class DropdownMenuItem<T> extends _DropdownMenuItemContainer {
|
|||||||
///
|
///
|
||||||
/// Eventually returned in a call to [DropdownButton.onChanged].
|
/// Eventually returned in a call to [DropdownButton.onChanged].
|
||||||
final T? value;
|
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]
|
/// An inherited widget that causes any descendant [DropdownButton]
|
||||||
@ -1197,7 +1206,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
|||||||
|| widget.items!.isEmpty
|
|| widget.items!.isEmpty
|
||||||
|| (widget.value == null &&
|
|| (widget.value == null &&
|
||||||
widget.items!
|
widget.items!
|
||||||
.where((DropdownMenuItem<T> item) => item.value == widget.value)
|
.where((DropdownMenuItem<T> item) => item.enabled && item.value == widget.value)
|
||||||
.isEmpty)) {
|
.isEmpty)) {
|
||||||
_selectedIndex = null;
|
_selectedIndex = null;
|
||||||
return;
|
return;
|
||||||
|
@ -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)
|
..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<String>(
|
||||||
|
value: value,
|
||||||
|
items: const <DropdownMenuItem<String>>[
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
enabled: false,
|
||||||
|
child: Text('disabled'),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
value: 'first',
|
||||||
|
child: Text('first'),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
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<String>(
|
||||||
|
value: 'enabled',
|
||||||
|
onChanged: onChanged,
|
||||||
|
items: const <DropdownMenuItem<String>>[
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
enabled: false,
|
||||||
|
child: Text('disabled'),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
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');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user