Skip to content

Commit

Permalink
Add LookupBoundary to Overlay (#116741)
Browse files Browse the repository at this point in the history
* Add LookupBoundary to Overlay

* fix analysis
  • Loading branch information
goderbauer committed Dec 12, 2022
1 parent a8c9f9c commit 5a229e2
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 8 deletions.
12 changes: 9 additions & 3 deletions packages/flutter/lib/src/widgets/debug.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<Overlay>() == null) {
if (LookupBoundary.findAncestorWidgetOfExactType<Overlay>(context) == null) {
final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<Overlay>(context);
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(
'${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(
Expand Down
23 changes: 23 additions & 0 deletions packages/flutter/lib/src/widgets/lookup_boundary.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<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
/// [RenderObjectWidget] with a [RenderObject] of the specified type `T`
/// from the provided [BuildContext].
Expand Down
17 changes: 12 additions & 5 deletions packages/flutter/lib/src/widgets/overlay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -372,8 +374,13 @@ class Overlay extends StatefulWidget {
final OverlayState? result = maybeOf(context, rootOverlay: rootOverlay);
assert(() {
if (result == null) {
final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorStateOfType<OverlayState>(context);
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.'),
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),
Expand All @@ -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:
///
Expand All @@ -413,8 +420,8 @@ class Overlay extends StatefulWidget {
bool rootOverlay = false,
}) {
return rootOverlay
? context.findRootAncestorStateOfType<OverlayState>()
: context.findAncestorStateOfType<OverlayState>();
? LookupBoundary.findRootAncestorStateOfType<OverlayState>(context)
: LookupBoundary.findAncestorStateOfType<OverlayState>(context);
}

@override
Expand Down
58 changes: 58 additions & 0 deletions packages/flutter/test/widgets/lookup_boundary_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
testWidgets('is hiding', (WidgetTester tester) async {
bool? isHidden;
Expand Down
119 changes: 119 additions & 0 deletions packages/flutter/test/widgets/overlay_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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>[
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 {
Expand Down

0 comments on commit 5a229e2

Please sign in to comment.