public inbox for yew-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Shannon Sterz <s.sterz@proxmox.com>
To: yew-devel@lists.proxmox.com
Subject: [PATCH yew-widget-toolkit 1/3] dropdown/align: make the picker render above or below a dropdown
Date: Wed,  6 May 2026 11:55:23 +0200	[thread overview]
Message-ID: <20260506095525.114495-2-s.sterz@proxmox.com> (raw)
In-Reply-To: <20260506095525.114495-1-s.sterz@proxmox.com>

and detect dropup mode. this allows better ux restricting the height
of the picker to the maximum of the space below or above a `Dropdown`.
the `DropdownController` is also extended to include whether the
picker should be rendered in `dropup` mode, meaning that it is
actually being rendered above the `Dropdown` component.

the `GridPicker` and `Combobox` components are extended to take
advantage of the new "dropup` mode. allowing the picker to be rendered
in reverse order, so that the filter box is rendered right next to the
`Dropdown` input field in dropup mode. reversing the flex column
direction here is preferable over changing the markup, as the filter
input field should still semantically be the first element in the
picker. it also has better behaviour in terms of scrolling, as the
filter input field would be otherwise scrolled out of view by default.

this implements the fix outlined in [1].

[1]:
https://git.proxmox.com/?p=proxmox-datacenter-manager.git;a=commit;f=ui/css/desktop-yew-style.scss;h=dafa21a3

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 src/dom/align.rs            |  6 +++-
 src/widget/dropdown.rs      | 69 +++++++++++++++++++++++++++++++------
 src/widget/form/combobox.rs |  3 +-
 src/widget/grid_picker.rs   | 20 +++++++++--
 4 files changed, 84 insertions(+), 14 deletions(-)

diff --git a/src/dom/align.rs b/src/dom/align.rs
index 603a5b8d..a20cc6ed 100644
--- a/src/dom/align.rs
+++ b/src/dom/align.rs
@@ -456,7 +456,11 @@ where
         style.remove_property("overflow")?;
     }
     let padding = 2.0 * options.viewport_padding;
-    style.set_property("max-height", &format!("calc(100dvh - {padding}px)"))?;
+
+    if style.get_property_value("max-height")? == "" {
+        style.set_property("max-height", &format!("calc(100dvh - {padding}px)"))?;
+    }
+
     style.set_property("max-width", &format!("calc(100dvw - {padding}px)"))?;
 
     if options.align_width {
diff --git a/src/widget/dropdown.rs b/src/widget/dropdown.rs
index 1af09633..023a4928 100644
--- a/src/widget/dropdown.rs
+++ b/src/widget/dropdown.rs
@@ -1,7 +1,11 @@
+use std::cmp;
+
 use html::Scope;
 use wasm_bindgen::JsCast;
 use web_sys::HtmlInputElement;
 
+use crate::dom::IntoHtmlElement;
+
 use yew::html::{IntoEventCallback, IntoPropValue};
 use yew::prelude::*;
 
@@ -19,6 +23,7 @@ use crate::dom::focus::{FocusTracker, element_is_focusable, get_first_focusable}
 #[derive(Clone)]
 pub struct DropdownController {
     link: Scope<PwtDropdown>,
+    dropup: bool,
 }
 
 impl DropdownController {
@@ -34,6 +39,12 @@ impl DropdownController {
             controller.change_value(key.to_string());
         })
     }
+
+    /// Whether the picker should be rendered in "dropup" mode, meaning it is being rendered above
+    /// the [Dropdown].
+    pub fn dropup(&self) -> bool {
+        self.dropup
+    }
 }
 /// Base widget to implement [Combobox](crate::widget::form::Combobox) like widgets.
 ///
