[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