patternfly_yew/components/
dropdown.rs

1use 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/// Dropdown menu component
36///
37/// ## Properties
38///
39/// Define by [`DropdownProperties`].
40///
41/// ## Contexts
42///
43/// Provides the following contexts to its children:
44///
45/// * [`CloseMenuContext`]
46#[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    // this defines what is "inside"
54    let inside_ref = use_node_ref();
55    let target_ref = use_node_ref();
56    let menu_ref = use_node_ref();
57
58    {
59        // click away unless it was on the inside, which covers the toggle as well as
60        // the menu content. As long as we use inline/absolute popover modes and not use
61        // a portal.
62        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}