diff --git a/packages/leptos/Cargo.toml b/packages/leptos/Cargo.toml index 87d16a3..ce0ca6f 100644 --- a/packages/leptos/Cargo.toml +++ b/packages/leptos/Cargo.toml @@ -9,6 +9,9 @@ repository.workspace = true version.workspace = true [dependencies] +cfg-if = "1.0.0" +# TODO: remove +log = "0.4.21" floating-ui-dom = { path = "../dom", version = "0.0.2" } leptos = { version = "0.6.9", features = ["nightly"] } web-sys.workspace = true diff --git a/packages/leptos/example/Cargo.toml b/packages/leptos/example/Cargo.toml new file mode 100644 index 0000000..a41973b --- /dev/null +++ b/packages/leptos/example/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "floating-ui-leptos-example" +description = "Example for Floating UI Leptos." +publish = false + +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[dependencies] +console_log = "1.0.0" +console_error_panic_hook = "0.1.7" +leptos = { version = "0.6.9", features = ["csr", "nightly"] } +log = "0.4.21" +floating-ui-leptos = { path = ".." } +wasm-bindgen.workspace = true +web-sys.workspace = true diff --git a/packages/leptos/example/index.html b/packages/leptos/example/index.html new file mode 100644 index 0000000..7f2ad66 --- /dev/null +++ b/packages/leptos/example/index.html @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/leptos/example/src/app.rs b/packages/leptos/example/src/app.rs new file mode 100644 index 0000000..a90a07b --- /dev/null +++ b/packages/leptos/example/src/app.rs @@ -0,0 +1,22 @@ +use floating_ui_leptos::{use_floating, UseFloatingOptions, UseFloatingReturn}; +use leptos::{ + html::{Div, Span}, + *, +}; + +#[component] +pub fn App() -> impl IntoView { + let reference = create_node_ref::(); + let floating = create_node_ref::
(); + + let UseFloatingReturn { + floating_styles, .. + } = use_floating(reference, floating, UseFloatingOptions::default().into()); + + view! { + Reference +
+ Floating +
+ } +} diff --git a/packages/leptos/example/src/main.rs b/packages/leptos/example/src/main.rs new file mode 100644 index 0000000..5b8c173 --- /dev/null +++ b/packages/leptos/example/src/main.rs @@ -0,0 +1,13 @@ +// TODO: remove after leptos is fixed +#![allow(clippy::empty_docs)] + +mod app; + +use crate::app::App; + +pub fn main() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + + leptos::mount_to_body(App); +} diff --git a/packages/leptos/src/lib.rs b/packages/leptos/src/lib.rs index b731a03..45e352b 100644 --- a/packages/leptos/src/lib.rs +++ b/packages/leptos/src/lib.rs @@ -1,3 +1,6 @@ +mod types; mod use_floating; +mod utils; +pub use types::*; pub use use_floating::*; diff --git a/packages/leptos/src/types.rs b/packages/leptos/src/types.rs new file mode 100644 index 0000000..e7b6d70 --- /dev/null +++ b/packages/leptos/src/types.rs @@ -0,0 +1,97 @@ +use floating_ui_dom::{Middleware, MiddlewareData, Placement, Strategy}; +use leptos::{Attribute, IntoAttribute, Signal}; +use web_sys::{Element, Window}; + +/// Options for [`use_floating`]. +#[derive(Clone, Default)] +pub struct UseFloatingOptions<'a> { + /// Represents the open/close state of the floating element. + /// + /// Defaults to `true`. + pub open: Option, + + /// Where to place the floating element relative to the reference element. + /// + /// Defaults to [`Placement::Bottom`]. + pub placement: Option, + + /// The strategy to use when positioning the floating element. + /// + /// Defaults to [`Strategy::Absolute`]. + pub strategy: Option, + + /// Array of middleware objects to modify the positioning or provide data for rendering. + /// + /// Defaults to an empty vector. + pub middleware: Option>>, + + /// Whether to use `transform` for positioning instead of `top` and `left` in the `floatingStyles` object. + /// + /// Defaults to `true`. + pub transform: Option, + + /// Callback to handle mounting/unmounting of the elements. + /// + ///Detauls to [`Option::None`]. + pub while_elements_mounted: Option, // TODO: type +} + +#[derive(Clone, Debug, PartialEq)] +pub struct FloatingStyles { + pub position: Strategy, + pub top: String, + pub left: String, + pub transform: Option, + pub will_change: Option, +} + +impl IntoAttribute for FloatingStyles { + fn into_attribute(self) -> Attribute { + Attribute::String( + format!( + "position: {:?}; top: {}; left: {};{}{}", + self.position, + self.top, + self.left, + self.transform + .map_or("".into(), |transform| format!(" transform: {};", transform),), + self.will_change.map_or("".into(), |will_change| format!( + " will-change: {};", + will_change + )) + ) + .into(), + ) + } + + fn into_attribute_boxed(self: Box) -> Attribute { + self.into_attribute() + } +} + +#[derive(Debug)] +pub struct UseFloatingReturn { + /// The x-coord of the floating element. + pub x: Signal, + + /// The y-coord of the floating element. + pub y: Signal, + + /// The stateful placement, which can be different from the initial `placement` passed as options. + pub placement: Signal, + + /// The strategy to use when positioning the floating element. + pub strategy: Signal, + + /// Additional data from middleware. + pub middleware_data: Signal, + + /// Indicates if the floating element has been positioned. + pub is_positioned: Signal, + + /// CSS styles to apply to the floating element to position it. + pub floating_styles: Signal, + + /// The function to update floating position manually. + pub update: bool, // TODO: type +} diff --git a/packages/leptos/src/use_floating.rs b/packages/leptos/src/use_floating.rs index 53b5cf7..a75ef55 100644 --- a/packages/leptos/src/use_floating.rs +++ b/packages/leptos/src/use_floating.rs @@ -1,60 +1,137 @@ -use floating_ui_dom::{Middleware, MiddlewareData, Placement, Strategy}; -use leptos::{create_signal, MaybeSignal}; -use web_sys::{Element, Window}; - -/// Options for [`use_floating`]. -#[derive(Clone, Default)] -pub struct UseFloatingOptions<'a> { - /// Where to place the floating element relative to the reference element. - /// - /// Defaults to [`Placement::Bottom`]. - pub placement: Option, - - /// The strategy to use when positioning the floating element. - /// - /// Defaults to [`Strategy::Absolute`]. - pub strategy: Option, - - /// Array of middleware objects to modify the positioning or provide data for rendering. - /// - /// Defaults to an empty vector. - pub middleware: Option>>, -} +use std::ops::Deref; -/// Data stored by [`use_floating`]. -pub struct UseFloatingData { - pub x: f64, - pub y: f64, - pub strategy: Strategy, - pub placement: Placement, - pub middleware_data: MiddlewareData, - pub is_positioned: bool, -} +use floating_ui_dom::{ + compute_position, ComputePositionConfig, MiddlewareData, Placement, Strategy, +}; +use leptos::{create_memo, create_signal, html::ElementDescriptor, MaybeSignal, NodeRef, Signal}; +use log::info; + +use crate::{ + types::{FloatingStyles, UseFloatingOptions, UseFloatingReturn}, + utils::{get_dpr::get_dpr, round_by_dpr::round_by_dpr}, +}; + +pub fn use_floating( + reference: NodeRef, + floating: NodeRef, + options: MaybeSignal, +) -> UseFloatingReturn +where + Reference: ElementDescriptor + Deref + Clone + 'static, + ReferenceEl: Deref, + Floating: ElementDescriptor + Deref + Clone + 'static, + FloatingEl: Deref, +{ + let options = Signal::derive(options); + + let open_option = move || options().open.unwrap_or(true); + let placement_option = move || options().placement.unwrap_or(Placement::Bottom); + let strategy_option = move || options().strategy.unwrap_or(Strategy::Absolute); + let middleware_option = move || options().middleware; + let transform_option = move || options().transform.unwrap_or(true); + + let (x, set_x) = create_signal(0.0); + let (y, set_y) = create_signal(0.0); + let (strategy, set_strategy) = create_signal(strategy_option()); + let (placement, set_placement) = create_signal(placement_option()); + let (middleware_data, set_middleware_data) = create_signal(MiddlewareData::default()); + let (is_positioned, set_is_positioned) = create_signal(false); + let floating_styles = create_memo(move |_| { + let initial_styles = FloatingStyles { + position: strategy(), + top: "0".into(), + left: "0".into(), + transform: None, + will_change: None, + }; + + info!("floating styles memo"); + + if let Some(floating_element) = floating.get() { + info!("floating element exists"); -pub fn use_floating(options: MaybeSignal) { - // let placement = create_memo(move |_| { - // options.with(|options| options.placement.unwrap_or(Placement::Bottom)) - // }); - - // let strategy = create_memo(move |_| { - // options.with(|options| options.strategy.unwrap_or(Strategy::Absolute)) - // }); - - let (_data, _set_data) = create_signal(UseFloatingData { - x: 0.0, - y: 0.0, - strategy: options().strategy.unwrap_or(Strategy::Absolute), - placement: options().placement.unwrap_or(Placement::Bottom), - middleware_data: MiddlewareData::default(), - is_positioned: false, + let x_val = round_by_dpr(&floating_element, x()); + let y_val = round_by_dpr(&floating_element, y()); + + if transform_option() { + FloatingStyles { + transform: Some(format!("translate({x_val}px, {y_val}px)")), + will_change: match get_dpr(&floating_element) >= 1.5 { + true => Some("transform".into()), + false => None, + }, + ..initial_styles + } + } else { + FloatingStyles { + left: format!("{x_val}px"), + top: format!("{y_val}px"), + ..initial_styles + } + } + } else { + initial_styles + } }); - // let (_latest_middleware, _set_latest_middleware) = create_signal(options.middleware); + let update = move || { + info!("update"); + if let Some(reference_element) = reference.get() { + info!("ref"); + if let Some(floating_element) = floating.get() { + info!("float"); + let config = ComputePositionConfig { + placement: Some(placement_option()), + strategy: Some(strategy_option()), + middleware: middleware_option(), + }; + + let position = + compute_position(&reference_element, &floating_element, Some(config)); + set_x(position.x); + set_y(position.x); + set_strategy(position.strategy); + set_placement(position.placement); + set_middleware_data(position.middleware_data); + set_is_positioned(true); + } + } + }; + + let cleanup = move || { + // TODO + }; + + let attach = move || { + cleanup(); + + // TODO: the rest of the function + update(); + }; + + let reset = move || { + if !open_option() { + set_is_positioned(false); + } + }; + + // TODO: call attach/reset - // // TODO: compare latest_middleware and options.middleware and update it + create_memo(move |_| { + info!("{} {}", reference.get().is_some(), floating.get().is_some()); - // let (_reference, _set_reference) = create_signal::>(None); - // let (_floating, _set_floating) = create_signal::>(None); + info!("memo"); + attach() + })(); - // // TODO: setReference and setFloating + UseFloatingReturn { + x: x.into(), + y: y.into(), + placement: placement.into(), + strategy: strategy.into(), + middleware_data: middleware_data.into(), + is_positioned: is_positioned.into(), + floating_styles: floating_styles.into(), + update: false, // TODO + } } diff --git a/packages/leptos/src/utils.rs b/packages/leptos/src/utils.rs new file mode 100644 index 0000000..002bd6d --- /dev/null +++ b/packages/leptos/src/utils.rs @@ -0,0 +1,2 @@ +pub mod get_dpr; +pub mod round_by_dpr; diff --git a/packages/leptos/src/utils/get_dpr.rs b/packages/leptos/src/utils/get_dpr.rs new file mode 100644 index 0000000..d5c0b3e --- /dev/null +++ b/packages/leptos/src/utils/get_dpr.rs @@ -0,0 +1,6 @@ +use floating_ui_dom::dom::get_window; +use web_sys::Element; + +pub fn get_dpr(element: &Element) -> f64 { + get_window(Some(element)).device_pixel_ratio() +} diff --git a/packages/leptos/src/utils/round_by_dpr.rs b/packages/leptos/src/utils/round_by_dpr.rs new file mode 100644 index 0000000..5109836 --- /dev/null +++ b/packages/leptos/src/utils/round_by_dpr.rs @@ -0,0 +1,8 @@ +use web_sys::Element; + +use crate::utils::get_dpr::get_dpr; + +pub fn round_by_dpr(element: &Element, value: f64) -> f64 { + let dpr = get_dpr(element); + (value * dpr).round() / dpr +} diff --git a/packages/utils/Cargo.toml b/packages/utils/Cargo.toml index cb5d3c6..9f2b730 100644 --- a/packages/utils/Cargo.toml +++ b/packages/utils/Cargo.toml @@ -15,3 +15,6 @@ web-sys = { workspace = true, optional = true } [features] default = [] dom = ["dep:web-sys"] + +[package.metadata.docs.rs] +all-features = true diff --git a/packages/utils/src/dom.rs b/packages/utils/src/dom.rs index 5783eb2..933f6ec 100644 --- a/packages/utils/src/dom.rs +++ b/packages/utils/src/dom.rs @@ -1,3 +1,5 @@ +//! Utility functions for the DOM. Requires `dom` feature. + use web_sys::{ css, wasm_bindgen::JsCast, window, CssStyleDeclaration, Document, Element, HtmlElement, Node, ShadowRoot, Window,