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 closure = {
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 use_memo((), move |_| {
95 wasm_bindgen::prelude::Closure::new(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 width_mods = ModifierFn(closure);
116
117 let modifiers = Vec::from([Modifier::Custom {
118 name: "widthMods".into(),
119 phase: Some("beforeWrite".into()),
120 enabled: Some(true),
121 r#fn: Some(width_mods),
122 }]);
123
124 html!(
125 <>
126 <div style="display: inline" ref={inside_ref.clone()}>
127 <MenuToggle
128 r#ref={target_ref.clone()}
129 text={props.text.clone()}
130 icon={props.icon.clone()}
131 disabled={props.disabled}
132 full_height={props.full_height}
133 full_width={props.full_width}
134 aria_label={&props.aria_label}
135 variant={props.variant}
136 expanded={*expanded}
137 {ontoggle}
138 />
139 <PortalPopper
140 visible={*expanded}
141 target={target_ref.clone()}
142 content={menu_ref.clone()}
143 {placement}
144 {modifiers}
145 {onstatechange}
146 >
147 <ContextProvider<CloseMenuContext> {context}>
148 <Menu r#ref={menu_ref} style={&(*style)}>{ props.children.clone() }</Menu>
149 </ContextProvider<CloseMenuContext>>
150 </PortalPopper>
151 </div>
152 </>
153 )
154}