patternfly_yew/components/menu/
mod.rs1mod checkbox;
3mod child;
4mod context;
5mod group;
6mod item;
7mod loading;
8mod toggle;
9mod variant;
10
11pub use checkbox::*;
12pub use child::*;
13pub use context::*;
14pub use group::*;
15pub use item::*;
16pub use loading::*;
17pub use toggle::*;
18pub use variant::*;
19
20use crate::ouia;
21use crate::prelude::OuiaComponentType;
22use crate::utils::{Ouia, OuiaSafe};
23use wasm_bindgen::JsCast;
24use web_sys::{Element, HtmlElement};
25use web_tools::prelude::*;
26use yew::{html::ChildrenRenderer, prelude::*};
27use yew_hooks::use_event_with_window;
28
29const OUIA: Ouia = ouia!("Menu");
30
31#[derive(Clone, Debug, PartialEq, Properties)]
32pub struct MenuProperties {
33 #[prop_or_default]
34 pub id: Option<String>,
35
36 #[prop_or_default]
37 pub style: AttrValue,
38
39 #[prop_or_default]
40 pub r#ref: NodeRef,
41
42 #[prop_or_default]
43 pub scrollable: bool,
44
45 #[prop_or_default]
46 pub plain: bool,
47
48 #[prop_or_default]
49 pub children: ChildrenRenderer<MenuChildVariant>,
50
51 #[prop_or_default]
53 pub ouia_id: Option<String>,
54 #[prop_or(OUIA.component_type())]
56 pub ouia_type: OuiaComponentType,
57 #[prop_or(OuiaSafe::TRUE)]
59 pub ouia_safe: OuiaSafe,
60}
61
62#[component]
63pub fn Menu(props: &MenuProperties) -> Html {
64 let ouia_id = use_memo(props.ouia_id.clone(), |id| {
65 id.clone().unwrap_or(OUIA.generated_id())
66 });
67 let mut class = classes!("pf-v6-c-menu");
68
69 if props.scrollable {
70 class.push(classes!("pf-m-scrollable"));
71 }
72
73 if props.plain {
74 class.push(classes!("pf-m-plain"));
75 }
76
77 html!(
78 <div
79 ref={props.r#ref.clone()}
80 id={props.id.clone()}
81 style={&props.style}
82 {class}
83 data-ouia-component-id={(*ouia_id).clone()}
84 data-ouia-component-type={props.ouia_type}
85 data-ouia-safe={props.ouia_safe}
86 >
87 <div class="pf-v6-c-menu__content">
88 <MenuList>{ props.children.clone() }</MenuList>
89 </div>
90 </div>
91 )
92}
93
94#[derive(Clone, Debug, PartialEq, Properties)]
95pub(crate) struct MenuListProperties {
96 pub(crate) children: ChildrenRenderer<MenuChildVariant>,
97}
98
99#[component]
100pub(crate) fn MenuList(props: &MenuListProperties) -> Html {
101 let r#ref = use_node_ref();
102
103 {
104 let r#ref = r#ref.clone();
105 use_event_with_window("keydown", move |e: KeyboardEvent| {
106 handle_key(&r#ref, e);
107 });
108 }
109
110 html!(<ul ref={r#ref} class="pf-v6-c-menu__list" role="menu">{ for props.children.iter() }</ul>)
111}
112
113fn focusable_element(element: &HtmlElement) -> Option<HtmlElement> {
114 element
115 .query_selector("a, button, input")
116 .ok()??
117 .dyn_into::<HtmlElement>()
118 .ok()
119}
120
121fn handle_key(node: &NodeRef, e: KeyboardEvent) {
122 match e.key().as_str() {
123 "Enter" => {
124 if let Some(active) = gloo_utils::document()
125 .active_element()
126 .and_then(|element| element.dyn_into::<HtmlElement>().ok())
127 {
128 e.prevent_default();
129 active.click();
130 }
131 }
132 "ArrowUp" | "ArrowDown" => handle_arrows(node, e),
133 _ => {}
134 }
135}
136
137fn handle_arrows(node: &NodeRef, e: KeyboardEvent) {
138 e.prevent_default();
139 e.stop_immediate_propagation();
140
141 let active = gloo_utils::document()
142 .active_element()
143 .and_then(|element| element.dyn_into::<HtmlElement>().ok());
144
145 let elements = match node
146 .cast::<Element>()
147 .map(|ele| ele.get_elements_by_tag_name("LI"))
148 {
149 Some(elements) => elements,
150 None => return,
151 };
152
153 let items = IterableHtmlCollection(&elements)
154 .into_iter()
155 .filter_map(|node| node.dyn_into::<HtmlElement>().ok())
156 .filter(|element| {
157 !element.class_list().contains("pf-m-disabled")
158 && !element.class_list().contains("pf-v6-c-divider")
159 })
160 .collect::<Vec<_>>();
161
162 let len = items.len();
163
164 let index = items
165 .iter()
166 .position(|node| focusable_element(node) == active);
167
168 let offset: isize = if e.key() == "ArrowDown" { 1 } else { -1 };
169
170 let next_index = index
171 .map(|index| index as isize + offset)
173 .map(|index| {
175 if index < 0 {
176 len.saturating_sub(1)
177 } else if index as usize >= len {
178 0
179 } else {
180 index as _
181 }
182 })
183 .unwrap_or_else(|| if offset > 0 { 0 } else { len.saturating_sub(1) });
185
186 let next_node = items
188 .get(next_index)
189 .and_then(focusable_element)
190 .and_then(|ele| ele.dyn_into::<HtmlElement>().ok());
191
192 if let Some(node) = &next_node {
194 if let Some(active) = &active {
195 active.set_tab_index(-1);
196 }
197
198 node.set_tab_index(0);
199 let _ = node.focus();
200 }
201}