Add a rootNavigator option to Navigator.of (#12580)

This commit is contained in:
xster 2017-10-18 01:31:29 -07:00 committed by GitHub
parent 964a138d80
commit 822084b235
5 changed files with 106 additions and 3 deletions

View file

@ -53,6 +53,9 @@ const BoxDecoration _kCupertinoDialogBackFill = const BoxDecoration(
/// dialog. Rather than using this widget directly, consider using
/// [CupertinoAlertDialog], which implement a specific kind of dialog.
///
/// Push with `Navigator.of(..., rootNavigator: true)` when using with
/// [CupertinoTabScaffold] to ensure that the dialog appears above the tabs.
///
/// See also:
///
/// * [CupertinoAlertDialog], which is a dialog with title, contents, and

View file

@ -461,7 +461,7 @@ Future<T> showDialog<T>({
bool barrierDismissible: true,
@required Widget child,
}) {
return Navigator.push(context, new _DialogRoute<T>(
return Navigator.of(context, rootNavigator: true).push(new _DialogRoute<T>(
child: child,
theme: Theme.of(context, shadowThemeOnly: true),
barrierDismissible: barrierDismissible,

View file

@ -2003,6 +2003,17 @@ abstract class BuildContext {
/// ```
State ancestorStateOfType(TypeMatcher matcher);
/// Returns the [State] object of the furthest ancestor [StatefulWidget] widget
/// that matches the given [TypeMatcher].
///
/// Functions the same way as [ancestorStateOfType] but keeps visiting subsequent
/// ancestors until there are none of the type matching [TypeMatcher] remaining.
/// Then returns the last one found.
///
/// This operation is O(N) as well though N is the entire widget tree rather than
/// a subtree.
State rootAncestorStateOfType(TypeMatcher matcher);
/// Returns the [RenderObject] object of the nearest ancestor [RenderObjectWidget] widget
/// that matches the given [TypeMatcher].
///
@ -3245,6 +3256,19 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
return statefulAncestor?.state;
}
@override
State rootAncestorStateOfType(TypeMatcher matcher) {
assert(_debugCheckStateIsActiveForAncestorLoopkup());
Element ancestor = _parent;
StatefulElement statefulAncestor;
while (ancestor != null) {
if (ancestor is StatefulElement && matcher.check(ancestor.state))
statefulAncestor = ancestor;
ancestor = ancestor._parent;
}
return statefulAncestor?.state;
}
@override
RenderObject ancestorRenderObjectOfType(TypeMatcher matcher) {
assert(_debugCheckStateIsActiveForAncestorLoopkup());

View file

@ -708,8 +708,17 @@ class Navigator extends StatefulWidget {
/// ..pop()
/// ..pushNamed('/settings');
/// ```
static NavigatorState of(BuildContext context) {
final NavigatorState navigator = context.ancestorStateOfType(const TypeMatcher<NavigatorState>());
///
/// If `rootNavigator` is set to true, the state from the furthest instance of
/// this class is given instead. Useful for pushing contents above all subsequent
/// instances of [Navigator].
static NavigatorState of(
BuildContext context, {
bool rootNavigator: false
}) {
final NavigatorState navigator = rootNavigator
? context.rootAncestorStateOfType(const TypeMatcher<NavigatorState>())
: context.ancestorStateOfType(const TypeMatcher<NavigatorState>());
assert(() {
if (navigator == null) {
throw new FlutterError(

View file

@ -179,6 +179,73 @@ void main() {
expect('$exception', startsWith('Navigator operation requested with a context'));
});
testWidgets('Navigator.of rootNavigator finds root Navigator', (WidgetTester tester) async {
await tester.pumpWidget(new MaterialApp(
home: new Material(
child: new Column(
children: <Widget>[
const SizedBox(
height: 300.0,
child: const Text('Root page'),
),
new SizedBox(
height: 300.0,
child: new Navigator(
onGenerateRoute: (RouteSettings settings) {
if (settings.isInitialRoute) {
return new MaterialPageRoute<Null>(
builder: (BuildContext context) {
return new RaisedButton(
child: const Text('Next'),
onPressed: () {
Navigator.of(context).push(
new MaterialPageRoute<Null>(
builder: (BuildContext context) {
return new RaisedButton(
child: const Text('Inner page'),
onPressed: () {
Navigator.of(context, rootNavigator: true).push(
new MaterialPageRoute<Null>(
builder: (BuildContext context) {
return const Text('Dialog');
}
),
);
},
);
}
),
);
},
);
},
);
}
},
),
),
],
),
),
));
await tester.tap(find.text('Next'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
// Both elements are on screen.
expect(tester.getTopLeft(find.text('Root page')).dy, 0.0);
expect(tester.getTopLeft(find.text('Inner page')).dy, greaterThan(300.0));
await tester.tap(find.text('Inner page'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
// Dialog is pushed to the whole page and is at the top of the screen, not
// inside the inner page.
expect(tester.getTopLeft(find.text('Dialog')).dy, 0.0);
});
testWidgets('Gestures between push and build are ignored', (WidgetTester tester) async {
final List<String> log = <String>[];
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{