Skip to main content

fc_dioxus/components/
fab.rs

1//! The floating action button and its menu, from fab.css.
2use crate::{
3    core::{
4        assets,
5        attrs::{availability_attributes, component_attributes},
6    },
7    prelude::*,
8    theme::use_stylesheets,
9};
10const BEM: Bem = Bem::new("fc-fab");
11const MENU: Bem = Bem::new("fc-fab-menu");
12const BTN: Bem = Bem::new("fc-btn");
13fc_modifier! {
14    /// FAB colors.
15    pub enum FabTone for BEM {
16        /// The tinted default.
17        Default => None,
18        /// The accent fill.
19        Primary => "primary",
20        /// A surface color, for busy backgrounds.
21        Surface => "surface",
22    }
23}
24fc_modifier! {
25    /// FAB sizes.
26    pub enum FabSize for BEM {
27        /// Default.
28        Md => None,
29        /// Small.
30        Sm => "sm",
31        /// Large.
32        Lg => "lg",
33    }
34}
35fc_modifier! { enum FabShape for BEM { Icon => None, Extended => "extended" } }
36fc_modifier! { enum FabElevation for BEM { Raised => None, Lowered => "lowered" } }
37fc_modifier! { enum FabPosition for BEM { Inline => None, Fixed => "fixed" } }
38
39/// The screen's primary action, floating over content.
40///
41/// The `label` names it. Icon-only by default, where the label is its accessible name;
42/// `extended` shows the label as text beside the icon instead.
43#[component]
44pub fn Fab(
45    icon: Element,
46    label: AccessibleName,
47    #[props(default)] extended: bool,
48    #[props(default)] tone: FabTone,
49    #[props(default)] size: FabSize,
50    #[props(default)] lowered: bool,
51    /// Pin to the viewport's end corner.
52    #[props(default)]
53    fixed: bool,
54    #[props(default)] availability: Availability,
55    #[props(default)] busy: bool,
56    /// The id of a [`FabMenu`] this button opens.
57    #[props(default)]
58    menu: Option<Id>,
59    #[props(default)] onclick: Option<EventHandler<MouseEvent>>,
60    #[props(default)] class: Option<String>,
61    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
62) -> Element {
63    let blocked = busy || availability != Availability::Enabled;
64    let classes = ClassList::new(BEM.block())
65        .with_modifier(BEM, tone)
66        .with_modifier(BEM, size)
67        .with_modifier(
68            BEM,
69            if extended {
70                FabShape::Extended
71            } else {
72                FabShape::Icon
73            },
74        )
75        .with_modifier(
76            BEM,
77            if lowered {
78                FabElevation::Lowered
79            } else {
80                FabElevation::Raised
81            },
82        )
83        .with_modifier(
84            BEM,
85            if fixed {
86                FabPosition::Fixed
87            } else {
88                FabPosition::Inline
89            },
90        )
91        .extend(class.as_deref());
92    let mut attributes = component_attributes(
93        attributes,
94        classes,
95        &[
96            "type",
97            "aria-label",
98            "aria-busy",
99            "disabled",
100            "aria-disabled",
101        ],
102    );
103    attributes.extend(availability_attributes(&availability));
104    if !extended {
105        attributes.push(Attribute::new("aria-label", label.to_string(), None, false));
106    }
107    if busy {
108        attributes.push(Attribute::new("aria-busy", "true", None, false));
109    }
110    if let Some(menu) = &menu {
111        attributes.push(Attribute::new(
112            "popovertarget",
113            menu.to_string(),
114            None,
115            false,
116        ));
117    }
118    rsx! {
119        if use_stylesheets() {
120            document::Stylesheet { href: assets::COMPONENTS_BUTTON_CSS }
121            document::Stylesheet { href: assets::COMPONENTS_FAB_CSS }
122        }
123        button { r#type: "button",
124            onclick: move |event: MouseEvent| {
125                if blocked { event.prevent_default(); event.stop_propagation(); }
126                else if let Some(onclick) = onclick { onclick.call(event); }
127            },
128            ..attributes,
129            if busy { span { class: BTN.element("spinner"), aria_hidden: "true" } }
130            else { span { class: BEM.element("icon"), aria_hidden: "true", {icon} } }
131            if extended { "{label}" }
132        }
133    }
134}
135
136/// The actions a [`Fab`] reveals, as a popover. The FAB opens it with `menu: id`.
137#[component]
138pub fn FabMenu(
139    id: Id,
140    #[props(default)] class: Option<String>,
141    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
142    children: Element,
143) -> Element {
144    let mut attributes = component_attributes(
145        attributes,
146        ClassList::new(MENU.block()).extend(class.as_deref()),
147        &["id", "popover"],
148    );
149    attributes.push(Attribute::new("popover", "auto", None, false));
150    rsx! {
151        if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_FAB_CSS } }
152        div { id: "{id}", ..attributes, {children} }
153    }
154}
155
156/// One action in a [`FabMenu`].
157#[component]
158pub fn FabMenuItem(
159    #[props(default)] availability: Availability,
160    #[props(default)] onclick: Option<EventHandler<MouseEvent>>,
161    children: Element,
162) -> Element {
163    let onclick = availability.guard(onclick);
164    let state = availability_attributes(&availability);
165    rsx! {
166        button { class: MENU.element("item"), r#type: "button",
167            onclick: move |event: MouseEvent| if let Some(onclick) = onclick { onclick.call(event); } else { event.prevent_default(); },
168            ..state, {children}
169        }
170    }
171}