diff --git a/packages/core/src/compute_coords_from_placement.rs b/packages/core/src/compute_coords_from_placement.rs index 5525a93..554f940 100644 --- a/packages/core/src/compute_coords_from_placement.rs +++ b/packages/core/src/compute_coords_from_placement.rs @@ -46,7 +46,7 @@ pub fn compute_coords_from_placement( if let Some(alignment) = get_alignment(placement) { coords.update_axis(alignment_axis, |value| { value - - common_align + + common_align * (if alignment == Alignment::End { 1.0 } else { diff --git a/packages/dom/src/auto_update.rs b/packages/dom/src/auto_update.rs index d5884d8..04169cd 100644 --- a/packages/dom/src/auto_update.rs +++ b/packages/dom/src/auto_update.rs @@ -5,7 +5,7 @@ use floating_ui_utils::{ dom::{OverflowAncestor, get_document_element, get_overflow_ancestors, get_window}, }; use web_sys::{ - AddEventListenerOptions, Element, EventTarget, IntersectionObserver, IntersectionObserverEntry, + Element, EventTarget, IntersectionObserver, IntersectionObserverEntry, IntersectionObserverInit, ResizeObserver, ResizeObserverEntry, wasm_bindgen::{JsCast, JsValue, closure::Closure}, window, @@ -30,7 +30,7 @@ fn cancel_animation_frame(handle: i32) { .expect("Cancel animation frame should be successful.") } -fn observe_move(element: Element, on_move: Rc) -> Box { +fn observe_move(element: Element, on_move: Rc, ancestor_resize: bool) -> Box { let io: Rc>> = Rc::new(RefCell::new(None)); let timeout_id: Rc>> = Rc::new(RefCell::new(None)); @@ -61,55 +61,65 @@ fn observe_move(element: Element, on_move: Rc) -> Box { let refresh_closure_clone = refresh_closure.clone(); let refresh_cleanup = cleanup_rc.clone(); - *refresh_closure_clone.borrow_mut() = Some(Box::new(move |skip: bool, threshold: f64| { - refresh_cleanup(); + *refresh_closure_clone.borrow_mut() = Some(Box::new({ + let element = element.clone(); - let element_rect_for_root_margin = element.get_bounding_client_rect(); + move |skip: bool, threshold: f64| { + refresh_cleanup(); - if !skip { - on_move(); - } - - if element_rect_for_root_margin.width() == 0.0 - || element_rect_for_root_margin.height() == 0.0 - { - return; - } - - let inset_top = element_rect_for_root_margin.top().floor(); - let inset_right = (root.client_width() as f64 - - (element_rect_for_root_margin.left() + element_rect_for_root_margin.width())) - .floor(); - let inset_bottom = (root.client_height() as f64 - - (element_rect_for_root_margin.top() + element_rect_for_root_margin.height())) - .floor(); - let inset_left = element_rect_for_root_margin.left().floor(); - let root_margin = format!( - "{}px {}px {}px {}px", - -inset_top, -inset_right, -inset_bottom, -inset_left - ); - - let is_first_update: Rc> = Rc::new(RefCell::new(true)); - - let timeout_refresh = refresh_closure.clone(); - let timeout_closure: Rc> = Rc::new(Closure::new(move || { - timeout_refresh - .borrow() - .as_ref() - .expect("Refresh closure should exist.")(false, 1e-7) - })); + let element_rect_for_root_margin = element.get_bounding_client_rect(); - let observe_timeout_id = timeout_id.clone(); - let observe_window = window.clone(); - let observe_refresh = refresh_closure.clone(); - let local_observe_closure = Closure::new({ - let element = element.clone(); + if !skip { + on_move(); + } - move |entries: Vec| { - let ratio = entries[0].intersection_ratio(); + if element_rect_for_root_margin.width() == 0.0 + || element_rect_for_root_margin.height() == 0.0 + { + return; + } - if ratio != threshold { - if !*is_first_update.borrow() { + let inset_top = element_rect_for_root_margin.top().floor(); + let inset_right = (root.client_width() as f64 + - (element_rect_for_root_margin.left() + element_rect_for_root_margin.width())) + .floor(); + let inset_bottom = (root.client_height() as f64 + - (element_rect_for_root_margin.top() + element_rect_for_root_margin.height())) + .floor(); + let inset_left = element_rect_for_root_margin.left().floor(); + let root_margin = format!( + "{}px {}px {}px {}px", + -inset_top, -inset_right, -inset_bottom, -inset_left + ); + + let is_first_update: Rc> = Rc::new(RefCell::new(true)); + + let timeout_refresh = refresh_closure.clone(); + let timeout_closure: Rc> = Rc::new(Closure::new(move || { + timeout_refresh + .borrow() + .as_ref() + .expect("Refresh closure should exist.")(false, 1e-7) + })); + + let observe_timeout_id = timeout_id.clone(); + let observe_window = window.clone(); + let observe_refresh = refresh_closure.clone(); + let local_observe_closure = Closure::new({ + let element = element.clone(); + + move |entries: Vec| { + let ratio = entries[0].intersection_ratio(); + + // The entry is a snapshot, so the reference may have moved since the + // intersection was computed (under performance constraints, or between + // consecutive frames of a multi-frame layout shift). The reported ratio + // and the observed area are stale in that case and cannot be trusted to + // detect subsequent movement, so refresh regardless of the ratio. + if !rects_are_equal( + &(&element_rect_for_root_margin).into(), + &element.get_bounding_client_rect().into(), + ) { observe_refresh .borrow() .as_ref() @@ -119,71 +129,85 @@ fn observe_move(element: Element, on_move: Rc) -> Box { return; } - if ratio == 0.0 { - // If the reference is clipped, the ratio is 0. Throttle the refresh to prevent an infinite loop of updates. - observe_timeout_id.replace(Some( - observe_window - .set_timeout_with_callback_and_timeout_and_arguments_0( - (*timeout_closure).as_ref().unchecked_ref(), - 1000, - ) - .expect("Set timeout should be successful."), - )); - } else { - observe_refresh - .borrow() - .as_ref() - .expect("Refresh closure should exist.")( - false, ratio - ); + if ratio != threshold { + if !*is_first_update.borrow() { + observe_refresh + .borrow() + .as_ref() + .expect("Refresh closure should exist.")( + false, 1.0 + ); + return; + } + + if ratio == 0.0 { + // If the reference is clipped in place, the ratio is 0. Throttle the refresh to prevent an infinite loop of updates. + observe_timeout_id.replace(Some( + observe_window + .set_timeout_with_callback_and_timeout_and_arguments_0( + (*timeout_closure).as_ref().unchecked_ref(), + 1000, + ) + .expect("Set timeout should be successful."), + )); + } else { + observe_refresh + .borrow() + .as_ref() + .expect("Refresh closure should exist.")( + false, ratio + ); + } } - } - if ratio == 1.0 - && !rects_are_equal( - &element_rect_for_root_margin.clone().into(), - &element.get_bounding_client_rect().into(), - ) - { - // It's possible that even though the ratio is reported as 1, the - // element is not actually fully within the IntersectionObserver's root - // area anymore. This can happen under performance constraints. This may - // be a bug in the browser's IntersectionObserver implementation. To - // work around this, we compare the element's bounding rect now with - // what it was at the time we created the IntersectionObserver. If they - // are not equal then the element moved, so we refresh. - observe_refresh - .borrow() - .as_ref() - .expect("Refresh closure should exist.")(false, 1.0); + is_first_update.replace(false); } + }); - is_first_update.replace(false); - } - }); + let options = IntersectionObserverInit::new(); + options.set_root_margin(&root_margin); + options.set_threshold(&JsValue::from_f64(threshold.clamp(0.0, 1.0))); - let options = IntersectionObserverInit::new(); - options.set_root_margin(&root_margin); - options.set_threshold(&JsValue::from_f64(threshold.clamp(0.0, 1.0))); + let local_io = IntersectionObserver::new_with_options( + local_observe_closure.as_ref().unchecked_ref(), + &options, + ) + .expect("Intersection observer should be created."); - let local_io = IntersectionObserver::new_with_options( - local_observe_closure.as_ref().unchecked_ref(), - &options, - ) - .expect("Intersection observer should be created."); + observe_closure.replace(Some(local_observe_closure)); - observe_closure.replace(Some(local_observe_closure)); - - local_io.observe(&element); - io.replace(Some(local_io)); + local_io.observe(&element); + io.replace(Some(local_io)); + } })); + // The window is a resize ancestor, so when `ancestor_resize` is enabled its + // listener already runs the update on resize. Here we only need to rebuild + // the `IntersectionObserver` for the new root size, skipping a redundant + // update. When `ancestor_resize` is disabled, this becomes the sole update. + let win = get_window(Some(&element)); + let handle_resize: Closure = Closure::new({ + let refresh_closure_clone = refresh_closure_clone.clone(); + + move || { + refresh_closure_clone + .borrow() + .as_ref() + .expect("Refresh closure should exist.")(ancestor_resize, 1.0); + } + }); + + win.add_event_listener_with_callback("resize", handle_resize.as_ref().unchecked_ref()) + .expect("Resize event listener should be added."); + refresh_closure_clone .borrow() .as_ref() .expect("Refresh closure should exist.")(true, 1.0); Box::new(move || { + win.remove_event_listener_with_callback("resize", handle_resize.as_ref().unchecked_ref()) + .expect("Resize event listener should be removed."); cleanup_rc(); }) } @@ -303,15 +327,8 @@ pub fn auto_update( }; if ancestor_scoll { - let options = AddEventListenerOptions::new(); - options.set_passive(true); - event_target - .add_event_listener_with_callback_and_add_event_listener_options( - "scroll", - update_closure.as_ref().unchecked_ref(), - &options, - ) + .add_event_listener_with_callback("scroll", update_closure.as_ref().unchecked_ref()) .expect("Scroll event listener should be added."); } @@ -323,7 +340,8 @@ pub fn auto_update( } let cleanup_observe_move = reference_element.as_ref().and_then(|reference_element| { - layout_shift.then(|| observe_move(reference_element.clone(), update.clone())) + layout_shift + .then(|| observe_move(reference_element.clone(), update.clone(), ancestor_resize)) }); let reobserve_frame: Rc>> = Rc::new(RefCell::new(None)); diff --git a/packages/dom/src/platform/convert_offset_parent_relative_rect_to_viewport_relative_rect.rs b/packages/dom/src/platform/convert_offset_parent_relative_rect_to_viewport_relative_rect.rs index 4e874d6..06695c9 100644 --- a/packages/dom/src/platform/convert_offset_parent_relative_rect_to_viewport_relative_rect.rs +++ b/packages/dom/src/platform/convert_offset_parent_relative_rect_to_viewport_relative_rect.rs @@ -52,7 +52,7 @@ pub fn convert_offset_parent_relative_rect_to_viewport_relative_rect( }); #[allow(clippy::nonminimal_bool)] - if is_offset_parent_an_element || (!is_offset_parent_an_element && !is_fixed) { + if is_offset_parent_an_element || !is_fixed { if let Some(offset_parent) = offset_parent.as_ref() && (get_node_name(offset_parent.into()) != "body" || is_overflow_element(&document_element)) diff --git a/packages/dom/src/platform/get_clipping_rect.rs b/packages/dom/src/platform/get_clipping_rect.rs index fafd89a..78224d3 100644 --- a/packages/dom/src/platform/get_clipping_rect.rs +++ b/packages/dom/src/platform/get_clipping_rect.rs @@ -14,8 +14,10 @@ use crate::{ platform::{Platform, get_scale::get_scale}, types::Boundary, utils::{ - get_bounding_client_rect::get_bounding_client_rect, get_document_rect::get_document_rect, - get_viewport_rect::get_viewport_rect, get_visual_offsets::get_visual_offsets, + get_bounding_client_rect::get_bounding_client_rect, + get_document_rect::get_document_rect, + get_viewport_rect::{ViewportRootBoundary, get_viewport_rect}, + get_visual_offsets::get_visual_offsets, }, }; @@ -50,9 +52,11 @@ fn get_client_rect_from_clipping_ancestor( get_inner_bounding_client_rect(&element, strategy) } ElementOrRootBoundary::RootBoundary(RootBoundary::Viewport) - | ElementOrRootBoundary::RootBoundary(RootBoundary::LayoutViewport) => { - get_viewport_rect(&get_document_element(Some(element.into())), strategy) - } + | ElementOrRootBoundary::RootBoundary(RootBoundary::LayoutViewport) => get_viewport_rect( + &get_document_element(Some(element.into())), + strategy, + ViewportRootBoundary::Viewport, + ), ElementOrRootBoundary::RootBoundary(RootBoundary::Document) => { get_document_rect(&get_document_element(Some(element.into()))) } diff --git a/packages/dom/src/utils/get_bounding_client_rect.rs b/packages/dom/src/utils/get_bounding_client_rect.rs index b83b0c5..2ec0970 100644 --- a/packages/dom/src/utils/get_bounding_client_rect.rs +++ b/packages/dom/src/utils/get_bounding_client_rect.rs @@ -51,58 +51,54 @@ pub fn get_bounding_client_rect( let mut width = client_rect.width / scale.x; let mut height = client_rect.height / scale.y; - if let Some(dom_element) = dom_element { + if let Some(dom_element) = dom_element + && let Some(offset_parent) = offset_parent + { let window = get_window(Some(&dom_element)); let offset_window = match offset_parent { - Some(DomElementOrWindow::Element(element)) => Some(get_window(Some(element))), - Some(DomElementOrWindow::Window(window)) => Some(window.clone()), - None => None, + DomElementOrWindow::Element(element) => get_window(Some(element)), + DomElementOrWindow::Window(window) => window.clone(), }; - if offset_parent.is_some() { - let mut current_window = window; - loop { - let current_iframe = get_frame_element(¤t_window); + let mut current_window = window; + loop { + let current_iframe = get_frame_element(¤t_window); - if let Some(current_iframe) = current_iframe.as_ref() { - if offset_window - .as_ref() - .is_some_and(|offset_window| offset_window != ¤t_window) - { - let iframe_scale = get_scale(current_iframe.into()); - let iframe_rect = current_iframe.get_bounding_client_rect(); - let css = get_computed_style(current_iframe); - let padding_left = css - .get_property_value("padding-left") - .expect("Computed style should have padding left.") - .parse::() - .expect("Padding left should be a number."); - let padding_top = css - .get_property_value("padding-right") - .expect("Computed style should have padding right.") - .parse::() - .expect("Padding right should be a number."); + if let Some(current_iframe) = current_iframe.as_ref() { + if offset_window != current_window { + let iframe_scale = get_scale(current_iframe.into()); + let iframe_rect = current_iframe.get_bounding_client_rect(); + let css = get_computed_style(current_iframe); + let padding_left = css + .get_property_value("padding-left") + .expect("Computed style should have padding left.") + .parse::() + .expect("Padding left should be a number."); + let padding_top = css + .get_property_value("padding-right") + .expect("Computed style should have padding right.") + .parse::() + .expect("Padding right should be a number."); - let left = iframe_rect.left() - + (current_iframe.client_left() as f64 + padding_left) * iframe_scale.x; - let top = iframe_rect.top() - + (current_iframe.client_top() as f64 + padding_top) * iframe_scale.y; + let left = iframe_rect.left() + + (current_iframe.client_left() as f64 + padding_left) * iframe_scale.x; + let top = iframe_rect.top() + + (current_iframe.client_top() as f64 + padding_top) * iframe_scale.y; - x *= iframe_scale.x; - y *= iframe_scale.y; - width *= iframe_scale.x; - height *= iframe_scale.y; + x *= iframe_scale.x; + y *= iframe_scale.y; + width *= iframe_scale.x; + height *= iframe_scale.y; - x += left; - y += top; + x += left; + y += top; - current_window = get_window(Some(current_iframe)); - } else { - break; - } + current_window = get_window(Some(current_iframe)); } else { break; } + } else { + break; } } } diff --git a/packages/dom/src/utils/get_document_rect.rs b/packages/dom/src/utils/get_document_rect.rs index dc06ee5..a049c0f 100644 --- a/packages/dom/src/utils/get_document_rect.rs +++ b/packages/dom/src/utils/get_document_rect.rs @@ -1,7 +1,4 @@ -use floating_ui_utils::{ - Rect, - dom::{get_document_element, get_node_scroll}, -}; +use floating_ui_utils::{Rect, dom::get_node_scroll}; use web_sys::Element; use crate::platform::is_rtl::is_rtl; @@ -9,10 +6,9 @@ use crate::platform::is_rtl::is_rtl; use super::get_window_scroll_bar_x::get_window_scroll_bar_x; /// Gets the entire size of the scrollable document area, even extending outside of the `` and `` rect bounds if horizontally scrollable. -pub fn get_document_rect(element: &Element) -> Rect { - let html = get_document_element(Some(element.into())); - let scroll = get_node_scroll(element.into()); - let body = element +pub fn get_document_rect(html: &Element) -> Rect { + let scroll = get_node_scroll(html.into()); + let body = html .owner_document() .expect("Element should have owner document.") .body() @@ -37,7 +33,7 @@ pub fn get_document_rect(element: &Element) -> Rect { .max() .expect("Iterator is not empty.") as f64; - let mut x = -scroll.scroll_left + get_window_scroll_bar_x(element, None); + let mut x = -scroll.scroll_left + get_window_scroll_bar_x(html, None); let y = -scroll.scroll_top; if is_rtl(&body) { diff --git a/packages/dom/src/utils/get_rect_relative_to_offset_parent.rs b/packages/dom/src/utils/get_rect_relative_to_offset_parent.rs index 9a9e7bc..2dd01bd 100644 --- a/packages/dom/src/utils/get_rect_relative_to_offset_parent.rs +++ b/packages/dom/src/utils/get_rect_relative_to_offset_parent.rs @@ -32,14 +32,8 @@ pub fn get_rect_relative_to_offset_parent( let mut scroll = NodeScroll::new(0.0); let mut offsets = Coords::new(0.0); - // If the scrollbar appears on the left (e.g. RTL systems). - // Use Firefox with layout.scrollbar.side = 3 in about:config to test this. - let set_left_rtl_scrollbar_offset = |offsets: &mut Coords| { - offsets.x = get_window_scroll_bar_x(&document_element, None); - }; - #[allow(clippy::nonminimal_bool)] - if is_offset_parent_an_element || (!is_offset_parent_an_element && !is_fixed) { + if is_offset_parent_an_element || !is_fixed { if get_node_name((&offset_parent).into()) != "body" || is_overflow_element(&document_element) { @@ -57,14 +51,14 @@ pub fn get_rect_relative_to_offset_parent( offsets.x = offset_rect.x + offset_parent.client_left() as f64; offsets.y = offset_rect.y + offset_parent.client_top() as f64; } - DomElementOrWindow::Window(_) => { - set_left_rtl_scrollbar_offset(&mut offsets); - } + DomElementOrWindow::Window(_) => {} } } - if is_fixed && !is_offset_parent_an_element { - set_left_rtl_scrollbar_offset(&mut offsets); + // If the scrollbar appears on the left (e.g. RTL systems). + // Use Firefox with layout.scrollbar.side = 3 in about:config to test this. + if !is_offset_parent_an_element { + offsets.x = get_window_scroll_bar_x(&document_element, None); } let html_offset = if !is_offset_parent_an_element && !is_fixed { diff --git a/packages/dom/src/utils/get_viewport_rect.rs b/packages/dom/src/utils/get_viewport_rect.rs index fc284de..43ac1e5 100644 --- a/packages/dom/src/utils/get_viewport_rect.rs +++ b/packages/dom/src/utils/get_viewport_rect.rs @@ -10,7 +10,18 @@ use crate::utils::get_window_scroll_bar_x::get_window_scroll_bar_x; // Most scrollbars leave 15-18px of space. const SCROLLBAR_MAX: f64 = 25.0; -pub fn get_viewport_rect(element: &Element, strategy: Strategy) -> Rect { +#[derive(Clone, Debug, PartialEq)] +pub enum ViewportRootBoundary { + Viewport, + LayoutViewport, +} + +pub fn get_viewport_rect( + element: &Element, + strategy: Strategy, + root_boundary: ViewportRootBoundary, +) -> Rect { + let is_layout_viewport = root_boundary == ViewportRootBoundary::LayoutViewport; let window = get_window(Some(element)); let html = get_document_element(Some(element.into())); let visual_viewport = window.visual_viewport(); @@ -21,19 +32,34 @@ pub fn get_viewport_rect(element: &Element, strategy: Strategy) -> Rect { let mut height = html.client_height() as f64; if let Some(visual_viewport) = visual_viewport { - width = visual_viewport.width(); - height = visual_viewport.height(); + // Client coordinates are relative to the layout viewport, except in WebKit with an `absolute` strategy, + // where they are relative to the visual viewport. + let layout_relative_client_coords = !is_web_kit() || strategy == Strategy::Fixed; + + if is_layout_viewport { + if !layout_relative_client_coords { + x = -visual_viewport.offset_left(); + y = -visual_viewport.offset_top(); + } + } else { + width = visual_viewport.width(); + height = visual_viewport.height(); - let visual_viewport_based = is_web_kit(); - if !visual_viewport_based || strategy == Strategy::Fixed { - x = visual_viewport.offset_left(); - y = visual_viewport.offset_top(); + if layout_relative_client_coords { + x = visual_viewport.offset_left(); + y = visual_viewport.offset_top(); + } } } let window_scrollbar_x = get_window_scroll_bar_x(&html, None); - // `overflow: hidden` + `scrollbar-gutter: stable` reduces the visual width of the , - // but this is not considered in the size of `html.client_width`. + // `scrollbar-gutter: stable` on the reserves gutter space that shrinks + // the visual width but isn't reflected in `html.clientWidth`, so subtract it. + // Only the inline-end (right) gutter can hold the scrollbar; `both-edges` also + // reserves an empty inline-start gutter that clips nothing, so exclude just + // the one scrollbar-side gutter — halve the measured (two-gutter) total. A + // left-side scrollbar (`window_scroll_bar_x > 0`) is already handled by + // `get_html_offset`/`visual_viewport.width`; skip it here. if window_scrollbar_x <= 0.0 { let doc = html .owner_document() @@ -54,17 +80,23 @@ pub fn get_viewport_rect(element: &Element, strategy: Strategy) -> Rect { } else { 0.0 }; - let clipping_stable_scrollbar_width = + let reserved_width = ((html.client_width() as f64) - (body.client_width() as f64) - body_margin_inline) .abs(); + let gutter = if get_computed_style(&html) + .get_property_value("scrollbar-gutter") + .ok() + .as_deref() + == Some("stable both-edges") + { + reserved_width / 2.0 + } else { + reserved_width + }; - if clipping_stable_scrollbar_width <= SCROLLBAR_MAX { - width -= clipping_stable_scrollbar_width; + if gutter <= SCROLLBAR_MAX { + width -= gutter; } - } else if window_scrollbar_x <= SCROLLBAR_MAX { - // If the scrollbar is on the left, the width needs to be extended - // by the scrollbar amount so there isn't extra space on the right. - width += window_scrollbar_x; } Rect { diff --git a/packages/dom/src/utils/get_visual_offsets.rs b/packages/dom/src/utils/get_visual_offsets.rs index 6907f69..9168ed9 100644 --- a/packages/dom/src/utils/get_visual_offsets.rs +++ b/packages/dom/src/utils/get_visual_offsets.rs @@ -1,24 +1,22 @@ use floating_ui_utils::{ Coords, - dom::{DomElementOrWindow, get_window}, + dom::{DomElementOrWindow, get_window, is_web_kit}, }; use web_sys::Element; -pub fn get_visual_offsets(_element: Option<&Element>) -> Coords { - // TODO: web-sys does not support VisualViewport +pub fn get_visual_offsets(element: Option<&Element>) -> Coords { + let window = get_window(element.map(|element| element.as_ref())); - // let window = get_window(element.map(|element| element.as_ref())); - - // if !is_web_kit() || !window.visual_viewport { - // Coords::new(0.0) - // } else { - // Coords { - // x: todo!(), - // y: todo!(), - // } - // } - - Coords::new(0.0) + if is_web_kit() + && let Some(visual_viewport) = window.visual_viewport() + { + Coords { + x: visual_viewport.offset_left(), + y: visual_viewport.offset_top(), + } + } else { + Coords::new(0.0) + } } pub fn should_add_visual_offsets( @@ -28,13 +26,8 @@ pub fn should_add_visual_offsets( ) -> bool { match floating_offset_parent { Some(DomElementOrWindow::Window(floating_offset_parent)) => { - if is_fixed - && *floating_offset_parent != get_window(element.map(|element| element.as_ref())) - { - false - } else { - is_fixed - } + is_fixed + && *floating_offset_parent == get_window(element.map(|element| element.as_ref())) } _ => false, } diff --git a/packages/leptos/tests/playwright.rs b/packages/leptos/tests/playwright.rs index 9fc3dd8..83b420d 100644 --- a/packages/leptos/tests/playwright.rs +++ b/packages/leptos/tests/playwright.rs @@ -7,10 +7,12 @@ const IMPLEMENTED_TESTS: [&str; 19] = [ "border", "containing-block", "decimal-size", + // "fixed-clipping", "flip", "hide", // "iframe", "inline", + // "layout-viewport", "offset", "placement", "relative", @@ -23,6 +25,7 @@ const IMPLEMENTED_TESTS: [&str; 19] = [ // "top-layer", "transform", "virtual-element", + // "viewport-boundary", ]; #[test] diff --git a/packages/leptos/tests/visual/src/app.rs b/packages/leptos/tests/visual/src/app.rs index 267c23e..653db90 100644 --- a/packages/leptos/tests/visual/src/app.rs +++ b/packages/leptos/tests/visual/src/app.rs @@ -4,7 +4,7 @@ use leptos_router::path; use crate::spec::arrow::Arrow; use crate::spec::auto_placement::AutoPlacement; -use crate::spec::auto_update::AutoUpdate; +use crate::spec::auto_update::{AutoUpdate, AutoUpdateRootResize}; use crate::spec::border::Border; use crate::spec::containing_block::ContainingBlock; use crate::spec::decimal_size::DecimalSize; @@ -23,7 +23,7 @@ use crate::spec::transform::Transform; use crate::spec::virtual_element::VirtualElement; use crate::utils::new::New; -const ROUTES: [&str; 23] = [ +const ROUTES: [&str; 27] = [ "placement", "relative", "transform", @@ -41,12 +41,16 @@ const ROUTES: [&str; 23] = [ "autoPlacement", "inline", "autoUpdate", + "autoUpdate-root-resize", "shadow-DOM", "containing-block", + "fixed-clipping", "virtual-element", "perf", "iframe", "top-layer", + "layout-viewport", + "viewport-boundary", ]; #[component] @@ -122,12 +126,16 @@ pub fn App() -> impl IntoView { + // + // // // // + // + // diff --git a/packages/leptos/tests/visual/src/spec/auto_update.rs b/packages/leptos/tests/visual/src/spec/auto_update.rs index f057757..d18130b 100644 --- a/packages/leptos/tests/visual/src/spec/auto_update.rs +++ b/packages/leptos/tests/visual/src/spec/auto_update.rs @@ -5,7 +5,8 @@ use floating_ui_leptos::{ use leptos::prelude::*; use leptos_node_ref::AnyNodeRef; use send_wrapper::SendWrapper; -use wasm_bindgen::JsCast; +use wasm_bindgen::{JsCast, closure::Closure}; +use web_sys::{HtmlElement, window}; #[derive(Copy, Clone, Debug, PartialEq)] enum LayoutShift { @@ -259,6 +260,34 @@ pub fn AutoUpdate() -> impl IntoView { } /> +

animationFrame

@@ -308,3 +337,99 @@ pub fn AutoUpdate() -> impl IntoView { } } + +#[component] +pub fn AutoUpdateRootResize() -> impl IntoView { + let reference_ref = AnyNodeRef::new(); + let floating_ref = AnyNodeRef::new(); + + let (moved, set_moved) = signal(false); + + let UseFloatingReturn { + x, + y, + strategy, + update, + .. + } = use_floating( + reference_ref, + floating_ref, + UseFloatingOptions::default().strategy(Strategy::Fixed), + ); + + type CleanupFn = Box; + let cleanup: StoredValue>> = StoredValue::new(None); + + Effect::new({ + let update = update.clone(); + + move |_| { + if let Some(reference) = reference_ref.get() + && let Some(floating) = floating_ref.get() + { + if let Some(cleanup) = &*cleanup.read_value() { + cleanup(); + } + + // Match React test behaviour by moving the size change from style attributes to here. + // The style attributes update after this effect, so `auto_update` would not use the correct size. + let style = reference.unchecked_ref::().style(); + + style + .set_property( + "width", + if moved.get() { + "650px" + } else { + "calc(100vw - 220px" + }, + ) + .expect("Style should be updated."); + + cleanup.set_value(Some(SendWrapper::new(auto_update( + (&reference).into(), + Some(&floating), + (*update).clone(), + AutoUpdateOptions::default() + .ancestor_resize(false) + .element_resize(false) + .layout_shift(false), + )))); + } + } + }); + + on_cleanup(move || { + if let Some(cleanup) = &*cleanup.read_value() { + cleanup(); + } + }); + + view! { +

AutoUpdate Root Resize

+ +
+ Floating +
+ } +} diff --git a/packages/leptos/tests/visual/src/spec/inline.rs b/packages/leptos/tests/visual/src/spec/inline.rs index 1046c56..ad212a9 100644 --- a/packages/leptos/tests/visual/src/spec/inline.rs +++ b/packages/leptos/tests/visual/src/spec/inline.rs @@ -1,4 +1,4 @@ -use std::{rc::Rc, time::Duration}; +use std::{collections::HashMap, rc::Rc, sync::LazyLock, time::Duration}; use convert_case::{Case, Casing}; use floating_ui_leptos::{ @@ -15,7 +15,7 @@ use send_wrapper::SendWrapper; use crate::utils::all_placements::ALL_PLACEMENTS; -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] enum ConnectedStatus { One, TwoDisjoined, @@ -23,6 +23,29 @@ enum ConnectedStatus { Three, } +// Hebrew (RTL script) copy used to exercise right-to-left line wrapping. In a +// disjoined wrap the two line fragments are ordered opposite to LTR: the top +// fragment sits to the *left* of the bottom fragment. +const RTL_BEFORE: &str = "לורם איפסום דולור סיט אמט קונסקטטור אדיפיסינג עלית סד דו איואיסמוד "; +const RTL_AFTER: &str = " אוט אאו מגנה אאו אאוגה אפיקיטור ביבנדום איד קומודו טלוס נולם גרבידה מי נק סודלס טינסידונט לורם אורסי אליקום אקס איד קומודו אראט ליברו אוט ריסוס נאם מולסטיה נון לקטוס סיט אמט טמפוס"; +static RTL_TEXT: LazyLock> = LazyLock::new(|| { + HashMap::from([ + (ConnectedStatus::One, "בדיקה"), + ( + ConnectedStatus::TwoDisjoined, + "נולה רוטרום דפיבוס טורפיס אאו וולוטפאט", + ), + ( + ConnectedStatus::TwoJoined, + "נולה רוטרום דפיבוס טורפיס אאו וולוטפאט דואיס קורסוס ניסי מאסה נון דיקטום", + ), + ( + ConnectedStatus::Three, + "נולה רוטרום דפיבוס טורפיס אאו וולוטפאט דואיס קורסוס ניסי מאסה נון דיקטום טורפיס אינטרדום אט נולה רוטרום דפיבוס טורפיס אאו וולוטפאט", + ), + ]) +}); + #[component] pub fn Inline() -> impl IntoView { let reference_ref = AnyNodeRef::new(); @@ -30,6 +53,7 @@ pub fn Inline() -> impl IntoView { let (placement, set_placement) = signal(Placement::Bottom); let (status, set_status) = signal(ConnectedStatus::TwoDisjoined); + let (rtl, set_rtl) = signal(false); let (open, set_open) = signal(false); let (mouse_coords, set_mouse_coords) = signal::>(None); @@ -164,21 +188,39 @@ pub fn Inline() -> impl IntoView {

Inline

The floating element should choose the most appropriate rect.

-

- "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - - {text} - ". Ut eu magna eu augue efficitur bibendum id commodo tellus. Nullam - gravida, mi nec sodales tincidunt, lorem orci aliquam ex, id commodo - erat libero ut risus. Nam molestie non lectus sit amet tempus. Vivamus - accumsan " - "nunc quis faucibus egestas"". " - "Duis cursus nisi massa, non dictum turpis interdum at." +

+ + {RTL_BEFORE} + + {RTL_TEXT.get(&status.get()).map(ToOwned::to_owned).unwrap_or_default()} + + {RTL_AFTER} + + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + {text} + ". Ut eu magna eu augue efficitur bibendum id commodo tellus. Nullam + gravida, mi nec sodales tincidunt, lorem orci aliquam ex, id commodo + erat libero ut risus. Nam molestie non lectus sit amet tempus. Vivamus + accumsan " + "nunc quis faucibus egestas"". " + "Duis cursus nisi massa, non dictum turpis interdum at." +

@@ -267,5 +309,26 @@ pub fn Inline() -> impl IntoView { } />
+ +

RTL

+
+ + {format!("{value}")} + + } + /> +
} } diff --git a/packages/utils/src/lib.rs b/packages/utils/src/lib.rs index 0e04d3d..ca82d44 100644 --- a/packages/utils/src/lib.rs +++ b/packages/utils/src/lib.rs @@ -341,6 +341,21 @@ cfg_if::cfg_if! { } } + impl From<&web_sys::DomRect> for ClientRectObject { + fn from(value: &web_sys::DomRect) -> Self { + Self { + x: value.x(), + y: value.y(), + width: value.width(), + height: value.height(), + top: value.top(), + right: value.right(), + bottom: value.bottom(), + left: value.left(), + } + } + } + impl From for ClientRectObject { fn from(value: web_sys::DomRect) -> Self { Self { diff --git a/upstream.toml b/upstream.toml index 9664e46..5f2812d 100644 --- a/upstream.toml +++ b/upstream.toml @@ -1,5 +1,5 @@ [releases] core = "1.8.0" -dom = "1.7.6" +dom = "1.8.0" utils = "0.2.12" vue = "2.0.0"