* [PATCH yew-widget-toolkit 2/2] widget: navigation drawer: make menu collapsing/expanding animated
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
1 sibling, 0 replies; 3+ messages in thread
From: Dominik Csapak @ 2026-06-19 7:44 UTC (permalink / raw)
To: yew-devel
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
^ permalink raw reply related [flat|nested] 3+ messages in thread