Unverified Commit 5a229e28 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Add LookupBoundary to Overlay (#116741)

* Add LookupBoundary to Overlay

* fix analysis
parent a8c9f9c6
...@@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart'; ...@@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'localizations.dart'; import 'localizations.dart';
import 'lookup_boundary.dart';
import 'media_query.dart'; import 'media_query.dart';
import 'overlay.dart'; import 'overlay.dart';
import 'table.dart'; import 'table.dart';
...@@ -468,12 +469,17 @@ bool debugCheckHasWidgetsLocalizations(BuildContext context) { ...@@ -468,12 +469,17 @@ bool debugCheckHasWidgetsLocalizations(BuildContext context) {
/// Does nothing if asserts are disabled. Always returns true. /// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasOverlay(BuildContext context) { bool debugCheckHasOverlay(BuildContext context) {
assert(() { assert(() {
if (context.widget is! Overlay && context.findAncestorWidgetOfExactType<Overlay>() == null) { if (LookupBoundary.findAncestorWidgetOfExactType<Overlay>(context) == null) {
final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Overlay>(context);
throw FlutterError.fromParts(<DiagnosticsNode>[ throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('No Overlay widget found.'), ErrorSummary('No Overlay widget found${hiddenByBoundary ? ' within the closest LookupBoundary' : ''}.'),
if (hiddenByBoundary)
ErrorDescription(
'There is an ancestor Overlay widget, but it is hidden by a LookupBoundary.'
),
ErrorDescription( ErrorDescription(
'${context.widget.runtimeType} widgets require an Overlay ' '${context.widget.runtimeType} widgets require an Overlay '
'widget ancestor.\n' 'widget ancestor within the closest LookupBoundary.\n'
'An overlay lets widgets float on top of other widget children.', 'An overlay lets widgets float on top of other widget children.',
), ),
ErrorHint( ErrorHint(
......
...@@ -273,6 +273,29 @@ class LookupBoundary extends InheritedWidget { ...@@ -273,6 +273,29 @@ class LookupBoundary extends InheritedWidget {
return result!; return result!;
} }
/// Returns true if a [LookupBoundary] is hiding the nearest [StatefulWidget]
/// with a [State] of the specified type `T` from the provided [BuildContext].
///
/// This method throws when asserts are disabled.
static bool debugIsHidingAncestorStateOfType<T extends State>(BuildContext context) {
bool? result;
assert(() {
bool hiddenByBoundary = false;
bool ancestorFound = false;
context.visitAncestorElements((Element ancestor) {
if (ancestor is StatefulElement && ancestor.state is T) {
ancestorFound = true;
return false;
}
hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary;
return true;
});
result = ancestorFound & hiddenByBoundary;
return true;
} ());
return result!;
}
/// Returns true if a [LookupBoundary] is hiding the nearest /// Returns true if a [LookupBoundary] is hiding the nearest
/// [RenderObjectWidget] with a [RenderObject] of the specified type `T` /// [RenderObjectWidget] with a [RenderObject] of the specified type `T`
/// from the provided [BuildContext]. /// from the provided [BuildContext].
......
...@@ -11,6 +11,7 @@ import 'package:flutter/scheduler.dart'; ...@@ -11,6 +11,7 @@ import 'package:flutter/scheduler.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'lookup_boundary.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
// Examples can assume: // Examples can assume:
...@@ -338,7 +339,8 @@ class Overlay extends StatefulWidget { ...@@ -338,7 +339,8 @@ class Overlay extends StatefulWidget {
final Clip clipBehavior; final Clip clipBehavior;
/// The [OverlayState] from the closest instance of [Overlay] that encloses /// The [OverlayState] from the closest instance of [Overlay] that encloses
/// the given context, and, in debug mode, will throw if one is not found. /// the given context within the closest [LookupBoundary], and, in debug mode,
/// will throw if one is not found.
/// ///
/// In debug mode, if the `debugRequiredFor` argument is provided and an /// In debug mode, if the `debugRequiredFor` argument is provided and an
/// overlay isn't found, then this function will throw an exception containing /// overlay isn't found, then this function will throw an exception containing
...@@ -372,8 +374,13 @@ class Overlay extends StatefulWidget { ...@@ -372,8 +374,13 @@ class Overlay extends StatefulWidget {
final OverlayState? result = maybeOf(context, rootOverlay: rootOverlay); final OverlayState? result = maybeOf(context, rootOverlay: rootOverlay);
assert(() { assert(() {
if (result == null) { if (result == null) {
final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorStateOfType<OverlayState>(context);
final List<DiagnosticsNode> information = <DiagnosticsNode>[ final List<DiagnosticsNode> information = <DiagnosticsNode>[
ErrorSummary('No Overlay widget found.'), ErrorSummary('No Overlay widget found${hiddenByBoundary ? ' within the closest LookupBoundary' : ''}.'),
if (hiddenByBoundary)
ErrorDescription(
'There is an ancestor Overlay widget, but it is hidden by a LookupBoundary.'
),
ErrorDescription('${debugRequiredFor?.runtimeType ?? 'Some'} widgets require an Overlay widget ancestor for correct operation.'), ErrorDescription('${debugRequiredFor?.runtimeType ?? 'Some'} widgets require an Overlay widget ancestor for correct operation.'),
ErrorHint('The most common way to add an Overlay to an application is to include a MaterialApp, CupertinoApp or Navigator widget in the runApp() call.'), ErrorHint('The most common way to add an Overlay to an application is to include a MaterialApp, CupertinoApp or Navigator widget in the runApp() call.'),
if (debugRequiredFor != null) DiagnosticsProperty<Widget>('The specific widget that failed to find an overlay was', debugRequiredFor, style: DiagnosticsTreeStyle.errorProperty), if (debugRequiredFor != null) DiagnosticsProperty<Widget>('The specific widget that failed to find an overlay was', debugRequiredFor, style: DiagnosticsTreeStyle.errorProperty),
...@@ -389,7 +396,7 @@ class Overlay extends StatefulWidget { ...@@ -389,7 +396,7 @@ class Overlay extends StatefulWidget {
} }
/// The [OverlayState] from the closest instance of [Overlay] that encloses /// The [OverlayState] from the closest instance of [Overlay] that encloses
/// the given context, if any. /// the given context within the closest [LookupBoundary], if any.
/// ///
/// Typical usage is as follows: /// Typical usage is as follows:
/// ///
...@@ -413,8 +420,8 @@ class Overlay extends StatefulWidget { ...@@ -413,8 +420,8 @@ class Overlay extends StatefulWidget {
bool rootOverlay = false, bool rootOverlay = false,
}) { }) {
return rootOverlay return rootOverlay
? context.findRootAncestorStateOfType<OverlayState>() ? LookupBoundary.findRootAncestorStateOfType<OverlayState>(context)
: context.findAncestorStateOfType<OverlayState>(); : LookupBoundary.findAncestorStateOfType<OverlayState>(context);
} }
@override @override
......
...@@ -1021,6 +1021,64 @@ void main() { ...@@ -1021,6 +1021,64 @@ void main() {
}); });
}); });
group('LookupBoundary.debugIsHidingAncestorStateOfType', () {
testWidgets('is hiding', (WidgetTester tester) async {
bool? isHidden;
await tester.pumpWidget(MyStatefulContainer(
child: LookupBoundary(
child: Builder(
builder: (BuildContext context) {
isHidden = LookupBoundary.debugIsHidingAncestorStateOfType<MyStatefulContainerState>(context);
return Container();
},
),
),
));
expect(isHidden, isTrue);
});
testWidgets('is not hiding entity within boundary', (WidgetTester tester) async {
bool? isHidden;
await tester.pumpWidget(MyStatefulContainer(
child: LookupBoundary(
child: MyStatefulContainer(
child: Builder(
builder: (BuildContext context) {
isHidden = LookupBoundary.debugIsHidingAncestorStateOfType<MyStatefulContainerState>(context);
return Container();
},
),
),
),
));
expect(isHidden, isFalse);
});
testWidgets('is not hiding if no boundary exists', (WidgetTester tester) async {
bool? isHidden;
await tester.pumpWidget(MyStatefulContainer(
child: Builder(
builder: (BuildContext context) {
isHidden = LookupBoundary.debugIsHidingAncestorStateOfType<MyStatefulContainerState>(context);
return Container();
},
),
));
expect(isHidden, isFalse);
});
testWidgets('is not hiding if no boundary and no entity exists', (WidgetTester tester) async {
bool? isHidden;
await tester.pumpWidget(Builder(
builder: (BuildContext context) {
isHidden = LookupBoundary.debugIsHidingAncestorStateOfType<MyStatefulContainerState>(context);
return Container();
},
));
expect(isHidden, isFalse);
});
});
group('LookupBoundary.debugIsHidingAncestorRenderObjectOfType', () { group('LookupBoundary.debugIsHidingAncestorRenderObjectOfType', () {
testWidgets('is hiding', (WidgetTester tester) async { testWidgets('is hiding', (WidgetTester tester) async {
bool? isHidden; bool? isHidden;
......
...@@ -1227,6 +1227,125 @@ void main() { ...@@ -1227,6 +1227,125 @@ void main() {
expect(error, isAssertionError); expect(error, isAssertionError);
}); });
}); });
group('LookupBoundary', () {
testWidgets('hides Overlay from Overlay.maybeOf', (WidgetTester tester) async {
OverlayState? overlay;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return LookupBoundary(
child: Builder(
builder: (BuildContext context) {
overlay = Overlay.maybeOf(context);
return Container();
},
),
);
},
),
],
),
),
);
expect(overlay, isNull);
});
testWidgets('hides Overlay from Overlay.of', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return LookupBoundary(
child: Builder(
builder: (BuildContext context) {
Overlay.of(context);
return Container();
},
),
);
},
),
],
),
),
);
final Object? exception = tester.takeException();
expect(exception, isFlutterError);
final FlutterError error = exception! as FlutterError;
expect(
error.toStringDeep(),
'FlutterError\n'
' No Overlay widget found within the closest LookupBoundary.\n'
' There is an ancestor Overlay widget, but it is hidden by a\n'
' LookupBoundary.\n'
' Some widgets require an Overlay widget ancestor for correct\n'
' operation.\n'
' The most common way to add an Overlay to an application is to\n'
' include a MaterialApp, CupertinoApp or Navigator widget in the\n'
' runApp() call.\n'
' The context from which that widget was searching for an overlay\n'
' was:\n'
' Builder\n'
);
});
testWidgets('hides Overlay from debugCheckHasOverlay', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return LookupBoundary(
child: Builder(
builder: (BuildContext context) {
debugCheckHasOverlay(context);
return Container();
},
),
);
},
),
],
),
),
);
final Object? exception = tester.takeException();
expect(exception, isFlutterError);
final FlutterError error = exception! as FlutterError;
expect(
error.toStringDeep(), startsWith(
'FlutterError\n'
' No Overlay widget found within the closest LookupBoundary.\n'
' There is an ancestor Overlay widget, but it is hidden by a\n'
' LookupBoundary.\n'
' Builder widgets require an Overlay widget ancestor within the\n'
' closest LookupBoundary.\n'
' An overlay lets widgets float on top of other widget children.\n'
' To introduce an Overlay widget, you can either directly include\n'
' one, or use a widget that contains an Overlay itself, such as a\n'
' Navigator, WidgetApp, MaterialApp, or CupertinoApp.\n'
' The specific widget that could not find a Overlay ancestor was:\n'
' Builder\n'
' The ancestors of this widget were:\n'
' LookupBoundary\n'
),
);
});
});
} }
class StatefulTestWidget extends StatefulWidget { class StatefulTestWidget extends StatefulWidget {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment