dioxus_floating/scrollable_view.rs
1use std::rc::Rc;
2
3use dioxus::{html::geometry::PixelsVector2D, prelude::*};
4
5use crate::ScrollState;
6
7/// A scrollable container that provides context for floating elements.
8///
9/// `ScrollableView` is the core component of the library. It tracks its own
10/// position, dimensions, and scroll state, providing this data via context
11/// to child hooks like `use_placement`.
12///
13/// # Note on Styles:
14/// Ensure you provide height and overflow styles (e.g., `h-full overflow-auto`)
15/// via the `class` or `style` props, as the component does not apply them by default.
16///
17/// # Example
18///
19/// ```rust,norun
20/// use dioxus::prelude::*;
21/// use dioxus_floating::{use_scroll_context, ScrollableView, ScrollState};
22///
23/// #[component]
24/// fn MyComponent() -> Element {
25/// rsx! {
26/// ScrollableView {
27/// class: "h-screen w-full overflow-y-auto",
28/// on_scroll: move |state: ScrollState| println!("Scrolled to: {}", state.state.y),
29///
30/// div { class: "h-[2000px]", "Very long content..." }
31///
32/// // Any floating elements inside will be positioned correctly
33/// MyDropdown {}
34/// }
35/// }
36/// }
37///
38/// #[component]
39/// fn MyDropdown() -> Element { let ctx = use_scroll_context(); rsx! {} }
40/// ```
41#[component]
42pub fn ScrollableView(
43 #[props(default)] id: Option<String>,
44 #[props(default)] class: String,
45 #[props(default)] style: String,
46 children: Element,
47 #[props(into)] on_scroll: Option<EventHandler<ScrollState>>,
48) -> Element {
49 let floating = crate::use_floating();
50
51 let mut scrollable_ref = use_signal(|| Option::<Rc<MountedData>>::None);
52 let mut scroll_state = use_signal(|| Option::<ScrollState>::None);
53
54 use_context_provider(move || ScrollableContext {
55 scrollable_ref,
56 scroll_state,
57 });
58
59 rsx! {
60 div { id: id, class: class, style: style,
61 onmounted: move |evt: MountedEvent| {
62 scrollable_ref.set(Some(evt.data.clone()));
63 let mounted_data = evt.data.clone();
64 spawn(async move {
65 let state = floating.generate_scroll_state_from_mounted(mounted_data).await;
66 scroll_state.set(Some(state));
67 });
68 },
69 onresize: move |evt: ResizeEvent| {
70 scroll_state.with_mut(move |sstate| {
71 if let Some(state) = sstate {
72 if let Ok(size) = evt.get_border_box_size() {
73 state.bounds = size;
74 }
75
76 *sstate = Some(state.to_owned());
77 }
78 });
79 if let Some(scrollable) = scrollable_ref() {
80 spawn(async move {
81 if let Ok(size) = scrollable.get_scroll_size().await {
82 scroll_state.with_mut(move |sstate| {
83 if let Some(state) = sstate {
84 state.size = size;
85 *sstate = Some(state.to_owned());
86 }
87 });
88 }
89 });
90 }
91 },
92 onscroll: move |evt: ScrollEvent| {
93 let new_state = floating.generate_scroll_state(evt);
94 scroll_state.set(Some(new_state));
95 if let Some(cb) = on_scroll { cb.call(new_state); }
96 },
97
98 {children}
99 }
100 }
101}
102
103/// Context provided by the [ScrollableView] component.
104///
105/// It contains reactive signals for the scroll state and a reference to the
106/// underlying DOM element, along with methods to programmatically control scrolling.
107#[derive(Debug, Clone, Copy)]
108pub struct ScrollableContext {
109 /// A reactive signal containing the [MountedData] of the scrollable container.
110 pub scrollable_ref: Signal<Option<Rc<MountedData>>>,
111
112 /// A reactive signal containing the current [ScrollState] (dimensions, offset, etc.).
113 pub scroll_state: Signal<Option<ScrollState>>,
114}
115
116impl ScrollableContext {
117 /// Forces a re-calculation of the scroll content size and current offset.
118 /// Useful when the content inside changes but the container's outer bounds remain the same.
119 pub async fn reload(&mut self) {
120 if let Some(data) = self.scrollable_ref.peek().as_ref() {
121 // Мы используем логику из Floating, которую ты уже написал
122 let floating = crate::Floating::default();
123 let new_state = floating
124 .generate_scroll_state_from_mounted(data.clone())
125 .await;
126
127 // Обновляем сигнал
128 self.scroll_state.set(Some(new_state));
129 }
130 }
131
132 /// Programmatically scrolls the container by a given offset.
133 ///
134 /// # Example
135 /// ```rust
136 /// use dioxus::prelude::*;
137 /// use dioxus::html::geometry::PixelsVector2D;
138 /// use dioxus_floating::use_scroll_context;
139 ///
140 /// #[component]
141 /// fn MyComponent() -> Element {
142 /// let ctx = use_scroll_context();
143 /// use_effect(move || {
144 /// spawn(async move {
145 /// ctx.scroll(PixelsVector2D::new(0.0, 100.0), ScrollBehavior::Smooth).await;
146 /// });
147 /// });
148 /// rsx! {}
149 /// }
150 /// ```
151 pub async fn scroll(&self, coordinates: PixelsVector2D, behavior: ScrollBehavior) {
152 if let Some(data) = self.scrollable_ref.peek().as_ref() {
153 let _ = data.scroll(coordinates, behavior).await;
154 }
155 }
156
157 /// Scrolls to a specific position (e.g., top or bottom) based on the behavior.
158 pub async fn scroll_to(&self, behavior: ScrollBehavior) {
159 if let Some(data) = self.scrollable_ref.peek().as_ref() {
160 let _ = data.scroll_to(behavior).await;
161 }
162 }
163
164 /// Scrolls the container using advanced options (like specific element alignment).
165 pub async fn scroll_to_with_options(&self, options: ScrollToOptions) {
166 if let Some(data) = self.scrollable_ref.peek().as_ref() {
167 let _ = data.scroll_to_with_options(options).await;
168 }
169 }
170}