Let CupertinoNavBar automatically have a back or close button (#12575)

* create auto back button behaviour

* cosmetic fidelity

* tests

* review

* document new icon
diff --git a/packages/flutter/lib/src/cupertino/button.dart b/packages/flutter/lib/src/cupertino/button.dart
index 172ef2f..2fa9e83 100644
--- a/packages/flutter/lib/src/cupertino/button.dart
+++ b/packages/flutter/lib/src/cupertino/button.dart
@@ -190,6 +190,7 @@
     final Color backgroundColor = widget.color;
 
     return new GestureDetector(
+      behavior: HitTestBehavior.opaque,
       onTapDown: enabled ? _handleTapDown : null,
       onTapUp: enabled ? _handleTapUp : null,
       onTapCancel: enabled ? _handleTapCancel : null,
diff --git a/packages/flutter/lib/src/cupertino/icons.dart b/packages/flutter/lib/src/cupertino/icons.dart
index cb65f09..b7048b6 100644
--- a/packages/flutter/lib/src/cupertino/icons.dart
+++ b/packages/flutter/lib/src/cupertino/icons.dart
@@ -77,4 +77,7 @@
 
   /// A checkmark in a circle.
   static const IconData check_mark_circled = const IconData(0xf41f, fontFamily: iconFont, fontPackage: iconFontPackage);
-}
\ No newline at end of file
+
+  /// A thicker left chevron used in iOS for the nav bar back button.
+  static const IconData back = const IconData(0xf3f0, fontFamily: iconFont, fontPackage: iconFontPackage);
+}
diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart
index ed4a604..50935fe 100644
--- a/packages/flutter/lib/src/cupertino/nav_bar.dart
+++ b/packages/flutter/lib/src/cupertino/nav_bar.dart
@@ -7,7 +7,9 @@
 import 'package:flutter/foundation.dart';
 import 'package:flutter/widgets.dart';
 
+import 'button.dart';
 import 'colors.dart';
+import 'icons.dart';
 
 /// Standard iOS navigation bar height without the status bar.
 const double _kNavBarPersistentHeight = 44.0;
@@ -22,6 +24,11 @@
 
 const double _kNavBarEdgePadding = 16.0;
 
+// The back chevron has a special padding in iOS.
+const double _kNavBarBackButtonPadding = 0.0;
+
+const double _kNavBarBackButtonTapWidth = 50.0;
+
 /// Title text transfer fade.
 const Duration _kNavBarTitleFadeDuration = const Duration(milliseconds: 150);
 
@@ -44,6 +51,10 @@
 /// It also supports a [leading] and [trailing] widget before and after the
 /// [middle] widget while keeping the [middle] widget centered.
 ///
+/// The [leading] widget will automatically be a back chevron icon button (or a
+/// close button in case of a fullscreen dialog) to pop the current route if none
+/// is provided and [automaticallyImplyLeading] is true (true by default).
+///
 /// It should be placed at top of the screen and automatically accounts for
 /// the OS's status bar.
 ///
@@ -56,25 +67,31 @@
 ///    [CupertinoNavigationBar].
 ///  * [CupertinoSliverNavigationBar] for a navigation bar to be placed in a
 ///    scrolling list and that supports iOS-11-style large titles.
-//
-// TODO(xster): document automatic addition of a CupertinoBackButton.
-// TODO(xster): add sample code using icons.
 class CupertinoNavigationBar extends StatelessWidget implements PreferredSizeWidget {
   /// Creates a navigation bar in the iOS style.
   const CupertinoNavigationBar({
     Key key,
     this.leading,
-    @required this.middle,
+    this.automaticallyImplyLeading: true,
+    this.middle,
     this.trailing,
     this.backgroundColor: _kDefaultNavBarBackgroundColor,
     this.actionsForegroundColor: CupertinoColors.activeBlue,
-  }) : assert(middle != null, 'There must be a middle widget, usually a title.'),
+  }) : assert(automaticallyImplyLeading != null),
        super(key: key);
 
   /// Widget to place at the start of the navigation bar. Normally a back button
   /// for a normal page or a cancel button for full page dialogs.
   final Widget leading;
 
