Skip to content

[Android] Fix for RefreshView triggering pull-to-refresh when scrolling inside a WebView with internal scrollable content#34614

Open
BagavathiPerumal wants to merge 1 commit intodotnet:mainfrom
BagavathiPerumal:fix-33510
Open

[Android] Fix for RefreshView triggering pull-to-refresh when scrolling inside a WebView with internal scrollable content#34614
BagavathiPerumal wants to merge 1 commit intodotnet:mainfrom
BagavathiPerumal:fix-33510

Conversation

@BagavathiPerumal
Copy link
Contributor

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Issue Details

When using a RefreshView that wraps a WebView in a .NET MAUI app on Android, the pull-to-refresh gesture is triggered as soon as the user scrolls up, even if the WebView content has not reached the top. This prevents normal upward scrolling through web content without accidentally refreshing the page.

Root Cause

The issue occurs because of how Android RefreshView determines whether to intercept a downward drag gesture. For a WebView, this decision is based on the native scroll state (ScrollY / CanScrollVertically(-1)).

In this scenario, the visible content is not scrolled by the native WebView itself, but by an internal HTML container (overflow-y: auto). While this internal DOM element is still mid-scroll, the native WebView may incorrectly report that it is already at the top.

As a result, RefreshView intercepts the gesture too early, triggering pull-to-refresh instead of allowing the web content to continue scrolling.

Description of Change

The fix involves adding Android-specific handling for the WebView + RefreshView interaction.

A lightweight WebView bridge is introduced to determine whether the touched DOM content can still scroll upward. MauiSwipeRefreshLayout uses this information when a gesture starts inside a WebView:

  • If the internal web content is not yet at the top, the gesture remains with the WebView
  • Once the internal web content reaches the top, RefreshView is allowed to intercept and trigger refresh

This approach preserves existing RefreshView behavior for other controls, while correctly handling the WebView scenario that native Android scroll checks cannot accurately detect.

Validated the behavior in the following platforms

  • Android
  • Windows
  • iOS
  • Mac

Issues Fixed

Fixes #33510

Output ScreenShot

Before After
BeforeFix-33510.mov
AfterFix-33510.mov

… WebView drag gestures when internal DOM content can still scroll up.
@github-actions
Copy link
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34614

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34614"

@dotnet-policy-service dotnet-policy-service bot added the partner/syncfusion Issues / PR's with Syncfusion collaboration label Mar 24, 2026
@sheiksyedm sheiksyedm marked this pull request as ready for review March 26, 2026 10:58
Copilot AI review requested due to automatic review settings March 26, 2026 10:58
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses Android-specific RefreshView behavior when wrapping a WebView whose visible scrolling happens inside an internal HTML overflow container (so native WebView scroll state can incorrectly indicate “at top”), causing pull-to-refresh to trigger too early.

Changes:

  • Add an Android WebView JavaScript bridge + injected observer script to report whether touched DOM content can still scroll up.
  • Update MauiSwipeRefreshLayout to consult the reported “can scroll up” state for WebView (and add intercept logic for gestures starting inside a WebView).
  • Add a HostApp repro page and an Android UI test for issue #33510.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt Records the new/changed Android public surface from overrides added in this PR.
src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs Introduces the JS bridge + observer script and exposes TryGetCanScrollUp for RefreshView decisions.
src/Core/src/Platform/Android/MauiWebViewClient.cs Resets scroll capture state on navigation start and injects the observer script on navigation finish.
src/Core/src/Platform/Android/MauiWebView.cs Attaches/detaches the JS interface lifecycle to the native MauiWebView.
src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs Uses the bridge-reported scrollability for WebView and adds intercept logic for gestures that start in a WebView.
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33510.cs Adds an Android UI test that validates pull-to-refresh doesn’t trigger until internal web scrolling reaches top.
src/Controls/tests/TestCases.HostApp/Issues/Issue33510.cs Adds a HostApp issue page with a RefreshView + WebView using internal overflow scrolling to reproduce the bug.

_webViewOwnsGesture = _touchStartedInWebView &&
RefreshViewWebViewScrollCapture.TryGetCanScrollUp(_activeTouchWebView, out var canScrollUpAtStart) &&
canScrollUpAtStart;
break;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OnInterceptTouchEvent decides WebView vs RefreshView ownership only on MotionEventActions.Down (via _webViewOwnsGesture). If the inner web content scrolls to the top during the same drag, RefreshView will never get a chance to intercept until the user lifts and starts a new gesture, which doesn’t match SwipeRefreshLayout’s typical behavior and the PR description (“once ... reaches the top, RefreshView is allowed to intercept”). Consider re-evaluating TryGetCanScrollUp on Move events and clearing _webViewOwnsGesture when canScrollUp becomes false so the base implementation can start intercepting mid-gesture.

Suggested change
break;
break;
case MotionEventActions.Move:
// Re-evaluate scrollability while moving so that once the WebView
// reaches the top, the RefreshLayout can start intercepting.
if (_touchStartedInWebView && _webViewOwnsGesture && _activeTouchWebView is not null)
{
if (!RefreshViewWebViewScrollCapture.TryGetCanScrollUp(_activeTouchWebView, out var canStillScrollUp) || !canStillScrollUp)
{
_webViewOwnsGesture = false;
}
}
break;

Copilot uses AI. Check for mistakes.
Comment on lines 66 to 71
handler.SyncPlatformCookiesToVirtualView(url);

handler?.PlatformView.UpdateCanGoBackForward(handler.VirtualView);
RefreshViewWebViewScrollCapture.InjectObserver(view);

base.OnPageFinished(view, url);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RefreshViewWebViewScrollCapture.InjectObserver(view) is now executed for every OnPageFinished on every MauiWebView. This injects a script which installs capture-phase touchstart/touchmove listeners and runs DOM/style queries on user input, which is a cross-cutting behavioral/perf change even for WebViews not inside a RefreshView. Please gate this injection so it only runs when the WebView is actually hosted inside a RefreshView/SwipeRefreshLayout (e.g., only attach/inject when RefreshView content contains a WebView, or mark such WebViews via a tag and check it here).

Copilot uses AI. Check for mistakes.
Comment on lines 13 to 17
public MauiWebView(WebViewHandler handler, Context context) : base(context)
{
_handler = handler ?? throw new ArgumentNullException(nameof(handler));
RefreshViewWebViewScrollCapture.Attach(this);
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RefreshViewWebViewScrollCapture.Attach(this) runs for every MauiWebView instance, meaning every WebView gets a new JavaScript interface (mauiRefreshViewHost) regardless of whether it’s used inside a RefreshView. Even though the exposed surface is small, this still changes global JS environment/interop for all WebViews and pairs with the unconditional observer injection. Consider only attaching this interface when a WebView is detected inside a RefreshView (e.g., from RefreshViewHandler/MauiSwipeRefreshLayout.UpdateContent) so normal WebView usage remains unaffected.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/android

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] RefreshView triggers pull-to-refresh immediately when scrolling up inside a WebView

3 participants