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)

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

FileChange
engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dartWheel overlay, event forwarding, click-through

Trade-offs

AspectImpact
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

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.