+  /// Controls whether we should try to imply the leading widget if null.
+  ///
+  /// If true and [leading] is null, automatically try to deduce what the [leading]
+  /// widget should be. If [leading] widget is not null, this parameter has no effect.
+  ///
+  /// This value cannot be null.
+  final bool automaticallyImplyLeading;
+
   /// Widget to place in the middle of the navigation bar. Normally a title or
   /// a segmented control.
   final Widget middle;
@@ -111,6 +128,7 @@
       backgroundColor: backgroundColor,
       child: new _CupertinoPersistentNavigationBar(
         leading: leading,
+        automaticallyImplyLeading: automaticallyImplyLeading,
         middle: middle,
         trailing: trailing,
         actionsForegroundColor: actionsForegroundColor,
@@ -137,6 +155,13 @@
 /// For advanced uses, an optional [middle] widget can be supplied to show a
 /// different widget in the middle of the navigation bar when the sliver is collapsed.
 ///
+/// Like [CupertinoNavigationBar], it also supports a [leading] and [trailing]
+/// widget on the static section on top that remains while scrolling.
+///
+/// The [leading] widget will automatically be a back chevron icon button (or a
+/// close button in case of a fullscreen dialog) to pop the current route if none
+/// is provided and [automaticallyImplyLeading] is true (true by default).
+///
 /// See also:
 ///
 ///  * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling
@@ -149,11 +174,13 @@
     Key key,
     @required this.largeTitle,
     this.leading,
+    this.automaticallyImplyLeading: true,
     this.middle,
     this.trailing,
     this.backgroundColor: _kDefaultNavBarBackgroundColor,
     this.actionsForegroundColor: CupertinoColors.activeBlue,
   }) : assert(largeTitle != null),
+       assert(automaticallyImplyLeading != null),
        super(key: key);
 
   /// The navigation bar's title.
@@ -178,6 +205,14 @@
   /// This widget is visible in both collapsed and expanded states.
   final Widget leading;
 
+  /// Controls whether we should try to imply the leading widget if null.
+  ///
+  /// If true and [leading] is null, automatically try to deduce what the [leading]
+  /// widget should be. If [leading] widget is not null, this parameter has no effect.
+  ///
+  /// This value cannot be null.
+  final bool automaticallyImplyLeading;
+
   /// A widget to place in the middle of the static navigation bar instead of
   /// the [largeTitle].
   ///
@@ -215,6 +250,7 @@
         persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
         title: largeTitle,
         leading: leading,
+        automaticallyImplyLeading: automaticallyImplyLeading,
         middle: middle,
         trailing: trailing,
         backgroundColor: backgroundColor,
