diff --git a/packages/flutter/lib/src/material/dropdown.dart b/packages/flutter/lib/src/material/dropdown.dart index d948bdb0f0..47fad791a8 100644 --- a/packages/flutter/lib/src/material/dropdown.dart +++ b/packages/flutter/lib/src/material/dropdown.dart @@ -97,6 +97,7 @@ class _DropdownMenuItemButton extends StatefulWidget { required this.buttonRect, required this.constraints, required this.itemIndex, + required this.enableFeedback, }) : super(key: key); final _DropdownRoute route; @@ -104,6 +105,7 @@ class _DropdownMenuItemButton extends StatefulWidget { final Rect buttonRect; final BoxConstraints constraints; final int itemIndex; + final bool enableFeedback; @override _DropdownMenuItemButtonState createState() => _DropdownMenuItemButtonState(); @@ -173,6 +175,7 @@ class _DropdownMenuItemButtonState extends State<_DropdownMenuItemButton> if (dropdownMenuItem.enabled) { child = InkWell( autofocus: widget.itemIndex == widget.route.selectedIndex, + enableFeedback: widget.enableFeedback, child: child, onTap: _handleOnTap, onFocusChange: _handleFocusChange, @@ -197,6 +200,7 @@ class _DropdownMenu extends StatefulWidget { required this.buttonRect, required this.constraints, this.dropdownColor, + required this.enableFeedback, }) : super(key: key); final _DropdownRoute route; @@ -204,6 +208,7 @@ class _DropdownMenu extends StatefulWidget { final Rect buttonRect; final BoxConstraints constraints; final Color? dropdownColor; + final bool enableFeedback; @override _DropdownMenuState createState() => _DropdownMenuState(); @@ -253,6 +258,7 @@ class _DropdownMenuState extends State<_DropdownMenu> { buttonRect: widget.buttonRect, constraints: widget.constraints, itemIndex: itemIndex, + enableFeedback: widget.enableFeedback, ), ]; @@ -411,6 +417,7 @@ class _DropdownRoute extends PopupRoute<_DropdownRouteResult> { this.itemHeight, this.dropdownColor, this.menuMaxHeight, + required this.enableFeedback, }) : assert(style != null), itemHeights = List.filled(items.length, itemHeight ?? kMinInteractiveDimension); @@ -424,7 +431,7 @@ class _DropdownRoute extends PopupRoute<_DropdownRouteResult> { final double? itemHeight; final Color? dropdownColor; final double? menuMaxHeight; - + final bool enableFeedback; final List itemHeights; ScrollController? scrollController; @@ -456,6 +463,7 @@ class _DropdownRoute extends PopupRoute<_DropdownRouteResult> { capturedThemes: capturedThemes, style: style, dropdownColor: dropdownColor, + enableFeedback: enableFeedback, ); }, ); @@ -551,6 +559,7 @@ class _DropdownRoutePage extends StatelessWidget { required this.capturedThemes, this.style, required this.dropdownColor, + required this.enableFeedback, }) : super(key: key); final _DropdownRoute route; @@ -563,6 +572,7 @@ class _DropdownRoutePage extends StatelessWidget { final CapturedThemes capturedThemes; final TextStyle? style; final Color? dropdownColor; + final bool enableFeedback; @override Widget build(BuildContext context) { @@ -586,6 +596,7 @@ class _DropdownRoutePage extends StatelessWidget { buttonRect: buttonRect, constraints: constraints, dropdownColor: dropdownColor, + enableFeedback: enableFeedback, ); return MediaQuery.removePadding( @@ -854,6 +865,7 @@ class DropdownButton extends StatefulWidget { this.autofocus = false, this.dropdownColor, this.menuMaxHeight, + this.enableFeedback, // When adding new arguments, consider adding similar arguments to // DropdownButtonFormField. }) : assert(items == null || items.isEmpty || value == null || @@ -1114,6 +1126,18 @@ class DropdownButton extends StatefulWidget { /// and bottom of the menu by at one menu item's height. final double? menuMaxHeight; + /// Whether detected gestures should provide acoustic and/or haptic feedback. + /// + /// For example, on Android a tap will produce a clicking sound and a + /// long-press will produce a short vibration, when feedback is enabled. + /// + /// By default, platform-specific feedback is enabled. + /// + /// See also: + /// + /// * [Feedback] for providing platform-specific feedback to certain actions. + final bool? enableFeedback; + @override _DropdownButtonState createState() => _DropdownButtonState(); } @@ -1266,6 +1290,7 @@ class _DropdownButtonState extends State> with WidgetsBindi itemHeight: widget.itemHeight, dropdownColor: widget.dropdownColor, menuMaxHeight: widget.menuMaxHeight, + enableFeedback: widget.enableFeedback ?? true, ); navigator.push(_dropdownRoute!).then((_DropdownRouteResult? newValue) { diff --git a/packages/flutter/test/material/dropdown_test.dart b/packages/flutter/test/material/dropdown_test.dart index a1c148e29f..4cec9a610d 100644 --- a/packages/flutter/test/material/dropdown_test.dart +++ b/packages/flutter/test/material/dropdown_test.dart @@ -12,6 +12,7 @@ import 'package:flutter/services.dart'; import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; +import 'feedback_tester.dart'; const List menuItems = ['one', 'two', 'three', 'four']; void onChanged(T _) { } @@ -3242,4 +3243,75 @@ void main() { 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'); }); + + group('feedback', () { + late FeedbackTester feedback; + + setUp(() { + feedback = FeedbackTester(); + }); + + tearDown(() { + feedback.dispose(); + }); + + Widget feedbackBoilerplate({bool? enableFeedback}) { + return MaterialApp( + home : Material( + child: DropdownButton( + value: 'One', + enableFeedback: enableFeedback, + underline: Container( + height: 2, + color: Colors.deepPurpleAccent, + ), + onChanged: (String? value) {}, + items: ['One', 'Two'].map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ), + ); + } + + testWidgets('Dropdown with enabled feedback', (WidgetTester tester) async { + const bool enableFeedback = true; + + await tester.pumpWidget(feedbackBoilerplate(enableFeedback: enableFeedback)); + + await tester.tap(find.text('One')); + await tester.pumpAndSettle(); + await tester.tap(find.widgetWithText(InkWell, 'One')); + await tester.pumpAndSettle(); + expect(feedback.clickSoundCount, 1); + expect(feedback.hapticCount, 0); + }); + + testWidgets('Dropdown with disabled feedback', (WidgetTester tester) async { + const bool enableFeedback = false; + + await tester.pumpWidget(feedbackBoilerplate(enableFeedback: enableFeedback)); + + await tester.tap(find.text('One')); + await tester.pumpAndSettle(); + await tester.tap(find.widgetWithText(InkWell, 'One')); + await tester.pumpAndSettle(); + expect(feedback.clickSoundCount, 0); + expect(feedback.hapticCount, 0); + }); + + testWidgets('Dropdown with enabled feedback by default', (WidgetTester tester) async { + await tester.pumpWidget(feedbackBoilerplate()); + + await tester.tap(find.text('One')); + await tester.pumpAndSettle(); + await tester.tap(find.widgetWithText(InkWell, 'Two')); + await tester.pumpAndSettle(); + expect(feedback.clickSoundCount, 1); + expect(feedback.hapticCount, 0); + }); + }); }