Flutter Web Scroll Event Handling Fixes

This directory documents the fixes for three related GitHub issues dealing with scroll event handling in Flutter web applications.

Issues Overview

IssueProblemKey Fix
#156985Scroll events bubble to parent page when Flutter is in iframepreventDefault() in iframe + explicit parent scroll via postMessage
#157435Touch scroll doesn't propagate to host page (embedded mode)Don't preventDefault() on touch + platform channel for scroll propagation
#113196Mouse scroll blocked over cross-origin iframe in HtmlElementViewTransparent overlay captures wheel events and forwards to Flutter

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         Host/Parent Page                         │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                    Flutter Web App                        │   │
│  │  ┌─────────────────────────────────────────────────────┐ │   │
│  │  │              Flutter Scrollables                     │ │   │
│  │  │  ┌───────────────────────────────────────────────┐  │ │   │
│  │  │  │   HtmlElementView (cross-origin iframe)       │  │ │   │
│  │  │  │   + Wheel Overlay (Issue #113196)             │  │ │   │
│  │  │  └───────────────────────────────────────────────┘  │ │   │
│  │  └─────────────────────────────────────────────────────┘ │   │
│  │                                                          │   │
│  │  pointer_binding.dart:                                   │   │
│  │  - Detects iframe embedding (Issue #156985)              │   │
│  │  - preventDefault() to block native scroll chaining       │   │
│  │  - Explicit parent scroll via postMessage                │   │
│  │  - Touch event handling (Issue #157435)                  │   │
│  └──────────────────────────────────────────────────────────┘   │
│                              │                                   │
│                    postMessage('flutter-scroll')                 │
│                              ▼                                   │
│           window.addEventListener('message', scrollBy)           │
└─────────────────────────────────────────────────────────────────┘

Files Modified

Engine (engine/src/flutter/lib/web_ui/lib/src/engine/)

FileChanges
pointer_binding.dartIframe detection, preventDefault in iframe, touch event handling, parent scroll
dom.dartscrollParentWindow() function, parent property on DomWindow
platform_dispatcher.dartflutter/scroll platform channel handler
platform_views/content_manager.dartWheel overlay for HtmlElementView
view_embedder/embedding_strategy/*.dartoverscroll-behavior: contain CSS

Framework (packages/flutter/lib/src/widgets/)

FileChanges
scrollable.dartrespond(allowPlatformDefault: false) when scroll handled
scroll_position_with_single_context.dartTouch scroll propagation via platform channel

Demo Apps

IssueBeforeAfter
#156985https://issue-156985-before.web.apphttps://issue-156985-after.web.app
#157435https://issue-157435-before.web.apphttps://issue-157435-after.web.app
#113196https://issue-113196-before.web.apphttps://issue-113196-after.web.app

Host Page Requirements

For iframe embedding (Issue #156985), the host page must listen for scroll messages:

<script>
  window.addEventListener('message', function(event) {
    if (event.data && event.data.type === 'flutter-scroll') {
      window.scrollBy(event.data.deltaX, event.data.deltaY);
    }
  });
</script>

Testing

# Build engine with changes
cd /Users/zhongliu/dev/flutter/engine/src/flutter/lib/web_ui
felt build

# Test demo app
cd /Users/zhongliu/dev/flutter-apps/issue_156985_after
flutter run -d chrome --local-web-sdk=wasm_release

# Verify:
# 1. Scroll inside Flutter - parent page should NOT scroll
# 2. Scroll to boundary - parent page SHOULD scroll
# 3. Nested scrollables - inner scrolls first, then outer, then parent

Key Design Decisions

  1. postMessage for cross-origin safety: Using postMessage instead of direct window.parent.scrollBy() ensures the solution works for both same-origin and cross-origin iframes.

  2. Two-flag system for nested scrollables: The _lastWheelEventAllowedDefault and _lastWheelEventHandledByWidget flags work together to ensure correct behavior with nested scrollables.

  3. Overlay for cross-origin iframes: Since cross-origin iframes completely isolate events, an overlay is the only way to capture wheel events without breaking iframe functionality.

  4. Don't block touch events: Touch scrolling uses browser native behavior, so we don't preventDefault() on touch to allow smooth scrolling experience.