Skip to main content

maud_ui/primitives/
toggle_group.rs

1//! Toggle group component — segmented control with roving tabindex
2use maud::{html, Markup};
3
4#[derive(Clone, Debug)]
5pub struct GroupItem {
6    pub value: String,
7    pub label: String,
8    pub pressed: bool,
9}
10
11#[derive(Clone, Debug, Default, PartialEq)]
12pub enum Size {
13    #[default]
14    Md,
15    Sm,
16}
17
18#[derive(Clone, Debug)]
19pub struct Props {
20    pub items: Vec<GroupItem>,
21    pub multiple: bool,
22    pub disabled: bool,
23    pub aria_label: String,
24    pub size: Size,
25}
26
27impl Default for Props {
28    fn default() -> Self {
29        Self {
30            items: vec![],
31            multiple: false,
32            disabled: false,
33            aria_label: "Toggle group".to_string(),
34            size: Size::Md,
35        }
36    }
37}
38
39pub fn render(props: Props) -> Markup {
40    let multiple_attr = if props.multiple { "true" } else { "false" };
41    let size_cls = match props.size {
42        Size::Md => "mui-toggle-group--md",
43        Size::Sm => "mui-toggle-group--sm",
44    };
45
46    // Find first focusable item: first pressed, or first item if none pressed
47    let first_focusable_idx = props
48        .items
49        .iter()
50        .position(|item| item.pressed)
51        .unwrap_or(0);
52
53    html! {
54        div class={"mui-toggle-group " (size_cls)}
55            role="group"
56            aria-label=(props.aria_label)
57            data-mui="toggle-group"
58            data-multiple=(multiple_attr)
59            data-disabled=[props.disabled.then(|| "true")]
60        {
61            @for (idx, item) in props.items.iter().enumerate() {
62                @let tabindex = if idx == first_focusable_idx { "0" } else { "-1" };
63                @let aria_pressed = if item.pressed { "true" } else { "false" };
64
65                button type="button" class="mui-toggle-group__item"
66                    aria-pressed=(aria_pressed)
67                    data-value=(item.value)
68                    tabindex=(tabindex)
69                    disabled[props.disabled]
70                {
71                    (item.label.clone())
72                }
73            }
74        }
75    }
76}
77
78pub fn showcase() -> Markup {
79    html! {
80        div.mui-showcase__grid {
81            // Text alignment picker — single select, editor context
82            section {
83                h2 { "Text alignment" }
84                p.mui-showcase__caption { "Paragraph alignment in the document editor." }
85                div.mui-showcase__row {
86                    (render(Props {
87                        items: vec![
88                            GroupItem { value: "left".into(), label: "Left".into(), pressed: true },
89                            GroupItem { value: "center".into(), label: "Center".into(), pressed: false },
90                            GroupItem { value: "right".into(), label: "Right".into(), pressed: false },
91                            GroupItem { value: "justify".into(), label: "Justify".into(), pressed: false },
92                        ],
93                        aria_label: "Text alignment".into(),
94                        ..Default::default()
95                    }))
96                }
97            }
98
99            // Text formatting — multi select
100            section {
101                h2 { "Text formatting" }
102                p.mui-showcase__caption { "Bold, italic, underline, strikethrough — multi-select." }
103                div.mui-showcase__row {
104                    (render(Props {
105                        items: vec![
106                            GroupItem { value: "bold".into(), label: "Bold".into(), pressed: true },
107                            GroupItem { value: "italic".into(), label: "Italic".into(), pressed: false },
108                            GroupItem { value: "underline".into(), label: "Underline".into(), pressed: true },
109                            GroupItem { value: "strike".into(), label: "Strike".into(), pressed: false },
110                        ],
111                        multiple: true,
112                        aria_label: "Text formatting".into(),
113                        ..Default::default()
114                    }))
115                }
116            }
117
118            // View mode — calendar picker
119            section {
120                h2 { "Calendar view" }
121                p.mui-showcase__caption { "Switch between Day, Week, and Month layouts." }
122                div.mui-showcase__row {
123                    (render(Props {
124                        items: vec![
125                            GroupItem { value: "day".into(), label: "Day".into(), pressed: false },
126                            GroupItem { value: "week".into(), label: "Week".into(), pressed: true },
127                            GroupItem { value: "month".into(), label: "Month".into(), pressed: false },
128                        ],
129                        aria_label: "Calendar view".into(),
130                        ..Default::default()
131                    }))
132                }
133                div.mui-showcase__row style="margin-top:0.5rem;" {
134                    span.mui-showcase__label { "Compact" }
135                    (render(Props {
136                        items: vec![
137                            GroupItem { value: "day".into(), label: "Day".into(), pressed: false },
138                            GroupItem { value: "week".into(), label: "Week".into(), pressed: true },
139                            GroupItem { value: "month".into(), label: "Month".into(), pressed: false },
140                        ],
141                        size: Size::Sm,
142                        aria_label: "Calendar view compact".into(),
143                        ..Default::default()
144                    }))
145                }
146            }
147        }
148    }
149}