diff --git a/packages/flutter/lib/src/widgets/debug.dart b/packages/flutter/lib/src/widgets/debug.dart index 9e346c1a086b..4ca4cb783323 100644 --- a/packages/flutter/lib/src/widgets/debug.dart +++ b/packages/flutter/lib/src/widgets/debug.dart @@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart'; import 'basic.dart'; import 'framework.dart'; import 'localizations.dart'; +import 'lookup_boundary.dart'; import 'media_query.dart'; import 'overlay.dart'; import 'table.dart'; @@ -468,12 +469,17 @@ bool debugCheckHasWidgetsLocalizations(BuildContext context) { /// Does nothing if asserts are disabled. Always returns true. bool debugCheckHasOverlay(BuildContext context) { assert(() { - if (context.widget is! Overlay && context.findAncestorWidgetOfExactType() == null) { + if (LookupBoundary.findAncestorWidgetOfExactType(context) == null) { + final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType(context); throw FlutterError.fromParts([ - 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( '${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.', ), ErrorHint( diff --git a/packages/flutter/lib/src/widgets/lookup_boundary.dart b/packages/flutter/lib/src/widgets/lookup_boundary.dart index e839b447c413..40ebaeaf6692 100644 --- a/packages/flutter/lib/src/widgets/lookup_boundary.dart +++ b/packages/flutter/lib/src/widgets/lookup_boundary.dart @@ -273,6 +273,29 @@ class LookupBoundary extends InheritedWidget { 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(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 /// [RenderObjectWidget] with a [RenderObject] of the specified type `T` /// from the provided [BuildContext]. diff --git a/packages/flutter/lib/src/widgets/overlay.dart b/packages/flutter/lib/src/widgets/overlay.dart index 15a5fe2aa437..e32479c12828 100644 --- a/packages/flutter/lib/src/widgets/overlay.dart +++ b/packages/flutter/lib/src/widgets/overlay.dart @@ -11,6 +11,7 @@ import 'package:flutter/scheduler.dart'; import 'basic.dart'; import 'framework.dart'; +import 'lookup_boundary.dart'; import 'ticker_provider.dart'; // Examples can assume: @@ -338,7 +339,8 @@ class Overlay extends StatefulWidget { final Clip clipBehavior; /// 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 /// overlay isn't found, then this function will throw an exception containing @@ -372,8 +374,13 @@ class Overlay extends StatefulWidget { final OverlayState? result = maybeOf(context, rootOverlay: rootOverlay); assert(() { if (result == null) { + final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorStateOfType(context); final List information = [ - 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.'), 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('The specific widget that failed to find an overlay was', debugRequiredFor, style: DiagnosticsTreeStyle.errorProperty), @@ -389,7 +396,7 @@ class Overlay extends StatefulWidget { } /// 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: /// @@ -413,8 +420,8 @@ class Overlay extends StatefulWidget { bool rootOverlay = false, }) { return rootOverlay - ? context.findRootAncestorStateOfType() - : context.findAncestorStateOfType(); + ? LookupBoundary.findRootAncestorStateOfType(context) + : LookupBoundary.findAncestorStateOfType(context); } @override diff --git a/packages/flutter/test/widgets/lookup_boundary_test.dart b/packages/flutter/test/widgets/lookup_boundary_test.dart index 41d18f3260f5..9b75cc98540c 100644 --- a/packages/flutter/test/widgets/lookup_boundary_test.dart +++ b/packages/flutter/test/widgets/lookup_boundary_test.dart @@ -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(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(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(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(context); + return Container(); + }, + )); + expect(isHidden, isFalse); + }); + }); + group('LookupBoundary.debugIsHidingAncestorRenderObjectOfType', () { testWidgets('is hiding', (WidgetTester tester) async { bool? isHidden; diff --git a/packages/flutter/test/widgets/overlay_test.dart b/packages/flutter/test/widgets/overlay_test.dart index 98c3ba24b191..eef17b9c6ad7 100644 --- a/packages/flutter/test/widgets/overlay_test.dart +++ b/packages/flutter/test/widgets/overlay_test.dart @@ -1227,6 +1227,125 @@ void main() { 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( + 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( + 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( + 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 {