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