[web] Adjust parent scroll handling
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart index bc602f7..4704efe 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart
@@ -118,6 +118,8 @@ external DomNavigator get navigator; external DomVisualViewport? get visualViewport; external DomPerformance get performance; + @JS('scrollBy') + external void scrollBy([num? x, num? y]); /// The parent window of this window. /// Returns null if this is the top-level window, or the same window @@ -2646,18 +2648,26 @@ external double get y; } -/// Scrolls the parent/host window by the given delta using postMessage. +/// Scrolls the parent/host window by the given delta. /// -/// Used when Flutter is embedded in an iframe and needs to scroll the parent -/// page. This uses postMessage for cross-origin safety - the host page must -/// add a message listener to handle the scroll request. +/// When same-origin, scrolls the parent directly via `scrollBy`. If access is +/// blocked (cross-origin), falls back to postMessage so a host listener can +/// handle it. /// -/// This fixes GitHub issue #156985 (scroll bubbling) and #157435 (touch scroll). +/// This addresses scroll bubbling (#156985) and touch scroll handoff (#157435) +/// without requiring host changes in same-origin embeds. void scrollParentWindow(double deltaX, double deltaY) { try { final DomWindow? parent = domWindow.parent; if (parent != null && !identical(parent, domWindow)) { - // Use postMessage for cross-origin safety + // Try direct scroll (same-origin or allowed access). + try { + parent.scrollBy(deltaX, deltaY); + return; + } catch (_) { + // Fall back to postMessage for cross-origin. + } + final JSAny message = <String, dynamic>{ 'type': 'flutter-scroll', 'deltaX': deltaX,
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart index f11e509..356c419 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart
@@ -641,6 +641,22 @@ // further effect after this point. _defaultRouteName = '/'; return; + + case 'flutter/scroll': + // Handle scroll propagation to parent/host window. + // Used for nested scrolling when Flutter is embedded in a host page. + // Fixes GitHub issue #157435 (touch scroll not propagating to host page). + const codec = StandardMessageCodec(); + final dynamic decoded = codec.decodeMessage(data); + if (decoded is Map) { + final double deltaX = (decoded['deltaX'] as num?)?.toDouble() ?? 0.0; + final double deltaY = (decoded['deltaY'] as num?)?.toDouble() ?? 0.0; + scrollParentWindow(deltaX, deltaY); + replyToPlatformMessage(callback, codec.encodeMessage(true)); + } else { + replyToPlatformMessage(callback, codec.encodeMessage(false)); + } + return; } if (pluginMessageCallHandler != null) {
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/pointer_binding.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/pointer_binding.dart index 1486248..24bc870 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/pointer_binding.dart
@@ -1074,7 +1074,16 @@ // rendered the next input element, leading to the focus incorrectly returning to // the main Flutter view instead. // A zero-length timer is sufficient in all tested browsers to achieve this. - event.preventDefault(); + // + // DON'T prevent default for touch events when embedded in an iframe. + // This allows browser's native touch scrolling to work, which provides + // smooth momentum scrolling and enables scroll propagation to parent page. + // See: https://github.com/flutter/flutter/issues/157435 + final isTouch = event.pointerType == 'touch'; + final bool isInIframe = _isEmbeddedInIframe(); + if (!isTouch || !isInIframe) { + event.preventDefault(); + } Timer(Duration.zero, () { EnginePlatformDispatcher.instance.requestViewFocusChange( viewId: _view.viewId,
diff --git a/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart b/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart index 96d77f6..317aacf 100644 --- a/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart +++ b/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart
@@ -11,18 +11,27 @@ import 'dart:math' as math; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/physics.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'basic.dart'; -import 'framework.dart'; import 'scroll_activity.dart'; import 'scroll_context.dart'; import 'scroll_notification.dart'; import 'scroll_physics.dart'; import 'scroll_position.dart'; +/// Platform channel for scroll propagation to parent/host window on web. +/// Used for nested scrolling when Flutter is embedded in a host page. +/// Fixes GitHub issue #157435 (touch scroll not propagating to host page). +const BasicMessageChannel<Object?> _scrollChannel = BasicMessageChannel<Object?>( + 'flutter/scroll', + StandardMessageCodec(), +); + /// A scroll position that manages scroll activities for a single /// [ScrollContext]. /// @@ -128,7 +137,48 @@ @override void applyUserOffset(double delta) { updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse); + + // Check if we're at boundary BEFORE applying the scroll. + // This is needed to detect overscroll for parent page propagation. + final bool wasAtMin = pixels <= minScrollExtent; + final bool wasAtMax = pixels >= maxScrollExtent; + setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta)); + + // On web, propagate scroll to parent when at boundary. + // This enables touch scroll propagation to the host page when Flutter + // is embedded in an iframe. + // See: https://github.com/flutter/flutter/issues/157435 + if (kIsWeb) { + // Scrolling down (negative delta = finger moving up = content moving up) + final bool shouldPropagateDown = delta < 0 && (wasAtMax || pixels >= maxScrollExtent); + // Scrolling up (positive delta = finger moving down = content moving down) + final bool shouldPropagateUp = delta > 0 && (wasAtMin || pixels <= minScrollExtent); + + if (shouldPropagateDown || shouldPropagateUp) { + _propagateOverscrollToParent(delta); + } + } + } + + /// Sends overscroll delta to the engine to scroll the parent/host window. + void _propagateOverscrollToParent(double overscroll) { + // Convert overscroll to x/y delta based on axis direction + var deltaX = 0.0; + var deltaY = 0.0; + switch (axisDirection) { + case AxisDirection.up: + deltaY = overscroll; + case AxisDirection.down: + deltaY = -overscroll; + case AxisDirection.left: + deltaX = overscroll; + case AxisDirection.right: + deltaX = -overscroll; + } + + // Send to engine via platform channel (fire-and-forget) + _scrollChannel.send(<String, dynamic>{'deltaX': deltaX, 'deltaY': deltaY}); } @override