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.

<!-- Links -->
[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
This commit is contained in:
Patrick Billingsley 2026-02-03 17:42:01 -06:00 committed by GitHub
parent 656937e6f7
commit 1e029098f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 137 additions and 2 deletions

View File

@ -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<OverlayPortal> {
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),
);

View File

@ -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: <OverlayEntry>[
outerEntry = OverlayEntry(
builder: (BuildContext context) {
return MediaQuery(
data: const MediaQueryData(padding: innerPadding),
child: Overlay(
initialEntries: <OverlayEntry>[
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: <OverlayEntry>[
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 {