Skip to main content

fc_dioxus/components/
icon.rs

1//! Upstream's built-in SVG icon set, styled by icon.css, and icon-only buttons.
2//!
3//! The paths come from the pinned upstream build (`dist/icons.json`, generated into
4//! [`IconName`]), so an icon here is the same drawing as upstream's `iconMarkup`.
5use crate::{
6    core::{
7        assets,
8        icons::{ICON_STROKE_WIDTH, ICON_VIEWBOX},
9    },
10    prelude::*,
11    theme::use_stylesheets,
12};
13
14const BEM: Bem = Bem::new("fc-icon");
15fc_modifier! {
16    /// Icon size, relative to the surrounding font size.
17    pub enum IconSize for BEM {
18        /// The text's size, slightly enlarged (upstream's default).
19        Md => None,
20        /// The text's size.
21        Sm => "sm",
22        /// Larger than the text.
23        Lg => "lg",
24    }
25}
26fc_modifier! { enum IconDirection for BEM { Fixed => None, Mirror => "mirror" } }
27
28/// An icon from upstream's set, drawn in the current text colour.
29///
30/// Decorative by default: it sits beside text or inside a named control, so it is
31/// hidden from assistive technology rather than read twice. Give it a `label` only
32/// when it stands alone and carries meaning (a status with no text); it is then an
33/// image with that name. For an icon-only control, use [`IconButton`], which puts
34/// the name on the button.
35#[component]
36pub fn Icon(
37    name: IconName,
38    #[props(default)] size: IconSize,
39    /// Makes the icon a named image.
40    #[props(default)]
41    label: Option<AccessibleName>,
42    #[props(default)] class: Option<String>,
43) -> Element {
44    let classes = ClassList::new(BEM.block())
45        .with_modifier(BEM, size)
46        .with_modifier(
47            BEM,
48            if name.mirrored() {
49                IconDirection::Mirror
50            } else {
51                IconDirection::Fixed
52            },
53        )
54        .extend(class.as_deref())
55        .to_string();
56    let labelled = label.is_some();
57    rsx! {
58        if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_ICON_CSS } }
59        // Presentation attributes as well as the class, so the icon draws before the
60        // stylesheet loads; author CSS outranks them once it applies.
61        svg { class: classes, view_box: ICON_VIEWBOX, fill: "none", stroke: "currentColor",
62            stroke_width: ICON_STROKE_WIDTH, stroke_linecap: "round", stroke_linejoin: "round",
63            role: labelled.then_some("img"),
64            "aria-label": label.map(|label| label.to_string()),
65            "aria-hidden": (!labelled).then_some("true"),
66            "focusable": (!labelled).then_some("false"),
67            path { d: name.path() }
68        }
69    }
70}
71
72/// An icon-only [`Button`]: square, with the accessible name required by type.
73///
74/// The `label` is the button's name, which a screen reader announces and voice
75/// control users speak; the icon itself stays decorative. Pair it with a
76/// [`Tooltip`](crate::prelude::Tooltip) when sighted users may not recognise the icon.
77#[component]
78pub fn IconButton(
79    icon: IconName,
80    label: AccessibleName,
81    #[props(default)] variant: ButtonVariant,
82    #[props(default)] size: ButtonSize,
83    #[props(default)] availability: Availability,
84    /// Shows the spinner in place of the icon and blocks activation.
85    #[props(default)]
86    busy: bool,
87    #[props(default)] pressed: Option<bool>,
88    #[props(default)] r#type: ButtonType,
89    #[props(default)] onclick: Option<EventHandler<MouseEvent>>,
90    #[props(default)] class: Option<String>,
91    #[props(extends = GlobalAttributes, extends = button)] attributes: Vec<Attribute>,
92) -> Element {
93    // Built as ButtonProps so the consumer's attributes reach the button itself.
94    let props = ButtonProps {
95        variant,
96        size,
97        block: false,
98        availability,
99        busy,
100        pressed,
101        r#type,
102        icon_label: Some(label),
103        onclick,
104        class,
105        attributes,
106        children: rsx! { if !busy { Icon { name: icon } } },
107    };
108    rsx! { Button { ..props } }
109}