Skip to main content

fc_dioxus/components/
alert.rs

1//! Static and announced feedback from alert.css.
2use crate::{
3    core::{assets, attrs::component_attributes},
4    prelude::*,
5    theme::use_stylesheets,
6};
7const BEM: Bem = Bem::new("fc-alert");
8fc_modifier! {
9    /// Feedback tones provided by upstream.
10    pub enum AlertTone for BEM {
11        /// Neutral information.
12        Neutral => None,
13        /// Informational callout.
14        Info => "info",
15        /// Successful result.
16        Success => "success",
17        /// Something needs attention.
18        Warning => "warning",
19        /// An error or destructive consequence.
20        Danger => "danger",
21    }
22}
23fc_modifier! { enum AlertLayout for BEM { Box => None, Banner => "banner" } }
24/// Whether newly inserted feedback should be announced.
25#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
26#[non_exhaustive]
27pub enum Urgency {
28    /// Ordinary content, with no live-region role.
29    #[default]
30    Static,
31    /// Announce after the current speech.
32    Polite,
33    /// Interrupt for an urgent result.
34    Assertive,
35}
36/// Decorative icon choice. The text always carries the meaning.
37#[derive(Debug, Default, Clone, PartialEq)]
38pub enum AlertIcon {
39    /// Upstream's text symbol for the tone.
40    #[default]
41    Default,
42    /// No icon.
43    None,
44    /// A consumer icon, hidden from assistive technology.
45    Custom(Element),
46}
47/// A dismiss control cannot be constructed without its accessible name.
48#[derive(Clone, PartialEq)]
49pub struct Dismiss {
50    /// Name of the dismiss action.
51    pub label: AccessibleName,
52    /// Request dismissal; the owner controls visibility and focus restoration.
53    pub ondismiss: EventHandler<()>,
54}
55/// An upstream alert; static by default to avoid unsolicited announcements.
56#[component]
57pub fn Alert(
58    #[props(default)] tone: AlertTone,
59    #[props(default)] title: Option<Element>,
60    #[props(default)] icon: AlertIcon,
61    #[props(default)] banner: bool,
62    #[props(default)] actions: Option<Element>,
63    #[props(default)] dismiss: Option<Dismiss>,
64    #[props(default)] urgency: Urgency,
65    #[props(default)] class: Option<String>,
66    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
67    children: Element,
68) -> Element {
69    let classes = ClassList::new(BEM.block())
70        .with_modifier(BEM, tone)
71        .with_modifier(
72            BEM,
73            if banner {
74                AlertLayout::Banner
75            } else {
76                AlertLayout::Box
77            },
78        )
79        .extend(class.as_deref());
80    let mut attributes = component_attributes(attributes, classes, &["role", "aria-live"]);
81    match urgency {
82        Urgency::Static => {}
83        Urgency::Polite => attributes.push(Attribute::new("role", "status", None, false)),
84        Urgency::Assertive => attributes.push(Attribute::new("role", "alert", None, false)),
85    }
86    let symbol = match tone {
87        AlertTone::Success => "✓",
88        AlertTone::Warning => "!",
89        AlertTone::Danger => "✕",
90        _ => "i",
91    };
92    rsx! {
93        if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_ALERT_CSS } }
94        div { ..attributes,
95            match icon {
96                AlertIcon::None => rsx! {},
97                AlertIcon::Default => rsx! { span { class: BEM.element("icon"), aria_hidden: "true", "{symbol}" } },
98                AlertIcon::Custom(icon) => rsx! { span { class: BEM.element("icon"), aria_hidden: "true", {icon} } },
99            }
100            div { class: BEM.element("content"),
101                if let Some(title) = title { p { class: BEM.element("title"), {title} } }
102                p { class: BEM.element("body"), {children} }
103                if let Some(actions) = actions { div { class: BEM.element("actions"), {actions} } }
104            }
105            if let Some(dismiss) = dismiss {
106                button { r#type: "button", class: BEM.element("dismiss"), aria_label: dismiss.label.to_string(),
107                    onclick: move |_| dismiss.ondismiss.call(()), "✕"
108                }
109            }
110        }
111    }
112}