Skip to content

Commit 8fe82bb

Browse files
authored
Add Scaffold drawers escape dismiss action. (#106186)
1 parent 1d5c189 commit 8fe82bb

File tree

4 files changed

+112
-36
lines changed

4 files changed

+112
-36
lines changed

packages/flutter/lib/src/material/drawer.dart

+3-2
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,9 @@ const Duration _kBaseSettleDuration = Duration(milliseconds: 246);
111111
/// ```
112112
/// {@end-tool}
113113
///
114-
/// An open drawer can be closed by calling [Navigator.pop]. For example
115-
/// a drawer item might close the drawer when tapped:
114+
/// An open drawer may be closed with a swipe to close gesture, pressing the
115+
/// the escape key, by tapping the scrim, or by calling pop route function such as
116+
/// [Navigator.pop]. For example a drawer item might close the drawer when tapped:
116117
///
117118
/// ```dart
118119
/// ListTile(

packages/flutter/lib/src/material/scaffold.dart

+42-20
Original file line numberDiff line numberDiff line change
@@ -1614,8 +1614,8 @@ class Scaffold extends StatefulWidget {
16141614
///
16151615
/// To open the drawer, use the [ScaffoldState.openDrawer] function.
16161616
///
1617-
/// To close the drawer, use either [ScaffoldState.closeDrawer] or
1618-
/// [Navigator.pop].
1617+
/// To close the drawer, use either [ScaffoldState.closeDrawer], [Navigator.pop]
1618+
/// or press the escape key on the keyboard.
16191619
///
16201620
/// {@tool dartpad}
16211621
/// To disable the drawer edge swipe on mobile, set the
@@ -1638,8 +1638,8 @@ class Scaffold extends StatefulWidget {
16381638
///
16391639
/// To open the drawer, use the [ScaffoldState.openEndDrawer] function.
16401640
///
1641-
/// To close the drawer, use either [ScaffoldState.closeEndDrawer] or
1642-
/// [Navigator.pop].
1641+
/// To close the drawer, use either [ScaffoldState.closeEndDrawer], [Navigator.pop]
1642+
/// or press the escape key on the keyboard.
16431643
///
16441644
/// {@tool dartpad}
16451645
/// To disable the drawer edge swipe, set the
@@ -2875,23 +2875,28 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
28752875
child: Material(
28762876
color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor,
28772877
child: AnimatedBuilder(animation: _floatingActionButtonMoveController, builder: (BuildContext context, Widget? child) {
2878-
return CustomMultiChildLayout(
2879-
delegate: _ScaffoldLayout(
2880-
extendBody: extendBody,
2881-
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
2882-
minInsets: minInsets,
2883-
minViewPadding: minViewPadding,
2884-
currentFloatingActionButtonLocation: _floatingActionButtonLocation!,
2885-
floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value,
2886-
floatingActionButtonMotionAnimator: _floatingActionButtonAnimator,
2887-
geometryNotifier: _geometryNotifier,
2888-
previousFloatingActionButtonLocation: _previousFloatingActionButtonLocation!,
2889-
textDirection: textDirection,
2890-
isSnackBarFloating: isSnackBarFloating,
2891-
extendBodyBehindMaterialBanner: extendBodyBehindMaterialBanner,
2892-
snackBarWidth: snackBarWidth,
2878+
return Actions(
2879+
actions: <Type, Action<Intent>>{
2880+
DismissIntent: _DismissDrawerAction(context),
2881+
},
2882+
child: CustomMultiChildLayout(
2883+
delegate: _ScaffoldLayout(
2884+
extendBody: extendBody,
2885+
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
2886+
minInsets: minInsets,
2887+
minViewPadding: minViewPadding,
2888+
currentFloatingActionButtonLocation: _floatingActionButtonLocation!,
2889+
floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value,
2890+
floatingActionButtonMotionAnimator: _floatingActionButtonAnimator,
2891+
geometryNotifier: _geometryNotifier,
2892+
previousFloatingActionButtonLocation: _previousFloatingActionButtonLocation!,
2893+
textDirection: textDirection,
2894+
isSnackBarFloating: isSnackBarFloating,
2895+
extendBodyBehindMaterialBanner: extendBodyBehindMaterialBanner,
2896+
snackBarWidth: snackBarWidth,
2897+
),
2898+
children: children,
28932899
),
2894-
children: children,
28952900
);
28962901
}),
28972902
),
@@ -2900,6 +2905,23 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, Resto
29002905
}
29012906
}
29022907

2908+
class _DismissDrawerAction extends DismissAction {
2909+
_DismissDrawerAction(this.context);
2910+
2911+
final BuildContext context;
2912+
2913+
@override
2914+
bool isEnabled(DismissIntent intent) {
2915+
return Scaffold.of(context).isDrawerOpen || Scaffold.of(context).isEndDrawerOpen;
2916+
}
2917+
2918+
@override
2919+
void invoke(DismissIntent intent) {
2920+
Scaffold.of(context).closeDrawer();
2921+
Scaffold.of(context).closeEndDrawer();
2922+
}
2923+
}
2924+
29032925
/// An interface for controlling a feature of a [Scaffold].
29042926
///
29052927
/// Commonly obtained from [ScaffoldMessengerState.showSnackBar] or

packages/flutter/test/material/debug_test.dart

+2
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,8 @@ void main() {
350350
' MediaQuery\n'
351351
' LayoutId-[<_ScaffoldSlot.snackBar>]\n'
352352
' CustomMultiChildLayout\n'
353+
' _ActionsMarker\n'
354+
' Actions\n'
353355
' AnimatedBuilder\n'
354356
' DefaultTextStyle\n'
355357
' AnimatedDefaultTextStyle\n'

packages/flutter/test/material/scaffold_test.dart

+65-14
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
66
import 'package:flutter/gestures.dart' show DragStartBehavior;
77
import 'package:flutter/material.dart';
88
import 'package:flutter/rendering.dart';
9+
import 'package:flutter/services.dart';
910
import 'package:flutter_test/flutter_test.dart';
1011

1112
import '../widgets/semantics_tester.dart';
@@ -37,17 +38,17 @@ void main() {
3738

3839
scaffoldState.openDrawer();
3940
await tester.pumpAndSettle();
40-
expect(true, isDrawerOpen);
41+
expect(isDrawerOpen, true);
4142
scaffoldState.openEndDrawer();
4243
await tester.pumpAndSettle();
43-
expect(false, isDrawerOpen);
44+
expect(isDrawerOpen, false);
4445

4546
scaffoldState.openEndDrawer();
4647
await tester.pumpAndSettle();
47-
expect(true, isEndDrawerOpen);
48+
expect(isEndDrawerOpen, true);
4849
scaffoldState.openDrawer();
4950
await tester.pumpAndSettle();
50-
expect(false, isEndDrawerOpen);
51+
expect(isEndDrawerOpen, false);
5152
});
5253

5354
testWidgets('Scaffold drawer callback test - only call when changed', (WidgetTester tester) async {
@@ -74,14 +75,14 @@ void main() {
7475
));
7576

7677
await tester.flingFrom(Offset.zero, const Offset(10.0, 0.0), 10.0);
77-
expect(false, onDrawerChangedCalled);
78+
expect(onDrawerChangedCalled, false);
7879

7980
await tester.pumpAndSettle();
8081

8182
final double width = tester.getSize(find.byType(MaterialApp)).width;
8283
await tester.flingFrom(Offset(width - 1, 0.0), const Offset(-10.0, 0.0), 10.0);
8384
await tester.pumpAndSettle();
84-
expect(false, onEndDrawerChangedCalled);
85+
expect(onEndDrawerChangedCalled, false);
8586
});
8687

8788
testWidgets('Scaffold control test', (WidgetTester tester) async {
@@ -1572,29 +1573,29 @@ void main() {
15721573

15731574
await tester.tap(drawerOpenButton);
15741575
await tester.pumpAndSettle();
1575-
expect(true, scaffoldState.isDrawerOpen);
1576+
expect(scaffoldState.isDrawerOpen, true);
15761577
await tester.tap(endDrawerOpenButton, warnIfMissed: false); // hits the modal barrier
15771578
await tester.pumpAndSettle();
1578-
expect(false, scaffoldState.isDrawerOpen);
1579+
expect(scaffoldState.isDrawerOpen, false);
15791580

15801581
await tester.tap(endDrawerOpenButton);
15811582
await tester.pumpAndSettle();
1582-
expect(true, scaffoldState.isEndDrawerOpen);
1583+
expect(scaffoldState.isEndDrawerOpen, true);
15831584
await tester.tap(drawerOpenButton, warnIfMissed: false); // hits the modal barrier
15841585
await tester.pumpAndSettle();
1585-
expect(false, scaffoldState.isEndDrawerOpen);
1586+
expect(scaffoldState.isEndDrawerOpen, false);
15861587

15871588
scaffoldState.openDrawer();
1588-
expect(true, scaffoldState.isDrawerOpen);
1589+
expect(scaffoldState.isDrawerOpen, true);
15891590
await tester.tap(endDrawerOpenButton, warnIfMissed: false); // hits the modal barrier
15901591
await tester.pumpAndSettle();
1591-
expect(false, scaffoldState.isDrawerOpen);
1592+
expect(scaffoldState.isDrawerOpen, false);
15921593

15931594
scaffoldState.openEndDrawer();
1594-
expect(true, scaffoldState.isEndDrawerOpen);
1595+
expect(scaffoldState.isEndDrawerOpen, true);
15951596

15961597
scaffoldState.openDrawer();
1597-
expect(true, scaffoldState.isDrawerOpen);
1598+
expect(scaffoldState.isDrawerOpen, true);
15981599
});
15991600

16001601
testWidgets('Dual Drawer Opening', (WidgetTester tester) async {
@@ -2405,6 +2406,8 @@ void main() {
24052406
' MediaQuery\n'
24062407
' LayoutId-[<_ScaffoldSlot.body>]\n'
24072408
' CustomMultiChildLayout\n'
2409+
' _ActionsMarker\n'
2410+
' Actions\n'
24082411
' AnimatedBuilder\n'
24092412
' DefaultTextStyle\n'
24102413
' AnimatedDefaultTextStyle\n'
@@ -2497,6 +2500,54 @@ void main() {
24972500
await tester.pumpAndSettle();
24982501
expect(find.text(snackBarContent), findsNothing);
24992502
});
2503+
2504+
testWidgets('Drawer can be dismissed with escape keyboard shortcut', (WidgetTester tester) async {
2505+
// Regression test for https://github.com/flutter/flutter/issues/106131
2506+
bool isDrawerOpen = false;
2507+
bool isEndDrawerOpen = false;
2508+
2509+
await tester.pumpWidget(MaterialApp(
2510+
home: Scaffold(
2511+
drawer: Container(
2512+
color: Colors.blue,
2513+
),
2514+
onDrawerChanged: (bool isOpen) {
2515+
isDrawerOpen = isOpen;
2516+
},
2517+
endDrawer: Container(
2518+
color: Colors.green,
2519+
),
2520+
onEndDrawerChanged: (bool isOpen) {
2521+
isEndDrawerOpen = isOpen;
2522+
},
2523+
body: Container(),
2524+
),
2525+
));
2526+
2527+
final ScaffoldState scaffoldState = tester.state(find.byType(Scaffold));
2528+
2529+
scaffoldState.openDrawer();
2530+
await tester.pumpAndSettle();
2531+
expect(isDrawerOpen, true);
2532+
expect(isEndDrawerOpen, false);
2533+
2534+
// Try to dismiss the drawer with the shortcut key
2535+
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
2536+
await tester.pumpAndSettle();
2537+
expect(isDrawerOpen, false);
2538+
expect(isEndDrawerOpen, false);
2539+
2540+
scaffoldState.openEndDrawer();
2541+
await tester.pumpAndSettle();
2542+
expect(isDrawerOpen, false);
2543+
expect(isEndDrawerOpen, true);
2544+
2545+
// Try to dismiss the drawer with the shortcut key
2546+
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
2547+
await tester.pumpAndSettle();
2548+
expect(isDrawerOpen, false);
2549+
expect(isEndDrawerOpen, false);
2550+
});
25002551
}
25012552

25022553
class _GeometryListener extends StatefulWidget {

0 commit comments

Comments
 (0)