1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
use crate::prelude::*;
use popper_rs::prelude::{State as PopperState, *};
use yew::{html::ChildrenRenderer, prelude::*};
use yew_hooks::prelude::*;

#[derive(Clone, PartialEq, Properties)]
pub struct DropdownProperties {
    #[prop_or_default]
    pub children: ChildrenRenderer<MenuChildVariant>,

    #[prop_or_default]
    pub text: Option<String>,
    #[prop_or_default]
    pub icon: Option<Html>,

    #[prop_or_default]
    pub aria_label: AttrValue,

    #[prop_or_default]
    pub disabled: bool,

    #[prop_or_default]
    pub full_height: bool,

    #[prop_or_default]
    pub full_width: bool,

    #[prop_or_default]
    pub variant: MenuToggleVariant,

    #[prop_or_default]
    pub position: Position,
}

/// Dropdown menu component
///
/// ## Properties
///
/// Define by [`DropdownProperties`].
///
/// ## Contexts
///
/// Provides the following contexts to its children:
///
/// * [`CloseMenuContext`]
#[function_component(Dropdown)]
pub fn drop_down(props: &DropdownProperties) -> Html {
    let expanded = use_state_eq(|| false);
    let ontoggle = use_callback(expanded.clone(), move |_, expanded| {
        expanded.set(!**expanded)
    });

    // this defines what is "inside"
    let inside_ref = use_node_ref();
    let target_ref = use_node_ref();
    let menu_ref = use_node_ref();

    {
        // click away unless it was on the inside, which covers the toggle as well as
        // the menu content. As long as we use inline/absolute popover modes and not use
        // a portal.
        let expanded = expanded.clone();
        use_click_away(inside_ref.clone(), move |_: Event| {
            expanded.set(false);
        });
    }

    let state = use_state_eq(PopperState::default);
    let onstatechange = use_callback(state.clone(), |new_state, state| state.set(new_state));

    let placement = match props.position {
        Position::Left => Placement::BottomStart,
        Position::Right => Placement::BottomEnd,
        Position::Top => Placement::TopStart,
    };

    let onclose = use_callback(expanded.clone(), |(), expanded| expanded.set(false));
    let context = CloseMenuContext::new(onclose);

    let mut style = state.styles.popper.extend_with("z-index", "1000");
    if let Some(elem) = inside_ref.cast::<web_sys::HtmlElement>() {
        style = style.extend_with("width", format!("{}px", elem.offset_width()));
    }
    let style = use_state_eq(|| style);

    let width_mods = {
        let style = style.clone();
        let inside_ref = inside_ref.clone();
        let state = state.clone();
        let full_width = props.full_width;
        ModifierFn(std::rc::Rc::new(wasm_bindgen::prelude::Closure::new(
            move |_: popper_rs::sys::ModifierArguments| {
                if let Some(elem) = inside_ref.cast::<web_sys::HtmlElement>() {
                    let mut new_style = state.styles.popper.extend_with("z-index", "1000");
                    if full_width {
                        new_style =
                            new_style.extend_with("width", format!("{}px", elem.offset_width()));
                    }
                    style.set(new_style)
                }
            },
        )))
    };

    let modifiers = Vec::from([Modifier::Custom {
        name: "widthMods".into(),
        phase: Some("beforeWrite".into()),
        enabled: Some(true),
        r#fn: Some(width_mods),
    }]);

    html!(
        <>
            <div style="display: inline;" ref={inside_ref}>
                <InlinePopper
                    target={target_ref.clone()}
                    content={menu_ref.clone()}
                    visible={*expanded}
                    {onstatechange}
                    {placement}
                    modifiers={modifiers}
                >
                    <ContextProvider<CloseMenuContext>
                        {context}
                    >
                        <Menu
                            r#ref={menu_ref}
                            style={&(*style)}
                        >
                            { props.children.clone() }
                        </Menu>
                    </ContextProvider<CloseMenuContext>>
                </InlinePopper>
                <MenuToggle
                    r#ref={target_ref}
                    text={props.text.clone()}
                    icon={props.icon.clone()}
                    disabled={props.disabled}
                    full_height={props.full_height}
                    full_width={props.full_width}
                    aria_label={&props.aria_label}
                    variant={props.variant}
                    expanded={*expanded}
                    {ontoggle}
                />
            </div>
        </>
    )
}