fc_dioxus/components/
carousel.rs1use crate::{
4 core::{
5 assets,
6 attrs::{carousel_control_attribute, component_attributes},
7 platform::DomBridge,
8 },
9 prelude::*,
10 theme::use_stylesheets,
11};
12const BEM: Bem = Bem::new("fc-carousel");
13const BTN: Bem = Bem::new("fc-btn");
14fc_modifier! {
15 pub enum CarouselLayout for BEM {
17 Row => None,
19 Hero => "hero",
21 Free => "free",
23 }
24}
25
26pub const PAGE: f64 = 0.85;
29
30#[must_use]
40pub fn edges(offset: f64, scroll_width: f64, client_width: f64) -> (bool, bool) {
41 let offset = offset.abs();
42 let max = scroll_width - client_width;
43 (offset <= 1.0, offset >= max - 1.0)
44}
45
46#[component]
50pub fn Carousel(
51 label: AccessibleName,
52 #[props(default)] layout: CarouselLayout,
53 #[props(default)] class: Option<String>,
54 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
55 children: Element,
57) -> Element {
58 let id = use_id();
59 let mut at = use_signal(|| (true, None::<bool>));
61 let attributes = component_attributes(
62 attributes,
63 ClassList::new(BEM.block())
64 .with_modifier(BEM, layout)
65 .extend(class.as_deref()),
66 &["aria-roledescription", "aria-label"],
67 );
68 let track_id = id.clone();
69 let sync = move || {
70 let id = track_id.clone();
71 async move {
72 if let Ok((start, end)) = DomBridge.scroll_edges(&id).await {
73 at.set((start, Some(end)));
74 }
75 }
76 };
77 let (start, end) = at();
78 let label_text = label.to_string();
79 rsx! {
80 if use_stylesheets() {
81 document::Stylesheet { href: assets::COMPONENTS_BUTTON_CSS }
82 document::Stylesheet { href: assets::COMPONENTS_CAROUSEL_CSS }
83 }
84 section { aria_roledescription: "carousel", aria_label: "{label_text}", ..attributes,
85 div { class: BEM.element("controls"),
86 button { class: control_class(), r#type: "button", aria_label: "Previous",
87 aria_disabled: start.then_some("true"),
88 onclick: { let id = id.clone(); move |_| { let id = id.clone(); async move { if !start { let _ = DomBridge.page(&id, false).await; } } } },
89 ..crate::spread![carousel_control_attribute(false)], "‹"
90 }
91 button { class: control_class(), r#type: "button", aria_label: "Next",
92 aria_disabled: end.map(|end| end.to_string()),
93 onclick: { let id = id.clone(); move |_| { let id = id.clone(); async move { if end != Some(true) { let _ = DomBridge.page(&id, true).await; } } } },
94 ..crate::spread![carousel_control_attribute(true)], "›"
95 }
96 }
97 ul { class: BEM.element("track"), id: "{id}", tabindex: "0", aria_label: "{label_text}",
98 onmounted: { let sync = sync.clone(); move |_| sync() },
99 onresize: { let sync = sync.clone(); move |_| sync() },
101 onscroll: move |_| sync(),
102 {children}
103 }
104 }
105 }
106}
107
108fn control_class() -> String {
109 ClassList::new(BTN.block())
110 .with(BTN.modifier_raw("secondary"))
111 .with(BTN.modifier_raw("icon"))
112 .with(BTN.modifier_raw("sm"))
113 .to_string()
114}
115
116#[component]
118pub fn Slide(
119 index: u32,
120 total: u32,
121 #[props(default)] caption: Option<Element>,
122 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
123 children: Element,
124) -> Element {
125 rsx! {
126 li { class: BEM.element("item"), aria_roledescription: "slide", aria_label: "{index} of {total}", ..attributes,
127 {children}
128 if let Some(caption) = caption { p { class: BEM.element("caption"), {caption} } }
129 }
130 }
131}