forked from firka/flutter
Fix: Remove attach target on deactivation of widget from overlay portal controller (#164439)
Fix: Remove attach target on deactivation of widget from overlay portal controller fixes: #164376 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing.
This commit is contained in:
@@ -1920,7 +1920,8 @@ class _OverlayPortalState extends State<OverlayPortal> {
|
||||
|
||||
void _setupController(OverlayPortalController controller) {
|
||||
assert(
|
||||
controller._attachTarget == null || controller._attachTarget == this,
|
||||
controller._attachTarget == this ||
|
||||
!((controller._attachTarget?.context as StatefulElement?)?.debugIsActive ?? false),
|
||||
'Failed to attach $controller to $this. It is already attached to ${controller._attachTarget}.',
|
||||
);
|
||||
final int? controllerZOrderIndex = controller._zOrderIndex;
|
||||
@@ -1951,8 +1952,13 @@ class _OverlayPortalState extends State<OverlayPortal> {
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
void activate() {
|
||||
assert(widget.controller._attachTarget == this);
|
||||
super.activate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller._attachTarget = null;
|
||||
_locationCache?._debugMarkLocationInvalid();
|
||||
_locationCache = null;
|
||||
|
||||
@@ -1365,6 +1365,179 @@ void main() {
|
||||
verifyTreeIsClean();
|
||||
});
|
||||
|
||||
testWidgets('PortalController can be assigned to another after deactivate', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final OverlayPortalController controller1 = OverlayPortalController();
|
||||
final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>();
|
||||
|
||||
final OverlayEntry overlayEntry1 = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return OverlayPortal(
|
||||
controller: controller1,
|
||||
overlayChildBuilder: (BuildContext context) => const Placeholder(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final OverlayEntry overlayEntry2 = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return OverlayPortal(
|
||||
controller: controller1,
|
||||
overlayChildBuilder: (BuildContext context) => const Placeholder(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
addTearDown(() {
|
||||
overlayEntry1
|
||||
..remove()
|
||||
..dispose();
|
||||
overlayEntry2.dispose();
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Overlay(key: overlayKey, initialEntries: <OverlayEntry>[overlayEntry1]),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Overlay(key: overlayKey, initialEntries: <OverlayEntry>[overlayEntry2]),
|
||||
),
|
||||
);
|
||||
|
||||
verifyTreeIsClean();
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('Reactivation maintains portal state', (WidgetTester tester) async {
|
||||
final OverlayPortalController controller1 = OverlayPortalController();
|
||||
final GlobalKey<State<OverlayPortal>> portalKey = GlobalKey<State<OverlayPortal>>();
|
||||
|
||||
late OverlayEntry overlayEntry1, overlayEntry2;
|
||||
addTearDown(() {
|
||||
overlayEntry1
|
||||
..remove()
|
||||
..dispose();
|
||||
overlayEntry2
|
||||
..remove()
|
||||
..dispose();
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
overlayEntry1 = OverlayEntry(
|
||||
builder:
|
||||
(BuildContext context) => OverlayPortal(
|
||||
key: portalKey,
|
||||
controller: controller1,
|
||||
overlayChildBuilder: (BuildContext context) => const Placeholder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
controller1.show();
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: SizedBox(
|
||||
child: Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
overlayEntry2 = OverlayEntry(
|
||||
builder:
|
||||
(BuildContext context) => OverlayPortal(
|
||||
key: portalKey,
|
||||
controller: controller1,
|
||||
overlayChildBuilder: (BuildContext context) => const Placeholder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(Placeholder), findsOneWidget);
|
||||
expect(controller1.isShowing, equals(true));
|
||||
});
|
||||
|
||||
testWidgets('attachTarget is restored after reparenting', (WidgetTester tester) async {
|
||||
final GlobalKey<State<OverlayPortal>> portalKey = GlobalKey<State<OverlayPortal>>();
|
||||
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
|
||||
final RenderBox overlayChildBox = RenderConstrainedBox(
|
||||
additionalConstraints: const BoxConstraints(),
|
||||
);
|
||||
|
||||
bool moveToSecondOverlay = false;
|
||||
|
||||
final Widget child = WidgetToRenderBoxAdapter(renderBox: childBox);
|
||||
final Widget overlayChild = WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
|
||||
|
||||
final OverlayEntry overlayEntry1 = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return !moveToSecondOverlay
|
||||
? OverlayPortal(
|
||||
key: portalKey,
|
||||
controller: controller1,
|
||||
overlayChildBuilder: (BuildContext context) => overlayChild,
|
||||
child: child,
|
||||
)
|
||||
: const SizedBox();
|
||||
},
|
||||
);
|
||||
final OverlayEntry overlayEntry2 = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return moveToSecondOverlay
|
||||
? OverlayPortal(
|
||||
key: portalKey,
|
||||
controller: controller1,
|
||||
overlayChildBuilder: (BuildContext context) => overlayChild,
|
||||
child: child,
|
||||
)
|
||||
: const SizedBox();
|
||||
},
|
||||
);
|
||||
addTearDown(() {
|
||||
overlayEntry1
|
||||
..remove()
|
||||
..dispose();
|
||||
overlayEntry2
|
||||
..remove()
|
||||
..dispose();
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Overlay(initialEntries: <OverlayEntry>[overlayEntry1]),
|
||||
Overlay(initialEntries: <OverlayEntry>[overlayEntry2]),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Move to second overlay
|
||||
moveToSecondOverlay = true;
|
||||
overlayEntry1.markNeedsBuild();
|
||||
overlayEntry2.markNeedsBuild();
|
||||
await tester.pump();
|
||||
|
||||
verifyTreeIsClean();
|
||||
});
|
||||
|
||||
group('GlobalKey Reparenting', () {
|
||||
testWidgets('child is laid out before overlay child after OverlayEntry shuffle', (
|
||||
WidgetTester tester,
|
||||
|
||||
Reference in New Issue
Block a user