From: Dominik Csapak <d.csapak@proxmox.com>
To: yew-devel@lists.proxmox.com
Subject: [PATCH yew-widget-toolkit 2/2] widget: navigation drawer: make menu collapsing/expanding animated
Date: Fri, 19 Jun 2026 09:44:28 +0200 [thread overview]
Message-ID: <20260619074434.705653-3-d.csapak@proxmox.com> (raw)
In-Reply-To: <20260619074434.705653-1-d.csapak@proxmox.com>
This is a common and popular pattern for menus in navigation menus, so
implement it here too, but with the capability to disable it.
To achieve that we have to wrap the submenus in two containers: one that
sets the height from outside and hides the overflow, and one that is
being sized so it gives a 'sliding' animation.
Since the elements are not in a flat list anymore, for keyboard
navigation we have to introduce recursive tabindex search. Copy
`roving_tabindex_next` to `roving_tabindex_next_recursive`, which
searches recursively, and leave the original in place for all other use
sites.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/dom/focus.rs | 30 ++++++++++++++++++++++++-----
src/widget/nav/navigation_drawer.rs | 24 ++++++++++++++++++-----
2 files changed, 44 insertions(+), 10 deletions(-)
diff --git a/src/dom/focus.rs b/src/dom/focus.rs
index 2dedce1..d14023f 100644
--- a/src/dom/focus.rs
+++ b/src/dom/focus.rs
@@ -96,7 +96,14 @@ pub fn get_first_focusable(item_el: web_sys::Element) -> Option<web_sys::HtmlEle
/// Move focus to the next/previous focusable child (calls [roving_tabindex_next_el]).
pub fn roving_tabindex_next(node_ref: &NodeRef, backwards: bool, roving: bool) {
if let Some(el) = node_ref.cast::<web_sys::HtmlElement>() {
- roving_tabindex_next_el(el, backwards, roving);
+ roving_tabindex_next_el(el, backwards, roving, false);
+ }
+}
+
+/// Move focus to the next/previous focusable child including nested ones (calls [roving_tabindex_next_el]) .
+pub fn roving_tabindex_next_recursive(node_ref: &NodeRef, backwards: bool, roving: bool) {
+ if let Some(el) = node_ref.cast::<web_sys::HtmlElement>() {
+ roving_tabindex_next_el(el, backwards, roving, true);
}
}
@@ -104,8 +111,15 @@ pub fn roving_tabindex_next(node_ref: &NodeRef, backwards: bool, roving: bool) {
///
/// If `roving` is enabled, this also sets the `tabindex` attribute for the active child to `"0"`,
/// and to `"-1"` for all other children.
-pub fn roving_tabindex_next_el(el: web_sys::HtmlElement, backwards: bool, roving: bool) {
- let list = roving_tabindex_members(&el);
+///
+/// If `nesting` is enabled, all nested focusable elements are included
+pub fn roving_tabindex_next_el(
+ el: web_sys::HtmlElement,
+ backwards: bool,
+ roving: bool,
+ recursive: bool,
+) {
+ let list = roving_tabindex_members(&el, recursive);
if list.is_empty() {
return;
@@ -251,7 +265,10 @@ pub fn update_roving_tabindex_el(el: web_sys::HtmlElement) {
///
/// This kind of member selection makes it possible to include more complex widget like
/// [MenuButton](crate::widget::menu::MenuButton)s inside a [Toolbar](crate::widget::Toolbar).
-pub fn roving_tabindex_members(el: &web_sys::HtmlElement) -> Vec<web_sys::HtmlElement> {
+pub fn roving_tabindex_members(
+ el: &web_sys::HtmlElement,
+ recursive: bool,
+) -> Vec<web_sys::HtmlElement> {
let mut members: Vec<web_sys::HtmlElement> = Vec::new();
if let Ok(child_list) = el.query_selector_all(":scope > *") {
@@ -263,6 +280,9 @@ pub fn roving_tabindex_members(el: &web_sys::HtmlElement) -> Vec<web_sys::HtmlEl
.unwrap();
if element_is_focusable(&node) {
members.push(node);
+ } else if recursive {
+ let mut recursive_members = roving_tabindex_members(&node, recursive);
+ members.append(&mut recursive_members);
} else if let Ok(Some(child)) = node.query_selector(FOCUSABLE_SELECTOR_ALL) {
let first_focusable_child = child.dyn_into::<web_sys::HtmlElement>().unwrap();
members.push(first_focusable_child);
@@ -285,7 +305,7 @@ pub fn init_roving_tabindex(node_ref: &NodeRef) {
/// This function makes sure that exactly one element has the `tabindex` attribute set to `"0"`. All
/// other elements get a `tabindex` of `"-1"`.
pub fn init_roving_tabindex_el(el: web_sys::HtmlElement, take_focus: bool) {
- let list = roving_tabindex_members(&el);
+ let list = roving_tabindex_members(&el, false);
if list.is_empty() {
return;
diff --git a/src/widget/nav/navigation_drawer.rs b/src/widget/nav/navigation_drawer.rs
index 89b4b4b..d3f79db 100644
--- a/src/widget/nav/navigation_drawer.rs
+++ b/src/widget/nav/navigation_drawer.rs
@@ -15,7 +15,7 @@ use crate::props::{
use crate::state::{NavigationContext, NavigationContextExt, Selection};
use crate::{impl_class_prop_builder, impl_yew_std_props_builder};
-use crate::dom::focus::roving_tabindex_next;
+use crate::dom::focus::roving_tabindex_next_recursive;
use crate::widget::{Column, Container, Fa};
use super::{Menu, MenuEntry, MenuItem};
@@ -75,6 +75,13 @@ pub struct NavigationDrawer {
#[builder]
#[prop_or_default]
router: bool,
+
+ /// Enables animations
+ ///
+ /// If enabled, expanding/collapsing is animated
+ #[builder]
+ #[prop_or(true)]
+ animated: bool,
}
impl AsClassesMut for NavigationDrawer {
@@ -221,7 +228,6 @@ impl PwtNavigationDrawer {
(!hidden).then_some(if is_active { "0" } else { "-1" }),
)
.class("pwt-nav-link")
- .class(hidden.then_some("pwt-d-none"))
.class(crate::css::AlignItems::Baseline)
.class(is_active.then_some("active"))
.onclick(onclick)
@@ -271,9 +277,16 @@ impl PwtNavigationDrawer {
menu.add_child(self.render_single_item(ctx, child, active, level, open, hidden));
if let Some(submenu) = &child.submenu {
+ let mut items = Column::new().min_height(0);
for sub in submenu.children.iter() {
- self.render_menu_entry(ctx, sub, menu, active, level + 1, !open)
+ self.render_menu_entry(ctx, sub, &mut items, active, level + 1, !open)
}
+ menu.add_child(
+ Container::new()
+ .class("pwt-nav-menu-animation-container")
+ .class(open.then_some("expanded"))
+ .with_child(items),
+ );
}
}
MenuEntry::Component(comp) => {
@@ -554,10 +567,10 @@ impl Component for PwtNavigationDrawer {
let onkeydown = Callback::from(move |event: KeyboardEvent| {
match event.key().as_str() {
"ArrowDown" => {
- roving_tabindex_next(&menu_ref, false, false);
+ roving_tabindex_next_recursive(&menu_ref, false, false);
}
"ArrowUp" => {
- roving_tabindex_next(&menu_ref, true, false);
+ roving_tabindex_next_recursive(&menu_ref, true, false);
}
_ => return,
}
@@ -571,6 +584,7 @@ impl Component for PwtNavigationDrawer {
.attribute("role", "navigation")
.attribute("aria-label", props.aria_label.clone())
.class("pwt-nav-menu")
+ .class(props.animated.then_some("animated"))
.class(OverflowX::Hidden)
.class(OverflowY::Auto)
.class(props.class.clone())
--
2.47.3
prev parent reply other threads:[~2026-06-19 7:44 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-19 7:44 [RFC yew-widget-toolkit/yew-widget-toolkit-assets 0/2] Animate navigation drawer menus collapsing/expanding Dominik Csapak
2026-06-19 7:44 ` [PATCH yew-widget-toolkit-assets 1/2] nav: add classes for animated navigation drawer menus Dominik Csapak
2026-06-19 7:44 ` Dominik Csapak [this message]
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=20260619074434.705653-3-d.csapak@proxmox.com \
--to=d.csapak@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