From 1e029098f8ff33eddfd7cf0d03af400a3b052eac Mon Sep 17 00:00:00 2001 From: Patrick Billingsley Date: Tue, 3 Feb 2026 17:42:01 -0600 Subject: [PATCH] Propagates Overlay's MediaQueryData to OverlayPortal child (#181579) Widgets like Scaffold strip MediaQueryData causing widgets rendered in the Overlay to be obscured, e.g., by the software keyboard. [Related Issue - 142921](https://github.com/flutter/flutter/issues/142921) [Related Issue - 157664](https://github.com/flutter/flutter/issues/157664) This does not directly fix these two issues but appears to be an underlying issue with both which merited this being a separate PR. I have a separate fix for the MenuAnchor that I will supply in a follow up PR. ## 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. - [ ] 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. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- packages/flutter/lib/src/widgets/overlay.dart | 16 ++- .../test/widgets/overlay_portal_test.dart | 123 ++++++++++++++++++ 2 files changed, 137 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/widgets/overlay.dart b/packages/flutter/lib/src/widgets/overlay.dart index b2104aca183..e60f585522a 100644 --- a/packages/flutter/lib/src/widgets/overlay.dart +++ b/packages/flutter/lib/src/widgets/overlay.dart @@ -26,6 +26,7 @@ import 'basic.dart'; import 'framework.dart'; import 'layout_builder.dart'; import 'lookup_boundary.dart'; +import 'media_query.dart'; import 'ticker_provider.dart'; /// The signature of the widget builder callback used in @@ -2029,11 +2030,22 @@ class _OverlayPortalState extends State { child: Semantics(traversalParentIdentifier: this, child: widget.child), ); } + + final _OverlayEntryLocation overlayLocation = _getLocation(zOrderIndex, widget.overlayLocation); + final MediaQueryData overlayData = MediaQuery.of(overlayLocation._childModel.context); + final MediaQueryData data = MediaQuery.of(context).copyWith( + padding: overlayData.padding, + viewInsets: overlayData.viewInsets, + viewPadding: overlayData.viewPadding, + ); return _OverlayPortal( - overlayLocation: _getLocation(zOrderIndex, widget.overlayLocation), + overlayLocation: overlayLocation, overlayChild: _DeferredLayout( childIdentifier: this, - child: Builder(builder: widget.overlayChildBuilder), + child: MediaQuery( + data: data, + child: Builder(builder: widget.overlayChildBuilder), + ), ), child: Semantics(traversalParentIdentifier: this, child: widget.child), ); diff --git a/packages/flutter/test/widgets/overlay_portal_test.dart b/packages/flutter/test/widgets/overlay_portal_test.dart index 0ad0b7f0268..437262cc5ed 100644 --- a/packages/flutter/test/widgets/overlay_portal_test.dart +++ b/packages/flutter/test/widgets/overlay_portal_test.dart @@ -154,6 +154,129 @@ void main() { expect(directionSeenByOverlayChild, textDirection); }); + testWidgets( + 'OverlayPortal overlayChild located in root Overlay receives MediaQuery properties from root Overlay context', + (WidgetTester tester) async { + final controller = OverlayPortalController(); + const rootPadding = EdgeInsets.all(10); + const innerPadding = EdgeInsets.all(20); + + MediaQueryData? overlayChildData; + OverlayEntry? outerEntry; + OverlayEntry? innerEntry; + addTearDown(() { + outerEntry?.remove(); + outerEntry?.dispose(); + innerEntry?.remove(); + innerEntry?.dispose(); + }); + + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(padding: rootPadding), + child: Directionality( + textDirection: TextDirection.ltr, + child: Overlay( + initialEntries: [ + outerEntry = OverlayEntry( + builder: (BuildContext context) { + return MediaQuery( + data: const MediaQueryData(padding: innerPadding), + child: Overlay( + initialEntries: [ + innerEntry = OverlayEntry( + builder: (BuildContext context) { + return OverlayPortal( + controller: controller, + overlayLocation: OverlayChildLocation.rootOverlay, + overlayChildBuilder: (BuildContext context) { + overlayChildData = MediaQuery.of(context); + return const SizedBox(); + }, + child: const SizedBox(), + ); + }, + ), + ], + ), + ); + }, + ), + ], + ), + ), + ), + ); + + controller.show(); + await tester.pump(); + + expect(overlayChildData?.padding, rootPadding); + }, + ); + + testWidgets('OverlayPortal overlayChild receives MediaQuery properties from Overlay context', ( + WidgetTester tester, + ) async { + final controller = OverlayPortalController(); + const expectedPadding = EdgeInsets.all(10); + const expectedViewInsets = EdgeInsets.only(bottom: 300); + const expectedViewPadding = EdgeInsets.only(top: 50, bottom: 20); + const expectedSize = Size(800, 600); + + MediaQueryData? overlayChildData; + OverlayEntry? entry; + addTearDown(() { + entry?.remove(); + entry?.dispose(); + }); + + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData( + padding: expectedPadding, + viewInsets: expectedViewInsets, + viewPadding: expectedViewPadding, + size: expectedSize, + ), + child: Directionality( + textDirection: TextDirection.ltr, + child: Overlay( + initialEntries: [ + entry = OverlayEntry( + builder: (BuildContext context) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + padding: EdgeInsets.zero, + viewInsets: EdgeInsets.zero, + viewPadding: EdgeInsets.zero, + ), + child: OverlayPortal( + controller: controller, + overlayChildBuilder: (BuildContext context) { + overlayChildData = MediaQuery.of(context); + return const SizedBox(); + }, + child: const SizedBox(), + ), + ); + }, + ), + ], + ), + ), + ), + ); + + controller.show(); + await tester.pump(); + + expect(overlayChildData?.padding, expectedPadding); + expect(overlayChildData?.viewInsets, expectedViewInsets); + expect(overlayChildData?.viewPadding, expectedViewPadding); + expect(overlayChildData?.size, expectedSize); + }); + testWidgets('The overlay portal update semantics does not dirty overlay', ( WidgetTester tester, ) async {