/ libs / primitives / src / alert_dialog.rs
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  }