From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 29B631FF0F4 for ; Mon, 10 Aug 2026 16:35:41 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id DF46E21784; Mon, 10 Aug 2026 16:35:40 +0200 (CEST) From: Dominik Csapak To: yew-devel@lists.proxmox.com Subject: [PATCH yew-widget-toolkit 3/4] touch: navigation rail: add an optional expand button Date: Mon, 10 Aug 2026 16:34:53 +0200 Message-ID: <20260810143536.31163-4-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260810143536.31163-1-d.csapak@proxmox.com> References: <20260810143536.31163-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 1.048 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust 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: 6NHUNUXSP52FJPPZHECG47G3AMKEA7UC X-Message-ID-Hash: 6NHUNUXSP52FJPPZHECG47G3AMKEA7UC 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: Material Design's expressive navigation rail offers a menu button that switches between the collapsed and the expanded layout, so users can keep the labels visible on a medium window or reclaim the space on a large one. The button is opt-in, existing rails keep following the viewport alone. Add a passthrough for AdaptiveScaffold too. Signed-off-by: Dominik Csapak --- src/touch/adaptive_scaffold.rs | 9 ++++- src/touch/navigation_rail.rs | 60 +++++++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/touch/adaptive_scaffold.rs b/src/touch/adaptive_scaffold.rs index db378e9..d70b1d8 100644 --- a/src/touch/adaptive_scaffold.rs +++ b/src/touch/adaptive_scaffold.rs @@ -99,6 +99,12 @@ pub struct AdaptiveScaffold { #[prop_or_default] pub rail_expanded_query: Option, + /// Let users switch the rail between its collapsed and expanded + /// layout (rail layout only, see [NavigationRail::expand_button]). + #[builder] + #[prop_or_default] + pub rail_expand_button: bool, + /// Selection forwarded to the active navigator. #[builder(IntoPropValue, into_prop_value)] #[prop_or_default] @@ -191,7 +197,8 @@ impl PwtAdaptiveScaffold { if let Some(on_select) = &props.on_select { rail = rail.on_select(on_select.clone()); } - rail.router(props.router) + rail.expand_button(props.rail_expand_button) + .router(props.router) } fn build_bar(props: &AdaptiveScaffold) -> NavigationBar { diff --git a/src/touch/navigation_rail.rs b/src/touch/navigation_rail.rs index 7cbc494..05c5a01 100644 --- a/src/touch/navigation_rail.rs +++ b/src/touch/navigation_rail.rs @@ -10,7 +10,8 @@ use crate::dom::ViewportQuery; use crate::prelude::*; use crate::props::{ContainerBuilder, EventSubscriber, WidgetBuilder}; use crate::state::{NavigationContext, NavigationContextExt, Selection}; -use crate::widget::Container; +use crate::tr; +use crate::widget::{ActionIcon, Container}; use crate::widget::TabBarItem; @@ -20,6 +21,12 @@ use pwt_macros::builder; /// where the expanded navigation rail is the recommended navigator. const DEFAULT_EXPANDED_QUERY: &str = "(min-width: 1200px)"; +/// Icon of the expand button in the collapsed layout, Material Design's menu icon. +const EXPAND_ICON: &str = "fa fa-bars"; + +/// Icon of the expand button in the expanded layout, Material Design's menu-open icon. +const COLLAPSE_ICON: &str = "fa fa-outdent"; + /// Navigation rail /// /// # Automatic routing. @@ -34,6 +41,9 @@ const DEFAULT_EXPANDED_QUERY: &str = "(min-width: 1200px)"; /// side by side, each item a full-width pill) as soon as /// [expanded_query](Self::expanded_query) matches. Set [expanded](Self::expanded) to pin one of the /// two variants instead. +/// +/// Setting [expand_button](Self::expand_button) additionally lets users switch between the two +/// layouts themselves. // Note: This is Similatr to TabBar without keyboard support. #[derive(Properties, Clone, PartialEq)] @@ -66,6 +76,22 @@ pub struct NavigationRail { #[prop_or(AttrValue::Static(DEFAULT_EXPANDED_QUERY))] pub expanded_query: AttrValue, + /// Show a button at the top of the rail that toggles between the collapsed and the expanded + /// layout. + /// + /// A manual toggle stays in effect until the layout gets selected anew, either by the viewport + /// crossing [expanded_query](Self::expanded_query) or by a changed + /// [expanded](Self::expanded) property. + #[builder] + #[prop_or_default] + pub expand_button: bool, + + /// Callback emitted whenever the layout switches between collapsed and expanded, with `true` + /// meaning expanded. + #[builder_cb(IntoEventCallback, into_event_callback, bool)] + #[prop_or_default] + pub on_expand_change: Option>, + /// Navigation bar items. items: Vec, @@ -151,6 +177,7 @@ pub enum Msg { Select(Option, bool), SelectionChange(Selection), ExpandedQueryChange(bool), + ToggleExpanded, } #[doc(hidden)] @@ -195,11 +222,16 @@ impl PwtNavigationRail { selection } - fn set_expanded(&mut self, expanded: bool) -> bool { + fn set_expanded(&mut self, props: &NavigationRail, expanded: bool) -> bool { if self.expanded == expanded { return false; } self.expanded = expanded; + + if let Some(on_expand_change) = &props.on_expand_change { + on_expand_change.emit(expanded); + } + true } } @@ -314,9 +346,10 @@ impl Component for PwtNavigationRail { // an explicitly pinned layout ignores the viewport match props.expanded { Some(_) => false, - None => self.set_expanded(matches), + None => self.set_expanded(props, matches), } } + Msg::ToggleExpanded => self.set_expanded(props, !self.expanded), } } @@ -333,7 +366,7 @@ impl Component for PwtNavigationRail { ); } if query_changed || props.expanded != old_props.expanded { - self.set_expanded(props.expanded.unwrap_or(self.query_matches)); + self.set_expanded(props, props.expanded.unwrap_or(self.query_matches)); } true } @@ -402,9 +435,28 @@ impl Component for PwtNavigationRail { .into() }); + let expand_button = props.expand_button.then(|| { + let (icon_class, aria_label) = match self.expanded { + true => (COLLAPSE_ICON, tr!("Collapse the navigation rail")), + false => (EXPAND_ICON, tr!("Expand the navigation rail")), + }; + + // the container gives the button the same icon column as the items, so both line up + Container::new() + .class("pwt-navigation-rail-expand-button") + .with_child( + ActionIcon::new(icon_class) + .tabindex(0) + .aria_label(aria_label) + .attribute("aria-expanded", self.expanded.to_string()) + .on_activate(ctx.link().callback(|_| Msg::ToggleExpanded)), + ) + }); + Container::new() .class("pwt-navigation-rail") .class(self.expanded.then_some("pwt-navigation-rail-expanded")) + .with_optional_child(expand_button) .with_optional_child(props.leading.clone()) .with_child( Container::new() -- 2.47.3