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. When set, renders as a button for interactivity.
112 #[props(default)]
113 pub onclick: Option<EventHandler<MouseEvent>>,
114 /// Item element. Empty (the default) renders `<li>`; `"div"` renders
115 /// `<div class="list-group-item">` for the generic form. Ignored when
116 /// `onclick` is set — an actionable item is always a `<button>`.
117 #[props(default)]
118 pub tag: String,
119 /// Additional CSS classes.
120 #[props(default)]
121 pub class: String,
122 /// Any additional HTML attributes.
123 #[props(extends = GlobalAttributes)]
124 attributes: Vec<Attribute>,
125 /// Child elements.
126 pub children: Element,
127}
128
129#[component]
130pub fn ListGroupItem(props: ListGroupItemProps) -> Element {
131 let mut classes = vec!["list-group-item".to_string()];
132 if props.active {
133 classes.push("active".to_string());
134 }
135 if props.disabled {
136 classes.push("disabled".to_string());
137 }
138 if let Some(ref c) = props.color {
139 classes.push(format!("list-group-item-{c}"));
140 }
141 if props.onclick.is_some() {
142 classes.push("list-group-item-action".to_string());
143 }
144 if !props.class.is_empty() {
145 classes.push(props.class.clone());
146 }
147 let full_class = classes.join(" ");
148
149 if let Some(handler) = &props.onclick {
150 let handler = *handler;
151 rsx! {
152 button {
153 class: "{full_class}",
154 r#type: "button",
155 disabled: props.disabled,
156 onclick: move |evt| handler.call(evt),
157 ..props.attributes,
158 {props.children}
159 }
160 }
161 } else if props.tag == "div" {
162 rsx! {
163 div { class: "{full_class}", ..props.attributes, {props.children} }
164 }
165 } else {
166 rsx! {
167 li { class: "{full_class}", ..props.attributes, {props.children} }
168 }
169 }
170}