From be2be38714f7aefe667e45edc1ac80c9de13da14 Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Sat, 24 Feb 2024 15:27:22 -0500 Subject: [PATCH 1/2] Prevent pull-to-refresh from triggering while scrolling in a nested scrollable element on the page --- turbo/src/main/assets/js/turbo_bridge.js | 34 +++++++++++++++++++ .../dev/hotwire/turbo/session/TurboSession.kt | 22 ++++++++++++ .../turbo/views/TurboSwipeRefreshLayout.kt | 10 ++++-- .../dev/hotwire/turbo/views/TurboWebView.kt | 5 +-- 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/turbo/src/main/assets/js/turbo_bridge.js b/turbo/src/main/assets/js/turbo_bridge.js index 7bc8bd15..74b8a395 100644 --- a/turbo/src/main/assets/js/turbo_bridge.js +++ b/turbo/src/main/assets/js/turbo_bridge.js @@ -179,6 +179,37 @@ } } + // Touch detection, allowing vertically scrollable elements + // to scroll properly without triggering pull-to-refresh. + + const elementTouchStart = (event) => { + if (!event.target) return + + var element = event.target + + while(element) { + const isScrollable = element.scrollHeight > element.clientHeight + const overflowY = window.getComputedStyle(element).overflowY + + if (isScrollable && (overflowY === "scroll" || overflowY === "auto")) { + TurboSession.elementTouchStarted(true) + break + } + + element = element.parentElement + } + + if (!element) { + TurboSession.elementTouchStarted(false) + } + } + + const elementTouchEnd = () => { + TurboSession.elementTouchEnded() + } + + // Setup and register adapter + window.turboNative = new TurboNative() const setup = function() { @@ -187,6 +218,9 @@ document.removeEventListener("turbo:load", setup) document.removeEventListener("turbolinks:load", setup) + + document.addEventListener("touchstart", elementTouchStart) + document.addEventListener("touchend", elementTouchEnd) } const setupOnLoad = () => { diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt index 7687ebf7..9d68cc5e 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt @@ -482,6 +482,28 @@ class TurboSession internal constructor( callback { it.onReceivedError(-1) } } + /** + * Called when a touched element event has started. + * + * Warning: This method is public so it can be used as a Javascript Interface. + * You should never call this directly as it could lead to unintended behavior. + */ + @JavascriptInterface + fun elementTouchStarted(scrollable: Boolean) { + webView.elementTouchIsScrollable = scrollable + } + + /** + * Called when a touched element event has ended. + * + * Warning: This method is public so it can be used as a Javascript Interface. + * You should never call this directly as it could lead to unintended behavior. + */ + @JavascriptInterface + fun elementTouchEnded() { + webView.elementTouchIsScrollable = false + } + // Private private fun visitLocation(visit: TurboVisit) { diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboSwipeRefreshLayout.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboSwipeRefreshLayout.kt index 38ff5bad..fe9a1f37 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboSwipeRefreshLayout.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboSwipeRefreshLayout.kt @@ -2,7 +2,6 @@ package dev.hotwire.turbo.views import android.content.Context import android.util.AttributeSet -import android.webkit.WebView import androidx.core.view.children import androidx.swiperefreshlayout.widget.SwipeRefreshLayout @@ -14,8 +13,13 @@ internal class TurboSwipeRefreshLayout @JvmOverloads constructor(context: Contex } override fun canChildScrollUp(): Boolean { - val webView = children.firstOrNull() as? WebView - return webView?.scrollY ?: 0 > 0 + val webView = children.firstOrNull() as? TurboWebView + + return if (webView != null) { + webView.scrollY > 0 || webView.elementTouchIsScrollable + } else { + false + } } /** diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboWebView.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboWebView.kt index 61af2d91..f69d56a9 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboWebView.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboWebView.kt @@ -6,13 +6,12 @@ import android.util.AttributeSet import android.webkit.WebView import android.widget.FrameLayout import android.widget.FrameLayout.LayoutParams.MATCH_PARENT -import android.widget.FrameLayout.LayoutParams.WRAP_CONTENT import androidx.webkit.WebViewCompat +import com.google.gson.GsonBuilder import dev.hotwire.turbo.util.contentFromAsset import dev.hotwire.turbo.util.runOnUiThread import dev.hotwire.turbo.util.toJson import dev.hotwire.turbo.visit.TurboVisitOptions -import com.google.gson.GsonBuilder /** * A Turbo-specific WebView that configures required settings and exposes some helpful info. @@ -72,6 +71,8 @@ open class TurboWebView @JvmOverloads constructor(context: Context, attrs: Attri } } + internal var elementTouchIsScrollable = false + private fun WebView.runJavascript(javascript: String, onComplete: (String?) -> Unit = {}) { context.runOnUiThread { evaluateJavascript(javascript) { From 8a53cb77c2f6859735e96ae2a0614aab7ac7bacc Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Sat, 24 Feb 2024 15:42:28 -0500 Subject: [PATCH 2/2] Cleanup --- turbo/src/main/assets/js/turbo_bridge.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/turbo/src/main/assets/js/turbo_bridge.js b/turbo/src/main/assets/js/turbo_bridge.js index 74b8a395..2d210cdb 100644 --- a/turbo/src/main/assets/js/turbo_bridge.js +++ b/turbo/src/main/assets/js/turbo_bridge.js @@ -187,11 +187,11 @@ var element = event.target - while(element) { - const isScrollable = element.scrollHeight > element.clientHeight + while (element) { + const canScroll = element.scrollHeight > element.clientHeight const overflowY = window.getComputedStyle(element).overflowY - if (isScrollable && (overflowY === "scroll" || overflowY === "auto")) { + if (canScroll && (overflowY === "scroll" || overflowY === "auto")) { TurboSession.elementTouchStarted(true) break }