toggle_group.rs
1 //! Defines the [`ToggleGroup`] component and its sub-components, which manage a group of toggle buttons with single or multiple selection. 2 3 use crate::{ 4 focus::{use_focus_controlled_item, use_focus_provider, FocusState}, 5 toggle::Toggle, 6 use_controlled, 7 }; 8 use dioxus::prelude::*; 9 use std::collections::HashSet; 10 11 // Todo: docs, test controlled version 12 13 #[derive(Clone, Copy)] 14 struct ToggleGroupCtx { 15 // State 16 disabled: ReadSignal<bool>, 17 pressed: Memo<HashSet<usize>>, 18 set_pressed: Callback<HashSet<usize>>, 19 20 allow_multiple_pressed: ReadSignal<bool>, 21 22 // Focus state 23 focus: FocusState, 24 25 horizontal: ReadSignal<bool>, 26 roving_loop: ReadSignal<bool>, 27 } 28 29 impl ToggleGroupCtx { 30 fn orientation(&self) -> &'static str { 31 match (self.horizontal)() { 32 true => "horizontal", 33 false => "vertical", 34 } 35 } 36 37 fn is_pressed(&self, id: usize) -> bool { 38 let pressed = (self.pressed)(); 39 pressed.contains(&id) 40 } 41 42 fn set_pressed(&self, id: usize, pressed: bool) { 43 let mut new_pressed = (self.pressed)(); 44 match pressed { 45 false => new_pressed.remove(&id), 46 true => { 47 if !(self.allow_multiple_pressed)() { 48 new_pressed.clear(); 49 } 50 new_pressed.insert(id) 51 } 52 }; 53 self.set_pressed.call(new_pressed); 54 } 55 56 fn is_horizontal(&self) -> bool { 57 (self.horizontal)() 58 } 59 60 fn focus_next(&mut self) { 61 if !(self.roving_loop)() { 62 return; 63 } 64 65 self.focus.focus_next(); 66 } 67 68 fn focus_prev(&mut self) { 69 if !(self.roving_loop)() { 70 return; 71 } 72 73 self.focus.focus_prev(); 74 } 75 76 fn is_roving_loop(&self) -> bool { 77 (self.roving_loop)() 78 } 79 } 80 81 /// The props for the [`ToggleGroup`] component 82 #[derive(Props, Clone, PartialEq)] 83 pub struct ToggleGroupProps { 84 /// The default pressed items if the component is not controlled. 85 #[props(default)] 86 pub default_pressed: HashSet<usize>, 87 88 /// The currently pressed items. This can be used to drive the component when controlled. 89 pub pressed: ReadSignal<Option<HashSet<usize>>>, 90 91 /// Callback to handle changes in pressed state 92 #[props(default)] 93 pub on_pressed_change: Callback<HashSet<usize>>, 94 95 /// Whether the toggle group is disabled 96 #[props(default)] 97 pub disabled: ReadSignal<bool>, 98 99 /// If multiple items can be pressed at the same time. If this is false, only one item can be pressed at a time (radio-style). 100 #[props(default)] 101 pub allow_multiple_pressed: ReadSignal<bool>, 102 103 /// Whether the toggle group is horizontal or vertical. 104 #[props(default)] 105 pub horizontal: ReadSignal<bool>, 106 107 /// Whether focus should loop around when reaching the end. 108 #[props(default = ReadSignal::new(Signal::new(true)))] 109 pub roving_loop: ReadSignal<bool>, 110 111 /// Additional attributes to apply to the toggle group element 112 #[props(extends = GlobalAttributes)] 113 pub attributes: Vec<Attribute>, 114 115 /// The children of the toggle group, which should include multiple [`ToggleItem`] components. 116 pub children: Element, 117 } 118 119 /// # ToggleGroup 120 /// 121 /// The `ToggleGroup` component manages a group of toggle buttons. It supports both single (radio-style) and multiple selection modes with keyboard navigation. 122 /// 123 /// ## Example 124 /// 125 /// ```rust 126 /// use dioxus::prelude::*; 127 /// use dioxus_primitives::toggle_group::{ToggleGroup, ToggleItem}; 128 /// #[component] 129 /// fn Demo() -> Element { 130 /// rsx! { 131 /// ToggleGroup { horizontal: true, allow_multiple_pressed: true, 132 /// ToggleItem { index: 0usize, em { "B" } } 133 /// ToggleItem { index: 1usize, i { "I" } } 134 /// ToggleItem { index: 2usize, u { "U" } } 135 /// } 136 /// } 137 /// } 138 /// ``` 139 /// 140 /// ## Styling 141 /// 142 /// The [`ToggleGroup`] component defines the following data attributes you can use to control styling: 143 /// - `data-orientation`: Indicates the orientation of the toggle group. Values are `horizontal` or `vertical`. 144 /// - `data-allow-multiple-pressed`: Indicates if multiple items can be pressed at the same time. Values are `true` or `false`. 145 #[component] 146 pub fn ToggleGroup(props: ToggleGroupProps) -> Element { 147 let (pressed, set_pressed) = use_controlled( 148 props.pressed, 149 props.default_pressed, 150 props.on_pressed_change, 151 ); 152 153 let focus = use_focus_provider(props.roving_loop); 154 let mut ctx = use_context_provider(|| ToggleGroupCtx { 155 pressed, 156 set_pressed, 157 allow_multiple_pressed: props.allow_multiple_pressed, 158 disabled: props.disabled, 159 160 focus, 161 horizontal: props.horizontal, 162 roving_loop: props.roving_loop, 163 }); 164 165 rsx! { 166 div { 167 onfocusout: move |_| ctx.focus.set_focus(None), 168 169 "data-orientation": ctx.orientation(), 170 "data-allow-multiple-pressed": ctx.allow_multiple_pressed, 171 ..props.attributes, 172 173 {props.children} 174 } 175 } 176 } 177 178 /// The props for the [`ToggleItem`] component 179 #[derive(Props, Clone, PartialEq)] 180 pub struct ToggleItemProps { 181 /// The index of the item within the [`ToggleGroup`]. This is used to order the items for keyboard navigation. 182 pub index: ReadSignal<usize>, 183 184 /// Whether the toggle item is disabled. 185 #[props(default)] 186 pub disabled: ReadSignal<bool>, 187 188 /// Additional attributes to apply to the toggle item element 189 #[props(extends = GlobalAttributes)] 190 pub attributes: Vec<Attribute>, 191 192 /// The children of the toggle item 193 pub children: Element, 194 } 195 196 /// # ToggleItem 197 /// 198 /// An individual toggle button within a [`ToggleGroup`] component. 199 /// 200 /// This must be used inside a [`ToggleGroup`] component. 201 /// 202 /// ## Example 203 /// 204 /// ```rust 205 /// use dioxus::prelude::*; 206 /// use dioxus_primitives::toggle_group::{ToggleGroup, ToggleItem}; 207 /// #[component] 208 /// fn Demo() -> Element { 209 /// rsx! { 210 /// ToggleGroup { horizontal: true, allow_multiple_pressed: true, 211 /// ToggleItem { index: 0usize, em { "B" } } 212 /// ToggleItem { index: 1usize, i { "I" } } 213 /// ToggleItem { index: 2usize, u { "U" } } 214 /// } 215 /// } 216 /// } 217 /// ``` 218 /// 219 /// ## Styling 220 /// 221 /// The [`ToggleItem`] component defines the following data attributes you can use to control styling: 222 /// - `data-state`: Indicates the state of the toggle. Values are `on` or `off`. 223 /// - `data-disabled`: Indicates if the toggle is disabled. Values are `true` or `false`. 224 /// - `data-orientation`: Indicates the orientation of the toggle group. Values are `horizontal` or `vertical`. 225 #[component] 226 pub fn ToggleItem(props: ToggleItemProps) -> Element { 227 let mut ctx: ToggleGroupCtx = use_context(); 228 229 // We need a kept-alive signal to control the toggle. 230 let mut pressed = use_signal(|| ctx.is_pressed(props.index.cloned())); 231 use_effect(move || { 232 let is_pressed = ctx.is_pressed(props.index.cloned()); 233 pressed.set(is_pressed); 234 }); 235 236 // Tab index for roving index 237 let tab_index = use_memo(move || { 238 if !ctx.is_roving_loop() { 239 return "0"; 240 } 241 242 match ctx.focus.recent_focus_or_default() == props.index.cloned() { 243 true => "0", 244 false => "-1", 245 } 246 }); 247 248 // Handle settings focus 249 let onmounted = use_focus_controlled_item(props.index); 250 251 rsx! { 252 Toggle { 253 onmounted, 254 onfocus: move |_| ctx.focus.set_focus(Some(props.index.cloned())), 255 onkeydown: move |event: Event<KeyboardData>| { 256 let key = event.key(); 257 let horizontal = ctx.is_horizontal(); 258 let mut prevent_default = true; 259 260 match key { 261 Key::ArrowUp if !horizontal => ctx.focus_prev(), 262 Key::ArrowDown if !horizontal => ctx.focus_next(), 263 Key::ArrowLeft if horizontal => ctx.focus_prev(), 264 Key::ArrowRight if horizontal => ctx.focus_next(), 265 Key::Home => ctx.focus.focus_first(), 266 Key::End => ctx.focus.focus_last(), 267 _ => prevent_default = false, 268 }; 269 270 if prevent_default { 271 event.prevent_default(); 272 } 273 }, 274 275 tabindex: tab_index, 276 disabled: (ctx.disabled)() || (props.disabled)(), 277 "data-orientation": ctx.orientation(), 278 279 pressed: pressed(), 280 on_pressed_change: move |pressed| { 281 ctx.set_pressed(props.index.cloned(), pressed); 282 }, 283 284 attributes: props.attributes.clone(), 285 286 {props.children} 287 } 288 } 289 }