Skip to main content

dioxus_bootstrap_css/
list_group.rs

1use dioxus::prelude::*;
2
3use crate::types::Color;
4
5/// Bootstrap ListGroup component.
6///
7/// # Bootstrap HTML → Dioxus
8///
9/// ```html
10/// <!-- Bootstrap HTML -->
11/// <ul class="list-group list-group-flush">
12///   <li class="list-group-item active">Active</li>
13///   <li class="list-group-item">Normal</li>
14///   <li class="list-group-item list-group-item-danger">Danger</li>
15///   <li class="list-group-item disabled">Disabled</li>
16/// </ul>
17/// ```
18///
19/// ```rust,no_run
20/// # use dioxus::prelude::*;
21/// # use dioxus_bootstrap_css::prelude::*;
22/// # fn _doctest() -> Element {
23/// # let handler = move |_: MouseEvent| {};
24/// rsx! {
25///     ListGroup { flush: true,
26///         ListGroupItem { active: true, "Active" }
27///         ListGroupItem { "Normal" }
28///         ListGroupItem { color: Color::Danger, "Danger" }
29///         ListGroupItem { disabled: true, "Disabled" }
30///     }
31///     // Clickable list group
32///     ListGroup {
33///         ListGroupItem { onclick: handler, "Click me" }
34///     }
35///     // Numbered list
36///     ListGroup { numbered: true,
37///         ListGroupItem { "First" }
38///         ListGroupItem { "Second" }
39///     }
40/// }
41/// # }
42/// ```
43#[derive(Clone, PartialEq, Props)]
44pub struct ListGroupProps {
45    /// Remove borders and rounded corners for use inside cards.
46    #[props(default)]
47    pub flush: bool,
48    /// Use numbered list style.
49    #[props(default)]
50    pub numbered: bool,
51    /// Container element. Empty (the default) renders `<ul>`, or `<ol>` when
52    /// `numbered`; `"div"` renders Bootstrap's generic `<div class="list-group">`
53    /// form, which is what a list of links or buttons requires — `<a>` and
54    /// `<button>` are not valid children of `<ul>`.
55    #[props(default)]
56    pub tag: String,
57    /// Additional CSS classes.
58    #[props(default)]
59    pub class: String,
60    /// Any additional HTML attributes.
61    #[props(extends = GlobalAttributes)]
62    attributes: Vec<Attribute>,
63    /// Child elements (ListGroupItem components).
64    pub children: Element,
65}
66
67#[component]
68pub fn ListGroup(props: ListGroupProps) -> Element {
69    let mut classes = vec!["list-group".to_string()];
70    if props.flush {
71        classes.push("list-group-flush".to_string());
72    }
73    if props.numbered {
74        classes.push("list-group-numbered".to_string());
75    }
76    if !props.class.is_empty() {
77        classes.push(props.class.clone());
78    }
79    let full_class = classes.join(" ");
80
81    // `div` first: the generic form is the one that can hold `<a>`/`<button>`
82    // children, so it must win over the numbered `<ol>` rather than be
83    // unreachable behind it.
84    if props.tag == "div" {
85        rsx! {
86            div { class: "{full_class}", ..props.attributes, {props.children} }
87        }
88    } else if props.numbered {
89        rsx! {
90            ol { class: "{full_class}", ..props.attributes, {props.children} }
91        }
92    } else {
93        rsx! {
94            ul { class: "{full_class}", ..props.attributes, {props.children} }
95        }
96    }
97}
98
99/// Bootstrap ListGroupItem component.
100#[derive(Clone, PartialEq, Props)]
101pub struct ListGroupItemProps {
102    /// Active state.
103    #[props(default)]
104    pub active: bool,
105    /// Disabled state.
106    #[props(default)]
107    pub disabled: bool,
108    /// Item color variant.
109    #[props(default)]
110    pub color: Option<Color>,
111    /// Click event handler. With the default `tag`, this renders the item as a
112    /// `<button>` — Bootstrap's actionable form.
113    #[props(default)]
114    pub onclick: Option<EventHandler<MouseEvent>>,
115    /// Item element. Empty (the default) renders `<li>`, or `<button>` when
116    /// `onclick` is set; `"div"` renders `<div class="list-group-item">`, the
117    /// generic form.
118    ///
119    /// `"div"` is honoured **even with `onclick`**. A `<button>` may not contain
120    /// interactive descendants, so a clickable row that holds its own input or
121    /// buttons cannot be one — the browser's tag-soup recovery reparents them and
122    /// the controls stop working. Such a row is an ordinary element carrying an
123    /// ordinary handler, which is what the escape hatches are for; it still gets
124    /// `list-group-item-action`, because that class is what Bootstrap defines for
125    /// the look, and the caller has said this row is actionable by handing it a
126    /// handler.
127    #[props(default)]
128    pub tag: String,
129    /// Additional CSS classes.
130    #[props(default)]
131    pub class: String,
132    /// Any additional HTML attributes.
133    #[props(extends = GlobalAttributes)]
134    attributes: Vec<Attribute>,
135    /// Child elements.
136    pub children: Element,
137}
138
139/// Which element a list-group item renders as, given its `tag` and whether a
140/// click handler was supplied. `tag` wins: see the note on the prop.
141fn list_group_item_element(tag: &str, has_onclick: bool) -> &'static str {
142    if tag == "div" {
143        "div"
144    } else if has_onclick {
145        "button"
146    } else {
147        "li"
148    }
149}
150
151#[component]
152pub fn ListGroupItem(props: ListGroupItemProps) -> Element {
153    let mut classes = vec!["list-group-item".to_string()];
154    if props.active {
155        classes.push("active".to_string());
156    }
157    if props.disabled {
158        classes.push("disabled".to_string());
159    }
160    if let Some(ref c) = props.color {
161        classes.push(format!("list-group-item-{c}"));
162    }
163    if props.onclick.is_some() {
164        classes.push("list-group-item-action".to_string());
165    }
166    if !props.class.is_empty() {
167        classes.push(props.class.clone());
168    }
169    let full_class = classes.join(" ");
170
171    // The element is chosen by one function, which the tests exercise directly —
172    // `tag` wins over `onclick`, for the reason on the prop.
173    match list_group_item_element(&props.tag, props.onclick.is_some()) {
174        "div" => {
175            let handler = props.onclick;
176            rsx! {
177                div {
178                    class: "{full_class}",
179                    onclick: move |evt| {
180                        if let Some(handler) = &handler {
181                            handler.call(evt);
182                        }
183                    },
184                    ..props.attributes,
185                    {props.children}
186                }
187            }
188        }
189        "button" => {
190            let handler = props.onclick.expect("button form implies a handler");
191            rsx! {
192                button {
193                    class: "{full_class}",
194                    r#type: "button",
195                    disabled: props.disabled,
196                    onclick: move |evt| handler.call(evt),
197                    ..props.attributes,
198                    {props.children}
199                }
200            }
201        }
202        _ => rsx! {
203            li { class: "{full_class}", ..props.attributes, {props.children} }
204        },
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn item_defaults_to_li() {
214        assert_eq!(list_group_item_element("", false), "li");
215    }
216
217    #[test]
218    fn a_handler_alone_makes_it_a_button() {
219        // Bootstrap's actionable item is an `<a>` or a `<button>`, and with no
220        // element named this is the one to pick.
221        assert_eq!(list_group_item_element("", true), "button");
222    }
223
224    #[test]
225    fn an_explicit_div_wins_over_the_handler() {
226        // The whole point of the prop. A `<button>` may not contain interactive
227        // descendants, so a clickable row holding its own input or buttons has to
228        // stay a `<div>` — if the handler silently overrode `tag` here, the markup
229        // would be invalid and the nested controls would stop working, while every
230        // class-level check still passed.
231        assert_eq!(list_group_item_element("div", true), "div");
232        assert_eq!(list_group_item_element("div", false), "div");
233    }
234}