* [PATCH yew-widget-toolkit v2 1/2] touch: side dialog: prevent gestures on scrolling inner elements
@ 2026-06-03 12:49 Dominik Csapak
2026-06-03 12:49 ` [PATCH yew-widget-toolkit v2 2/2] touch: fab menu: allow infinite children with the Sheet variant Dominik Csapak
0 siblings, 1 reply; 2+ messages in thread
From: Dominik Csapak @ 2026-06-03 12:49 UTC (permalink / raw)
To: yew-devel
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 <d.csapak@proxmox.com>
---
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<SideDialog> 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<EventTarget>,
+ boundary: NodeRef,
+ location: SideDialogLocation,
+) -> bool {
+ let Some(element) = target.and_then(|t| t.dyn_into::<Element>().ok()) else {
+ return false;
+ };
+
+ let Some(boundary) = boundary.cast::<Element>() else {
+ return false;
+ };
+
+ let mut element = Some(element);
+
+ while let Some(el) = element {
+ if el == boundary {
+ break;
+ }
+ if let Some(html) = el.dyn_ref::<HtmlElement>()
+ && 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
^ permalink raw reply related [flat|nested] 2+ messages in thread* [PATCH yew-widget-toolkit v2 2/2] touch: fab menu: allow infinite children with the Sheet variant
2026-06-03 12:49 [PATCH yew-widget-toolkit v2 1/2] touch: side dialog: prevent gestures on scrolling inner elements Dominik Csapak
@ 2026-06-03 12:49 ` Dominik Csapak
0 siblings, 0 replies; 2+ messages in thread
From: Dominik Csapak @ 2026-06-03 12:49 UTC (permalink / raw)
To: yew-devel
Since the SideDialog now has proper protection against it's child
scrolling, we can now lift the 6 element limit for the Sheet variant.
We keep it for the Material3 variant, since that cannot be scrolled
sensibly.
To properly show the child list, put them into their own container, so
the 'Cancel' button is always visible, and limit the height of the
SideDialog to 90% of the dynamic viewport height.
To prevent triggering the refresh logic of mobile browsers, add
'overscroll-behavior: contain' to the scrollable list.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
| 23 +++++++++++++++--------
1 file changed, 15 insertions(+), 8 deletions(-)
--git a/src/touch/fab_menu.rs b/src/touch/fab_menu.rs
index a834f2d..161fee0 100644
--- a/src/touch/fab_menu.rs
+++ b/src/touch/fab_menu.rs
@@ -1,7 +1,9 @@
use yew::prelude::*;
use crate::css::{self, ColorScheme};
-use crate::props::{ContainerBuilder, CssPaddingBuilder, EventSubscriber, WidgetBuilder};
+use crate::props::{
+ ContainerBuilder, CssPaddingBuilder, EventSubscriber, WidgetBuilder, WidgetStyleBuilder,
+};
use crate::touch::{SideDialog, SideDialogController};
use crate::tr;
use crate::widget::{Button, Column, Container};
@@ -219,15 +221,18 @@ impl Component for PwtFabMenu {
.class(fab_classes)
.on_activate(ctx.link().callback(|_| Msg::Toggle));
- let btn_class = match props.variant {
- FabMenuVariant::Sheet => classes!("pwt-button-text"),
- FabMenuVariant::Material3 => classes!(color, "pwt-fab-menu-item", "medium"),
+ let (btn_class, btn_limit) = match props.variant {
+ FabMenuVariant::Sheet => (classes!("pwt-button-text"), None),
+ FabMenuVariant::Material3 => (classes!(color, "pwt-fab-menu-item", "medium"), Some(6)),
};
let children = props.children.iter().enumerate().filter_map(|(i, child)| {
- if i >= 6 {
- log::error!("FabMenu only supports 6 child buttons.");
- return None;
+ match btn_limit {
+ Some(limit) if i >= limit => {
+ log::error!("FabMenu only supports '{limit}' child buttons.");
+ return None;
+ }
+ _ => {}
}
let on_activate = child.on_activate.clone();
@@ -253,12 +258,14 @@ impl Component for PwtFabMenu {
.controller(controller.clone())
.location(crate::touch::SideDialogLocation::Bottom)
.on_close(ctx.link().callback(|_| Msg::Toggle))
+ .style("max-height", "90dvh")
.with_child(
Column::new()
.class(css::FlexFit)
+ .style("overscroll-behavior", "contain")
.padding(2)
.gap(1)
- .children(children)
+ .with_child(Column::new().class(css::FlexFit).children(children))
.with_child(html!(<hr />))
.with_child(
Button::new(tr!("Cancel"))
--
2.47.3
^ permalink raw reply related [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-06-03 12:52 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-03 12:49 [PATCH yew-widget-toolkit v2 1/2] touch: side dialog: prevent gestures on scrolling inner elements Dominik Csapak
2026-06-03 12:49 ` [PATCH yew-widget-toolkit v2 2/2] touch: fab menu: allow infinite children with the Sheet variant Dominik Csapak
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.