When a Flutter web app is embedded in a host page using multi-view/custom element embedding (not iframe), touch scrolling inside the Flutter view does not propagate to the host page when the Flutter content reaches its scroll boundary.
touch-action: none: Flutter sets touch-action: none on the host element to ensure Flutter receives all touch events for gesture recognition.
preventDefault() on pointerdown: The engine was calling preventDefault() on ALL pointerdown events, which prevents the browser from initiating native touch scrolling.
No touch scroll propagation mechanism: Unlike wheel events, there was no way for Flutter to propagate touch scroll deltas to the host page when at boundary.
pointer_binding.dart)Don't preventDefault() for touch events on pointerdown:
// In pointerdown handler if (event.pointerType != 'touch') { event.preventDefault(); } // Touch events: let browser handle scroll initiation
This allows the browser's touch scrolling to work, while Flutter still handles touch events first via touch-action: none.
scroll_position_with_single_context.dart)Added touch scroll propagation via platform channel:
static const BasicMessageChannel<Object?> _scrollChannel = BasicMessageChannel<Object?>('flutter/scroll', JSONMessageCodec()); @override void applyUserOffset(double delta) { // Check if at boundary before applying scroll final bool wasAtMin = pixels <= minScrollExtent; final bool wasAtMax = pixels >= maxScrollExtent; // Apply the scroll setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta)); // On web, propagate to parent when at boundary if (kIsWeb) { final bool shouldPropagateDown = delta < 0 && (wasAtMax || pixels >= maxScrollExtent); final bool shouldPropagateUp = delta > 0 && (wasAtMin || pixels <= minScrollExtent); if (shouldPropagateDown || shouldPropagateUp) { _propagateOverscrollToParent(delta); } } }
platform_dispatcher.dart)Added handler for flutter/scroll platform channel:
case 'flutter/scroll': final dynamic decoded = messageCodec.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, messageCodec.encodeMessage(true)); } return;
Added overscroll-behavior: contain to prevent any residual scroll chaining:
// In custom_element_embedding_strategy.dart & full_page_embedding_strategy.dart setElementStyle(rootElement, 'overscroll-behavior', 'contain');
| File | Change |
|---|---|
engine/src/flutter/lib/web_ui/lib/src/engine/pointer_binding.dart | Don't preventDefault on touch pointerdown |
packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart | Touch scroll propagation via platform channel |
engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart | flutter/scroll channel handler |
engine/src/flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/*.dart | overscroll-behavior: contain CSS |