@@ -155,15 +166,18 @@ impl PwtDropdown {
     }
 
     fn update_picker_placer(&mut self, _props: &Dropdown) {
-        let align_options = _props.align_options.clone().unwrap_or(
-            AlignOptions::new(
-                Point::BottomStart,
-                Point::TopStart,
-                GrowDirection::TopBottom,
-            )
-            .viewport_padding(5.0)
-            .align_width(true),
-        );
+        let align_options =
+            AlignOptions::new(Point::BottomStart, Point::TopStart, GrowDirection::None)
+                .viewport_padding(5.0)
+                .align_width(true)
+                .with_fallback_placement(Point::TopStart, Point::BottomStart, GrowDirection::None)
+                .with_fallback_placement(
+                    Point::BottomStart,
+                    Point::TopStart,
+                    GrowDirection::TopBottom,
+                );
+
+        let align_options = _props.align_options.clone().unwrap_or(align_options);
         self.picker_placer = match AutoFloatingPlacement::new(
             self.dropdown_ref.clone(),
             self.picker_ref.clone(),
@@ -337,8 +351,44 @@ impl Component for PwtDropdown {
             Msg::Input(input.value())
         });
 
+        // intentionally fail silently here; if any of these values aren't available, falling
+        // back to the default logic is fine, this should just provide improved ui/ux.
+        let dropdown_rect = self
+            .dropdown_ref
+            .clone()
+            .into_html_element()
+            .map(|e| e.get_bounding_client_rect());
+
+        let window_height = web_sys::window()
+            .and_then(|w| w.inner_height().ok())
+            .and_then(|h| h.as_f64().map(|h| h as i64));
+
+        let mut dropup = false;
+
+        if let Some(dropdown_rect) = dropdown_rect
+            && let Some(window_height) = window_height
+        {
+            let top = dropdown_rect.y() as i64;
+            let bottom = window_height - (top + (dropdown_rect.height() as i64));
+            let height = cmp::max(top, bottom) - 5;
+
+            if let Some(picker) = self.picker_ref.clone().into_html_element() {
+                let _ = picker
+                    .style()
+                    .set_property("max-height", &format!("{height}px"))
+                    .ok();
+
+                let height = picker.get_bounding_client_rect().height() as i64;
+
+                if height > bottom && height <= top {
+                    dropup = true
+                }
+            }
+        }
+
         let controller = DropdownController {
             link: ctx.link().clone(),
+            dropup,
         };
 
         let data_show = self.show.then_some("true");
@@ -497,7 +547,6 @@ impl Component for PwtDropdown {
     fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
         if first_render {
             let props = ctx.props();
-
             self.update_picker_placer(props);
 
             if props.input_props.autofocus {
diff --git a/src/widget/form/combobox.rs b/src/widget/form/combobox.rs
index 40466280..46afc82a 100644
--- a/src/widget/form/combobox.rs
+++ b/src/widget/form/combobox.rs
@@ -321,7 +321,8 @@ impl Component for PwtCombobox {
                 .show_filter(show_filter)
                 .filter(filter.clone())
                 .autoselect_filter(auto_select_filter)
-                .on_select(args.controller.on_select_callback());
+                .on_select(args.controller.on_select_callback())
+                .filter_below(args.controller.dropup());
 
             if show_filter {
                 picker.set_on_filter_change({
diff --git a/src/widget/grid_picker.rs b/src/widget/grid_picker.rs
index 304e54a2..30772733 100644
--- a/src/widget/grid_picker.rs
+++ b/src/widget/grid_picker.rs
@@ -8,10 +8,11 @@ use yew::html::{IntoEventCallback, IntoPropValue};
 use yew::prelude::*;
 use yew::virtual_dom::{Key, VComp, VNode};
 
+use crate::css::{Display, FlexDirection};
 use crate::props::{FilterFn, IntoTextFilterFn, TextFilterFn};
 use crate::state::{DataStore, Selection};
 use crate::widget::data_table::DataTable;
-use crate::widget::{Column, Input, Row};
+use crate::widget::{Container, Input, Row};
 use crate::{impl_yew_std_props_builder, prelude::*};
 
 use pwt_macros::builder;
@@ -79,6 +80,13 @@ pub struct GridPicker<S: DataStore> {
     #[builder(IntoPropValue, into_prop_value)]
     #[prop_or_default]
     pub autoselect_filter: Option<bool>,
+
+    /// Whether to render the filter above or below the table. Useful when used in a
+    /// [Dropdown](crate::widget::Dropdown) where the picker could appear above or below the
+    /// dropdown's input field.
+    #[builder(IntoPropValue, into_prop_value)]
+    #[prop_or_default]
+    pub filter_below: Option<bool>,
 }
 
 impl<S: DataStore> GridPicker<S> {
@@ -185,7 +193,15 @@ impl<S: DataStore + 'static> Component for PwtGridPicker<S> {
             .selection(self.selection.clone())
             .into();
 
-        let mut view = Column::new().class("pwt-flex-fill pwt-overflow-auto");
+        let mut view = Container::new()
+            .class(Display::Flex)
+            .class("pwt-flex-fill pwt-overflow-auto");
+
+        view = if props.filter_below == Some(true) {
+            view.class(FlexDirection::ColumnReverse)
+        } else {
+            view.class(FlexDirection::Column)
+        };
 
         let show_filter = props
             .show_filter
-- 
2.47.3





  reply	other threads:[~2026-05-06  9:55 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-06  9:55 [PATCH datacenter-manager/yew-comp/yew-widget-toolkit 0/3] minor ui/ux tweaks for pwt and yew-comp Shannon Sterz
2026-05-06  9:55 ` Shannon Sterz [this message]
2026-05-06  9:55 ` [PATCH yew-comp 2/3] task_viewer/syslog: make padding margin to improve ux Shannon Sterz
2026-05-06  9:55 ` [PATCH datacenter-manager 3/3] Revert "ui: css: limit max-height of dropdown picker" Shannon Sterz

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260506095525.114495-2-s.sterz@proxmox.com \
    --to=s.sterz@proxmox.com \
    --cc=yew-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal