5.5 KiB
Issue #113196: Mouse Scroll Blocked Over HtmlElementView/Cross-Origin iframe
Problem Description
When using HtmlElementView to embed a cross-origin iframe (e.g., YouTube video, Google Maps, third-party content) inside a Flutter web app, mouse wheel scrolling over the iframe is completely blocked. The user cannot scroll the Flutter page while hovering over the embedded iframe.
User Experience Impact
- Users cannot scroll the page when mouse is over embedded content
- Have to move mouse away from iframe to scroll
- Makes pages with large embedded iframes very difficult to navigate
Difference from Other Issues
- Issue #156985: Flutter IN an iframe, scroll bubbling UP to parent
- Issue #157435: Touch scroll in embedded mode
- Issue #113196: Scroll OVER a cross-origin iframe INSIDE Flutter
Root Cause Analysis
-
Cross-Origin Isolation: Cross-origin iframes completely isolate all events due to browser security. Wheel events that occur inside the iframe never reach the parent Flutter page.
-
Browser Security Model: When the mouse is over a cross-origin iframe, the browser sends wheel events to the iframe's document, not the parent. The parent document receives nothing.
-
No Event Forwarding: Unlike same-origin iframes, cross-origin iframes cannot forward events to the parent due to the Same-Origin Policy.
Solution
Transparent Overlay Approach
Add a transparent overlay element on top of the platform view that captures wheel events and forwards them to Flutter:
Engine Changes (content_manager.dart)
DomElement _safelyCreatePlatformViewSlot(int viewId, String viewType, String slotName) {
return _contents.putIfAbsent(viewId, () {
final DomElement wrapper = domDocument.createElement('flt-platform-view')
..id = getPlatformViewDomId(viewId)
..setAttribute('slot', slotName)
..style.position = 'relative'
..style.width = '100%'
..style.height = '100%'
..style.display = 'block';
// ... create content ...
// Add transparent overlay to capture wheel events
final DomElement wheelOverlay = domDocument.createElement('div')
..style.position = 'absolute'
..style.top = '0'
..style.left = '0'
..style.width = '100%'
..style.height = '100%'
..style.zIndex = '1000'
..style.pointerEvents = 'auto';
_setupWheelEventForwarding(wheelOverlay, wrapper);
wrapper.append(wheelOverlay);
return wrapper;
});
}
Wheel Event Forwarding
void _setupWheelEventForwarding(DomElement overlay, DomElement wrapper) {
overlay.addEventListener(
'wheel',
createDomEventListener((DomEvent event) {
event.stopPropagation();
event.preventDefault();
// Find flutter-view element
DomElement? flutterView = wrapper.parentElement;
while (flutterView != null && flutterView.tagName != 'FLUTTER-VIEW') {
flutterView = flutterView.parentElement;
}
if (flutterView != null) {
// Create and dispatch new wheel event to flutter-view
final DomWheelEvent wheelEvent = event as DomWheelEvent;
final DomWheelEvent newEvent = createDomWheelEvent(
'wheel',
<String, dynamic>{
'bubbles': true,
'cancelable': true,
'clientX': wheelEvent.clientX,
'clientY': wheelEvent.clientY,
'deltaX': wheelEvent.deltaX,
'deltaY': wheelEvent.deltaY,
'deltaMode': wheelEvent.deltaMode,
'buttons': wheelEvent.buttons,
},
);
flutterView.dispatchEvent(newEvent);
}
}),
<String, bool>{'capture': false, 'passive': false}.jsify()!,
);
}
Click-Through Handling
For clicks and other pointer events, temporarily disable the overlay:
void _forwardPointerEventToContent(DomMouseEvent event, DomElement overlay) {
// Temporarily hide overlay to allow click-through
final String originalPointerEvents = overlay.style.pointerEvents;
overlay.style.pointerEvents = 'none';
// Use microtask to restore after browser dispatches event
Future<void>.microtask(() {
overlay.style.pointerEvents = originalPointerEvents;
});
}
Files Changed
| File | Change |
|---|---|
engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart |
Wheel overlay, event forwarding, click-through |
Trade-offs
| Aspect | Impact |
|---|---|
| Wheel scrolling | ✅ Works - overlay captures and forwards to Flutter |
| Click/tap on iframe | ✅ Works - overlay temporarily disables for clicks |
| Iframe interactivity | ⚠️ Limited - complex interactions inside iframe may not work |
| Keyboard input | ✅ Works - overlay doesn't capture keyboard events |
Demo
- Before Fix: https://issue-113196-before.web.app
- After Fix: https://issue-113196-after.web.app
Behavior After Fix
- ✅ Mouse wheel scrolling over cross-origin iframes scrolls the Flutter page
- ✅ Clicking on the iframe content still works (video play, map interaction)
- ✅ Flutter scrollables above/below the iframe work normally
- ⚠️ Some complex iframe interactions may require clicking first to "focus" the iframe
Alternative Approaches Considered
- CSS
pointer-events: noneon iframe: Would block all iframe interaction - iframe sandbox: Would break iframe functionality
- postMessage coordination: Requires cooperation from iframe content (not possible for third-party)
The overlay approach is the best balance of scroll functionality and iframe interactivity.