Support scrollIntoView alignment options (#3952)

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Otger RoglaandJonathan Kelley authored and GitHub committed 2025-05-07 13:09:17 -07:00
1 parent 85b373deff
commit 28d38307a2
7 files changed
+79 -19

No files matched your search

+2 -2
View File
@@ -79,12 +79,12 @@ impl RenderedElementBacking for DesktopElement {
fn scroll_to(
&self,
behavior: dioxus_html::ScrollBehavior,
options: dioxus_html::ScrollToOptions,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.scrollTo({}, {});",
self.id.0,
serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
serde_json::to_string(&options).expect("Failed to serialize ScrollToOptions")
);
let webview = self
.webview
+54 -2
View File
@@ -34,7 +34,7 @@ pub trait RenderedElementBacking: std::any::Any {
/// Scroll to make the element visible
fn scroll_to(
&self,
_behavior: ScrollBehavior,
_options: ScrollToOptions,
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
Box::pin(async { Err(MountedError::NotSupported) })
}
@@ -72,6 +72,46 @@ pub enum ScrollBehavior {
Smooth,
}
/// The desired final position within the scrollable ancestor container for a given axis.
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[doc(alias = "ScrollIntoViewOptions")]
pub enum ScrollLogicalPosition {
/// Aligns the element's start edge (top or left) with the start of the scrollable container,
/// making the element appear at the start of the visible area.
#[cfg_attr(feature = "serialize", serde(rename = "start"))]
Start,
/// Aligns the element at the center of the scrollable container,
/// positioning it in the middle of the visible area.
#[cfg_attr(feature = "serialize", serde(rename = "center"))]
Center,
/// Aligns the element's end edge (bottom or right) with the end of the scrollable container,
/// making the element appear at the end of the visible area
#[cfg_attr(feature = "serialize", serde(rename = "end"))]
End,
/// Scrolls the element to the nearest edge in the given axis.
/// This minimizes the scrolling distance.
#[cfg_attr(feature = "serialize", serde(rename = "nearest"))]
Nearest,
}
/// The way that scrolling should be performed
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[doc(alias = "ScrollIntoViewOptions")]
pub struct ScrollToOptions {
pub behavior: ScrollBehavior,
pub vertical: ScrollLogicalPosition,
pub horizontal: ScrollLogicalPosition,
}
impl Default for ScrollToOptions {
fn default() -> Self {
Self {
behavior: ScrollBehavior::Smooth,
vertical: ScrollLogicalPosition::Start,
horizontal: ScrollLogicalPosition::Center,
}
}
}
/// An Element that has been rendered and allows reading and modifying information about it.
///
/// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries.
@@ -125,7 +165,19 @@ impl MountedData {
&self,
behavior: ScrollBehavior,
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
self.inner.scroll_to(behavior)
self.inner.scroll_to(ScrollToOptions {
behavior,
..ScrollToOptions::default()
})
}
/// Scroll to make the element visible
#[doc(alias = "scrollIntoView")]
pub fn scroll_to_with_options(
&self,
options: ScrollToOptions,
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
self.inner.scroll_to(options)
}
/// Scroll to the given element offsets
+1 -1
View File
@@ -1 +1 @@
[6449103750905854967, 17669692872757955279, 13069001215487072322, 11420464406527728232, 3770103091118609057, 5444526391971481782, 15257049569425934744, 5052021921702764563, 10988859153374944111, 16153602427306015669]
[6449103750905854967, 17669692872757955279, 13069001215487072322, 11420464406527728232, 3770103091118609057, 5444526391971481782, 15100726369461302769, 5052021921702764563, 10988859153374944111, 16153602427306015669]
+1 -1
View File
@@ -1 +1 @@
function retrieveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){let formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retrieveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){let detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail));else if(detail instanceof IntersectionObserverEntry)extend(serializeIntersectionEventDetail(detail))}if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);if(event.type==="scroll")extend(serializeScrollEvent(event));return contents}function toSerializableResizeObserverSize(size,is_inline_width){return[is_inline_width?size.inlineSize:size.blockSize,is_inline_width?size.blockSize:size.inlineSize]}function serializeResizeEventDetail(detail){let is_inline_width=!0;if(detail.target instanceof HTMLElement){if(window.getComputedStyle(detail.target).getPropertyValue("writing-mode")!=="horizontal-tb")is_inline_width=!1}return{border_box_size:detail.borderBoxSize!==void 0?toSerializableResizeObserverSize(detail.borderBoxSize[0],is_inline_width):detail.contentRect,content_box_size:detail.contentBoxSize!==void 0?toSerializableResizeObserverSize(detail.contentBoxSize[0],is_inline_width):detail.contentRect,content_rect:detail.contentRect}}function serializeIntersectionEventDetail(detail){return{bounding_client_rect:detail.boundingClientRect,intersection_ratio:detail.intersectionRatio,intersection_rect:detail.intersectionRect,is_intersecting:detail.isIntersecting,root_bounds:detail.rootBounds,time_ms:Math.floor(Date.now()+detail.time)}}function serializeInputEvent(event,target){let contents={};if(target instanceof HTMLElement){let values=retrieveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents}function serializeWheelEvent(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}}function serializeTouchEvent(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}}function serializePointerEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}}function serializeMouseEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.ofLine truncated
function retrieveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){let formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retrieveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){let detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail));else if(detail instanceof IntersectionObserverEntry)extend(serializeIntersectionEventDetail(detail))}if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);if(event.type==="scroll")extend(serializeScrollEvent(event));return contents}function toSerializableResizeObserverSize(size,is_inline_width){return[is_inline_width?size.inlineSize:size.blockSize,is_inline_width?size.blockSize:size.inlineSize]}function serializeResizeEventDetail(detail){let is_inline_width=!0;if(detail.target instanceof HTMLElement){if(window.getComputedStyle(detail.target).getPropertyValue("writing-mode")!=="horizontal-tb")is_inline_width=!1}return{border_box_size:detail.borderBoxSize!==void 0?toSerializableResizeObserverSize(detail.borderBoxSize[0],is_inline_width):detail.contentRect,content_box_size:detail.contentBoxSize!==void 0?toSerializableResizeObserverSize(detail.contentBoxSize[0],is_inline_width):detail.contentRect,content_rect:detail.contentRect}}function serializeIntersectionEventDetail(detail){return{bounding_client_rect:detail.boundingClientRect,intersection_ratio:detail.intersectionRatio,intersection_rect:detail.intersectionRect,is_intersecting:detail.isIntersecting,root_bounds:detail.rootBounds,time_ms:Math.floor(Date.now()+detail.time)}}function serializeInputEvent(event,target){let contents={};if(target instanceof HTMLElement){let values=retrieveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents}function serializeWheelEvent(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}}function serializeTouchEvent(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}}function serializePointerEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}}function serializeMouseEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.ofLine truncated
+2 -2
View File
@@ -106,10 +106,10 @@ export class NativeInterpreter extends JSChannel_ {
return JSON.stringify({ method, params });
}
scrollTo(id: NodeId, behavior: ScrollBehavior): boolean {
scrollTo(id: NodeId, options: ScrollIntoViewOptions): boolean {
const node = this.nodes[id];
if (node instanceof HTMLElement) {
node.scrollIntoView({ behavior });
node.scrollIntoView(options);
return true;
}
return false;
+2 -2
View File
@@ -72,12 +72,12 @@ impl RenderedElementBacking for LiveviewElement {
fn scroll_to(
&self,
behavior: dioxus_html::ScrollBehavior,
options: dioxus_html::ScrollToOptions,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.scrollTo({}, {});",
self.id.0,
serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
serde_json::to_string(&options).expect("Failed to serialize ScrollToOptions")
);
let fut = self.query.new_query::<bool>(&script).resolve();
+17 -9
View File
@@ -66,17 +66,25 @@ impl dioxus_html::RenderedElementBacking for Synthetic<web_sys::Element> {
fn scroll_to(
&self,
behavior: dioxus_html::ScrollBehavior,
input_options: dioxus_html::ScrollToOptions,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = dioxus_html::MountedResult<()>>>> {
let options = web_sys::ScrollIntoViewOptions::new();
match behavior {
dioxus_html::ScrollBehavior::Instant => {
options.set_behavior(web_sys::ScrollBehavior::Instant);
}
dioxus_html::ScrollBehavior::Smooth => {
options.set_behavior(web_sys::ScrollBehavior::Smooth);
}
}
options.set_behavior(match input_options.behavior {
dioxus_html::ScrollBehavior::Instant => web_sys::ScrollBehavior::Instant,
dioxus_html::ScrollBehavior::Smooth => web_sys::ScrollBehavior::Smooth,
});
options.set_block(match input_options.vertical {
dioxus_html::ScrollLogicalPosition::Start => web_sys::ScrollLogicalPosition::Start,
dioxus_html::ScrollLogicalPosition::Center => web_sys::ScrollLogicalPosition::Center,
dioxus_html::ScrollLogicalPosition::End => web_sys::ScrollLogicalPosition::End,
dioxus_html::ScrollLogicalPosition::Nearest => web_sys::ScrollLogicalPosition::Nearest,
});
options.set_inline(match input_options.horizontal {
dioxus_html::ScrollLogicalPosition::Start => web_sys::ScrollLogicalPosition::Start,
dioxus_html::ScrollLogicalPosition::Center => web_sys::ScrollLogicalPosition::Center,
dioxus_html::ScrollLogicalPosition::End => web_sys::ScrollLogicalPosition::End,
dioxus_html::ScrollLogicalPosition::Nearest => web_sys::ScrollLogicalPosition::Nearest,
});
self.event
.scroll_into_view_with_scroll_into_view_options(&options);