mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
156 lines
5.5 KiB
Markdown
156 lines
5.5 KiB
Markdown
# 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
|
|
|
|
1. **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.
|
|
|
|
2. **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.
|
|
|
|
3. **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`)
|
|
|
|
```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
|
|
|
|
```dart
|
|
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:
|
|
|
|
```dart
|
|
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
|
|
|
|
1. ✅ Mouse wheel scrolling over cross-origin iframes scrolls the Flutter page
|
|
2. ✅ Clicking on the iframe content still works (video play, map interaction)
|
|
3. ✅ Flutter scrollables above/below the iframe work normally
|
|
4. ⚠️ Some complex iframe interactions may require clicking first to "focus" the iframe
|
|
|
|
## Alternative Approaches Considered
|
|
|
|
1. **CSS `pointer-events: none` on iframe**: Would block all iframe interaction
|
|
2. **iframe sandbox**: Would break iframe functionality
|
|
3. **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.
|
|
|