* [PATCH yew-widget-toolkit 1/3] dropdown/align: make the picker render above or below a dropdown
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
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
2 siblings, 0 replies; 4+ messages in thread
From: Shannon Sterz @ 2026-05-06 9:55 UTC (permalink / raw)
To: yew-devel
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
^ permalink raw reply related [flat|nested] 4+ messages in thread