Skip to main content

fc_dioxus/components/
loading.rs

1//! Skeletons, linear progress, and the progress ring, from loading.css.
2use crate::{
3    core::{
4        assets,
5        attrs::component_attributes,
6        measure::{custom_property, CustomProperty},
7    },
8    prelude::*,
9    theme::use_stylesheets,
10};
11const SKELETON: Bem = Bem::new("fc-skeleton");
12const PROGRESS: Bem = Bem::new("fc-progress");
13const RING: Bem = Bem::new("fc-progress-ring");
14fc_modifier! {
15    /// The shape of the content a skeleton stands in for.
16    pub enum SkeletonShape for SKELETON {
17        /// One line box.
18        Default => None,
19        /// A line of text, slightly shorter than its line box.
20        Text => "text",
21        /// A block such as an image; `--fc-skeleton-size` sets its height.
22        Block => "block",
23        /// A circle such as an avatar.
24        Circle => "circle",
25    }
26}
27fc_modifier! { enum ProgressSize for PROGRESS { Md => None, Sm => "sm" } }
28fc_modifier! {
29    /// Progress ring sizes.
30    pub enum RingSize for RING {
31        /// Default.
32        Md => None,
33        /// Small, without room for a label.
34        Sm => "sm",
35        /// Large.
36        Lg => "lg",
37    }
38}
39fc_modifier! { enum RingTone for RING { Normal => None, Error => "error" } }
40
41/// A placeholder in the shape of content that is still loading. It is an empty element
42/// with nothing to announce; the surrounding [`Loading`] region says what is coming.
43#[component]
44pub fn Skeleton(
45    #[props(default)] shape: SkeletonShape,
46    #[props(default)] class: Option<String>,
47    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
48) -> Element {
49    let attributes = component_attributes(
50        attributes,
51        ClassList::new(SKELETON.block())
52            .with_modifier(SKELETON, shape)
53            .extend(class.as_deref()),
54        &[],
55    );
56    rsx! {
57        if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_LOADING_CSS } }
58        span { ..attributes }
59    }
60}
61
62/// A named busy region holding skeletons until its content arrives.
63///
64/// It has no class of its own: upstream styles the skeletons, and the region exists only
65/// to carry `aria-busy` and say what is loading.
66#[component]
67pub fn Loading(
68    label: AccessibleName,
69    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
70    children: Element,
71) -> Element {
72    let attributes: Vec<_> = attributes
73        .into_iter()
74        .filter(|attr| !["role", "aria-busy", "aria-label"].contains(&attr.name))
75        .collect();
76    rsx! {
77        div { role: "group", aria_busy: "true", aria_label: label.to_string(), ..attributes, {children} }
78    }
79}
80
81/// How much of a task is done, if that is known.
82#[derive(Debug, Clone, Copy, PartialEq)]
83pub enum ProgressValue {
84    /// Work is happening but its extent is unknown.
85    Indeterminate,
86    /// `value` of `max` is done.
87    Of {
88        /// Completed amount.
89        value: f64,
90        /// Total amount.
91        max: f64,
92    },
93}
94
95/// Linear progress, as the native `<progress>` element assistive technology understands.
96#[component]
97pub fn Progress(
98    label: AccessibleName,
99    value: ProgressValue,
100    #[props(default)] small: bool,
101    #[props(default)] class: Option<String>,
102    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
103) -> Element {
104    let mut attributes = component_attributes(
105        attributes,
106        ClassList::new(PROGRESS.block())
107            .with_modifier(
108                PROGRESS,
109                if small {
110                    ProgressSize::Sm
111                } else {
112                    ProgressSize::Md
113                },
114            )
115            .extend(class.as_deref()),
116        &["value", "max", "aria-label"],
117    );
118    if let ProgressValue::Of { value, max } = value {
119        let max = if max > 0.0 { max } else { 1.0 };
120        attributes.push(Attribute::new(
121            "value",
122            value.clamp(0.0, max).to_string(),
123            None,
124            false,
125        ));
126        attributes.push(Attribute::new("max", max.to_string(), None, false));
127    }
128    rsx! {
129        if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_LOADING_CSS } }
130        progress { aria_label: label.to_string(), ..attributes }
131    }
132}
133
134/// Circular determinate progress. An unknown amount is a [`Spinner`](super::spinner::Spinner);
135/// the ring never pretends to know.
136#[component]
137pub fn ProgressRing(
138    label: AccessibleName,
139    percent: Percent,
140    #[props(default)] size: RingSize,
141    /// Print the percentage inside the ring. Small rings have no room for it.
142    #[props(default)]
143    show_label: bool,
144    #[props(default)] error: bool,
145    #[props(default)] class: Option<String>,
146    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
147) -> Element {
148    let mut attributes = component_attributes(
149        attributes,
150        ClassList::new(RING.block())
151            .with_modifier(RING, size)
152            .with_modifier(
153                RING,
154                if error {
155                    RingTone::Error
156                } else {
157                    RingTone::Normal
158                },
159            )
160            .extend(class.as_deref()),
161        &[
162            "role",
163            "aria-label",
164            "aria-valuemin",
165            "aria-valuemax",
166            "aria-valuenow",
167            "style",
168        ],
169    );
170    // The drawn value and aria-valuenow are written together so they cannot disagree.
171    attributes.push(custom_property(CustomProperty::ProgressValue, percent));
172    rsx! {
173        if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_LOADING_CSS } }
174        span { role: "progressbar", aria_label: label.to_string(), aria_valuemin: "0", aria_valuemax: "100",
175            aria_valuenow: "{percent}", ..attributes,
176            if show_label && size != RingSize::Sm { span { class: RING.element("label"), "{percent}%" } }
177        }
178    }
179}