alert_dialog.rs
1 //! Defines the [`AlertDialogRoot`] component and its sub-components. 2 3 use crate::use_global_escape_listener; 4 use crate::{use_animated_open, use_id_or, use_unique_id, FOCUS_TRAP_JS}; 5 use dioxus::document; 6 use dioxus::prelude::*; 7 8 #[derive(Clone)] 9 struct AlertDialogCtx { 10 open: Memo<bool>, 11 set_open: Callback<bool>, 12 labelledby: String, 13 describedby: String, 14 } 15 16 /// The props for the [`AlertDialogRoot`] component. 17 #[derive(Props, Clone, PartialEq)] 18 pub struct AlertDialogRootProps { 19 /// The id of the alert dialog root element. If not provided, a unique id will be generated. 20 pub id: ReadSignal<Option<String>>, 21 /// Whether the alert dialog should be open by default. This is only used if the `open` signal is not provided. 22 #[props(default)] 23 pub default_open: bool, 24 /// The open state of the alert dialog. If this is provided, it will be used to control the open state of the dialog. 25 #[props(default)] 26 pub open: ReadSignal<Option<bool>>, 27 /// Callback to handle changes in the open state of the dialog. 28 #[props(default)] 29 pub on_open_change: Callback<bool>, 30 /// Additional attributes to extend the root element. 31 #[props(extends = GlobalAttributes)] 32 pub attributes: Vec<Attribute>, 33 /// The children of the alert dialog root element. 34 pub children: Element, 35 } 36 37 /// # AlertDialogRoot 38 /// 39 /// The entry point for the alert dialog. It manages the open state of the dialog and provides context to its children. You 40 /// can use it to create a backdrop for the dialog if needed. The contents will only be rendered when the dialog is open. 41 /// 42 /// ## Example 43 /// 44 /// ```rust 45 /// use dioxus::prelude::*; 46 /// use dioxus_primitives::alert_dialog::*; 47 /// 48 /// #[component] 49 /// fn Demo() -> Element { 50 /// let mut open = use_signal(|| false); 51 /// 52 /// rsx! { 53 /// button { 54 /// onclick: move |_| open.set(true), 55 /// "Show Alert Dialog" 56 /// } 57 /// AlertDialogRoot { 58 /// open: open(), 59 /// on_open_change: move |v| open.set(v), 60 /// AlertDialogContent { 61 /// AlertDialogTitle { "Delete item" } 62 /// AlertDialogDescription { "Are you sure you want to delete this item? This action cannot be undone." } 63 /// AlertDialogActions { 64 /// AlertDialogCancel { "Cancel" } 65 /// AlertDialogAction { 66 /// on_click: move |_| tracing::info!("Item deleted"), 67 /// "Delete" 68 /// } 69 /// } 70 /// } 71 /// } 72 /// } 73 /// } 74 /// ``` 75 /// 76 /// ## Styling 77 /// 78 /// The [`AlertDialogRoot`] component defines the following data attributes you can use to control styling: 79 /// - `data-state`: Indicates if the alert dialog is open or closed. It can be either "open" or "closed". 80 #[component] 81 pub fn AlertDialogRoot(props: AlertDialogRootProps) -> Element { 82 let labelledby = use_unique_id().to_string(); 83 let describedby = use_unique_id().to_string(); 84 let mut open_signal = use_signal(|| props.default_open); 85 let set_open = use_callback(move |v: bool| { 86 open_signal.set(v); 87 props.on_open_change.call(v); 88 }); 89 let open = use_memo(move || (props.open)().unwrap_or_else(&*open_signal)); 90 use_context_provider(|| AlertDialogCtx { 91 open, 92 set_open, 93 labelledby, 94 describedby, 95 }); 96 97 let id = use_unique_id(); 98 let id = use_id_or(id, props.id); 99 let render_element = use_animated_open(id, open); 100 101 rsx! { 102 document::Script { 103 src: FOCUS_TRAP_JS, 104 defer: true 105 } 106 if render_element() { 107 div { 108 id, 109 class: "alert-dialog-overlay", 110 "data-state": if open() { "open" } else { "closed" }, 111 ..props.attributes, 112 {props.children} 113 } 114 } 115 } 116 } 117 118 /// The props for the [`AlertDialogContent`] component. 119 #[derive(Props, Clone, PartialEq)] 120 pub struct AlertDialogContentProps { 121 /// The id of the alert dialog content element. If not provided, a unique id will be generated. 122 pub id: ReadSignal<Option<String>>, 123 124 /// The class to apply to the alert dialog content element. 125 #[props(default)] 126 pub class: Option<String>, 127 128 /// Additional attributes to extend the content element. 129 #[props(extends = GlobalAttributes)] 130 pub attributes: Vec<Attribute>, 131 /// The children of the alert dialog content element. 132 pub children: Element, 133 } 134 135 /// # AlertDialogContent 136 /// 137 /// The content of the alert dialog. Any interactive content in the dialog should be placed 138 /// inside this component. It will trap focus within the dialog while it is open 139 /// 140 /// This must be used inside an [`AlertDialogRoot`] component. 141 /// 142 /// ## Example 143 /// 144 /// ```rust 145 /// use dioxus::prelude::*; 146 /// use dioxus_primitives::alert_dialog::*; 147 /// 148 /// #[component] 149 /// fn Demo() -> Element { 150 /// let mut open = use_signal(|| false); 151 /// 152 /// rsx! { 153 /// button { 154 /// onclick: move |_| open.set(true), 155 /// "Show Alert Dialog" 156 /// } 157 /// AlertDialogRoot { 158 /// open: open(), 159 /// on_open_change: move |v| open.set(v), 160 /// AlertDialogContent { 161 /// AlertDialogTitle { "Delete item" } 162 /// AlertDialogDescription { "Are you sure you want to delete this item? This action cannot be undone." } 163 /// AlertDialogActions { 164 /// AlertDialogCancel { "Cancel" } 165 /// AlertDialogAction { 166 /// on_click: move |_| tracing::info!("Item deleted"), 167 /// "Delete" 168 /// } 169 /// } 170 /// } 171 /// } 172 /// } 173 /// } 174 /// ``` 175 #[component] 176 pub fn AlertDialogContent(props: AlertDialogContentProps) -> Element { 177 let ctx: AlertDialogCtx = use_context(); 178 179 let open = ctx.open; 180 let set_open = ctx.set_open; 181 182 // Add a escape key listener to the document when the dialog is open. We can't 183 // just add this to the dialog itself because it might not be focused if the user 184 // is highlighting text or interacting with another element. 185 use_global_escape_listener(move || set_open.call(false)); 186 187 let gen_id = use_unique_id(); 188 let id = use_id_or(gen_id, props.id); 189 use_effect(move || { 190 let eval = document::eval( 191 r#"let id = await dioxus.recv(); 192 let is_open = await dioxus.recv(); 193 let dialog = document.getElementById(id); 194 195 if (is_open) { 196 dialog.trap = window.createFocusTrap(dialog); 197 } 198 if (!is_open && dialog.trap) { 199 dialog.trap.remove(); 200 dialog.trap = null; 201 }"#, 202 ); 203 let _ = eval.send(id.to_string()); 204 let _ = eval.send(open.cloned()); 205 }); 206 207 rsx! { 208 div { 209 id, 210 role: "alertdialog", 211 aria_modal: "true", 212 aria_labelledby: ctx.labelledby.clone(), 213 aria_describedby: ctx.describedby.clone(), 214 class: props.class.clone().unwrap_or_else(|| "alert-dialog".to_string()), 215 ..props.attributes, 216 {props.children} 217 } 218 } 219 } 220 221 /// The props for the [`AlertDialogTitle`] component. 222 #[derive(Props, Clone, PartialEq)] 223 pub struct AlertDialogTitleProps { 224 /// Additional attributes to extend the title element. 225 #[props(extends = GlobalAttributes)] 226 pub attributes: Vec<Attribute>, 227 /// The children of the title element. 228 pub children: Element, 229 } 230 231 /// # AlertDialogTitle 232 /// 233 /// The title of the alert dialog. This will be used to label the dialog for accessibility purposes. 234 /// 235 /// This must be used inside an [`AlertDialogRoot`] component and should be placed inside an [`AlertDialogContent`] component. 236 /// 237 /// ## Example 238 /// 239 /// ```rust 240 /// use dioxus::prelude::*; 241 /// use dioxus_primitives::alert_dialog::*; 242 /// 243 /// #[component] 244 /// fn Demo() -> Element { 245 /// let mut open = use_signal(|| false); 246 /// 247 /// rsx! { 248 /// button { 249 /// onclick: move |_| open.set(true), 250 /// "Show Alert Dialog" 251 /// } 252 /// AlertDialogRoot { 253 /// open: open(), 254 /// on_open_change: move |v| open.set(v), 255 /// AlertDialogContent { 256 /// AlertDialogTitle { "Delete item" } 257 /// AlertDialogDescription { "Are you sure you want to delete this item? This action cannot be undone." } 258 /// AlertDialogActions { 259 /// AlertDialogCancel { "Cancel" } 260 /// AlertDialogAction { 261 /// on_click: move |_| tracing::info!("Item deleted"), 262 /// "Delete" 263 /// } 264 /// } 265 /// } 266 /// } 267 /// } 268 /// } 269 /// ``` 270 #[component] 271 pub fn AlertDialogTitle(props: AlertDialogTitleProps) -> Element { 272 let ctx: AlertDialogCtx = use_context(); 273 rsx! { 274 h2 { id: ctx.labelledby.clone(), class: "alert-dialog-title", ..props.attributes, {props.children} } 275 } 276 } 277 278 /// The props for the [`AlertDialogDescription`] component. 279 #[derive(Props, Clone, PartialEq)] 280 pub struct AlertDialogDescriptionProps { 281 /// Additional attributes to extend the description element. 282 #[props(extends = GlobalAttributes)] 283 pub attributes: Vec<Attribute>, 284 /// The children of the description element. 285 pub children: Element, 286 } 287 288 /// # AlertDialogDescription 289 /// 290 /// The description of the alert dialog. This will be used to describe the dialog for accessibility purposes. 291 /// 292 /// This must be used inside an [`AlertDialogRoot`] component and should be placed inside an [`AlertDialogContent`] component. 293 /// 294 /// ## Example 295 /// 296 /// ```rust 297 /// use dioxus::prelude::*; 298 /// use dioxus_primitives::alert_dialog::*; 299 /// 300 /// #[component] 301 /// fn Demo() -> Element { 302 /// let mut open = use_signal(|| false); 303 /// 304 /// rsx! { 305 /// button { 306 /// onclick: move |_| open.set(true), 307 /// "Show Alert Dialog" 308 /// } 309 /// AlertDialogRoot { 310 /// open: open(), 311 /// on_open_change: move |v| open.set(v), 312 /// AlertDialogContent { 313 /// AlertDialogTitle { "Delete item" } 314 /// AlertDialogDescription { "Are you sure you want to delete this item? This action cannot be undone." } 315 /// AlertDialogActions { 316 /// AlertDialogCancel { "Cancel" } 317 /// AlertDialogAction { 318 /// on_click: move |_| tracing::info!("Item deleted"), 319 /// "Delete" 320 /// } 321 /// } 322 /// } 323 /// } 324 /// } 325 /// } 326 /// ``` 327 #[component] 328 pub fn AlertDialogDescription(props: AlertDialogDescriptionProps) -> Element { 329 let ctx: AlertDialogCtx = use_context(); 330 rsx! { 331 p { id: ctx.describedby.clone(), class: "alert-dialog-description", ..props.attributes, {props.children} } 332 } 333 } 334 335 /// The props for the [`AlertDialogActions`] component. 336 #[derive(Props, Clone, PartialEq)] 337 pub struct AlertDialogActionsProps { 338 /// Additional attributes to extend the actions element. 339 #[props(extends = GlobalAttributes)] 340 pub attributes: Vec<Attribute>, 341 /// The children of the actions element. 342 pub children: Element, 343 } 344 345 /// # AlertDialogActions 346 /// 347 /// The actions of the alert dialog. This will be used to group the actions. 348 /// 349 /// This must be used inside an [`AlertDialogRoot`] component and should be placed inside an [`AlertDialogContent`] component. 350 /// 351 /// ## Example 352 /// 353 /// ```rust 354 /// use dioxus::prelude::*; 355 /// use dioxus_primitives::alert_dialog::*; 356 /// 357 /// #[component] 358 /// fn Demo() -> Element { 359 /// let mut open = use_signal(|| false); 360 /// 361 /// rsx! { 362 /// button { 363 /// onclick: move |_| open.set(true), 364 /// "Show Alert Dialog" 365 /// } 366 /// AlertDialogRoot { 367 /// open: open(), 368 /// on_open_change: move |v| open.set(v), 369 /// AlertDialogContent { 370 /// AlertDialogTitle { "Delete item" } 371 /// AlertDialogDescription { "Are you sure you want to delete this item? This action cannot be undone." } 372 /// AlertDialogActions { 373 /// AlertDialogCancel { "Cancel" } 374 /// AlertDialogAction { 375 /// on_click: move |_| tracing::info!("Item deleted"), 376 /// "Delete" 377 /// } 378 /// } 379 /// } 380 /// } 381 /// } 382 /// } 383 /// ``` 384 #[component] 385 pub fn AlertDialogActions(props: AlertDialogActionsProps) -> Element { 386 rsx! { 387 div { ..props.attributes, {props.children} } 388 } 389 } 390 391 /// The props for the [`AlertDialogAction`] component. 392 #[derive(Props, Clone, PartialEq)] 393 pub struct AlertDialogActionProps { 394 /// The click event handler for the action button. 395 #[props(default)] 396 pub on_click: Option<EventHandler<MouseEvent>>, 397 /// Additional attributes to extend the action button element. 398 #[props(extends = GlobalAttributes)] 399 pub attributes: Vec<Attribute>, 400 /// The children of the action button. 401 pub children: Element, 402 } 403 404 /// # AlertDialogAction 405 /// 406 /// An action button for the alert dialog. In addition to running the `on_click` callback, it will also close the dialog when clicked. 407 /// 408 /// This must be used inside an [`AlertDialogRoot`] component and should be placed inside an [`AlertDialogContent`] component. 409 /// 410 /// ## Example 411 /// 412 /// ```rust 413 /// use dioxus::prelude::*; 414 /// use dioxus_primitives::alert_dialog::*; 415 /// 416 /// #[component] 417 /// fn Demo() -> Element { 418 /// let mut open = use_signal(|| false); 419 /// 420 /// rsx! { 421 /// button { 422 /// onclick: move |_| open.set(true), 423 /// "Show Alert Dialog" 424 /// } 425 /// AlertDialogRoot { 426 /// open: open(), 427 /// on_open_change: move |v| open.set(v), 428 /// AlertDialogContent { 429 /// AlertDialogTitle { "Delete item" } 430 /// AlertDialogDescription { "Are you sure you want to delete this item? This action cannot be undone." } 431 /// AlertDialogActions { 432 /// AlertDialogCancel { "Cancel" } 433 /// AlertDialogAction { 434 /// on_click: move |_| tracing::info!("Item deleted"), 435 /// "Delete" 436 /// } 437 /// } 438 /// } 439 /// } 440 /// } 441 /// } 442 /// ``` 443 #[component] 444 pub fn AlertDialogAction(props: AlertDialogActionProps) -> Element { 445 let ctx: AlertDialogCtx = use_context(); 446 let open = ctx.open; 447 let set_open = ctx.set_open; 448 let user_on_click = props.on_click; 449 let on_click = use_callback(move |evt: MouseEvent| { 450 set_open.call(false); 451 if let Some(cb) = &user_on_click { 452 cb.call(evt.clone()); 453 } 454 }); 455 rsx! { 456 button { 457 tabindex: if open() { "0" } else { "-1" }, 458 type: "button", 459 onclick: on_click, 460 ..props.attributes, 461 {props.children} 462 } 463 } 464 } 465 466 /// The props for the [`AlertDialogCancel`] component. 467 #[derive(Props, Clone, PartialEq)] 468 pub struct AlertDialogCancelProps { 469 /// The click event handler for the cancel button. 470 #[props(default)] 471 pub on_click: Option<EventHandler<MouseEvent>>, 472 /// Additional attributes to extend the cancel button element. 473 #[props(extends = GlobalAttributes)] 474 pub attributes: Vec<Attribute>, 475 /// The children of the cancel button. 476 pub children: Element, 477 } 478 479 /// # AlertDialogCancel 480 /// 481 /// An cancel button for the alert dialog. In addition to running the `on_click` callback, it will also close the dialog when clicked. 482 /// 483 /// This must be used inside an [`AlertDialogRoot`] component and should be placed inside an [`AlertDialogContent`] component. 484 /// 485 /// ## Example 486 /// 487 /// ```rust 488 /// use dioxus::prelude::*; 489 /// use dioxus_primitives::alert_dialog::*; 490 /// 491 /// #[component] 492 /// fn Demo() -> Element { 493 /// let mut open = use_signal(|| false); 494 /// 495 /// rsx! { 496 /// button { 497 /// onclick: move |_| open.set(true), 498 /// "Show Alert Dialog" 499 /// } 500 /// AlertDialogRoot { 501 /// open: open(), 502 /// on_open_change: move |v| open.set(v), 503 /// AlertDialogContent { 504 /// AlertDialogTitle { "Delete item" } 505 /// AlertDialogDescription { "Are you sure you want to delete this item? This action cannot be undone." } 506 /// AlertDialogActions { 507 /// AlertDialogCancel { "Cancel" } 508 /// AlertDialogAction { 509 /// on_click: move |_| tracing::info!("Item deleted"), 510 /// "Delete" 511 /// } 512 /// } 513 /// } 514 /// } 515 /// } 516 /// } 517 /// ``` 518 #[component] 519 pub fn AlertDialogCancel(props: AlertDialogCancelProps) -> Element { 520 let ctx: AlertDialogCtx = use_context(); 521 let open = ctx.open; 522 let set_open = ctx.set_open; 523 let user_on_click = props.on_click; 524 let on_click = use_callback(move |evt: MouseEvent| { 525 set_open.call(false); 526 if let Some(cb) = &user_on_click { 527 cb.call(evt.clone()); 528 } 529 }); 530 531 rsx! { 532 button { 533 tabindex: if open() { "0" } else { "-1" }, 534 type: "button", 535 onclick: on_click, 536 ..props.attributes, 537 {props.children} 538 } 539 } 540 }