diff --git a/packages/flutter/lib/src/material/drawer.dart b/packages/flutter/lib/src/material/drawer.dart index 86b0250b6ba2..afcae8ef04e9 100644 --- a/packages/flutter/lib/src/material/drawer.dart +++ b/packages/flutter/lib/src/material/drawer.dart @@ -111,8 +111,9 @@ const Duration _kBaseSettleDuration = Duration(milliseconds: 246); /// ``` /// {@end-tool} /// -/// An open drawer can be closed by calling [Navigator.pop]. For example -/// a drawer item might close the drawer when tapped: +/// An open drawer may be closed with a swipe to close gesture, pressing the +/// the escape key, by tapping the scrim, or by calling pop route function such as +/// [Navigator.pop]. For example a drawer item might close the drawer when tapped: /// /// ```dart /// ListTile( diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index a18184bd3b0e..5be81b2ac2aa 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -1614,8 +1614,8 @@ class Scaffold extends StatefulWidget { /// /// To open the drawer, use the [ScaffoldState.openDrawer] function. /// - /// To close the drawer, use either [ScaffoldState.closeDrawer] or - /// [Navigator.pop]. + /// To close the drawer, use either [ScaffoldState.closeDrawer], [Navigator.pop] + /// or press the escape key on the keyboard. /// /// {@tool dartpad} /// To disable the drawer edge swipe on mobile, set the @@ -1638,8 +1638,8 @@ class Scaffold extends StatefulWidget { /// /// To open the drawer, use the [ScaffoldState.openEndDrawer] function. /// - /// To close the drawer, use either [ScaffoldState.closeEndDrawer] or - /// [Navigator.pop]. + /// To close the drawer, use either [ScaffoldState.closeEndDrawer], [Navigator.pop] + /// or press the escape key on the keyboard. /// /// {@tool dartpad} /// To disable the drawer edge swipe, set the @@ -2875,23 +2875,28 @@ class ScaffoldState extends State with TickerProviderStateMixin, Resto child: Material( color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor, child: AnimatedBuilder(animation: _floatingActionButtonMoveController, builder: (BuildContext context, Widget? child) { - return CustomMultiChildLayout( - delegate: _ScaffoldLayout( - extendBody: extendBody, - extendBodyBehindAppBar: widget.extendBodyBehindAppBar, - minInsets: minInsets, - minViewPadding: minViewPadding, - currentFloatingActionButtonLocation: _floatingActionButtonLocation!, - floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value, - floatingActionButtonMotionAnimator: _floatingActionButtonAnimator, - geometryNotifier: _geometryNotifier, - previousFloatingActionButtonLocation: _previousFloatingActionButtonLocation!, - textDirection: textDirection, - isSnackBarFloating: isSnackBarFloating, - extendBodyBehindMaterialBanner: extendBodyBehindMaterialBanner, - snackBarWidth: snackBarWidth, + return Actions( + actions: >{ + DismissIntent: _DismissDrawerAction(context), + }, + child: CustomMultiChildLayout( + delegate: _ScaffoldLayout( + extendBody: extendBody, + extendBodyBehindAppBar: widget.extendBodyBehindAppBar, + minInsets: minInsets, + minViewPadding: minViewPadding, + currentFloatingActionButtonLocation: _floatingActionButtonLocation!, + floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value, + floatingActionButtonMotionAnimator: _floatingActionButtonAnimator, + geometryNotifier: _geometryNotifier, + previousFloatingActionButtonLocation: _previousFloatingActionButtonLocation!, + textDirection: textDirection, + isSnackBarFloating: isSnackBarFloating, + extendBodyBehindMaterialBanner: extendBodyBehindMaterialBanner, + snackBarWidth: snackBarWidth, + ), + children: children, ), - children: children, ); }), ), @@ -2900,6 +2905,23 @@ class ScaffoldState extends State with TickerProviderStateMixin, Resto } } +class _DismissDrawerAction extends DismissAction { + _DismissDrawerAction(this.context); + + final BuildContext context; + + @override + bool isEnabled(DismissIntent intent) { + return Scaffold.of(context).isDrawerOpen || Scaffold.of(context).isEndDrawerOpen; + } + + @override + void invoke(DismissIntent intent) { + Scaffold.of(context).closeDrawer(); + Scaffold.of(context).closeEndDrawer(); + } +} + /// An interface for controlling a feature of a [Scaffold]. /// /// Commonly obtained from [ScaffoldMessengerState.showSnackBar] or diff --git a/packages/flutter/test/material/debug_test.dart b/packages/flutter/test/material/debug_test.dart index ef90ff95c038..4375e2b700a4 100644 --- a/packages/flutter/test/material/debug_test.dart +++ b/packages/flutter/test/material/debug_test.dart @@ -350,6 +350,8 @@ void main() { ' MediaQuery\n' ' LayoutId-[<_ScaffoldSlot.snackBar>]\n' ' CustomMultiChildLayout\n' + ' _ActionsMarker\n' + ' Actions\n' ' AnimatedBuilder\n' ' DefaultTextStyle\n' ' AnimatedDefaultTextStyle\n' diff --git a/packages/flutter/test/material/scaffold_test.dart b/packages/flutter/test/material/scaffold_test.dart index 219fdf00996f..b970288a2df6 100644 --- a/packages/flutter/test/material/scaffold_test.dart +++ b/packages/flutter/test/material/scaffold_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/semantics_tester.dart'; @@ -37,17 +38,17 @@ void main() { scaffoldState.openDrawer(); await tester.pumpAndSettle(); - expect(true, isDrawerOpen); + expect(isDrawerOpen, true); scaffoldState.openEndDrawer(); await tester.pumpAndSettle(); - expect(false, isDrawerOpen); + expect(isDrawerOpen, false); scaffoldState.openEndDrawer(); await tester.pumpAndSettle(); - expect(true, isEndDrawerOpen); + expect(isEndDrawerOpen, true); scaffoldState.openDrawer(); await tester.pumpAndSettle(); - expect(false, isEndDrawerOpen); + expect(isEndDrawerOpen, false); }); testWidgets('Scaffold drawer callback test - only call when changed', (WidgetTester tester) async { @@ -74,14 +75,14 @@ void main() { )); await tester.flingFrom(Offset.zero, const Offset(10.0, 0.0), 10.0); - expect(false, onDrawerChangedCalled); + expect(onDrawerChangedCalled, false); await tester.pumpAndSettle(); final double width = tester.getSize(find.byType(MaterialApp)).width; await tester.flingFrom(Offset(width - 1, 0.0), const Offset(-10.0, 0.0), 10.0); await tester.pumpAndSettle(); - expect(false, onEndDrawerChangedCalled); + expect(onEndDrawerChangedCalled, false); }); testWidgets('Scaffold control test', (WidgetTester tester) async { @@ -1572,29 +1573,29 @@ void main() { await tester.tap(drawerOpenButton); await tester.pumpAndSettle(); - expect(true, scaffoldState.isDrawerOpen); + expect(scaffoldState.isDrawerOpen, true); await tester.tap(endDrawerOpenButton, warnIfMissed: false); // hits the modal barrier await tester.pumpAndSettle(); - expect(false, scaffoldState.isDrawerOpen); + expect(scaffoldState.isDrawerOpen, false); await tester.tap(endDrawerOpenButton); await tester.pumpAndSettle(); - expect(true, scaffoldState.isEndDrawerOpen); + expect(scaffoldState.isEndDrawerOpen, true); await tester.tap(drawerOpenButton, warnIfMissed: false); // hits the modal barrier await tester.pumpAndSettle(); - expect(false, scaffoldState.isEndDrawerOpen); + expect(scaffoldState.isEndDrawerOpen, false); scaffoldState.openDrawer(); - expect(true, scaffoldState.isDrawerOpen); + expect(scaffoldState.isDrawerOpen, true); await tester.tap(endDrawerOpenButton, warnIfMissed: false); // hits the modal barrier await tester.pumpAndSettle(); - expect(false, scaffoldState.isDrawerOpen); + expect(scaffoldState.isDrawerOpen, false); scaffoldState.openEndDrawer(); - expect(true, scaffoldState.isEndDrawerOpen); + expect(scaffoldState.isEndDrawerOpen, true); scaffoldState.openDrawer(); - expect(true, scaffoldState.isDrawerOpen); + expect(scaffoldState.isDrawerOpen, true); }); testWidgets('Dual Drawer Opening', (WidgetTester tester) async { @@ -2405,6 +2406,8 @@ void main() { ' MediaQuery\n' ' LayoutId-[<_ScaffoldSlot.body>]\n' ' CustomMultiChildLayout\n' + ' _ActionsMarker\n' + ' Actions\n' ' AnimatedBuilder\n' ' DefaultTextStyle\n' ' AnimatedDefaultTextStyle\n' @@ -2497,6 +2500,54 @@ void main() { await tester.pumpAndSettle(); expect(find.text(snackBarContent), findsNothing); }); + + testWidgets('Drawer can be dismissed with escape keyboard shortcut', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/106131 + bool isDrawerOpen = false; + bool isEndDrawerOpen = false; + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + drawer: Container( + color: Colors.blue, + ), + onDrawerChanged: (bool isOpen) { + isDrawerOpen = isOpen; + }, + endDrawer: Container( + color: Colors.green, + ), + onEndDrawerChanged: (bool isOpen) { + isEndDrawerOpen = isOpen; + }, + body: Container(), + ), + )); + + final ScaffoldState scaffoldState = tester.state(find.byType(Scaffold)); + + scaffoldState.openDrawer(); + await tester.pumpAndSettle(); + expect(isDrawerOpen, true); + expect(isEndDrawerOpen, false); + + // Try to dismiss the drawer with the shortcut key + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pumpAndSettle(); + expect(isDrawerOpen, false); + expect(isEndDrawerOpen, false); + + scaffoldState.openEndDrawer(); + await tester.pumpAndSettle(); + expect(isDrawerOpen, false); + expect(isEndDrawerOpen, true); + + // Try to dismiss the drawer with the shortcut key + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pumpAndSettle(); + expect(isDrawerOpen, false); + expect(isEndDrawerOpen, false); + }); } class _GeometryListener extends StatefulWidget {