Add Scaffold drawers escape dismiss action. (#106186)
diff --git a/packages/flutter/lib/src/material/drawer.dart b/packages/flutter/lib/src/material/drawer.dart
index 86b0250..afcae8e 100644
--- a/packages/flutter/lib/src/material/drawer.dart
+++ b/packages/flutter/lib/src/material/drawer.dart
@@ -111,8 +111,9 @@
/// ```
/// {@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 a18184b..5be81b2 100644
--- a/packages/flutter/lib/src/material/scaffold.dart
+++ b/packages/flutter/lib/src/material/scaffold.dart
@@ -1614,8 +1614,8 @@
///
/// 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 @@
///
/// 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 @@
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: <Type, Action<Intent>>{
+ 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 _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 ef90ff9..4375e2b 100644
--- a/packages/flutter/test/material/debug_test.dart
+++ b/packages/flutter/test/material/debug_test.dart
@@ -350,6 +350,8 @@
' 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 219fdf0..b970288 100644
--- a/packages/flutter/test/material/scaffold_test.dart
+++ b/packages/flutter/test/material/scaffold_test.dart
@@ -6,6 +6,7 @@
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 @@
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 @@
));
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 @@
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 @@
' MediaQuery\n'
' LayoutId-[<_ScaffoldSlot.body>]\n'
' CustomMultiChildLayout\n'
+ ' _ActionsMarker\n'
+ ' Actions\n'
' AnimatedBuilder\n'
' DefaultTextStyle\n'
' AnimatedDefaultTextStyle\n'
@@ -2497,6 +2500,54 @@
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 {