Skip to content

Commit 85b373d

Browse files
sehnryrjkelleyrtp
andauthored
feat(html): add scroll method to MountedData (#3722)
* feat(html): add `scroll` method to MountedData * feat(desktop): implement `scroll` method for DesktopElement * feat(liveview): implement `scroll` method for LiveviewElement * feat(web): implement `scroll` method for web_sys::Element * docs(examples): add new example for `scroll` method * fix(web): remove unncecessary f64 cast (clippy) * fix(interpreter): return missing boolean from js * docs(example): unwrap result instead of discarding its error * docs(examples): create scroll restoring example on routing * fix clippy --------- Co-authored-by: Jonathan Kelley <[email protected]>
1 parent 60c1b0e commit 85b373d

File tree

11 files changed

+757
-607
lines changed

11 files changed

+757
-607
lines changed

Cargo.lock

+516-605
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/router_restore_scroll.rs

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use std::rc::Rc;
2+
3+
use dioxus::html::geometry::PixelsVector2D;
4+
use dioxus::prelude::*;
5+
6+
#[derive(Clone, Routable, Debug, PartialEq)]
7+
enum Route {
8+
#[route("/")]
9+
Home {},
10+
#[route("/blog/:id")]
11+
Blog { id: i32 },
12+
}
13+
14+
fn main() {
15+
dioxus::launch(App);
16+
}
17+
18+
#[component]
19+
fn App() -> Element {
20+
use_context_provider(|| Signal::new(Scroll::default()));
21+
22+
rsx! {
23+
Router::<Route> {}
24+
}
25+
}
26+
27+
#[component]
28+
fn Blog(id: i32) -> Element {
29+
rsx! {
30+
GoBackButton { "Go back" }
31+
div { "Blog post {id}" }
32+
}
33+
}
34+
35+
type Scroll = Option<PixelsVector2D>;
36+
37+
#[component]
38+
fn Home() -> Element {
39+
let mut element: Signal<Option<Rc<MountedData>>> = use_signal(|| None);
40+
let mut scroll = use_context::<Signal<Scroll>>();
41+
42+
_ = use_resource(move || async move {
43+
if let (Some(element), Some(scroll)) = (element.read().as_ref(), *scroll.peek()) {
44+
element
45+
.scroll(scroll, ScrollBehavior::Instant)
46+
.await
47+
.unwrap();
48+
}
49+
});
50+
51+
rsx! {
52+
div {
53+
height: "300px",
54+
overflow_y: "auto",
55+
border: "1px solid black",
56+
57+
onmounted: move |event| element.set(Some(event.data())),
58+
59+
onscroll: move |_| async move {
60+
if let Some(element) = element.cloned() {
61+
scroll.set(Some(element.get_scroll_offset().await.unwrap()))
62+
}
63+
},
64+
65+
for i in 0..100 {
66+
div { height: "20px",
67+
68+
Link { to: Route::Blog { id: i }, "Blog {i}" }
69+
}
70+
}
71+
}
72+
}
73+
}

examples/scroll_to_offset.rs

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//! Scroll elements using their MountedData
2+
//!
3+
//! Dioxus exposes a few helpful APIs around elements (mimicking the DOM APIs) to allow you to interact with elements
4+
//! across the renderers. This includes scrolling, reading dimensions, and more.
5+
//!
6+
//! In this example we demonstrate how to scroll to a given y offset of the scrollable parent using the `scroll` method on the `MountedData`
7+
8+
use dioxus::html::geometry::PixelsVector2D;
9+
use dioxus::prelude::*;
10+
11+
fn main() {
12+
dioxus::launch(app);
13+
}
14+
15+
fn app() -> Element {
16+
rsx! {
17+
ScrollToCoordinates {}
18+
ScrollToCoordinates {}
19+
}
20+
}
21+
22+
#[component]
23+
fn ScrollToCoordinates() -> Element {
24+
let mut element = use_signal(|| None);
25+
26+
rsx! {
27+
div { border: "1px solid black", position: "relative",
28+
29+
div {
30+
height: "300px",
31+
overflow_y: "auto",
32+
33+
onmounted: move |event| element.set(Some(event.data())),
34+
35+
for i in 0..100 {
36+
div { height: "20px", "Item {i}" }
37+
}
38+
}
39+
40+
div { position: "absolute", top: 0, right: 0,
41+
input {
42+
r#type: "number",
43+
min: "0",
44+
max: "99",
45+
oninput: move |event| async move {
46+
if let Some(ul) = element.cloned() {
47+
let data = event.data();
48+
if let Ok(value) = data.parsed::<f64>() {
49+
ul.scroll(PixelsVector2D::new(0.0, 20.0 * value), ScrollBehavior::Smooth)
50+
.await
51+
.unwrap();
52+
}
53+
}
54+
},
55+
}
56+
}
57+
}
58+
}
59+
}

packages/desktop/src/element.rs

+30
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,36 @@ impl RenderedElementBacking for DesktopElement {
104104
})
105105
}
106106

