From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 67CF51FF13B for ; Wed, 03 Jun 2026 14:52:19 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 3D8D4B591; Wed, 3 Jun 2026 14:52:19 +0200 (CEST) From: Dominik Csapak To: yew-devel@lists.proxmox.com Subject: [PATCH yew-widget-toolkit v2 1/2] touch: side dialog: prevent gestures on scrolling inner elements Date: Wed, 3 Jun 2026 14:49:05 +0200 Message-ID: <20260603125144.3065467-1-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.049 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: ROJCFDGCBCYG6T6B7NENXEJQ7M6J75RT X-Message-ID-Hash: ROJCFDGCBCYG6T6B7NENXEJQ7M6J75RT X-MailFrom: d.csapak@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Yew framework devel list at Proxmox List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: When the SideDialog contains a child that is itself scrollable, the GestureDetector would happily apply and call the drag/swipe etc. gesture callbacks and e.g. drag the Sheet down while it was being scrolled. To fix that, check the elements from the event target up to the SideDialog container if any of these are scrolling and omit the handling of the events. It's currently unclear if it has any advantages of doing this in the gesture detector itself, but it seems not necessary until now. If it turns out it is, moving and adapting the code there should not be that difficult. Since this change might ignore GesturePhase::Start/End events, the drag logic must be slightly adapted: * always handle GesturePhase::End first and ignore scrolling potential (on drag end there can't be any scrolling anyway) * use self.drag_start as indicator if we should start dragging this enables changing from scrolling to dragging seamlessly and does not leave the widget in a weird state. Signed-off-by: Dominik Csapak --- changes from v1: * change drag state handling since i encountered some smaller bugs with the v1 implementation (dismissing wouldn't always work correctly) src/touch/side_dialog.rs | 130 +++++++++++++++++++++++++++++++-------- 1 file changed, 104 insertions(+), 26 deletions(-) diff --git a/src/touch/side_dialog.rs b/src/touch/side_dialog.rs index 9bc23c9..3a8e05b 100644 --- a/src/touch/side_dialog.rs +++ b/src/touch/side_dialog.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use wasm_bindgen::JsCast; -use web_sys::HtmlElement; +use web_sys::{Element, EventTarget, HtmlElement}; use yew::html::{IntoEventCallback, IntoPropValue}; use yew::prelude::*; @@ -262,10 +262,40 @@ impl Component for PwtSideDialog { true } Msg::Drag(event) => { + if event.phase == GesturePhase::End { + let mut dismiss = false; + let threshold = 100.0; + if let Some((delta_x, delta_y)) = self.drag_delta { + dismiss = match props.location { + SideDialogLocation::Left => delta_x < -threshold, + SideDialogLocation::Right => delta_x > threshold, + SideDialogLocation::Top => delta_y < -threshold, + SideDialogLocation::Bottom => delta_y > threshold, + }; + } + self.drag_start = None; + self.drag_delta = None; + + if dismiss { + ctx.link().send_message(Msg::Dismiss); + } + return true; + } + + if scrolling_element_in_range( + event.target(), + self.slider_ref.clone(), + props.location, + ) { + // don't do anything, children is scrolling + return false; + } + let x = event.x() as f64; let y = event.y() as f64; - match event.phase { - GesturePhase::Start => { + + match self.drag_start { + None => { if x > 0.0 && y > 0.0 { // prevent divide by zero self.drag_start = Some((x, y)); @@ -273,34 +303,21 @@ impl Component for PwtSideDialog { } false } - GesturePhase::Update => { - if let Some(start) = &self.drag_start { - self.drag_delta = Some((x - start.0, y - start.1)); - } - true - } - GesturePhase::End => { - let mut dismiss = false; - let threshold = 100.0; - if let Some((delta_x, delta_y)) = self.drag_delta { - dismiss = match props.location { - SideDialogLocation::Left => delta_x < -threshold, - SideDialogLocation::Right => delta_x > threshold, - SideDialogLocation::Top => delta_y < -threshold, - SideDialogLocation::Bottom => delta_y > threshold, - }; - } - self.drag_start = None; - self.drag_delta = None; - - if dismiss { - ctx.link().send_message(Msg::Dismiss); - } + Some(start) => { + self.drag_delta = Some((x - start.0, y - start.1)); true } } } Msg::Swipe(event) => { + if scrolling_element_in_range( + event.target(), + self.slider_ref.clone(), + props.location, + ) { + // don't do anything, children is scrolling + return false; + } let angle = event.direction; // -180 to + 180 let dismiss = match props.location { SideDialogLocation::Left => !(-135.0..=135.0).contains(&angle), @@ -448,3 +465,64 @@ impl From for VNode { VNode::from(comp) } } + +/// Checks if there is any element in the range from `target` to `boundary` that is scrollable +/// in the direction we would close the side dialog. (`target` must be a descendant of `boundary`). +fn scrolling_element_in_range( + target: Option, + boundary: NodeRef, + location: SideDialogLocation, +) -> bool { + let Some(element) = target.and_then(|t| t.dyn_into::().ok()) else { + return false; + }; + + let Some(boundary) = boundary.cast::() else { + return false; + }; + + let mut element = Some(element); + + while let Some(el) = element { + if el == boundary { + break; + } + if let Some(html) = el.dyn_ref::() + && check_scrolling(html, location) + { + return true; + } + element = el.parent_element(); + } + + false +} + +/// Returns true if the element is in a state where it can scroll relative to the direction we +/// would like to close the side dialog, e.g. for SideDialogLocation::Bottom it means returning tru +/// if the element can scroll up, etc. +fn check_scrolling(el: &HtmlElement, location: SideDialogLocation) -> bool { + match location { + SideDialogLocation::Bottom => { + if el.scroll_top() > 0 { + return true; + } + } + SideDialogLocation::Top => { + if el.scroll_top() != el.scroll_height() - el.offset_height() { + return true; + } + } + SideDialogLocation::Right => { + if el.scroll_left() > 0 { + return true; + } + } + SideDialogLocation::Left => { + if el.scroll_left() != el.scroll_width() - el.offset_width() { + return true; + } + } + } + false +} -- 2.47.3