2025-12-01 09:26:03 -08:00

116 lines
4.3 KiB
Markdown

# Issue #157435: Touch Scroll Not Propagating to Host Page (Embedded Mode)
## Problem Description
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.
### User Experience Impact
- On mobile devices, users cannot scroll the host page by swiping on the Flutter embedded view
- Touch scrolling feels "stuck" at Flutter content boundaries
- Poor mobile UX for embedded Flutter content
### Difference from Issue #156985
- Issue #156985 is about **mouse wheel** scrolling in **iframe** embedding
- Issue #157435 is about **touch** scrolling in **custom element** embedding
- Both require scroll propagation to parent, but through different mechanisms
## Root Cause Analysis
1. **`touch-action: none`**: Flutter sets `touch-action: none` on the host element to ensure Flutter receives all touch events for gesture recognition.
2. **`preventDefault()` on pointerdown**: The engine was calling `preventDefault()` on ALL pointerdown events, which prevents the browser from initiating native touch scrolling.
3. **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.
## Solution
### Engine Changes (`pointer_binding.dart`)
**Don't `preventDefault()` for touch events on pointerdown**:
```dart
// 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`.
### Framework Changes (`scroll_position_with_single_context.dart`)
Added touch scroll propagation via platform channel:
```dart
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);
}
}
}
```
### Engine Changes (`platform_dispatcher.dart`)
Added handler for `flutter/scroll` platform channel:
```dart
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;
```
### CSS Changes (Embedding Strategies)
Added `overscroll-behavior: contain` to prevent any residual scroll chaining:
```dart
// In custom_element_embedding_strategy.dart & full_page_embedding_strategy.dart
setElementStyle(rootElement, 'overscroll-behavior', 'contain');
```
## Files Changed
| 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 |
## Demo
- **Before Fix**: https://issue-157435-before.web.app
- **After Fix**: https://issue-157435-after.web.app
## Behavior After Fix
1. ✅ Touch scrolling inside Flutter embedded view works normally
2. ✅ When Flutter content is at boundary, continued swiping scrolls the host page
3. ✅ Flutter gesture recognition still works (tap, drag, etc.)
4. ✅ Works for both vertical and horizontal scrolling