yew_bs/components/
dropdown.rs

1use yew::prelude::*;
2use web_sys::Element;
3use wasm_bindgen::prelude::*;
4use crate::components::common::Variant;
5use crate::interop::BsDropdown;
6#[derive(Properties, PartialEq)]
7pub struct DropdownProps {
8    #[prop_or_default]
9    pub children: Children,
10    #[prop_or_default]
11    pub label: Option<Html>,
12    #[prop_or(Variant::Primary)]
13    pub variant: Variant,
14    #[prop_or_default]
15    pub split: bool,
16    #[prop_or_default]
17    pub direction: Option<DropdownDirection>,
18    #[prop_or_default]
19    pub disabled: bool,
20    #[prop_or_default]
21    pub class: Option<AttrValue>,
22    #[prop_or_default]
23    pub node_ref: NodeRef,
24}
25#[derive(Clone, Copy, PartialEq, Debug)]
26pub enum DropdownDirection {
27    Up,
28    End,
29    Start,
30    Down,
31}
32impl DropdownDirection {
33    pub fn as_str(&self) -> &'static str {
34        match self {
35            DropdownDirection::Up => "dropup",
36            DropdownDirection::End => "dropend",
37            DropdownDirection::Start => "dropstart",
38            DropdownDirection::Down => "dropdown",
39        }
40    }
41}
42/// Bootstrap Dropdown component
43#[function_component(Dropdown)]
44pub fn dropdown(props: &DropdownProps) -> Html {
45    let dropdown_ref = use_node_ref();
46    let bs_dropdown = use_state(|| None::<BsDropdown>);
47    {
48        let dropdown_ref = dropdown_ref.clone();
49        let bs_dropdown = bs_dropdown.clone();
50        use_effect(move || {
51            let dropdown_ref = dropdown_ref.clone();
52            let bs_dropdown = bs_dropdown.clone();
53            let init_dropdown = Closure::wrap(
54                Box::new(move || {
55                    if let Some(element) = dropdown_ref.cast::<Element>() {
56                        let toggle_button = element
57                            .query_selector("[data-bs-toggle='dropdown']")
58                            .ok()
59                            .flatten();
60                        if let Some(toggle) = toggle_button {
61                            bs_dropdown.set(Some(BsDropdown::new(&toggle, None)));
62                        }
63                    }
64                }) as Box<dyn FnMut()>,
65            );
66            if let Some(window) = web_sys::window() {
67                window
68                    .set_timeout_with_callback_and_timeout_and_arguments_0(
69                        init_dropdown.as_ref().unchecked_ref(),
70                        50,
71                    )
72                    .unwrap();
73                init_dropdown.forget();
74            }
75            || {}
76        });
77    }
78    let mut container_classes = Classes::new();
79    if let Some(direction) = &props.direction {
80        container_classes.push(direction.as_str());
81    } else {
82        container_classes.push("dropdown");
83    }
84    if let Some(class) = &props.class {
85        container_classes.push(class.to_string());
86    }
87    let mut button_classes = Classes::new();
88    button_classes.push("btn");
89    button_classes.push(format!("btn-{}", props.variant.as_str()));
90    if props.split {
91        button_classes.push("dropdown-toggle-split");
92    } else {
93        button_classes.push("dropdown-toggle");
94    }
95    html! {
96        < div class = { container_classes } ref = { dropdown_ref.clone() } > if props
97        .split { < button type = "button" class = { format!("btn btn-{}", props.variant
98        .as_str()) } disabled = { props.disabled } > if let Some(label) = & props.label {
99        { label.clone() } } </ button > < button type = "button" class = { button_classes
100        .clone() } data - bs - toggle = "dropdown" aria - expanded = "false" disabled = {
101        props.disabled } > < span class = "visually-hidden" > { "Toggle Dropdown" } </
102        span > </ button > } else { < button type = "button" class = { button_classes
103        .clone() } data - bs - toggle = "dropdown" aria - expanded = "false" disabled = {
104        props.disabled } > if let Some(label) = & props.label { { label.clone() } } </
105        button > } < ul class = "dropdown-menu" > { for props.children.iter() } </ ul >
106        </ div >
107    }
108}
109/// Props for the DropdownItem component
110#[derive(Properties, PartialEq)]
111pub struct DropdownItemProps {
112    /// DropdownItem children
113    #[prop_or_default]
114    pub children: Children,
115    /// Whether the item is active
116    #[prop_or_default]
117    pub active: bool,
118    /// Whether the item is disabled
119    #[prop_or_default]
120    pub disabled: bool,
121    /// Click event handler
122    #[prop_or_default]
123    pub onclick: Option<Callback<MouseEvent>>,
124    /// Link href (if item is a link)
125    #[prop_or_default]
126    pub href: Option<AttrValue>,
127    /// Additional CSS classes
128    #[prop_or_default]
129    pub class: Option<AttrValue>,
130    /// Additional HTML attributes
131    #[prop_or_default]
132    pub node_ref: NodeRef,
133}
134/// Bootstrap DropdownItem component
135#[function_component(DropdownItem)]
136pub fn dropdown_item(props: &DropdownItemProps) -> Html {
137    let mut classes = Classes::new();
138    classes.push("dropdown-item");
139    if props.active {
140        classes.push("active");
141    }
142    if props.disabled {
143        classes.push("disabled");
144    }
145    if let Some(class) = &props.class {
146        classes.push(class.to_string());
147    }
148    if let Some(href) = &props.href {
149        html! {
150            < li > < a class = { classes } href = { href.clone() } onclick = { props
151            .onclick.clone() } aria - disabled = { props.disabled.to_string() } ref = {
152            props.node_ref.clone() } > { for props.children.iter() } </ a > </ li >
153        }
154    } else {
155        html! {
156            < li > < button class = { classes } type = "button" onclick = { props.onclick
157            .clone() } disabled = { props.disabled } ref = { props.node_ref.clone() } > {
158            for props.children.iter() } </ button > </ li >
159        }
160    }
161}
162/// Props for the DropdownDivider component
163#[derive(Properties, PartialEq)]
164pub struct DropdownDividerProps {
165    /// Additional CSS classes
166    #[prop_or_default]
167    pub class: Option<AttrValue>,
168    /// Additional HTML attributes
169    #[prop_or_default]
170    pub node_ref: NodeRef,
171}
172/// Bootstrap DropdownDivider component
173#[function_component(DropdownDivider)]
174pub fn dropdown_divider(props: &DropdownDividerProps) -> Html {
175    let mut classes = Classes::new();
176    classes.push("dropdown-divider");
177    if let Some(class) = &props.class {
178        classes.push(class.to_string());
179    }
180    html! {
181        < li > < hr class = { classes } ref = { props.node_ref.clone() } /> </ li >
182    }
183}
184/// Props for the DropdownHeader component
185#[derive(Properties, PartialEq)]
186pub struct DropdownHeaderProps {
187    /// DropdownHeader children
188    #[prop_or_default]
189    pub children: Children,
190    /// Additional CSS classes
191    #[prop_or_default]
192    pub class: Option<AttrValue>,
193    /// Additional HTML attributes
194    #[prop_or_default]
195    pub node_ref: NodeRef,
196}
197/// Bootstrap DropdownHeader component
198#[function_component(DropdownHeader)]
199pub fn dropdown_header(props: &DropdownHeaderProps) -> Html {
200    let mut classes = Classes::new();
201    classes.push("dropdown-header");
202    if let Some(class) = &props.class {
203        classes.push(class.to_string());
204    }
205    html! {
206        < li > < h6 class = { classes } ref = { props.node_ref.clone() } > { for props
207        .children.iter() } </ h6 > </ li >
208    }
209}