,
11 | ) -> impl IntoView
12 | where
13 | F: Fn() -> IV + 'static,
14 | IV: IntoView + 'static,
15 | {
16 | view! {
17 |
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/book/theme/trunk.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Change active file of files.
3 | *
4 | * @param {Element} container
5 | * @param {string | null} name
6 | */
7 | const changeTrunkFile = (container, name) => {
8 | for (const child of container.children) {
9 | if (!(child instanceof HTMLElement)) {
10 | continue;
11 | }
12 |
13 | if (child.classList.contains('mdbook-trunk-files')) {
14 | for (const file of child.children) {
15 | if (!(file instanceof HTMLElement)) {
16 | continue;
17 | }
18 |
19 | if (file.dataset.file === name) {
20 | file.classList.add('active');
21 | } else {
22 | file.classList.remove('active');
23 | }
24 | }
25 | } else if (child.classList.contains('mdbook-trunk-file-content')) {
26 | if (child.dataset.file === name) {
27 | child.classList.remove('hidden');
28 | } else {
29 | child.classList.add('hidden');
30 | }
31 | }
32 | }
33 | };
34 |
35 | document.addEventListener('DOMContentLoaded', () => {
36 | const files = document.querySelectorAll('.mdbook-trunk-file');
37 | for (const file of files) {
38 | file.addEventListener('click', () => {
39 | if (!(file instanceof HTMLElement)) {
40 | return;
41 | }
42 |
43 | if (!file.parentElement || !file.parentElement.parentElement) {
44 | return;
45 | }
46 |
47 | const container = file.parentElement.parentElement;
48 | const name = file.dataset.file;
49 |
50 | changeTrunkFile(container, file.classList.contains('active') ? null : name);
51 | });
52 | }
53 | });
54 |
--------------------------------------------------------------------------------
/packages/leptos/tests/visual/src/utils/use_resize.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | rc::Rc,
3 | sync::{Arc, Mutex},
4 | };
5 |
6 | use leptos::{html::Div, prelude::*};
7 | use send_wrapper::SendWrapper;
8 | use wasm_bindgen::{JsCast, prelude::Closure};
9 | use web_sys::{ResizeObserver, ResizeObserverEntry};
10 |
11 | pub fn use_resize(node_ref: NodeRef, update: SendWrapper>) {
12 | type CleanupFn = dyn Fn();
13 | let cleanup: Arc>>>> = Arc::new(Mutex::new(None));
14 |
15 | Effect::new({
16 | let cleanup = cleanup.clone();
17 |
18 | move |_| {
19 | if let Some(cleanup) = cleanup.lock().expect("Lock should be acquired.").as_ref() {
20 | cleanup();
21 | }
22 |
23 | if let Some(element) = node_ref.get() {
24 | let resize_closure: Closure)> = Closure::new({
25 | let update = update.clone();
26 |
27 | move |_entries: Vec| {
28 | update();
29 | }
30 | });
31 |
32 | let observer = ResizeObserver::new(resize_closure.into_js_value().unchecked_ref())
33 | .expect("Resize observer should be created.");
34 |
35 | observer.observe(&element);
36 |
37 | *cleanup.lock().expect("Lock should be acquired.") =
38 | Some(SendWrapper::new(Box::new(move || {
39 | observer.unobserve(&element);
40 | })));
41 | }
42 | }
43 | });
44 |
45 | on_cleanup(move || {
46 | if let Some(cleanup) = cleanup.lock().expect("Lock should be acquired.").as_ref() {
47 | cleanup();
48 | }
49 | });
50 | }
51 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | "book-examples",
4 | "packages/*",
5 | "packages/*/example",
6 | "packages/*/tests/*",
7 | "scripts",
8 | ]
9 | resolver = "2"
10 |
11 | [workspace.package]
12 | authors = ["Rust for Web "]
13 | edition = "2024"
14 | license = "MIT"
15 | repository = "https://github.com/RustForWeb/floating-ui"
16 | version = "0.6.0"
17 |
18 | [workspace.dependencies]
19 | cfg-if = "1.0.0"
20 | console_error_panic_hook = "0.1.7"
21 | console_log = "1.0.0"
22 | dioxus = "0.7.0"
23 | dyn_derive = "0.3.4"
24 | dyn_std = "0.3.3"
25 | floating-ui-core = { path = "./packages/core", version = "0.6.0" }
26 | floating-ui-dioxus = { path = "./packages/dioxus", version = "0.6.0" }
27 | floating-ui-dom = { path = "./packages/dom", version = "0.6.0" }
28 | floating-ui-leptos = { path = "./packages/leptos", version = "0.6.0" }
29 | floating-ui-utils = { path = "./packages/utils", version = "0.6.0" }
30 | floating-ui-yew = { path = "./packages/yew", version = "0.6.0" }
31 | leptos = "0.8.0"
32 | leptos-node-ref = "0.2.0"
33 | leptos_router = "0.8.0"
34 | log = "0.4.22"
35 | send_wrapper = "0.6.0"
36 | serde = { version = "1.0.209", features = ["derive"] }
37 | serde_json = "1.0.127"
38 | wasm-bindgen = "0.2.93"
39 | wasm-bindgen-test = "0.3.43"
40 | yew = "0.22.0"
41 | yew-router = "0.19.0"
42 |
43 | [workspace.dependencies.web-sys]
44 | version = "0.3.70"
45 | features = [
46 | "css",
47 | "AddEventListenerOptions",
48 | "CssStyleDeclaration",
49 | "Document",
50 | "DomRect",
51 | "DomRectList",
52 | "Element",
53 | "Event",
54 | "EventTarget",
55 | "HtmlElement",
56 | "HtmlSlotElement",
57 | "IntersectionObserver",
58 | "IntersectionObserverEntry",
59 | "IntersectionObserverInit",
60 | "Node",
61 | "Range",
62 | "ResizeObserver",
63 | "ResizeObserverEntry",
64 | "Selection",
65 | "ShadowRoot",
66 | "VisualViewport",
67 | "Window",
68 | ]
69 |
--------------------------------------------------------------------------------
/book-examples/src/positioning/size.rs:
--------------------------------------------------------------------------------
1 | use floating_ui_leptos::{
2 | DetectOverflowOptions, MiddlewareVec, Offset, OffsetOptions, Padding, RootBoundary, Size,
3 | SizeOptions,
4 | };
5 | use leptos::prelude::*;
6 | use send_wrapper::SendWrapper;
7 |
8 | use crate::components::{Chrome, Floating, GridItem, Reference, Scrollable};
9 |
10 | #[component]
11 | pub fn SizeDemo() -> impl IntoView {
12 | view! {
13 |
23 |
39 | Dropdown
40 |
41 | }
42 | reference=move |node_ref| view! {
43 |
44 | }
45 | />
46 |
47 | }
48 | />
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/packages/leptos/tests/visual/src/spec/virtual_element.rs:
--------------------------------------------------------------------------------
1 | use std::rc::Rc;
2 |
3 | use floating_ui_leptos::{
4 | DefaultVirtualElement, Strategy, UseFloatingOptions, UseFloatingReturn, VirtualElement,
5 | use_floating,
6 | };
7 | use leptos::prelude::*;
8 | use leptos_node_ref::AnyNodeRef;
9 |
10 | use crate::utils::use_scroll::{UseScrollOptions, UseScrollReturn, use_scroll};
11 |
12 | #[component]
13 | pub fn VirtualElement() -> impl IntoView {
14 | let reference_ref = AnyNodeRef::new();
15 | let floating_ref = AnyNodeRef::new();
16 | let virtual_element = MaybeProp::derive(move || {
17 | let context_element = reference_ref.get();
18 | context_element.map(|context_element| {
19 | let element: &web_sys::Element = context_element.as_ref();
20 | (Box::new(
21 | DefaultVirtualElement::new(Rc::new({
22 | let context_element = context_element.clone();
23 |
24 | move || context_element.get_bounding_client_rect().into()
25 | }))
26 | .context_element(element.clone()),
27 | ) as Box>)
28 | .into()
29 | })
30 | });
31 |
32 | let UseFloatingReturn {
33 | x,
34 | y,
35 | strategy,
36 | update,
37 | ..
38 | } = use_floating(
39 | virtual_element,
40 | floating_ref,
41 | UseFloatingOptions::default()
42 | .strategy(Strategy::Fixed)
43 | .while_elements_mounted_auto_update(),
44 | );
45 |
46 | let UseScrollReturn { scroll_ref, .. } = use_scroll(UseScrollOptions {
47 | reference_ref,
48 | floating_ref,
49 | update,
50 | rtl: None::.into(),
51 | disable_ref_updates: None,
52 | });
53 |
54 | view! {
55 | Virtual Element
56 |
57 |
64 |
65 |
72 | Floating
73 |
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/packages/yew/src/arrow.rs:
--------------------------------------------------------------------------------
1 | use floating_ui_dom::{
2 | ARROW_NAME, Arrow as CoreArrow, ArrowOptions as CoreArrowOptions, Middleware, MiddlewareReturn,
3 | MiddlewareState, Padding,
4 | };
5 | use web_sys::wasm_bindgen::JsCast;
6 | use yew::NodeRef;
7 |
8 | /// Options for [`Arrow`].
9 | #[derive(Clone, PartialEq)]
10 | pub struct ArrowOptions {
11 | /// The arrow element to be positioned.
12 | pub element: NodeRef,
13 |
14 | /// The padding between the arrow element and the floating element edges.
15 | /// Useful when the floating element has rounded corners.
16 | ///
17 | /// Defaults to `0` on all sides.
18 | pub padding: Option,
19 | }
20 |
21 | impl ArrowOptions {
22 | pub fn new(element: NodeRef) -> Self {
23 | ArrowOptions {
24 | element,
25 | padding: None,
26 | }
27 | }
28 |
29 | /// Set `element` option.
30 | pub fn element(mut self, value: NodeRef) -> Self {
31 | self.element = value;
32 | self
33 | }
34 |
35 | /// Set `padding` option.
36 | pub fn padding(mut self, value: Padding) -> Self {
37 | self.padding = Some(value);
38 | self
39 | }
40 | }
41 |
42 | /// Arrow middleware.
43 | ///
44 | /// Provides data to position an inner element of the floating element so that it appears centered to the reference element.
45 | ///
46 | /// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/arrow.html) for more documentation.
47 | #[derive(Clone, PartialEq)]
48 | pub struct Arrow {
49 | options: ArrowOptions,
50 | }
51 |
52 | impl Arrow {
53 | pub fn new(options: ArrowOptions) -> Self {
54 | Arrow { options }
55 | }
56 | }
57 |
58 | impl Middleware for Arrow {
59 | fn name(&self) -> &'static str {
60 | ARROW_NAME
61 | }
62 |
63 | fn compute(
64 | &self,
65 | state: MiddlewareState,
66 | ) -> MiddlewareReturn {
67 | match self.options.element.get() {
68 | Some(element) => CoreArrow::new(CoreArrowOptions {
69 | element: element
70 | .dyn_into()
71 | .expect("Arrow element should be an Element."),
72 | padding: self.options.padding.clone(),
73 | })
74 | .compute(state),
75 | _ => MiddlewareReturn {
76 | x: None,
77 | y: None,
78 | data: None,
79 | reset: None,
80 | },
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/packages/dioxus/src/arrow.rs:
--------------------------------------------------------------------------------
1 | use std::rc::Rc;
2 |
3 | use dioxus::{html::MountedData, signals::Signal, web::WebEventExt};
4 | use floating_ui_dom::{
5 | ARROW_NAME, Arrow as CoreArrow, ArrowOptions as CoreArrowOptions, Middleware, MiddlewareReturn,
6 | MiddlewareState, Padding,
7 | };
8 |
9 | /// Options for [`Arrow`].
10 | #[derive(Clone, PartialEq)]
11 | pub struct ArrowOptions {
12 | /// The arrow element to be positioned.
13 | pub element: Signal>>,
14 |
15 | /// The padding between the arrow element and the floating element edges.
16 | /// Useful when the floating element has rounded corners.
17 | ///
18 | /// Defaults to `0` on all sides.
19 | pub padding: Option,
20 | }
21 |
22 | impl ArrowOptions {
23 | pub fn new(element: Signal>>) -> Self {
24 | ArrowOptions {
25 | element,
26 | padding: None,
27 | }
28 | }
29 |
30 | /// Set `element` option.
31 | pub fn element(mut self, value: Signal >>) -> Self {
32 | self.element = value;
33 | self
34 | }
35 |
36 | /// Set `padding` option.
37 | pub fn padding(mut self, value: Padding) -> Self {
38 | self.padding = Some(value);
39 | self
40 | }
41 | }
42 |
43 | /// Arrow middleware.
44 | ///
45 | /// Provides data to position an inner element of the floating element so that it appears centered to the reference element.
46 | ///
47 | /// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/arrow.html) for more documentation.
48 | #[derive(Clone, PartialEq)]
49 | pub struct Arrow {
50 | options: ArrowOptions,
51 | }
52 |
53 | impl Arrow {
54 | pub fn new(options: ArrowOptions) -> Self {
55 | Arrow { options }
56 | }
57 | }
58 |
59 | impl Middleware for Arrow {
60 | fn name(&self) -> &'static str {
61 | ARROW_NAME
62 | }
63 |
64 | fn compute(
65 | &self,
66 | state: MiddlewareState,
67 | ) -> MiddlewareReturn {
68 | match (self.options.element)().map(|element| element.as_web_event()) {
69 | Some(element) => CoreArrow::new(CoreArrowOptions {
70 | element,
71 | padding: self.options.padding.clone(),
72 | })
73 | .compute(state),
74 | _ => MiddlewareReturn {
75 | x: None,
76 | y: None,
77 | data: None,
78 | reset: None,
79 | },
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/book-examples/src/positioning/flip.rs:
--------------------------------------------------------------------------------
1 | use floating_ui_leptos::{
2 | DetectOverflowOptions, Flip, FlipOptions, MiddlewareVec, Offset, OffsetOptions, Placement,
3 | RootBoundary,
4 | };
5 | use leptos::prelude::*;
6 | use leptos_node_ref::AnyNodeRef;
7 | use send_wrapper::SendWrapper;
8 |
9 | use crate::{
10 | components::{Chrome, Floating, GridItem, Reference, Scrollable},
11 | utils::rem_to_px,
12 | };
13 |
14 | #[component]
15 | pub fn FlipDemo() -> impl IntoView {
16 | let boundary_ref = AnyNodeRef::new();
17 |
18 | Effect::new(move |_| {
19 | if let Some(boundary) = boundary_ref.get() {
20 | boundary
21 | .first_element_child()
22 | .expect("First element child should exist.")
23 | .set_scroll_top(rem_to_px(275.0 / 16.0) as i32);
24 | }
25 | });
26 |
27 | view! {
28 |
33 |
39 |
53 | Tooltip
54 |
55 | }
56 | reference=move |node_ref| view! {
57 |
58 | }
59 | />
60 |
61 |
62 | }
63 | />
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/book-examples/src/components/floating.rs:
--------------------------------------------------------------------------------
1 | use floating_ui_leptos::{
2 | MiddlewareVec, Placement, Strategy, UseFloatingOptions, UseFloatingReturn, use_floating,
3 | };
4 | use leptos::prelude::*;
5 | use leptos_node_ref::AnyNodeRef;
6 | use send_wrapper::SendWrapper;
7 | use tailwind_fuse::tw_merge;
8 |
9 | #[component]
10 | pub fn Floating(
11 | #[prop(into, optional)] class: MaybeProp,
12 | #[prop(into, optional)] strategy: MaybeProp,
13 | #[prop(into, optional)] placement: MaybeProp,
14 | #[prop(into, optional)] middleware: MaybeProp>,
15 | #[prop(default = false.into(), into)] arrow: Signal,
16 | content: CF,
17 | reference: RF,
18 | ) -> impl IntoView
19 | where
20 | CF: Fn() -> CIV + 'static,
21 | CIV: IntoView + 'static,
22 | RF: Fn(AnyNodeRef) -> RIV + 'static,
23 | RIV: IntoView + 'static,
24 | {
25 | let floating_ref = AnyNodeRef::new();
26 | let reference_ref = AnyNodeRef::new();
27 | let arrow_ref = AnyNodeRef::new();
28 |
29 | let UseFloatingReturn {
30 | floating_styles, ..
31 | } = use_floating(
32 | reference_ref,
33 | floating_ref,
34 | UseFloatingOptions::default()
35 | .while_elements_mounted_auto_update()
36 | .placement(placement)
37 | .strategy(strategy)
38 | .middleware(middleware),
39 | );
40 |
41 | view! {
42 | {reference(reference_ref)}
43 |
44 |
61 |
{content()}
62 |
63 |
68 |
69 |
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Rust Floating UI
8 |
9 | Rust port of [Floating UI](https://floating-ui.com).
10 |
11 | [Floating UI](https://floating-ui.com) is a library that helps you create "floating" elements such as tooltips, popovers, dropdowns, and more.
12 |
13 | ## Frameworks
14 |
15 | Rust Floating UI is available for these Rust frameworks:
16 |
17 | - [DOM](./packages/dom) ([`web-sys`](https://rustwasm.github.io/wasm-bindgen/web-sys/index.html))
18 | - [Dioxus](https://dioxuslabs.com/)
19 | - [Leptos](./packages/leptos)
20 | - [Yew](https://yew.rs/)
21 |
22 | ## Examples
23 |
24 | See [the Rust Floating UI book](https://floating-ui.rustforweb.org/) for examples.
25 |
26 | Each framework has an implementations of the [Floating UI tutorial](https://floating-ui.com/docs/tutorial) as an example:
27 |
28 | - [DOM](./packages/dom/example)
29 | - [Dioxus](./packages/dioxus/example)
30 | - [Leptos](./packages/leptos/example)
31 | - [Yew](./packages/yew/example)
32 |
33 | Additionally, implementations of [Floating UI tests](https://github.com/floating-ui/floating-ui/tree/master/packages/dom/test) are more complex examples:
34 |
35 | - [Dioxus](./packages/dioxus/tests)
36 | - [Leptos](./packages/leptos/tests)
37 | - [Yew](./packages/yew/tests)
38 |
39 | ## Documentation
40 |
41 | See [the Rust Floating UI book](https://floating-ui.rustforweb.org/) for documentation.
42 |
43 | Documentation for the crates is available on [Docs.rs](https://docs.rs/):
44 |
45 | - [`floating-ui-core`](https://docs.rs/floating-ui-core/latest/floating_ui_core/)
46 | - [`floating-ui-dioxus`](https://docs.rs/floating-ui-dioxus/latest/floating_ui_dioxus/)
47 | - [`floating-ui-dom`](https://docs.rs/floating-ui-dom/latest/floating_ui_dom/)
48 | - [`floating-ui-leptos`](https://docs.rs/floating-ui-leptos/latest/floating_ui_leptos/)
49 | - [`floating-ui-utils`](https://docs.rs/floating-ui-utils/latest/floating_ui_utils/)
50 | - [`floating-ui-yew`](https://docs.rs/floating-ui-yew/latest/floating_ui_yew/)
51 |
52 | ## Credits
53 |
54 | The logo is a combination of the [Floating UI logo](https://github.com/floating-ui/floating-ui#credits) and [Ferris the Rustacean](https://rustacean.net/).
55 |
56 | ## License
57 |
58 | This project is available under the [MIT license](LICENSE.md).
59 |
60 | ## Rust for Web
61 |
62 | The Rust Floating UI project is part of [Rust for Web](https://github.com/RustForWeb).
63 |
64 | [Rust for Web](https://github.com/RustForWeb) creates and ports web libraries for Rust. All projects are free and open source.
65 |
--------------------------------------------------------------------------------
/book/theme/tabs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Change active tab of tabs.
3 | *
4 | * @param {Element} container
5 | * @param {string} name
6 | */
7 | const changeTab = (container, name) => {
8 | for (const child of container.children) {
9 | if (!(child instanceof HTMLElement)) {
10 | continue;
11 | }
12 |
13 | if (child.classList.contains('mdbook-tabs')) {
14 | for (const tab of child.children) {
15 | if (!(tab instanceof HTMLElement)) {
16 | continue;
17 | }
18 |
19 | if (tab.dataset.tabname === name) {
20 | tab.classList.add('active');
21 | } else {
22 | tab.classList.remove('active');
23 | }
24 | }
25 | } else if (child.classList.contains('mdbook-tab-content')) {
26 | if (child.dataset.tabname === name) {
27 | child.classList.remove('hidden');
28 | } else {
29 | child.classList.add('hidden');
30 | }
31 | }
32 | }
33 | };
34 |
35 | document.addEventListener('DOMContentLoaded', () => {
36 | const tabs = document.querySelectorAll('.mdbook-tab');
37 | for (const tab of tabs) {
38 | tab.addEventListener('click', () => {
39 | if (!(tab instanceof HTMLElement)) {
40 | return;
41 | }
42 |
43 | if (!tab.parentElement || !tab.parentElement.parentElement) {
44 | return;
45 | }
46 |
47 | const container = tab.parentElement.parentElement;
48 | const name = tab.dataset.tabname;
49 | const global = container.dataset.tabglobal;
50 |
51 | changeTab(container, name);
52 |
53 | if (global) {
54 | localStorage.setItem(`mdbook-tabs-${global}`, name);
55 |
56 | const globalContainers = document.querySelectorAll(
57 | `.mdbook-tabs-container[data-tabglobal="${global}"]`
58 | );
59 | for (const globalContainer of globalContainers) {
60 | changeTab(globalContainer, name);
61 | }
62 | }
63 | });
64 | }
65 |
66 | const containers = document.querySelectorAll('.mdbook-tabs-container[data-tabglobal]');
67 | for (const container of containers) {
68 | const global = container.dataset.tabglobal;
69 |
70 | const name = localStorage.getItem(`mdbook-tabs-${global}`);
71 | if (name && document.querySelector(`.mdbook-tab[data-tabname=${name}]`)) {
72 | changeTab(container, name);
73 | }
74 | }
75 | });
76 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request: {}
5 | push:
6 | branches:
7 | - main
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | lint:
14 | name: Lint
15 | runs-on: ubuntu-latest
16 |
17 | env:
18 | RUSTFLAGS: '-Dwarnings'
19 |
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@v6
23 |
24 | - name: Set up Rust toolchain
25 | uses: actions-rust-lang/setup-rust-toolchain@v1
26 | with:
27 | components: clippy, rustfmt
28 | target: wasm32-unknown-unknown
29 |
30 | - name: Install Cargo Binary Install
31 | uses: cargo-bins/cargo-binstall@main
32 |
33 | - name: Install crates
34 | run: cargo binstall -y --force cargo-deny cargo-machete cargo-sort
35 |
36 | - name: Lint
37 | run: cargo clippy --all-features --locked
38 |
39 | - name: Check dependencies
40 | run: cargo deny check
41 |
42 | - name: Check unused dependencies
43 | run: cargo machete
44 |
45 | - name: Check manifest formatting
46 | run: cargo sort --workspace --check
47 |
48 | - name: Check formatting
49 | run: cargo fmt --all --check
50 | test:
51 | name: Test
52 | runs-on: ubuntu-latest
53 |
54 | steps:
55 | - name: Checkout
56 | uses: actions/checkout@v6
57 |
58 | - name: Set up Rust toolchain
59 | uses: actions-rust-lang/setup-rust-toolchain@v1
60 | with:
61 | components: clippy, rustfmt
62 | target: wasm32-unknown-unknown
63 |
64 | - name: Install Cargo Binary Install
65 | uses: cargo-bins/cargo-binstall@main
66 |
67 | - name: Install Trunk
68 | run: cargo binstall --force -y trunk
69 |
70 | - name: Set up Node.js
71 | uses: actions/setup-node@v6
72 | with:
73 | node-version: 'lts/*'
74 |
75 | - name: Set up pnpm
76 | uses: pnpm/action-setup@v4
77 | with:
78 | version: 'latest'
79 |
80 | - name: Test
81 | run: cargo test --all-features --locked --release
82 |
83 | - name: Upload visual snapshot diffs
84 | uses: actions/upload-artifact@v6
85 | if: always()
86 | with:
87 | name: visual-snapshots-diff
88 | path: target/tmp/floating-ui/packages/dom/test-results
89 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | bump:
7 | description: 'Bump version by semver keyword.'
8 | required: true
9 | type: choice
10 | options:
11 | - patch
12 | - minor
13 | - major
14 |
15 | permissions:
16 | contents: write
17 |
18 | jobs:
19 | release:
20 | name: Release
21 | runs-on: ubuntu-latest
22 |
23 | steps:
24 | - name: Generate GitHub App token
25 | id: app-token
26 | uses: getsentry/action-github-app-token@v3
27 | with:
28 | app_id: ${{ secrets.APP_ID }}
29 | private_key: ${{ secrets.APP_PRIVATE_KEY }}
30 |
31 | - name: Checkout
32 | uses: actions/checkout@v6
33 |
34 | - name: Set up Rust toolchain
35 | uses: actions-rust-lang/setup-rust-toolchain@v1
36 | with:
37 | target: wasm32-unknown-unknown
38 |
39 | - name: Install Cargo Binary Install
40 | uses: cargo-bins/cargo-binstall@main
41 |
42 | - name: Install crates
43 | run: cargo binstall --force -y cargo-workspaces toml-cli
44 |
45 | - name: Bump version
46 | run: cargo workspaces version --all --no-git-commit --yes ${{ inputs.bump }}
47 |
48 | - name: Extract version
49 | id: extract-version
50 | run: echo "VERSION=v$(toml get Cargo.toml workspace.package.version --raw)" >> "$GITHUB_OUTPUT"
51 |
52 | - name: Add changes
53 | run: git add .
54 |
55 | - name: Commit
56 | uses: dsanders11/github-app-commit-action@v1
57 | with:
58 | message: ${{ steps.extract-version.outputs.VERSION }}
59 | token: ${{ steps.app-token.outputs.token }}
60 |
61 | - name: Reset and pull
62 | run: git reset --hard && git pull
63 |
64 | - name: Tag
65 | uses: bruno-fs/repo-tagger@1.0.0
66 | with:
67 | tag: ${{ steps.extract-version.outputs.VERSION }}
68 | env:
69 | GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
70 |
71 | - name: Release
72 | uses: softprops/action-gh-release@v2
73 | with:
74 | generate_release_notes: true
75 | make_latest: true
76 | tag_name: ${{ steps.extract-version.outputs.VERSION }}
77 | token: ${{ steps.app-token.outputs.token }}
78 |
79 | - name: Publish
80 | run: cargo workspaces publish --publish-as-is --token "${{ secrets.CRATES_IO_TOKEN }}"
81 |
--------------------------------------------------------------------------------
/packages/leptos/src/arrow.rs:
--------------------------------------------------------------------------------
1 | use floating_ui_dom::{
2 | ARROW_NAME, Arrow as CoreArrow, ArrowOptions as CoreArrowOptions, Middleware, MiddlewareReturn,
3 | MiddlewareState, Padding,
4 | };
5 | use leptos::prelude::*;
6 | use leptos_node_ref::AnyNodeRef;
7 | use web_sys::wasm_bindgen::JsCast;
8 |
9 | /// Options for [`Arrow`].
10 | #[derive(Clone)]
11 | pub struct ArrowOptions {
12 | /// The arrow element to be positioned.
13 | pub element: AnyNodeRef,
14 |
15 | /// The padding between the arrow element and the floating element edges.
16 | /// Useful when the floating element has rounded corners.
17 | ///
18 | /// Defaults to `0` on all sides.
19 | pub padding: Option,
20 | }
21 |
22 | impl ArrowOptions {
23 | pub fn new(element: AnyNodeRef) -> Self {
24 | ArrowOptions {
25 | element,
26 | padding: None,
27 | }
28 | }
29 |
30 | /// Set `element` option.
31 | pub fn element(mut self, value: AnyNodeRef) -> Self {
32 | self.element = value;
33 | self
34 | }
35 |
36 | /// Set `padding` option.
37 | pub fn padding(mut self, value: Padding) -> Self {
38 | self.padding = Some(value);
39 | self
40 | }
41 | }
42 |
43 | impl PartialEq for ArrowOptions {
44 | fn eq(&self, other: &Self) -> bool {
45 | self.element.get_untracked() == other.element.get_untracked()
46 | && self.padding == other.padding
47 | }
48 | }
49 |
50 | /// Arrow middleware.
51 | ///
52 | /// Provides data to position an inner element of the floating element so that it appears centered to the reference element.
53 | ///
54 | /// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/arrow.html) for more documentation.
55 | #[derive(Clone, PartialEq)]
56 | pub struct Arrow {
57 | options: ArrowOptions,
58 | }
59 |
60 | impl Arrow {
61 | pub fn new(options: ArrowOptions) -> Self {
62 | Arrow { options }
63 | }
64 | }
65 |
66 | impl Middleware for Arrow {
67 | fn name(&self) -> &'static str {
68 | ARROW_NAME
69 | }
70 |
71 | fn compute(
72 | &self,
73 | state: MiddlewareState,
74 | ) -> MiddlewareReturn {
75 | let element = self
76 | .options
77 | .element
78 | .get_untracked()
79 | .and_then(|element| element.dyn_into::().ok());
80 |
81 | if let Some(element) = element {
82 | CoreArrow::new(CoreArrowOptions {
83 | element,
84 | padding: self.options.padding.clone(),
85 | })
86 | .compute(state)
87 | } else {
88 | MiddlewareReturn {
89 | x: None,
90 | y: None,
91 | data: None,
92 | reset: None,
93 | }
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/packages/dom/src/platform.rs:
--------------------------------------------------------------------------------
1 | pub mod convert_offset_parent_relative_rect_to_viewport_relative_rect;
2 | pub mod get_client_length;
3 | pub mod get_client_rects;
4 | pub mod get_clipping_rect;
5 | pub mod get_dimensions;
6 | pub mod get_element_rects;
7 | pub mod get_offset_parent;
8 | pub mod get_scale;
9 | pub mod is_rtl;
10 |
11 | use floating_ui_core::{
12 | ConvertOffsetParentRelativeRectToViewportRelativeRectArgs, GetClippingRectArgs,
13 | GetElementRectsArgs, Platform as CorePlatform,
14 | };
15 | use floating_ui_utils::dom::get_document_element;
16 | use floating_ui_utils::{
17 | ClientRectObject, Coords, Dimensions, ElementRects, Length, OwnedElementOrWindow, Rect,
18 | };
19 | use web_sys::{Element, Window};
20 |
21 | use crate::types::ElementOrVirtual;
22 |
23 | use self::convert_offset_parent_relative_rect_to_viewport_relative_rect::convert_offset_parent_relative_rect_to_viewport_relative_rect;
24 | use self::get_client_length::get_client_length;
25 | use self::get_client_rects::get_client_rects;
26 | use self::get_clipping_rect::get_clipping_rect;
27 | use self::get_dimensions::get_dimensions;
28 | use self::get_element_rects::get_element_rects;
29 | use self::get_offset_parent::get_offset_parent;
30 | use self::get_scale::get_scale;
31 | use self::is_rtl::is_rtl;
32 |
33 | #[derive(Debug)]
34 | pub struct Platform {}
35 |
36 | impl CorePlatform for Platform {
37 | fn get_element_rects(&self, args: GetElementRectsArgs) -> ElementRects {
38 | get_element_rects(self, args)
39 | }
40 |
41 | fn get_clipping_rect(&self, args: GetClippingRectArgs) -> Rect {
42 | get_clipping_rect(self, args)
43 | }
44 |
45 | fn get_dimensions(&self, element: &Element) -> Dimensions {
46 | get_dimensions(element)
47 | }
48 |
49 | fn convert_offset_parent_relative_rect_to_viewport_relative_rect(
50 | &self,
51 | args: ConvertOffsetParentRelativeRectToViewportRelativeRectArgs,
52 | ) -> Option {
53 | Some(convert_offset_parent_relative_rect_to_viewport_relative_rect(args))
54 | }
55 |
56 | fn get_offset_parent(
57 | &self,
58 | element: &Element,
59 | ) -> Option> {
60 | Some(get_offset_parent(element, None))
61 | }
62 |
63 | fn get_document_element(&self, element: &Element) -> Option {
64 | Some(get_document_element(Some(element.into())))
65 | }
66 |
67 | fn get_client_rects(&self, element: ElementOrVirtual) -> Option> {
68 | Some(get_client_rects(element))
69 | }
70 |
71 | fn is_rtl(&self, element: &Element) -> Option {
72 | Some(is_rtl(element))
73 | }
74 |
75 | fn get_scale(&self, element: &Element) -> Option {
76 | Some(get_scale(element.into()))
77 | }
78 |
79 | fn get_client_length(&self, element: &Element, length: Length) -> Option {
80 | Some(get_client_length(element, length))
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/packages/dom/src/utils/get_viewport_rect.rs:
--------------------------------------------------------------------------------
1 | use floating_ui_utils::{
2 | Rect, Strategy,
3 | dom::{get_computed_style, get_document_element, get_window, is_web_kit},
4 | };
5 | use web_sys::Element;
6 |
7 | use crate::utils::get_window_scroll_bar_x::get_window_scroll_bar_x;
8 |
9 | // Safety check: ensure the scrollbar space is reasonable in case this calculation is affected by unusual styles.
10 | // Most scrollbars leave 15-18px of space.
11 | const SCROLLBAR_MAX: f64 = 25.0;
12 |
13 | pub fn get_viewport_rect(element: &Element, strategy: Strategy) -> Rect {
14 | let window = get_window(Some(element));
15 | let html = get_document_element(Some(element.into()));
16 | let visual_viewport = window.visual_viewport();
17 |
18 | let mut x = 0.0;
19 | let mut y = 0.0;
20 | let mut width = html.client_width() as f64;
21 | let mut height = html.client_height() as f64;
22 |
23 | if let Some(visual_viewport) = visual_viewport {
24 | width = visual_viewport.width();
25 | height = visual_viewport.height();
26 |
27 | let visual_viewport_based = is_web_kit();
28 | if !visual_viewport_based || strategy == Strategy::Fixed {
29 | x = visual_viewport.offset_left();
30 | y = visual_viewport.offset_top();
31 | }
32 | }
33 |
34 | let window_scrollbar_x = get_window_scroll_bar_x(&html, None);
35 | // `overflow: hidden` + `scrollbar-gutter: stable` reduces the visual width of the ,
36 | // but this is not considered in the size of `html.client_width`.
37 | if window_scrollbar_x <= 0.0 {
38 | let doc = html
39 | .owner_document()
40 | .expect("Element should have owner document.");
41 | let body = doc.body().expect("Document should have body.");
42 | let body_styles = get_computed_style(&body);
43 | let body_margin_inline = if doc.compat_mode() == "CSS1Compat" {
44 | body_styles
45 | .get_property_value("margin-left")
46 | .expect("Computed style should have margin left.")
47 | .parse::()
48 | .unwrap_or(0.0)
49 | + body_styles
50 | .get_property_value("margin-right")
51 | .expect("Computed style should have margin right.")
52 | .parse::()
53 | .unwrap_or(0.0)
54 | } else {
55 | 0.0
56 | };
57 | let clipping_stable_scrollbar_width =
58 | ((html.client_width() as f64) - (body.client_width() as f64) - body_margin_inline)
59 | .abs();
60 |
61 | if clipping_stable_scrollbar_width <= SCROLLBAR_MAX {
62 | width -= clipping_stable_scrollbar_width;
63 | }
64 | } else if window_scrollbar_x <= SCROLLBAR_MAX {
65 | // If the scrollbar is on the left, the width needs to be extended
66 | // by the scrollbar amount so there isn't extra space on the right.
67 | width += window_scrollbar_x;
68 | }
69 |
70 | Rect {
71 | x,
72 | y,
73 | width,
74 | height,
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/packages/dom/src/utils/get_rect_relative_to_offset_parent.rs:
--------------------------------------------------------------------------------
1 | use floating_ui_utils::{
2 | Coords, Rect, Strategy,
3 | dom::{
4 | DomElementOrWindow, NodeScroll, get_document_element, get_node_name, get_node_scroll,
5 | is_overflow_element,
6 | },
7 | };
8 |
9 | use crate::{
10 | types::ElementOrVirtual,
11 | utils::{
12 | get_bounding_client_rect::get_bounding_client_rect, get_html_offset::get_html_offset,
13 | get_window_scroll_bar_x::get_window_scroll_bar_x,
14 | },
15 | };
16 |
17 | pub fn get_rect_relative_to_offset_parent(
18 | element_or_virtual: ElementOrVirtual,
19 | offset_parent: DomElementOrWindow,
20 | strategy: Strategy,
21 | ) -> Rect {
22 | let is_offset_parent_an_element = matches!(offset_parent, DomElementOrWindow::Element(_));
23 | let document_element = get_document_element(Some((&offset_parent).into()));
24 | let is_fixed = strategy == Strategy::Fixed;
25 | let rect = get_bounding_client_rect(
26 | element_or_virtual,
27 | true,
28 | is_fixed,
29 | Some(offset_parent.clone()),
30 | );
31 |
32 | let mut scroll = NodeScroll::new(0.0);
33 | let mut offsets = Coords::new(0.0);
34 |
35 | // If the scrollbar appears on the left (e.g. RTL systems).
36 | // Use Firefox with layout.scrollbar.side = 3 in about:config to test this.
37 | let set_left_rtl_scrollbar_offset = |offsets: &mut Coords| {
38 | offsets.x = get_window_scroll_bar_x(&document_element, None);
39 | };
40 |
41 | #[allow(clippy::nonminimal_bool)]
42 | if is_offset_parent_an_element || (!is_offset_parent_an_element && !is_fixed) {
43 | if get_node_name((&offset_parent).into()) != "body"
44 | || is_overflow_element(&document_element)
45 | {
46 | scroll = get_node_scroll(offset_parent.clone());
47 | }
48 |
49 | match offset_parent {
50 | DomElementOrWindow::Element(offset_parent) => {
51 | let offset_rect = get_bounding_client_rect(
52 | offset_parent.into(),
53 | true,
54 | is_fixed,
55 | Some(offset_parent.into()),
56 | );
57 | offsets.x = offset_rect.x + offset_parent.client_left() as f64;
58 | offsets.y = offset_rect.y + offset_parent.client_top() as f64;
59 | }
60 | DomElementOrWindow::Window(_) => {
61 | set_left_rtl_scrollbar_offset(&mut offsets);
62 | }
63 | }
64 | }
65 |
66 | if is_fixed && !is_offset_parent_an_element {
67 | set_left_rtl_scrollbar_offset(&mut offsets);
68 | }
69 |
70 | let html_offset = if !is_offset_parent_an_element && !is_fixed {
71 | get_html_offset(&document_element, &scroll)
72 | } else {
73 | Coords::new(0.0)
74 | };
75 |
76 | let x = rect.left + scroll.scroll_left - offsets.x - html_offset.x;
77 | let y = rect.top + scroll.scroll_top - offsets.y - html_offset.y;
78 |
79 | Rect {
80 | x,
81 | y,
82 | width: rect.width,
83 | height: rect.height,
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/packages/dom/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Rust port of [Floating UI](https://floating-ui.com/).
2 | //!
3 | //! This is the library to use Floating UI on the web, wrapping [`floating_ui_core`] with DOM interface logic.
4 | //!
5 | //! See [the Rust Floating UI book](https://floating-ui.rustforweb.org/) for more documenation.
6 | //!
7 | //! See [@floating-ui/dom](https://www.npmjs.com/package/@floating-ui/dom) for the original package.
8 |
9 | mod auto_update;
10 | mod middleware;
11 | mod platform;
12 | mod types;
13 | mod utils;
14 |
15 | pub use self::platform::Platform;
16 | pub use crate::auto_update::*;
17 | pub use crate::middleware::*;
18 | pub use crate::types::*;
19 | pub use floating_ui_core::{
20 | Boundary, ComputePositionReturn, Derivable, DerivableFn, DetectOverflowOptions, ElementContext,
21 | Middleware, MiddlewareData, MiddlewareReturn, MiddlewareState, MiddlewareWithOptions,
22 | RootBoundary,
23 | };
24 | #[doc(no_inline)]
25 | pub use floating_ui_utils::{
26 | AlignedPlacement, Alignment, Axis, ClientRectObject, Coords, Dimensions, ElementRects, Length,
27 | Padding, PartialSideObject, Placement, Rect, Side, SideObject, Strategy, VirtualElement, dom,
28 | };
29 |
30 | use floating_ui_core::{
31 | ComputePositionConfig as CoreComputePositionConfig, compute_position as compute_position_core,
32 | };
33 | use web_sys::Element;
34 |
35 | const PLATFORM: Platform = Platform {};
36 |
37 | /// Options for [`compute_position`].
38 | #[derive(Clone, Default)]
39 | pub struct ComputePositionConfig {
40 | /// Where to place the floating element relative to the reference element.
41 | ///
42 | /// Defaults to [`Placement::Bottom`].
43 | pub placement: Option,
44 |
45 | /// The strategy to use when positioning the floating element.
46 | ///
47 | /// Defaults to [`Strategy::Absolute`].
48 | pub strategy: Option,
49 |
50 | /// Vector of middleware objects to modify the positioning or provide data for rendering.
51 | ///
52 | /// Defaults to an empty vector.
53 | pub middleware: Option,
54 | }
55 |
56 | impl ComputePositionConfig {
57 | /// Set `placement` option.
58 | pub fn placement(mut self, value: Placement) -> Self {
59 | self.placement = Some(value);
60 | self
61 | }
62 |
63 | /// Set `strategy` option.
64 | pub fn strategy(mut self, value: Strategy) -> Self {
65 | self.strategy = Some(value);
66 | self
67 | }
68 |
69 | /// Set `middleware` option.
70 | pub fn middleware(mut self, value: MiddlewareVec) -> Self {
71 | self.middleware = Some(value);
72 | self
73 | }
74 | }
75 |
76 | /// Computes the `x` and `y` coordinates that will place the floating element next to a given reference element.
77 | pub fn compute_position(
78 | reference: ElementOrVirtual,
79 | floating: &Element,
80 | config: ComputePositionConfig,
81 | ) -> ComputePositionReturn {
82 | // TODO: cache
83 |
84 | compute_position_core(
85 | reference,
86 | floating,
87 | CoreComputePositionConfig {
88 | platform: &PLATFORM,
89 | placement: config.placement,
90 | strategy: config.strategy,
91 | middleware: config.middleware,
92 | },
93 | )
94 | }
95 |
--------------------------------------------------------------------------------
/packages/dom/src/platform/convert_offset_parent_relative_rect_to_viewport_relative_rect.rs:
--------------------------------------------------------------------------------
1 | use floating_ui_core::ConvertOffsetParentRelativeRectToViewportRelativeRectArgs;
2 | use floating_ui_utils::{
3 | Coords, ElementOrWindow, Rect, Strategy,
4 | dom::{
5 | NodeScroll, get_document_element, get_node_name, get_node_scroll, is_overflow_element,
6 | is_top_layer,
7 | },
8 | };
9 | use web_sys::{Element, Window};
10 |
11 | use crate::{
12 | platform::get_scale::get_scale,
13 | utils::{get_bounding_client_rect::get_bounding_client_rect, get_html_offset::get_html_offset},
14 | };
15 |
16 | pub fn convert_offset_parent_relative_rect_to_viewport_relative_rect(
17 | ConvertOffsetParentRelativeRectToViewportRelativeRectArgs {
18 | elements,
19 | rect,
20 | offset_parent,
21 | strategy,
22 | }: ConvertOffsetParentRelativeRectToViewportRelativeRectArgs,
23 | ) -> Rect {
24 | let is_fixed = strategy == Strategy::Fixed;
25 | let document_element = get_document_element(
26 | offset_parent
27 | .as_ref()
28 | .map(|offset_parent| offset_parent.into()),
29 | );
30 | let top_layer = elements.is_some_and(|elements| is_top_layer(elements.floating));
31 |
32 | if offset_parent
33 | .as_ref()
34 | .is_some_and(|offset_parent| match offset_parent {
35 | ElementOrWindow::Element(element) => *element == &document_element,
36 | ElementOrWindow::Window(_) => false,
37 | })
38 | || (top_layer && is_fixed)
39 | {
40 | return rect;
41 | }
42 |
43 | let mut scroll = NodeScroll::new(0.0);
44 | let mut scale = Coords::new(1.0);
45 | let mut offsets = Coords::new(0.0);
46 | let is_offset_parent_an_element =
47 | offset_parent
48 | .as_ref()
49 | .is_some_and(|offset_parent| match offset_parent {
50 | ElementOrWindow::Element(_) => true,
51 | ElementOrWindow::Window(_) => false,
52 | });
53 |
54 | #[allow(clippy::nonminimal_bool)]
55 | if is_offset_parent_an_element || (!is_offset_parent_an_element && !is_fixed) {
56 | if let Some(offset_parent) = offset_parent.as_ref()
57 | && (get_node_name(offset_parent.into()) != "body"
58 | || is_overflow_element(&document_element))
59 | {
60 | scroll = get_node_scroll(offset_parent.into());
61 | }
62 |
63 | if let Some(ElementOrWindow::Element(offset_parent)) = offset_parent {
64 | let offset_rect = get_bounding_client_rect(offset_parent.into(), false, false, None);
65 | scale = get_scale(offset_parent.into());
66 | offsets.x = offset_rect.x + offset_parent.client_left() as f64;
67 | offsets.y = offset_rect.y + offset_parent.client_top() as f64;
68 | }
69 | }
70 |
71 | let html_offset = if !is_offset_parent_an_element && !is_fixed {
72 | get_html_offset(&document_element, &scroll)
73 | } else {
74 | Coords::new(0.0)
75 | };
76 |
77 | Rect {
78 | x: rect.x * scale.x - scroll.scroll_left * scale.x + offsets.x + html_offset.x,
79 | y: rect.y * scale.y - scroll.scroll_top * scale.y + offsets.y + html_offset.y,
80 | width: rect.width * scale.x,
81 | height: rect.height * scale.y,
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/.github/workflows/website.yml:
--------------------------------------------------------------------------------
1 | name: Website
2 | on:
3 | pull_request: {}
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: read
10 |
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.ref }}
13 | cancel-in-progress: false
14 |
15 | jobs:
16 | book-test:
17 | name: Test Book
18 | runs-on: ubuntu-latest
19 |
20 | steps:
21 | - uses: actions/checkout@v6
22 |
23 | - name: Set up Rust toolchain
24 | uses: actions-rust-lang/setup-rust-toolchain@v1
25 | with:
26 | target: wasm32-unknown-unknown
27 |
28 | - name: Install Cargo Binary Install
29 | uses: cargo-bins/cargo-binstall@main
30 |
31 | - name: Install mdBook
32 | run: cargo binstall --force -y mdbook mdbook-tabs mdbook-trunk
33 |
34 | - name: Run tests
35 | run: mdbook test
36 | working-directory: book
37 |
38 | book-build:
39 | name: Build Book
40 | needs: book-test
41 | runs-on: ubuntu-latest
42 |
43 | steps:
44 | - uses: actions/checkout@v6
45 | with:
46 | fetch-depth: 0
47 |
48 | - name: Set up Rust toolchain
49 | uses: actions-rust-lang/setup-rust-toolchain@v1
50 | with:
51 | target: wasm32-unknown-unknown
52 |
53 | - name: Install Cargo Binary Install
54 | uses: cargo-bins/cargo-binstall@main
55 |
56 | - name: Install mdBook and Trunk
57 | run: cargo binstall --force -y mdbook mdbook-tabs mdbook-trunk trunk
58 |
59 | - name: Install Node.js dependencies
60 | run: npm ci
61 |
62 | - name: Build Book
63 | run: mdbook build
64 | working-directory: book
65 |
66 | - name: Combine Book Outputs
67 | run: mdbook-trunk combine
68 | working-directory: book
69 |
70 | - name: Upload artifact
71 | uses: actions/upload-artifact@v6
72 | with:
73 | name: book
74 | path: book/dist
75 | retention-days: 1
76 | if-no-files-found: error
77 |
78 | deploy:
79 | name: Deploy
80 | needs: book-build
81 | if: github.ref == 'refs/heads/main'
82 | runs-on: ubuntu-latest
83 |
84 | permissions:
85 | contents: read
86 | pages: write
87 | id-token: write
88 |
89 | steps:
90 | - uses: actions/checkout@v6
91 | with:
92 | fetch-depth: 0
93 |
94 | - name: Download artifacts
95 | uses: actions/download-artifact@v7
96 | with:
97 | path: dist
98 | merge-multiple: true
99 |
100 | - name: Setup Pages
101 | uses: actions/configure-pages@v5
102 |
103 | - name: Upload artifact
104 | uses: actions/upload-pages-artifact@v4
105 | with:
106 | path: dist
107 |
108 | - name: Deploy to GitHub Pages
109 | id: deployment
110 | uses: actions/deploy-pages@v4
111 |
--------------------------------------------------------------------------------
/book-examples/src/positioning/shift.rs:
--------------------------------------------------------------------------------
1 | use floating_ui_leptos::{
2 | Boundary, DetectOverflowOptions, MiddlewareVec, Offset, OffsetOptions, Padding,
3 | PartialSideObject, Placement, RootBoundary, Shift, ShiftOptions,
4 | };
5 | use leptos::prelude::*;
6 | use leptos_node_ref::AnyNodeRef;
7 | use send_wrapper::SendWrapper;
8 |
9 | use crate::{
10 | components::{Chrome, Floating, GridItem, Reference, Scrollable},
11 | utils::rem_to_px,
12 | };
13 |
14 | #[component]
15 | pub fn ShiftDemo() -> impl IntoView {
16 | let boundary_ref = AnyNodeRef::new();
17 |
18 | Effect::new(move |_| {
19 | if let Some(boundary) = boundary_ref.get() {
20 | boundary
21 | .first_element_child()
22 | .expect("First element child should exist.")
23 | .set_scroll_top(rem_to_px(200.0 / 16.0) as i32);
24 | }
25 | });
26 |
27 | view! {
28 |
33 |
39 |
64 | Popover
65 |
66 | }
67 | reference=move |node_ref| view! {
68 |
69 | }
70 | />
71 |
72 |
73 | }
74 | />
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/rust,node
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=rust,node
3 |
4 | ### Node ###
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 | .pnpm-debug.log*
13 |
14 | # Diagnostic reports (https://nodejs.org/api/report.html)
15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
16 |
17 | # Runtime data
18 | pids
19 | *.pid
20 | *.seed
21 | *.pid.lock
22 |
23 | # Directory for instrumented libs generated by jscoverage/JSCover
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 | coverage
28 | *.lcov
29 |
30 | # nyc test coverage
31 | .nyc_output
32 |
33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
34 | .grunt
35 |
36 | # Bower dependency directory (https://bower.io/)
37 | bower_components
38 |
39 | # node-waf configuration
40 | .lock-wscript
41 |
42 | # Compiled binary addons (https://nodejs.org/api/addons.html)
43 | build/Release
44 |
45 | # Dependency directories
46 | node_modules/
47 | jspm_packages/
48 |
49 | # Snowpack dependency directory (https://snowpack.dev/)
50 | web_modules/
51 |
52 | # TypeScript cache
53 | *.tsbuildinfo
54 |
55 | # Optional npm cache directory
56 | .npm
57 |
58 | # Optional eslint cache
59 | .eslintcache
60 |
61 | # Optional stylelint cache
62 | .stylelintcache
63 |
64 | # Microbundle cache
65 | .rpt2_cache/
66 | .rts2_cache_cjs/
67 | .rts2_cache_es/
68 | .rts2_cache_umd/
69 |
70 | # Optional REPL history
71 | .node_repl_history
72 |
73 | # Output of 'npm pack'
74 | *.tgz
75 |
76 | # Yarn Integrity file
77 | .yarn-integrity
78 |
79 | # dotenv environment variable files
80 | .env
81 | .env.development.local
82 | .env.test.local
83 | .env.production.local
84 | .env.local
85 |
86 | # parcel-bundler cache (https://parceljs.org/)
87 | .cache
88 | .parcel-cache
89 |
90 | # Next.js build output
91 | .next
92 | out
93 |
94 | # Nuxt.js build / generate output
95 | .nuxt
96 | dist
97 |
98 | # Gatsby files
99 | .cache/
100 | # Comment in the public line in if your project uses Gatsby and not Next.js
101 | # https://nextjs.org/blog/next-9-1#public-directory-support
102 | # public
103 |
104 | # vuepress build output
105 | .vuepress/dist
106 |
107 | # vuepress v2.x temp and cache directory
108 | .temp
109 |
110 | # Docusaurus cache and generated files
111 | .docusaurus
112 |
113 | # Serverless directories
114 | .serverless/
115 |
116 | # FuseBox cache
117 | .fusebox/
118 |
119 | # DynamoDB Local files
120 | .dynamodb/
121 |
122 | # TernJS port file
123 | .tern-port
124 |
125 | # Stores VSCode versions used for testing VSCode extensions
126 | .vscode-test
127 |
128 | # yarn v2
129 | .yarn/cache
130 | .yarn/unplugged
131 | .yarn/build-state.yml
132 | .yarn/install-state.gz
133 | .pnp.*
134 |
135 | ### Node Patch ###
136 | # Serverless Webpack directories
137 | .webpack/
138 |
139 | # Optional stylelint cache
140 |
141 | # SvelteKit build / generate output
142 | .svelte-kit
143 |
144 | ### Rust ###
145 | # Generated by Cargo
146 | # will have compiled files and executables
147 | debug/
148 | target/
149 |
150 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
151 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
152 | # Cargo.lock
153 |
154 | # These are backup files generated by rustfmt
155 | **/*.rs.bk
156 |
157 | # MSVC Windows builds of rustc generate these, which store debugging information
158 | *.pdb
159 |
160 | # End of https://www.toptal.com/developers/gitignore/api/rust,node
161 |
162 | # mdBook
163 | book/book/
164 |
165 | # Tailwind CSS ouput
166 | tailwind.output.css
167 |
--------------------------------------------------------------------------------
/packages/leptos/tests/visual/src/spec/placement.rs:
--------------------------------------------------------------------------------
1 | use convert_case::{Case, Casing};
2 | use floating_ui_leptos::{Placement, UseFloatingOptions, UseFloatingReturn, use_floating};
3 | use leptos::prelude::*;
4 | use leptos_node_ref::AnyNodeRef;
5 |
6 | use crate::utils::{all_placements::ALL_PLACEMENTS, use_size::use_size};
7 |
8 | #[component]
9 | pub fn Placement() -> impl IntoView {
10 | let reference_ref = AnyNodeRef::new();
11 | let floating_ref = AnyNodeRef::new();
12 |
13 | let (rtl, set_rtl) = signal(false);
14 | let (placement, set_placement) = signal(Placement::Bottom);
15 |
16 | let UseFloatingReturn {
17 | floating_styles,
18 | update,
19 | ..
20 | } = use_floating(
21 | reference_ref,
22 | floating_ref,
23 | UseFloatingOptions::default()
24 | .placement(placement)
25 | .while_elements_mounted_auto_update(),
26 | );
27 |
28 | let (size, set_size) = use_size(None, None);
29 |
30 | Effect::new(move || {
31 | _ = rtl.get();
32 | update();
33 | });
34 |
35 | view! {
36 | Placement
37 |
38 | The floating element should be correctly positioned when given each of the 12 placements.
39 |
40 |
45 |
46 | Reference
47 |
48 |
49 | Floating
50 |
51 |
52 |
53 |
54 | Size
55 |
65 |
66 |
67 |
68 |
81 | {format!("{local_placement:?}").to_case(Case::Kebab)}
82 |
83 | }
84 | />
85 |
86 |
87 | RTL
88 |
89 |
102 | {format!("{value}")}
103 |
104 | }
105 | />
106 |
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/packages/dom/src/middleware.rs:
--------------------------------------------------------------------------------
1 | use floating_ui_core::middleware::{
2 | Arrow as CoreArrow, AutoPlacement as CoreAutoPlacement, Flip as CoreFlip, Hide as CoreHide,
3 | Inline as CoreInline, Offset as CoreOffset, Shift as CoreShift, Size as CoreSize,
4 | };
5 | use web_sys::{Element, Window};
6 |
7 | pub use floating_ui_core::middleware::{
8 | ARROW_NAME, AUTO_PLACEMENT_NAME, ApplyState, ArrowData, ArrowOptions, AutoPlacementData,
9 | AutoPlacementDataOverflow, AutoPlacementOptions, CrossAxis, DefaultLimiter, FLIP_NAME,
10 | FallbackStrategy, FlipData, FlipDataOverflow, FlipOptions, HIDE_NAME, HideData, HideOptions,
11 | HideStrategy, INLINE_NAME, InlineOptions, LimitShift, LimitShiftOffset, LimitShiftOffsetValues,
12 | LimitShiftOptions, OFFSET_NAME, OffsetData, OffsetOptions, OffsetOptionsValues, SHIFT_NAME,
13 | SIZE_NAME, ShiftData, ShiftOptions, SizeOptions,
14 | };
15 |
16 | /// Arrow middleware.
17 | ///
18 | /// Provides data to position an inner element of the floating element so that it appears centered to the reference element.
19 | ///
20 | /// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/arrow.html) for more documentation.
21 | pub type Arrow<'a> = CoreArrow<'a, Element, Window>;
22 |
23 | /// Auto placement middleware.
24 | ///
25 | /// Optimizes the visibility of the floating element by choosing the placement that has the most space available automatically,
26 | /// without needing to specify a preferred placement.
27 | ///
28 | /// Alternative to [`Flip`].
29 | ///
30 | /// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/auto-placement.html) for more documentation.
31 | pub type AutoPlacement<'a> = CoreAutoPlacement<'a, Element, Window>;
32 |
33 | /// Flip middleware.
34 | ///
35 | /// Optimizes the visibility of the floating element by flipping the `placement` in order to keep it in view when the preferred placement(s) will overflow the clipping boundary.
36 | /// Alternative to [`AutoPlacement`].
37 | ///
38 | /// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/flip.html) for more documentation.
39 | pub type Flip<'a> = CoreFlip<'a, Element, Window>;
40 |
41 | /// Hide middleware.
42 | ///
43 | /// Provides data to hide the floating element in applicable situations,
44 | /// such as when it is not in the same clipping context as the reference element.
45 | ///
46 | /// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/hide.html) for more documentation.
47 | pub type Hide<'a> = CoreHide<'a, Element, Window>;
48 |
49 | /// Inline middleware.
50 | ///
51 | /// Provides improved positioning for inline reference elements that can span over multiple lines, such as hyperlinks or range selections.
52 | ///
53 | /// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/inline.html) for more documentation.
54 | pub type Inline<'a> = CoreInline<'a, Element, Window>;
55 |
56 | /// Offset middleware.
57 | ///
58 | /// Modifies the placement by translating the floating element along the specified axes.
59 | ///
60 | /// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/offset.html) for more documentation.
61 | pub type Offset<'a> = CoreOffset<'a, Element, Window>;
62 |
63 | /// Shift middleware.
64 | ///
65 | /// Optimizes the visibility of the floating element by shifting it in order to keep it in view when it will overflow the clipping boundary.
66 | ///
67 | /// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/shift.html) for more documentation.
68 | pub type Shift<'a> = CoreShift<'a, Element, Window>;
69 |
70 | /// Size middleware.
71 | ///
72 | /// Provides data that allows you to change the size of the floating element -
73 | /// for instance, prevent it from overflowing the clipping boundary or match the width of the reference element.
74 | ///
75 | /// See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/size.html) for more documentation.
76 | pub type Size<'a> = CoreSize<'a, Element, Window>;
77 |
--------------------------------------------------------------------------------
/book/src/virtual-elements.md:
--------------------------------------------------------------------------------
1 | # Virtual Elements
2 |
3 | Position a floating element relative to a custom reference area, useful for context menus, range selections, following the cursor, and more.
4 |
5 | ## Usage
6 |
7 | A virtual element must implement the `VirtualElement` trait.
8 |
9 | ```rust,ignore
10 | pub trait VirtualElement: Clone + PartialEq {
11 | fn get_bounding_client_rect(&self) -> ClientRectObject;
12 |
13 | fn get_client_rects(&self) -> Option>;
14 |
15 | fn context_element(&self) -> Option;
16 | }
17 | ```
18 |
19 | A default implementation called `DefaultVirtualElement` is provided for convience.
20 |
21 | ```rust,ignore
22 | let virtual_el: Box> = Box::new(
23 | DefaultVirtualElement::new(get_bounding_client_rect)
24 | .get_client_rects(get_client_rects)
25 | .context_element(context_element),
26 | );
27 | ```
28 |
29 | {{#tabs global="package" }}
30 | {{#tab name="Core" }}
31 |
32 | ```rust,ignore
33 | compute_position(virtual_el.into(), floating_el, ComputePositionConfig::new(platform))
34 | ```
35 |
36 | {{#endtab }}
37 | {{#tab name="DOM" }}
38 |
39 | ```rust,ignore
40 | compute_position(virtual_el.into(), floating_el, ComputePositionConfig::default())
41 | ```
42 |
43 | {{#endtab }}
44 | {{#tab name="Dioxus" }}
45 |
46 | ```rust,ignore
47 | use_floating(virtual_el.into(), floating_el, UseFloatingOptions::default())
48 | ```
49 |
50 | {{#endtab }}
51 | {{#tab name="Leptos" }}
52 |
53 | ```rust,ignore
54 | use_floating(virtual_el.into(), floating_el, UseFloatingOptions::default())
55 | ```
56 |
57 | {{#endtab }}
58 | {{#tab name="Yew" }}
59 |
60 | ```rust,ignore
61 | use_floating(virtual_el.into(), floating_el, UseFloatingOptions::default())
62 | ```
63 |
64 | {{#endtab }}
65 | {{#endtabs }}
66 |
67 | ### `get_bounding_client_rect`
68 |
69 | The most basic virtual element is a plain object that has a `get_bounding_client_rect` method, which mimics a real element's one:
70 |
71 | ```rust,ignore
72 | // A virtual element that is 20 x 20 px starting from (0, 0)
73 | let virtual_el: Box> = Box::new(
74 | DefaultVirtualElement::new(Rc::new(|| {
75 | ClientRectObject {
76 | x: 0.0,
77 | y: 0.0,
78 | top: 0.0,
79 | left: 0.0,
80 | bottom: 20.0,
81 | right: 20.0,
82 | width: 20.0,
83 | height: 20.0,
84 | }
85 | }))
86 | );
87 | ```
88 |
89 |
90 |
91 |
92 | ### `context_element`
93 |
94 | This option is useful if your `get_bounding_client_rect` method is derived from a real element, to ensure clipping and position update detection works as expected.
95 |
96 | ```rust,ignore
97 | let virtual_el: Box> = Box::new(
98 | DefaultVirtualElement::new(get_bounding_client_rect)
99 | .context_element(
100 | web_sys::window()
101 | .expext("Window should exist.")
102 | .document()
103 | .expect("Document should exist.")
104 | .query_selector("#context")
105 | .expect("Document should be queried.")
106 | .expect("Element should exist."),
107 | ),
108 | );
109 | ```
110 |
111 | ### `get_client_rects`
112 |
113 | This option is useful when using [range selections](https://developer.mozilla.org/en-US/docs/Web/API/Range) and the `Inline` middleware.
114 |
115 | ```rust,ignore
116 | let virtual_el: Box> = Box::new(
117 | DefaultVirtualElement::new(|| range.get_bounding_client_rect().into())
118 | .get_client_rects(|| ClientRectObject::from_dom_rect_list(
119 | range.get_client_rects().expect("Range should have client rects."),
120 | )),
121 | );
122 | ```
123 |
124 | ## See Also
125 |
126 | - [Floating UI documentation](https://floating-ui.com/docs/virtual-elements)
127 |
--------------------------------------------------------------------------------
/packages/dom/src/platform/get_offset_parent.rs:
--------------------------------------------------------------------------------
1 | use floating_ui_utils::OwnedElementOrWindow;
2 | use floating_ui_utils::dom::{
3 | DomNodeOrWindow, get_computed_style, get_containing_block, get_document_element,
4 | get_parent_node, get_window, is_containing_block, is_element, is_html_element,
5 | is_last_traversable_node, is_table_element, is_top_layer,
6 | };
7 | use web_sys::Window;
8 | use web_sys::{Element, HtmlElement, wasm_bindgen::JsCast};
9 |
10 | use crate::utils::is_static_positioned::is_static_positioned;
11 |
12 | pub type Polyfill = Box Option>;
13 |
14 | pub fn get_true_offset_parent(element: &Element, polyfill: &Option) -> Option {
15 | if !is_html_element(element)
16 | || get_computed_style(element)
17 | .get_property_value("position")
18 | .expect("Computed style should have position.")
19 | == "fixed"
20 | {
21 | None
22 | } else {
23 | let element = element.unchecked_ref::();
24 |
25 | if let Some(polyfill) = polyfill {
26 | polyfill(element)
27 | } else {
28 | let raw_offset_parent = element.offset_parent();
29 |
30 | // Firefox returns the element as the offsetParent if it's non-static, while Chrome and Safari return the element.
31 | // The element must be used to perform the correct calculations even if the element is non-static.
32 | if let Some(raw_offset_parent) = raw_offset_parent.as_ref()
33 | && get_document_element(Some(DomNodeOrWindow::Node(raw_offset_parent)))
34 | == *raw_offset_parent
35 | {
36 | return Some(
37 | raw_offset_parent
38 | .owner_document()
39 | .expect("Element should have owner document.")
40 | .body()
41 | .expect("Document should have body.")
42 | .unchecked_into::(),
43 | );
44 | }
45 |
46 | raw_offset_parent
47 | }
48 | }
49 | }
50 |
51 | /// Gets the closest ancestor positioned element. Handles some edge cases, such as table ancestors and cross browser bugs.
52 | pub fn get_offset_parent(
53 | element: &Element,
54 | polyfill: Option,
55 | ) -> OwnedElementOrWindow {
56 | let window = get_window(Some(element));
57 |
58 | if is_top_layer(element) {
59 | return OwnedElementOrWindow::Window(window);
60 | }
61 |
62 | if !is_html_element(element) {
63 | let mut svg_offset_parent = Some(get_parent_node(element));
64 | while let Some(parent) = svg_offset_parent.as_ref() {
65 | if is_last_traversable_node(parent) {
66 | break;
67 | }
68 |
69 | if is_element(parent) {
70 | let element = parent.unchecked_ref::();
71 | if !is_static_positioned(element) {
72 | return OwnedElementOrWindow::Element(element.clone());
73 | }
74 | }
75 | svg_offset_parent = Some(get_parent_node(parent))
76 | }
77 | return OwnedElementOrWindow::Window(window);
78 | }
79 |
80 | let mut offset_parent = get_true_offset_parent(element, &polyfill);
81 |
82 | while let Some(parent) = offset_parent.as_ref() {
83 | if is_table_element(parent) && is_static_positioned(parent) {
84 | offset_parent = get_true_offset_parent(parent, &polyfill);
85 | } else {
86 | break;
87 | }
88 | }
89 |
90 | if let Some(parent) = offset_parent.as_ref()
91 | && is_last_traversable_node(parent)
92 | && is_static_positioned(parent)
93 | && !is_containing_block(parent.into())
94 | {
95 | return OwnedElementOrWindow::Window(window);
96 | }
97 |
98 | offset_parent
99 | .map(OwnedElementOrWindow::Element)
100 | .or(get_containing_block(element)
101 | .map(|element| OwnedElementOrWindow::Element(element.into())))
102 | .unwrap_or(OwnedElementOrWindow::Window(window))
103 | }
104 |
--------------------------------------------------------------------------------
/packages/leptos/example/src/app.rs:
--------------------------------------------------------------------------------
1 | use floating_ui_leptos::{
2 | ARROW_NAME, Arrow, ArrowData, ArrowOptions, DetectOverflowOptions, Flip, FlipOptions,
3 | MiddlewareVec, Offset, OffsetOptions, Padding, Placement, Shift, ShiftOptions, Side,
4 | UseFloatingOptions, UseFloatingReturn, use_floating,
5 | };
6 | use leptos::prelude::*;
7 | use leptos_node_ref::AnyNodeRef;
8 | use send_wrapper::SendWrapper;
9 |
10 | #[component]
11 | pub fn App() -> impl IntoView {
12 | let reference_ref = AnyNodeRef::new();
13 | let floating_ref = AnyNodeRef::new();
14 | let arrow_ref = AnyNodeRef::new();
15 |
16 | let (open, set_open) = signal(false);
17 |
18 | let middleware: MiddlewareVec = vec![
19 | Box::new(Offset::new(OffsetOptions::Value(6.0))),
20 | Box::new(Flip::new(FlipOptions::default())),
21 | Box::new(Shift::new(ShiftOptions::default().detect_overflow(
22 | DetectOverflowOptions::default().padding(Padding::All(5.0)),
23 | ))),
24 | Box::new(Arrow::new(ArrowOptions::new(arrow_ref))),
25 | ];
26 |
27 | let UseFloatingReturn {
28 | placement,
29 | floating_styles,
30 | middleware_data,
31 | ..
32 | } = use_floating(
33 | reference_ref,
34 | floating_ref,
35 | UseFloatingOptions::default()
36 | .open(open)
37 | .placement(Placement::Top)
38 | .middleware(SendWrapper::new(middleware))
39 | .while_elements_mounted_auto_update(),
40 | );
41 |
42 | let static_side = Signal::derive(move || placement.get().side().opposite());
43 | let arrow_data =
44 | Signal::derive(move || -> Option { middleware_data.get().get_as(ARROW_NAME) });
45 | let arrow_x = Signal::derive(move || {
46 | arrow_data
47 | .get()
48 | .and_then(|arrow_data| arrow_data.x.map(|x| format!("{x}px")))
49 | });
50 | let arrow_y = Signal::derive(move || {
51 | arrow_data
52 | .get()
53 | .and_then(|arrow_data| arrow_data.y.map(|y| format!("{y}px")))
54 | });
55 |
56 | view! {
57 |
66 | My button
67 |
68 |
69 |