107+
fn scroll(
108+
&self,
109+
coordinates: PixelsVector2D,
110+
behavior: dioxus_html::ScrollBehavior,
111+
) -> std::pin::Pin<Box<dyn std::future::Future<Output = MountedResult<()>>>> {
112+
let script = format!(
113+
"return window.interpreter.scroll({}, {}, {}, {});",
114+
self.id.0,
115+
coordinates.x,
116+
coordinates.y,
117+
serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
118+
);
119+
let webview = self
120+
.webview
121+
.upgrade()
122+
.expect("Webview should be alive if the element is being queried");
123+
let fut = self.query.new_query::<bool>(&script, webview).resolve();
124+
Box::pin(async move {
125+
match fut.await {
126+
Ok(true) => Ok(()),
127+
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
128+
Box::new(DesktopQueryError::FailedToQuery),
129+
)),
130+
Err(err) => {
131+
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
132+
}
133+
}
134+
})
135+
}
136+
107137
fn set_focus(
108138
&self,
109139
focus: bool,

packages/html/src/events/mounted.rs

+19
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ pub trait RenderedElementBacking: std::any::Any {
3939
Box::pin(async { Err(MountedError::NotSupported) })
4040
}
4141

42+
/// Scroll to the given element offsets
43+
fn scroll(
44+
&self,
45+
_coordinates: PixelsVector2D,
46+
_behavior: ScrollBehavior,
47+
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
48+
Box::pin(async { Err(MountedError::NotSupported) })
49+
}
50+
4251
/// Set the focus on the element
4352
fn set_focus(&self, _focus: bool) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
4453
Box::pin(async { Err(MountedError::NotSupported) })
@@ -119,6 +128,16 @@ impl MountedData {
119128
self.inner.scroll_to(behavior)
120129
}
121130

131+
/// Scroll to the given element offsets
132+
#[doc(alias = "scrollTo")]
133+
pub fn scroll(
134+
&self,
135+
coordinates: PixelsVector2D,
136+
behavior: ScrollBehavior,
137+
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
138+
self.inner.scroll(coordinates, behavior)
139+
}
140+
122141
/// Set the focus on the element
123142
#[doc(alias = "focus")]
124143
#[doc(alias = "blur")]

packages/interpreter/src/js/hash.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
[6449103750905854967, 17669692872757955279, 13069001215487072322, 11420464406527728232, 3770103091118609057, 5444526391971481782, 7965007982501706197, 5052021921702764563, 10988859153374944111, 16153602427306015669]
1+
[6449103750905854967, 17669692872757955279, 13069001215487072322, 11420464406527728232, 3770103091118609057, 5444526391971481782, 15257049569425934744, 5052021921702764563, 10988859153374944111, 16153602427306015669]

packages/interpreter/src/js/native.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/interpreter/src/ts/native.ts

+9
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ export class NativeInterpreter extends JSChannel_ {
115115
return false;
116116
}
117117

118+
scroll(id: NodeId, x: number, y: number, behavior: ScrollBehavior): boolean {
119+
const node = this.nodes[id];
120+
if (node instanceof HTMLElement) {
121+
node.scroll({ top: y, left: x, behavior });
122+
return true;
123+
}
124+
return false;
125+
}
126+
118127
getScrollHeight(id: NodeId): number | undefined {
119128
const node = this.nodes[id];
120129
if (node instanceof HTMLElement) {

packages/liveview/src/element.rs

+27
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,33 @@ impl RenderedElementBacking for LiveviewElement {
9494
})
9595
}
9696

97+
fn scroll(
98+
&self,
99+
coordinates: PixelsVector2D,
100+
behavior: dioxus_html::ScrollBehavior,
101+
) -> std::pin::Pin<Box<dyn std::future::Future<Output = MountedResult<()>>>> {
102+
let script = format!(
103+
"return window.interpreter.scroll({}, {}, {}, {});",
104+
self.id.0,
105+
coordinates.x,
106+
coordinates.y,
107+
serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
108+
);
109+
110+
let fut = self.query.new_query::<bool>(&script).resolve();
111+
Box::pin(async move {
112+
match fut.await {
113+
Ok(true) => Ok(()),
114+
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
115+
Box::new(DesktopQueryError::FailedToQuery),
116+
)),
117+
Err(err) => {
118+
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
119+
}
120+
}
121+
})
122+
}
123+
97124
fn set_focus(
98125
&self,
99126
focus: bool,

packages/web/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ features = [
7070
"ResizeObserverEntry",
7171
"ResizeObserverSize",
7272
"ScrollRestoration",
73+
"ScrollToOptions",
7374
"Text",
7475
"Touch",
7576
"TouchEvent",

packages/web/src/events/mounted.rs

+21
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,27 @@ impl dioxus_html::RenderedElementBacking for Synthetic<web_sys::Element> {
8383
Box::pin(async { Ok(()) })
8484
}
8585

86+
fn scroll(
87+
&self,
88+
coordinates: dioxus_html::geometry::PixelsVector2D,
89+
behavior: dioxus_html::ScrollBehavior,
90+
) -> std::pin::Pin<Box<dyn std::future::Future<Output = dioxus_html::MountedResult<()>>>> {
91+
let options = web_sys::ScrollToOptions::new();
92+
options.set_top(coordinates.y);
93+
options.set_left(coordinates.x);
94+
match behavior {
95+
dioxus_html::ScrollBehavior::Instant => {
96+
options.set_behavior(web_sys::ScrollBehavior::Instant);
97+
}
98+
dioxus_html::ScrollBehavior::Smooth => {
99+
options.set_behavior(web_sys::ScrollBehavior::Smooth);
100+
}
101+
}
102+
self.event.scroll_with_scroll_to_options(&options);
103+
104+
Box::pin(async { Ok(()) })
105+
}
106+
86107
fn set_focus(
87108
&self,
88109
focus: bool,

0 commit comments

Comments
 (0)