@@ -261,7 +297,8 @@
   const _CupertinoPersistentNavigationBar({
     Key key,
     this.leading,
-    @required this.middle,
+    this.automaticallyImplyLeading,
+    this.middle,
     this.trailing,
     this.actionsForegroundColor,
     this.middleVisible,
@@ -269,6 +306,8 @@
 
   final Widget leading;
 
+  final bool automaticallyImplyLeading;
+
   final Widget middle;
 
   final Widget trailing;
@@ -320,7 +359,27 @@
         child: styledMiddle,
       );
 
-    // TODO(xster): automatically build a CupertinoBackButton.
+    // Auto add back button if leading not provided.
+    Widget backOrCloseButton;
+    bool useBackButton = false;
+    if (styledLeading == null && automaticallyImplyLeading) {
+      final ModalRoute<dynamic> currentRoute = ModalRoute.of(context);
+      if (currentRoute?.canPop == true) {
+        useBackButton = !(currentRoute is PageRoute && currentRoute?.fullscreenDialog == true);
+        backOrCloseButton = new CupertinoButton(
+          child: useBackButton
+              ? new Container(
+                height: _kNavBarPersistentHeight,
+                width: _kNavBarBackButtonTapWidth,
+                alignment: Alignment.centerLeft,
+                child: const Icon(CupertinoIcons.back, size: 34.0,)
+              )
+              : const Text('Close'),
+          padding: EdgeInsets.zero,
+          onPressed: () { Navigator.of(context).maybePop(); },
+        );
+      }
+    }
 
     return new SizedBox(
       height: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
@@ -332,16 +391,14 @@
         child: new Padding(
           padding: new EdgeInsets.only(
             top: MediaQuery.of(context).padding.top,
-            // TODO(xster): dynamically reduce padding when an automatic
-            // CupertinoBackButton is present.
-            left: _kNavBarEdgePadding,
+            left: useBackButton ? _kNavBarBackButtonPadding : _kNavBarEdgePadding,
             right: _kNavBarEdgePadding,
           ),
           child: new MediaQuery.removePadding(
             context: context,
             removeTop: true,
             child: new NavigationToolbar(
-              leading: styledLeading,
+              leading: styledLeading ?? backOrCloseButton,
               middle: animatedStyledMiddle,
               trailing: styledTrailing,
               centerMiddle: true,
@@ -358,6 +415,7 @@
     @required this.persistentHeight,
     @required this.title,
     this.leading,
+    this.automaticallyImplyLeading,
     this.middle,
     this.trailing,
     this.backgroundColor,
@@ -370,6 +428,8 @@
 
   final Widget leading;
 
+  final bool automaticallyImplyLeading;
+
   final Widget middle;
 
   final Widget trailing;
@@ -391,6 +451,7 @@
     final _CupertinoPersistentNavigationBar persistentNavigationBar =
         new _CupertinoPersistentNavigationBar(
       leading: leading,
+      automaticallyImplyLeading: automaticallyImplyLeading,
       middle: middle ?? title,
       trailing: trailing,
       // If middle widget exists, always show it. Otherwise, show title
diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart
index f3ebcd4..74c8e9a 100644
--- a/packages/flutter/test/cupertino/nav_bar_test.dart
+++ b/packages/flutter/test/cupertino/nav_bar_test.dart
@@ -275,6 +275,70 @@
 
     expect(tester.getBottomLeft(find.text('Title')).dy, 44.0 - 8.0); // Extension gone, (static part - padding) left.
   });
+
+  testWidgets('Auto back/close button', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      new WidgetsApp(
+        color: const Color(0xFFFFFFFF),
+        onGenerateRoute: (RouteSettings settings) {
+          return new CupertinoPageRoute<Null>(
+            settings: settings,
+            builder: (BuildContext context) {
+              return const CupertinoNavigationBar(
+                middle: const Text('Home page'),
+              );
+            },
+          );
+        },
+      ),
+    );
+
+    expect(find.byType(CupertinoButton), findsNothing);
+
+    tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<Null>(
+      builder: (BuildContext context) {
+        return const CupertinoNavigationBar(
+          middle: const Text('Page 2'),
+        );
+      },
+    ));
+
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 200));
+
+    expect(find.byType(CupertinoButton), findsOneWidget);
+    expect(find.byType(Icon), findsOneWidget);
+
+    tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<Null>(
+      fullscreenDialog: true,
+      builder: (BuildContext context) {
+        return const CupertinoNavigationBar(
+          middle: const Text('Dialog page'),
+        );
+      },
+    ));
+
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 200));
+
+    expect(find.byType(CupertinoButton), findsNWidgets(2));
+    expect(find.text('Close'), findsOneWidget);
+
+    // Test popping goes back correctly.
+    await tester.tap(find.text('Close'));
+
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 200));
+
+    expect(find.text('Page 2'), findsOneWidget);
+
+    await tester.tap(find.byType(Icon));
+
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 200));
+
+    expect(find.text('Home page'), findsOneWidget);
+  });
 }
 
 class _ExpectStyles extends StatelessWidget {