From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 6C7ED1FF13B for ; Wed, 06 May 2026 11:55:37 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 12EF81BDEE; Wed, 6 May 2026 11:55:37 +0200 (CEST) From: Shannon Sterz 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 Message-ID: <20260506095525.114495-2-s.sterz@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260506095525.114495-1-s.sterz@proxmox.com> References: <20260506095525.114495-1-s.sterz@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1778061222456 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.118 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: GBXTZV2NE75NAOG26KGZKAMU734ZFPNI X-Message-ID-Hash: GBXTZV2NE75NAOG26KGZKAMU734ZFPNI X-MailFrom: s.sterz@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: 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 --- 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, + 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, 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 { #[builder(IntoPropValue, into_prop_value)] #[prop_or_default] pub autoselect_filter: Option, + + /// 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, } impl GridPicker { @@ -185,7 +193,15 @@ impl Component for PwtGridPicker { .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