patternfly_yew/components/
dropdown.rs1use crate::prelude::*;
2use popper_rs::prelude::{State as PopperState, *};
3use wasm_bindgen::JsCast;
4use yew::{html::ChildrenRenderer, prelude::*};
5use yew_hooks::prelude::*;
6
7#[derive(Clone, PartialEq, Properties)]
8pub struct DropdownProperties {
9 #[prop_or_default]
10 pub children: ChildrenRenderer<MenuChildVariant>,
11
12 #[prop_or_default]
13 pub text: Option<Html>,
14 #[prop_or_default]
15 pub icon: Option<Html>,
16
17 #[prop_or_default]
18 pub aria_label: AttrValue,
19
20 #[prop_or_default]
21 pub disabled: bool,
22
23 #[prop_or_default]
24 pub full_height: bool,
25
26 #[prop_or_default]
27 pub full_width: bool,
28
29 #[prop_or_default]
30 pub variant: MenuToggleVariant,
31
32 #[prop_or_default]
33 pub position: Position,
34}
35
36#[component]
48pub fn Dropdown(props: &DropdownProperties) -> Html {
49 let expanded = use_state_eq(|| false);
50 let ontoggle = use_callback(expanded.clone(), move |_, expanded| {
51 expanded.set(!**expanded)
52 });
53
54 let inside_ref = use_node_ref();
56 let target_ref = use_node_ref();
57 let menu_ref = use_node_ref();
58
59 {
60 let expanded = expanded.clone();
64 let menu_ref = menu_ref.clone();
65 use_click_away(inside_ref.clone(), move |event: Event| {
66 if let Some(menu) = menu_ref.cast::<web_sys::HtmlElement>()
67 && !menu.contains(event.target().unwrap().dyn_ref::<web_sys::Node>())
68 {
69 expanded.set(false)
70 }
71 });
72 }
73
74 let state = use_state_eq(PopperState::default);
75 let onstatechange = use_callback(state.clone(), |new_state, state| state.set(new_state));
76
77 let placement = match props.position {
78 Position::Left => Placement::BottomStart,
79 Position::Right => Placement::BottomEnd,
80 Position::Top => Placement::TopStart,
81 };
82
83 let onclose = use_callback(expanded.clone(), |(), expanded| expanded.set(false));
84 let context = CloseMenuContext::new(onclose);
85
86 let style = use_state_eq(|| state.styles.popper.clone());
87
88 let width_mods = {
89 let style = style.clone();
90 let inside_ref = inside_ref.clone();
91 let state = state.clone();
92 let full_width = props.full_width;
93
94 ModifierFn(std::rc::Rc::new(wasm_bindgen::prelude::Closure::new(
95 move |_: popper_rs::sys::ModifierArguments| {
96 if let Some(elem) = inside_ref.cast::<web_sys::HtmlElement>() {
97 let mut new_style = state
98 .styles
99 .popper
100 .extend_with("z-index", "9999")
101 .extend_with("opacity", "1")
102 .extend_with("transition", "opacity cubic-bezier(0.54, 1.5, 0.38, 1.11)");
103
104 if full_width {
105 new_style =
106 new_style.extend_with("width", format!("{}px", elem.offset_width()));
107 }
108
109 style.set(new_style)
110 }
111 },
112 )))
113 };
114
115 let modifiers = Vec::from([Modifier::Custom {
116 name: "widthMods".into(),
117 phase: Some("beforeWrite".into()),
118 enabled: Some(true),
119 r#fn: Some(width_mods),
120 }]);
121
122 html!(
123 <>
124 <div style="display: inline" ref={inside_ref.clone()}>
125 <MenuToggle
126 r#ref={target_ref.clone()}
127 text={props.text.clone()}
128 icon={props.icon.clone()}
129 disabled={props.disabled}
130 full_height={props.full_height}
131 full_width={props.full_width}
132 aria_label={&props.aria_label}
133 variant={props.variant}
134 expanded={*expanded}
135 {ontoggle}
136 />
137 <PortalPopper
138 visible={*expanded}
139 target={target_ref.clone()}
140 content={menu_ref.clone()}
141 {placement}
142 {modifiers}
143 {onstatechange}
144 >
145 <ContextProvider<CloseMenuContext> {context}>
146 <Menu r#ref={menu_ref} style={&(*style)}>{ props.children.clone() }</Menu>
147 </ContextProvider<CloseMenuContext>>
148 </PortalPopper>
149 </div>
150 </>
151 )
152}