Skip to main content

fc_dioxus/components/
split_button.rs

1//! A main action joined to a menu of related actions, from split-button.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-split-btn");
11const BTN: Bem = Bem::new("fc-btn");
12
13/// The main action and a toggle that opens a menu of alternatives.
14///
15/// The toggle reports `aria-expanded` from `expanded`; render the menu itself in `menu`,
16/// for example a popover the toggle opens through `ontoggle`. While the action is busy
17/// the toggle stays focusable but inactive, so the group does not shift under the user.
18#[component]
19pub fn SplitButton(
20    /// Names the group, e.g. "Send".
21    label: AccessibleName,
22    /// Names the toggle, e.g. "More send options".
23    menu_label: AccessibleName,
24    #[props(default)] variant: ButtonVariant,
25    #[props(default)] size: ButtonSize,
26    #[props(default)] availability: Availability,
27    #[props(default)] busy: bool,
28    #[props(default)] expanded: bool,
29    #[props(default)] onclick: Option<EventHandler<MouseEvent>>,
30    #[props(default)] ontoggle: Option<EventHandler<()>>,
31    #[props(default)] menu: Option<Element>,
32    #[props(default)] class: Option<String>,
33    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
34    children: Element,
35) -> Element {
36    let attributes = component_attributes(
37        attributes,
38        ClassList::new(BEM.block()).extend(class.as_deref()),
39        &["role", "aria-label"],
40    );
41    // While the action is busy the toggle is inactive but stays focusable, so focus is
42    // not lost; the busy action beside it is the explanation, as in upstream's markup.
43    let mut toggle_state = availability_attributes(&availability);
44    if busy && availability == Availability::Enabled {
45        toggle_state.push(Attribute::new("aria-disabled", "true", None, false));
46    }
47    let toggle_blocked = busy || availability != Availability::Enabled;
48    let toggle_class = ClassList::new(BTN.block())
49        .with_modifier(BTN, variant)
50        .with_modifier(BTN, size)
51        .with(BTN.modifier_raw("icon"))
52        .with(BEM.element("toggle"))
53        .to_string();
54    rsx! {
55        if use_stylesheets() {
56            document::Stylesheet { href: assets::COMPONENTS_BUTTON_CSS }
57            document::Stylesheet { href: assets::COMPONENTS_SPLIT_BUTTON_CSS }
58        }
59        div { role: "group", aria_label: label.to_string(), ..attributes,
60            Button { variant, size, availability: availability.clone(), busy, onclick, class: BEM.element("action"), {children} }
61            button { class: toggle_class, r#type: "button", aria_haspopup: "menu", aria_expanded: expanded.to_string(),
62                aria_label: menu_label.to_string(),
63                onclick: move |event| if toggle_blocked { event.prevent_default(); } else if let Some(ontoggle) = ontoggle { ontoggle.call(()); },
64                ..toggle_state,
65                span { class: BEM.element("chevron"), aria_hidden: "true" }
66            }
67            {menu}
68        }
69    }
70}