fc_dioxus/components/
toolbar.rs1use crate::{
5 behavior::roving::{self, use_member, Roster},
6 core::{
7 assets,
8 attrs::{availability_attributes, component_attributes},
9 },
10 prelude::*,
11 theme::use_stylesheets,
12};
13const BEM: Bem = Bem::new("fc-toolbar");
14const ITEM: Bem = Bem::new("fc-toolbar__item");
15fc_modifier! {
16 pub enum ToolbarSurface for BEM {
18 Plain => None,
20 Floating => "floating",
22 Docked => "docked",
24 }
25}
26fc_modifier! { enum ItemShape for ITEM { Text => None, Icon => "icon" } }
27
28#[derive(Clone, Copy)]
29struct ToolbarContext {
30 stop: Signal<Option<u64>>,
31 orientation: Orientation,
32}
33
34#[component]
36pub fn Toolbar(
37 label: AccessibleName,
38 #[props(default)] orientation: Orientation,
39 #[props(default)] surface: ToolbarSurface,
40 #[props(default)] class: Option<String>,
41 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
42 children: Element,
43) -> Element {
44 use_context_provider(Roster::new);
46 use_context_provider(|| ToolbarContext {
47 stop: Signal::new(None),
48 orientation,
49 });
50 let attributes = component_attributes(
51 attributes,
52 ClassList::new(BEM.block())
53 .with_modifier(BEM, surface)
54 .extend(class.as_deref()),
55 &["role", "aria-label", "aria-orientation"],
56 );
57 rsx! {
58 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_TOOLBAR_CSS } }
59 div { role: "toolbar", aria_label: label.to_string(),
60 aria_orientation: (orientation == Orientation::Vertical).then_some("vertical"),
61 ..attributes, {children}
62 }
63 }
64}
65
66#[component]
68pub fn ToolbarItem(
69 #[props(default)] icon_label: Option<AccessibleName>,
70 #[props(default)] pressed: Option<bool>,
71 #[props(default)] availability: Availability,
72 #[props(default)] onclick: Option<EventHandler<MouseEvent>>,
73 children: Element,
74) -> Element {
75 let focusable = availability != Availability::Disabled;
76 let joined = use_member("", focusable, availability == Availability::Enabled);
77 let direction = use_direction();
78 let context = try_consume_context::<ToolbarContext>();
79 let class = ClassList::new(ITEM.block())
80 .with_modifier(
81 ITEM,
82 if icon_label.is_some() {
83 ItemShape::Icon
84 } else {
85 ItemShape::Text
86 },
87 )
88 .to_string();
89 let state = availability_attributes(&availability);
90 let onclick = availability.guard(onclick);
91 let (Some(mut context), Some((roster, key))) = (context, joined) else {
92 return rsx! {};
93 };
94 let stop = match *context.stop.read() {
96 Some(stop) => stop == key,
97 None => roster.read().iter().find(|m| m.focusable).map(|m| m.key) == Some(key),
98 };
99 rsx! {
100 button { r#type: "button", class, aria_label: icon_label.map(|l| l.to_string()),
101 aria_pressed: pressed.map(|p| p.to_string()),
102 tabindex: if stop { "0" } else { "-1" },
103 onmounted: move |event| roster.mount(key, event.data()),
104 onfocus: move |_| context.stop.set(Some(key)),
105 onclick: move |event| if let Some(onclick) = onclick { onclick.call(event); },
106 onkeydown: move |event| {
107 let Some(step) = roving::intent(&event.key(), context.orientation, direction) else { return };
108 let members = roster.peek();
109 let Some(index) = members.iter().position(|m| m.key == key) else { return };
110 let Some(next) = roving::target(members.len(), index, step, |i| members.get(i).is_some_and(|m| m.focusable)).and_then(|i| members.get(i)) else { return };
111 event.prevent_default();
112 context.stop.set(Some(next.key));
113 roving::focus(next);
114 },
115 ..state, {children}
116 }
117 }
118}
119
120#[component]
122pub fn ToolbarLabel(children: Element) -> Element {
123 rsx! { span { class: BEM.element("label"), {children} } }
124}
125
126#[component]
128pub fn ToolbarDivider() -> Element {
129 rsx! { span { class: BEM.element("divider"), role: "separator" } }
130}