Skip to main content

patternfly_yew/components/
dropdown.rs

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