├── .github ├── FUNDING.yml ├── renovate.json └── workflows │ ├── ci.yml │ ├── release.yml │ ├── upstream.yml │ └── website.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── book-examples ├── Cargo.toml ├── Trunk.toml ├── index.html ├── main.js ├── src │ ├── app.rs │ ├── components.rs │ ├── components │ │ ├── chrome.rs │ │ ├── floating.rs │ │ ├── grid_item.rs │ │ └── reference.rs │ ├── main.rs │ ├── positioning.rs │ ├── positioning │ │ ├── flip.rs │ │ ├── placement.rs │ │ ├── shift.rs │ │ └── size.rs │ ├── utils.rs │ └── utils │ │ └── rem_to_px.rs ├── style │ └── tailwind.css └── tailwind.config.js ├── book ├── book.toml ├── src │ ├── SUMMARY.md │ ├── auto-update.md │ ├── compute-position.md │ ├── detect-overflow.md │ ├── examples.md │ ├── frameworks │ │ ├── README.md │ │ ├── dom.md │ │ ├── leptos.md │ │ └── yew.md │ ├── images │ │ └── logo.svg │ ├── introduction.md │ ├── middleware │ │ ├── README.md │ │ ├── arrow.md │ │ ├── auto-placement.md │ │ ├── flip.md │ │ ├── hide.md │ │ ├── inline.md │ │ ├── offset.md │ │ ├── shift.md │ │ └── size.md │ ├── platform.md │ └── virtual-elements.md └── theme │ ├── tabs.css │ ├── tabs.js │ ├── theme.css │ ├── theme.js │ ├── trunk.css │ └── trunk.js ├── logo.svg ├── package-lock.json ├── package.json ├── packages ├── core │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── compute_coords_from_placement.rs │ │ ├── compute_position.rs │ │ ├── detect_overflow.rs │ │ ├── lib.rs │ │ ├── middleware.rs │ │ ├── middleware │ │ ├── arrow.rs │ │ ├── auto_placement.rs │ │ ├── flip.rs │ │ ├── hide.rs │ │ ├── inline.rs │ │ ├── offset.rs │ │ ├── shift.rs │ │ └── size.rs │ │ ├── test_utils.rs │ │ └── types.rs ├── dom │ ├── Cargo.toml │ ├── README.md │ ├── example │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── index.html │ │ └── src │ │ │ └── lib.rs │ └── src │ │ ├── auto_update.rs │ │ ├── lib.rs │ │ ├── middleware.rs │ │ ├── platform.rs │ │ ├── platform │ │ ├── convert_offset_parent_relative_rect_to_viewport_relative_rect.rs │ │ ├── get_client_length.rs │ │ ├── get_client_rects.rs │ │ ├── get_clipping_rect.rs │ │ ├── get_dimensions.rs │ │ ├── get_element_rects.rs │ │ ├── get_offset_parent.rs │ │ ├── get_scale.rs │ │ └── is_rtl.rs │ │ ├── types.rs │ │ ├── utils.rs │ │ └── utils │ │ ├── get_bounding_client_rect.rs │ │ ├── get_css_dimensions.rs │ │ ├── get_document_rect.rs │ │ ├── get_html_offset.rs │ │ ├── get_rect_relative_to_offset_parent.rs │ │ ├── get_viewport_rect.rs │ │ ├── get_visual_offsets.rs │ │ ├── get_window_scroll_bar_x.rs │ │ ├── is_static_positioned.rs │ │ └── rects_are_equal.rs ├── leptos │ ├── Cargo.toml │ ├── README.md │ ├── example │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── index.html │ │ └── src │ │ │ ├── app.rs │ │ │ └── main.rs │ ├── src │ │ ├── arrow.rs │ │ ├── lib.rs │ │ ├── node_ref.rs │ │ ├── types.rs │ │ ├── use_floating.rs │ │ ├── utils.rs │ │ └── utils │ │ │ ├── get_dpr.rs │ │ │ └── round_by_dpr.rs │ └── tests │ │ ├── README.md │ │ ├── playwright.rs │ │ └── visual │ │ ├── Cargo.toml │ │ ├── index.css │ │ ├── index.html │ │ └── src │ │ ├── app.rs │ │ ├── main.rs │ │ ├── spec.rs │ │ ├── spec │ │ ├── arrow.rs │ │ ├── auto_placement.rs │ │ ├── auto_update.rs │ │ ├── border.rs │ │ ├── containing_block.rs │ │ ├── decimal_size.rs │ │ ├── flip.rs │ │ ├── hide.rs │ │ ├── inline.rs │ │ ├── offset.rs │ │ ├── placement.rs │ │ ├── relative.rs │ │ ├── scroll.rs │ │ ├── scrollbars.rs │ │ ├── shift.rs │ │ ├── size.rs │ │ ├── table.rs │ │ ├── transform.rs │ │ └── virtual_element.rs │ │ ├── utils.rs │ │ └── utils │ │ ├── all_placements.rs │ │ ├── new.rs │ │ ├── use_resize.rs │ │ ├── use_scroll.rs │ │ └── use_size.rs ├── utils │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── dom.rs │ │ └── lib.rs └── yew │ ├── Cargo.toml │ ├── README.md │ ├── example │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ └── src │ │ ├── app.rs │ │ └── main.rs │ ├── src │ ├── arrow.rs │ ├── lib.rs │ ├── types.rs │ ├── use_auto_update.rs │ ├── use_floating.rs │ ├── utils.rs │ └── utils │ │ ├── get_dpr.rs │ │ └── round_by_dpr.rs │ └── tests │ ├── README.md │ ├── playwright.rs │ └── visual │ ├── Cargo.toml │ ├── index.css │ ├── index.html │ └── src │ ├── app.rs │ ├── main.rs │ ├── spec.rs │ ├── spec │ ├── arrow.rs │ ├── placement.rs │ └── relative.rs │ ├── utils.rs │ └── utils │ ├── all_placements.rs │ ├── new.rs │ ├── use_scroll.rs │ └── use_size.rs ├── rust-toolchain.toml ├── scripts ├── Cargo.toml └── src │ ├── bin │ └── upstream.rs │ └── lib.rs └── upstream.toml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: RustForWeb 2 | open_collective: rustforweb 3 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>RustForWeb/.github:renovate-config"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | 14 | env: 15 | RUSTFLAGS: '-Dwarnings' 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Rust toolchain 22 | run: rustup toolchain install nightly --no-self-update --profile default --target wasm32-unknown-unknown 23 | 24 | - name: Set up Rust cache 25 | uses: swatinem/rust-cache@v2 26 | with: 27 | cache-on-failure: true 28 | save-if: ${{ github.ref == 'refs/heads/main' }} 29 | 30 | - name: Check formatting 31 | run: cargo fmt --all --check 32 | 33 | - name: Lint 34 | run: cargo clippy --all-features 35 | 36 | test: 37 | name: Test 38 | runs-on: ubuntu-latest 39 | 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v4 43 | 44 | - name: Set up Rust toolchain 45 | run: rustup toolchain install nightly --no-self-update --profile default --target wasm32-unknown-unknown 46 | 47 | - name: Set up Rust cache 48 | uses: swatinem/rust-cache@v2 49 | with: 50 | cache-on-failure: true 51 | save-if: ${{ github.ref == 'refs/heads/main' }} 52 | 53 | - name: Install Cargo Binary Install 54 | uses: cargo-bins/cargo-binstall@main 55 | 56 | - name: Install Trunk 57 | run: cargo binstall --force -y trunk 58 | 59 | - name: Set up Node.js 60 | uses: actions/setup-node@v4 61 | with: 62 | node-version: 'lts/*' 63 | 64 | - name: Set up pnpm 65 | uses: pnpm/action-setup@v4 66 | with: 67 | version: 'latest' 68 | 69 | - name: Test 70 | run: cargo test --all-features 71 | 72 | - name: Upload visual snapshot diffs 73 | uses: actions/upload-artifact@v4 74 | if: always() 75 | with: 76 | name: visual-snapshots-diff 77 | path: target/tmp/floating-ui/packages/dom/test-results 78 | -------------------------------------------------------------------------------- /.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 | jobs: 16 | release: 17 | name: Release 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Generate GitHub App token 22 | id: app-token 23 | uses: getsentry/action-github-app-token@v3 24 | with: 25 | app_id: ${{ secrets.APP_ID }} 26 | private_key: ${{ secrets.APP_PRIVATE_KEY }} 27 | 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | 31 | - name: Set up Rust toolchain 32 | run: rustup toolchain install nightly --no-self-update --profile default --target wasm32-unknown-unknown 33 | 34 | - name: Set up Rust cache 35 | uses: swatinem/rust-cache@v2 36 | with: 37 | cache-on-failure: true 38 | save-if: ${{ github.ref == 'refs/heads/main' }} 39 | 40 | - name: Install Cargo Binary Install 41 | uses: cargo-bins/cargo-binstall@main 42 | 43 | - name: Install crates 44 | run: cargo binstall --force -y cargo-workspaces toml-cli 45 | 46 | - name: Bump version 47 | run: cargo workspaces version --all --no-git-commit --yes ${{ inputs.bump }} 48 | 49 | - name: Extract version 50 | id: extract-version 51 | run: echo "VERSION=v$(toml get Cargo.toml workspace.package.version --raw)" >> "$GITHUB_OUTPUT" 52 | 53 | - name: Add changes 54 | run: git add . 55 | 56 | - name: Commit 57 | uses: dsanders11/github-app-commit-action@v1 58 | with: 59 | message: ${{ steps.extract-version.outputs.VERSION }} 60 | token: ${{ steps.app-token.outputs.token }} 61 | 62 | - name: Reset and pull 63 | run: git reset --hard && git pull 64 | 65 | - name: Tag 66 | uses: bruno-fs/repo-tagger@1.0.0 67 | with: 68 | tag: ${{ steps.extract-version.outputs.VERSION }} 69 | env: 70 | GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} 71 | 72 | - name: Release 73 | uses: softprops/action-gh-release@v2 74 | with: 75 | generate_release_notes: true 76 | make_latest: true 77 | tag_name: ${{ steps.extract-version.outputs.VERSION }} 78 | token: ${{ steps.app-token.outputs.token }} 79 | 80 | - name: Publish 81 | run: cargo workspaces publish --publish-as-is --token "${{ secrets.CRATES_IO_TOKEN }}" 82 | -------------------------------------------------------------------------------- /.github/workflows/upstream.yml: -------------------------------------------------------------------------------- 1 | name: Check for upstream releases 2 | 3 | on: 4 | schedule: 5 | - cron: '00 16 * * *' 6 | workflow_dispatch: {} 7 | 8 | jobs: 9 | check: 10 | name: Check for upstream releases 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Rust toolchain 18 | run: rustup toolchain install nightly --no-self-update --profile default 19 | 20 | - name: Set up Rust cache 21 | uses: swatinem/rust-cache@v2 22 | with: 23 | cache-on-failure: true 24 | save-if: ${{ github.ref == 'refs/heads/main' }} 25 | 26 | - name: Check for upstream releases 27 | run: cargo run -p scripts --bin upstream 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | GIT_USER_NAME: github-actions[bot] 31 | GIT_USER_EMAIL: github-actions[bot]@users.noreply.github.com 32 | RUST_LOG: upstream=debug 33 | -------------------------------------------------------------------------------- /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | name: Website 2 | on: 3 | pull_request: {} 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: false 11 | 12 | jobs: 13 | book-test: 14 | name: Test Book 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Rust toolchain 20 | run: | 21 | rustup toolchain install stable --no-self-update --profile minimal 22 | rustup target add wasm32-unknown-unknown 23 | 24 | - name: Set up Rust cache 25 | uses: swatinem/rust-cache@v2 26 | with: 27 | cache-on-failure: true 28 | save-if: ${{ github.ref == 'refs/heads/main' }} 29 | 30 | - name: Install Cargo Binary Install 31 | uses: cargo-bins/cargo-binstall@main 32 | 33 | - name: Install mdBook 34 | run: cargo binstall --force -y mdbook mdbook-tabs mdbook-trunk 35 | 36 | - name: Run tests 37 | run: mdbook test 38 | working-directory: book 39 | 40 | book-build: 41 | name: Build Book 42 | needs: book-test 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v4 46 | with: 47 | fetch-depth: 0 48 | 49 | - name: Set up Rust toolchain 50 | run: | 51 | rustup toolchain install stable --no-self-update --profile minimal 52 | rustup target add wasm32-unknown-unknown 53 | 54 | - name: Set up Rust cache 55 | uses: swatinem/rust-cache@v2 56 | with: 57 | cache-on-failure: true 58 | save-if: ${{ github.ref == 'refs/heads/main' }} 59 | 60 | - name: Install Cargo Binary Install 61 | uses: cargo-bins/cargo-binstall@main 62 | 63 | - name: Install mdBook and Trunk 64 | run: cargo binstall --force -y mdbook mdbook-tabs mdbook-trunk trunk 65 | 66 | - name: Install Node.js dependencies 67 | run: npm install 68 | 69 | - name: Build Book 70 | run: mdbook build 71 | working-directory: book 72 | 73 | - name: Combine Book Outputs 74 | run: mdbook-trunk combine 75 | working-directory: book 76 | 77 | - name: Upload artifact 78 | uses: actions/upload-artifact@v4 79 | with: 80 | name: book 81 | path: book/dist 82 | retention-days: 1 83 | if-no-files-found: error 84 | 85 | deploy: 86 | name: Deploy 87 | needs: book-build 88 | if: github.ref == 'refs/heads/main' 89 | runs-on: ubuntu-latest 90 | permissions: 91 | contents: read 92 | pages: write 93 | id-token: write 94 | steps: 95 | - uses: actions/checkout@v4 96 | with: 97 | fetch-depth: 0 98 | 99 | - name: Download artifacts 100 | uses: actions/download-artifact@v4 101 | with: 102 | path: dist 103 | merge-multiple: true 104 | 105 | - name: Setup Pages 106 | uses: actions/configure-pages@v5 107 | 108 | - name: Upload artifact 109 | uses: actions/upload-pages-artifact@v3 110 | with: 111 | path: dist 112 | 113 | - name: Deploy to GitHub Pages 114 | id: deployment 115 | uses: actions/deploy-pages@v4 116 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/mirrors-prettier 3 | rev: v3.1.0 4 | hooks: 5 | - id: prettier 6 | additional_dependencies: 7 | - prettier@^3.3.3 8 | - prettier-plugin-tailwindcss@^0.6.6 9 | - repo: https://github.com/doublify/pre-commit-rust 10 | rev: v1.0 11 | hooks: 12 | - id: fmt 13 | - id: clippy 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"], 3 | "bracketSpacing": false, 4 | "printWidth": 120, 5 | "singleQuote": true, 6 | "tabWidth": 4, 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /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.4.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 | dyn_derive = "0.3.4" 23 | dyn_std = "0.3.3" 24 | floating-ui-core = { path = "./packages/core", version = "0.4.0" } 25 | floating-ui-dom = { path = "./packages/dom", version = "0.4.0" } 26 | floating-ui-leptos = { path = "./packages/leptos", version = "0.4.0" } 27 | floating-ui-utils = { path = "./packages/utils", version = "0.4.0" } 28 | floating-ui-yew = { path = "./packages/yew", version = "0.4.0" } 29 | leptos = "0.8.0" 30 | leptos_router = "0.8.0" 31 | leptos-node-ref = "0.2.0" 32 | log = "0.4.22" 33 | send_wrapper = "0.6.0" 34 | serde = { version = "1.0.209", features = ["derive"] } 35 | serde_json = "1.0.127" 36 | wasm-bindgen = "0.2.93" 37 | wasm-bindgen-test = "0.3.43" 38 | yew = "0.21.0" 39 | yew-router = "0.18.0" 40 | 41 | [workspace.dependencies.web-sys] 42 | version = "0.3.70" 43 | features = [ 44 | "css", 45 | "AddEventListenerOptions", 46 | "CssStyleDeclaration", 47 | "Document", 48 | "DomRect", 49 | "DomRectList", 50 | "Element", 51 | "Event", 52 | "EventTarget", 53 | "HtmlElement", 54 | "HtmlSlotElement", 55 | "IntersectionObserver", 56 | "IntersectionObserverEntry", 57 | "IntersectionObserverInit", 58 | "Node", 59 | "Range", 60 | "ResizeObserver", 61 | "ResizeObserverEntry", 62 | "Selection", 63 | "ShadowRoot", 64 | "Window", 65 | ] 66 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rust for Web 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Rust Floating UI Logo 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 | - [Leptos](./packages/leptos) 19 | - [Yew](https://yew.rs/) 20 | 21 | The following frameworks are under consideration: 22 | 23 | - [Dioxus](https://dioxuslabs.com/) 24 | 25 | ## Examples 26 | 27 | See [the Rust Floating UI book](https://floating-ui.rustforweb.org/) for examples. 28 | 29 | Each framework has an implementations of the [Floating UI tutorial](https://floating-ui.com/docs/tutorial) as an example: 30 | 31 | - [DOM](./packages/dom/example) 32 | - [Leptos](./packages/leptos/example) 33 | - [Yew](./packages/yew/example) 34 | 35 | Additionally, implementations of [Floating UI tests](https://github.com/floating-ui/floating-ui/tree/master/packages/dom/test) are more complex examples: 36 | 37 | - [Leptos](./packages/leptos/tests) 38 | - [Yew](./packages/yew/tests) 39 | 40 | ## Documentation 41 | 42 | See [the Rust Floating UI book](https://floating-ui.rustforweb.org/) for documentation. 43 | 44 | Documentation for the crates is available on [Docs.rs](https://docs.rs/): 45 | 46 | - [`floating-ui-core`](https://docs.rs/floating-ui-core/latest/floating_ui_core/) 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 UI libraries for Rust. All projects are free and open source. 65 | -------------------------------------------------------------------------------- /book-examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floating-ui-book" 3 | description = "Book examples for Floating UI." 4 | publish = false 5 | 6 | authors.workspace = true 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | version.workspace = true 11 | 12 | [dependencies] 13 | console_error_panic_hook.workspace = true 14 | console_log.workspace = true 15 | convert_case = "0.8.0" 16 | floating-ui-leptos.workspace = true 17 | leptos = { workspace = true, features = ["csr"] } 18 | leptos-node-ref.workspace = true 19 | log.workspace = true 20 | send_wrapper.workspace = true 21 | tailwind_fuse = "0.3.1" 22 | 23 | [features] 24 | default = ["arrow", "flip", "placement", "shift", "size", "virtual"] 25 | arrow = [] 26 | flip = [] 27 | placement = [] 28 | shift = [] 29 | size = [] 30 | virtual = [] 31 | -------------------------------------------------------------------------------- /book-examples/Trunk.toml: -------------------------------------------------------------------------------- 1 | [[hooks]] 2 | stage = "pre_build" 3 | command = "sh" 4 | command_arguments = [ 5 | "-c", 6 | "npx tailwindcss -i style/tailwind.css -o style/tailwind.output.css", 7 | ] 8 | -------------------------------------------------------------------------------- /book-examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /book-examples/main.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | const resizeObserver = new ResizeObserver(() => { 3 | if (window.top) { 4 | window.top.postMessage({ 5 | mdbookTrunk: { 6 | width: document.body.scrollWidth, 7 | height: document.body.scrollHeight 8 | } 9 | }); 10 | } 11 | }); 12 | resizeObserver.observe(document.body); 13 | }); 14 | -------------------------------------------------------------------------------- /book-examples/src/app.rs: -------------------------------------------------------------------------------- 1 | use leptos::prelude::*; 2 | 3 | #[component] 4 | pub fn App() -> impl IntoView { 5 | let mut views: Vec = vec![]; 6 | 7 | #[cfg(feature = "placement")] 8 | { 9 | use crate::positioning::placement::PlacementDemo; 10 | views.push( 11 | view! { 12 | 13 | } 14 | .into_any(), 15 | ); 16 | } 17 | #[cfg(feature = "shift")] 18 | { 19 | use crate::positioning::shift::ShiftDemo; 20 | views.push( 21 | view! { 22 | 23 | } 24 | .into_any(), 25 | ); 26 | } 27 | #[cfg(feature = "flip")] 28 | { 29 | use crate::positioning::flip::FlipDemo; 30 | views.push( 31 | view! { 32 | 33 | } 34 | .into_any(), 35 | ); 36 | } 37 | #[cfg(feature = "size")] 38 | { 39 | use crate::positioning::size::SizeDemo; 40 | views.push( 41 | view! { 42 | 43 | } 44 | .into_any(), 45 | ); 46 | } 47 | 48 | views.into_view() 49 | } 50 | -------------------------------------------------------------------------------- /book-examples/src/components.rs: -------------------------------------------------------------------------------- 1 | mod chrome; 2 | mod floating; 3 | mod grid_item; 4 | mod reference; 5 | 6 | pub use chrome::*; 7 | pub use floating::*; 8 | pub use grid_item::*; 9 | pub use reference::*; 10 | -------------------------------------------------------------------------------- /book-examples/src/components/chrome.rs: -------------------------------------------------------------------------------- 1 | // TODO: remove 2 | #![allow(unused)] 3 | 4 | use leptos::{context::Provider, html::Div, prelude::*}; 5 | use tailwind_fuse::tw_merge; 6 | 7 | #[derive(Clone, Copy, Debug, PartialEq)] 8 | pub enum Scrollable { 9 | None, 10 | X, 11 | Y, 12 | Both, 13 | } 14 | 15 | #[derive(Clone)] 16 | pub struct ChromeContext(pub NodeRef
); 17 | 18 | #[component] 19 | pub fn Chrome( 20 | #[prop(default = false.into(), into)] center: Signal, 21 | #[prop(default = Scrollable::None.into(), into)] scrollable: Signal, 22 | #[prop(default = true.into(), into)] relative: Signal, 23 | #[prop(into, optional)] label: MaybeProp, 24 | #[prop(default = 305.into(), into)] scroll_height: Signal, 25 | #[prop(default = true.into(), into)] shadow: Signal, 26 | #[prop(default = false.into(), into)] tall: Signal, 27 | children: Children, 28 | ) -> impl IntoView { 29 | let scrollable_ref: NodeRef
= NodeRef::new(); 30 | 31 | let scrollable_x = 32 | Signal::derive(move || matches!(scrollable.get(), Scrollable::X | Scrollable::Both)); 33 | let scrollable_y = 34 | Signal::derive(move || matches!(scrollable.get(), Scrollable::Y | Scrollable::Both)); 35 | let is_scrollable = Signal::derive(move || scrollable_x.get() || scrollable_y.get()); 36 | 37 | Effect::new(move |_| { 38 | if let Some(scrollable) = scrollable_ref.get() { 39 | if scrollable_y.get() { 40 | scrollable.set_scroll_top( 41 | scrollable.scroll_height() / 2 - scrollable.offset_height() / 2, 42 | ); 43 | } 44 | 45 | if scrollable_x.get() { 46 | scrollable 47 | .set_scroll_left(scrollable.scroll_width() / 2 - scrollable.offset_width() / 2); 48 | } 49 | } 50 | }); 51 | 52 | view! { 53 |
59 |
60 |
61 |
65 |
69 |
73 |
74 |
75 | {move || label.get()} 76 |
77 |
78 |
79 |
90 | 91 |
99 | 100 | 101 | {children()} 102 | 103 | 104 |
112 | 113 |
114 |
115 |
116 | } 117 | } 118 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /book-examples/src/components/grid_item.rs: -------------------------------------------------------------------------------- 1 | use leptos::prelude::*; 2 | use tailwind_fuse::tw_merge; 3 | 4 | #[component] 5 | pub fn GridItem( 6 | #[prop(into)] title: Signal, 7 | #[prop(into)] description: Signal, 8 | chrome: F, 9 | // #[prop(into)] demo_link: Signal, 10 | #[prop(default = false.into(), into)] hidden: Signal, 11 | ) -> impl IntoView 12 | where 13 | F: Fn() -> IV + 'static, 14 | IV: IntoView + 'static, 15 | { 16 | view! { 17 |
23 |
24 |

{title}

25 |

{description}

26 |
27 |
28 | {chrome()} 29 |
30 | // 36 | // CodeSandbox 37 | // 38 |
39 | } 40 | } 41 | -------------------------------------------------------------------------------- /book-examples/src/components/reference.rs: -------------------------------------------------------------------------------- 1 | use leptos::prelude::*; 2 | use leptos_node_ref::AnyNodeRef; 3 | use tailwind_fuse::tw_merge; 4 | 5 | #[component] 6 | pub fn Reference( 7 | #[prop(into, optional)] class: MaybeProp, 8 | #[prop(into, optional)] node_ref: AnyNodeRef, 9 | ) -> impl IntoView { 10 | view! { 11 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /book-examples/src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod components; 3 | mod positioning; 4 | mod utils; 5 | 6 | use leptos::prelude::*; 7 | 8 | use crate::app::App; 9 | 10 | pub fn main() { 11 | _ = console_log::init_with_level(log::Level::Debug); 12 | console_error_panic_hook::set_once(); 13 | 14 | mount_to_body(App); 15 | } 16 | -------------------------------------------------------------------------------- /book-examples/src/positioning.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "flip")] 2 | pub mod flip; 3 | #[cfg(feature = "placement")] 4 | pub mod placement; 5 | #[cfg(feature = "shift")] 6 | pub mod shift; 7 | #[cfg(feature = "size")] 8 | pub mod size; 9 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /book-examples/src/utils.rs: -------------------------------------------------------------------------------- 1 | mod rem_to_px; 2 | 3 | pub use rem_to_px::*; 4 | -------------------------------------------------------------------------------- /book-examples/src/utils/rem_to_px.rs: -------------------------------------------------------------------------------- 1 | use leptos::prelude::*; 2 | 3 | pub fn rem_to_px(value: f64) -> f64 { 4 | document() 5 | .document_element() 6 | .map(|document_element| { 7 | value 8 | * window() 9 | .get_computed_style(&document_element) 10 | .expect("Valid element.") 11 | .expect("Element should have computed style.") 12 | .get_property_value("font-size") 13 | .expect("Computed style should have font size.") 14 | .replace("px", "") 15 | .parse::() 16 | .expect("Font size should be a float.") 17 | }) 18 | .unwrap_or(value * 16.0) 19 | } 20 | -------------------------------------------------------------------------------- /book-examples/style/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Daniëlle Huisman"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Rust Floating UI" 7 | 8 | [preprocessor.tabs] 9 | 10 | [preprocessor.trunk] 11 | 12 | [output.html] 13 | additional-css = ["theme/tabs.css", "theme/theme.css", "theme/trunk.css"] 14 | additional-js = ["theme/tabs.js", "theme/theme.js", "theme/trunk.js"] 15 | edit-url-template = "https://github.com/RustForWeb/floating-ui/edit/main/book/{path}" 16 | git-repository-url = "https://github.com/RustForWeb/floating-ui" 17 | 18 | [output.trunk] 19 | serve = true 20 | 21 | [rust] 22 | edition = "2024" 23 | -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./introduction.md) 4 | - [Examples](./examples.md) 5 | - [Tutorial]() 6 | - [Compute Position](./compute-position.md) 7 | - [Auto Update](./auto-update.md) 8 | - [Middleware](./middleware/README.md) 9 | - [Arrow](./middleware/arrow.md) 10 | - [Auto Placement](./middleware/auto-placement.md) 11 | - [Flip](./middleware/flip.md) 12 | - [Hide](./middleware/hide.md) 13 | - [Inline](./middleware/inline.md) 14 | - [Offset](./middleware/offset.md) 15 | - [Shift](./middleware/shift.md) 16 | - [Size](./middleware/size.md) 17 | - [Detect Overflow](./detect-overflow.md) 18 | - [Virtual Elements](./virtual-elements.md) 19 | - [Platform](./platform.md) 20 | - [Frameworks](./frameworks/README.md) 21 | - [DOM](./frameworks/dom.md) 22 | - [Leptos](./frameworks/leptos.md) 23 | - [Yew](./frameworks/yew.md) 24 | - [Contributing]() 25 | -------------------------------------------------------------------------------- /book/src/detect-overflow.md: -------------------------------------------------------------------------------- 1 | # Detect Overflow 2 | 3 | Detects when the floating or reference element is overflowing a clipping container or custom boundary. 4 | 5 | TODO 6 | 7 | ## See Also 8 | 9 | - [Floating UI documentation](https://floating-ui.com/docs/detectOverflow) 10 | -------------------------------------------------------------------------------- /book/src/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Smart Anchor Positioning 4 | 5 | Anchor a floating element next to another element while making sure it stays in view by **avoiding collisions**. This lets you position tooltips, popovers, or dropdowns optimally. 6 | 7 | ```toml,trunk 8 | package = "floating-ui-book" 9 | features = ["arrow", "flip", "placement", "shift", "size", "virtual"] 10 | ``` 11 | -------------------------------------------------------------------------------- /book/src/frameworks/README.md: -------------------------------------------------------------------------------- 1 | # Frameworks 2 | 3 | Rust Floating UI is available for the following frameworks: 4 | 5 | - [DOM (`web-sys`)](./dom.md) 6 | - [Leptos](./leptos.md) 7 | - [Yew](./yew.md) 8 | -------------------------------------------------------------------------------- /book/src/frameworks/dom.md: -------------------------------------------------------------------------------- 1 | # DOM 2 | 3 | This package provides [DOM (`web-sys`)](https://rustwasm.github.io/wasm-bindgen/web-sys/index.html) bindings for `floating-ui-core` - a library that provides anchor positioning for a floating element to position it next to a given reference element. 4 | 5 | ## Installation 6 | 7 | ```shell 8 | cargo add floating-ui-dom 9 | ``` 10 | 11 | - [View on crates.io](https://crates.io/crates/floating-ui-dom) 12 | - [View on docs.rs](https://docs.rs/floating-ui-dom/latest/floating_ui_dom/) 13 | - [View source](https://github.com/RustForWeb/floating-ui/tree/main/packages/dom) 14 | 15 | ## Usage 16 | 17 | TODO 18 | -------------------------------------------------------------------------------- /book/src/frameworks/yew.md: -------------------------------------------------------------------------------- 1 | # Yew 2 | 3 | This package provides [Yew](https://yew.rs/) bindings for `floating-ui-dom` - a library that provides anchor positioning for a floating element to position it next to a given reference element. 4 | 5 | ## Installation 6 | 7 | ```shell 8 | cargo add floating-ui-yew 9 | ``` 10 | 11 | - [View on crates.io](https://crates.io/crates/floating-ui-yew) 12 | - [View on docs.rs](https://docs.rs/floating-ui-yew/latest/floating_ui_yew/) 13 | - [View source](https://github.com/RustForWeb/floating-ui/tree/main/packages/yew) 14 | 15 | ## Usage 16 | 17 | TODO 18 | -------------------------------------------------------------------------------- /book/src/introduction.md: -------------------------------------------------------------------------------- 1 |

2 | Rust Floating UI Logo 3 |

4 | 5 | # Introduction 6 | 7 | Rust Floating UI is a Rust port of [Floating UI](https://floating-ui.com/). 8 | 9 | [Floating UI](https://floating-ui.com) is a library that helps you create “floating” elements such as tooltips, popovers, dropdowns, and more. 10 | 11 | It provides a toolkit of positioning features that let you robustly anchor an absolutely-positioned floating element next to a given reference element. For example, a popover floats next to and remains anchored to its triggering button, even while the page scrolls. 12 | 13 | It also provides features to avoid collisions with the viewport, as absolute positioning often leads to unwanted overflow depending on the location of the positioning reference. 14 | 15 | ## Frameworks 16 | 17 | Rust Floating UI is available for the following frameworks: 18 | 19 | - [DOM (`web-sys`)](https://rustwasm.github.io/wasm-bindgen/web-sys/index.html) 20 | - [Leptos](https://leptos.dev/) 21 | - [Yew](https://yew.rs/) 22 | 23 | The following frameworks are under consideration: 24 | 25 | - [Dioxus](https://dioxuslabs.com/) 26 | 27 | See [Frameworks](./frameworks/index.md) for documentation for each framework. 28 | 29 | ## Examples 30 | 31 | See [Examples](./examples.md). 32 | 33 | ## License 34 | 35 | This project is available under the [MIT license](https://github.com/RustForWeb/floating-ui/blob/main/LICENSE.md). 36 | 37 | ## Rust for Web 38 | 39 | The Rust Floating UI project is part of [Rust for Web](https://github.com/RustForWeb). 40 | 41 | [Rust for Web](https://github.com/RustForWeb) creates and ports web UI libraries for Rust. All projects are free and open source. 42 | -------------------------------------------------------------------------------- /book/src/middleware/arrow.md: -------------------------------------------------------------------------------- 1 | # Arrow 2 | 3 | Provides positioning data for an arrow element (triangle or caret) inside the floating element, such that it appears to be pointing toward the center of the reference element. 4 | 5 | This is useful to add an additional visual cue to the floating element about which element it is referring to. 6 | 7 | TODO 8 | 9 | ## See Also 10 | 11 | - [Floating UI documentation](https://floating-ui.com/docs/arrow) 12 | -------------------------------------------------------------------------------- /book/src/middleware/auto-placement.md: -------------------------------------------------------------------------------- 1 | # Auto Placement 2 | 3 | Chooses the placement that has the most space available automatically. 4 | 5 | This is useful when you don't know which placement will be best for the floating element, or don't want to have to explicitly specify it. 6 | 7 | TODO 8 | 9 | ## See Also 10 | 11 | - [Floating UI documentation](https://floating-ui.com/docs/autoPlacement) 12 | -------------------------------------------------------------------------------- /book/src/middleware/flip.md: -------------------------------------------------------------------------------- 1 | # Flip 2 | 3 | Changes the placement of the floating element to keep it in view. 4 | 5 | This prevents the floating element from overflowing along its side axis by flipping it to the opposite side by default. 6 | 7 | TODO 8 | 9 | ## See Also 10 | 11 | - [Floating UI documentation](https://floating-ui.com/docs/flip) 12 | -------------------------------------------------------------------------------- /book/src/middleware/hide.md: -------------------------------------------------------------------------------- 1 | # Hide 2 | 3 | A data provider that allows you to hide the floating element in applicable situations. 4 | 5 | This is useful for situations where you want to hide the floating element because it appears detached from the reference element (or attached to nothing). 6 | 7 | TODO 8 | 9 | ## See Also 10 | 11 | - [Floating UI documentation](https://floating-ui.com/docs/hide) 12 | -------------------------------------------------------------------------------- /book/src/middleware/inline.md: -------------------------------------------------------------------------------- 1 | # Inline 2 | 3 | Improves positioning for inline reference elements that span over multiple lines. 4 | 5 | This is useful for reference elements such as hyperlinks or range selections, as the default positioning using `getBoundingClientRect()` may appear “detached” when measuring over the bounding box. 6 | 7 | TODO 8 | 9 | ## See Also 10 | 11 | - [Floating UI documentation](https://floating-ui.com/docs/inline) 12 | -------------------------------------------------------------------------------- /book/src/middleware/offset.md: -------------------------------------------------------------------------------- 1 | # Offset 2 | 3 | Translates the floating element along the specified axes. 4 | 5 | This lets you add distance (margin or spacing) between the reference and floating element, slightly alter the placement, or even create 6 | custom placements. 7 | 8 | Type: `Placement Modifier` 9 | 10 | 11 | 12 | ## Usage 13 | 14 | {{#tabs global="package" }} 15 | {{#tab name="Core" }} 16 | 17 | ```rust,ignore 18 | use floating_ui_core::{compute_position, ComputePositionConfig, Offset, OffsetOptions}; 19 | 20 | compute_position( 21 | reference_el, 22 | floating_el, 23 | ComputePositionConfig::new(platform) 24 | .middleware(vec![ 25 | Box::new(Offset::new(OffsetOptions::default())), 26 | ]), 27 | ); 28 | ``` 29 | 30 | {{#endtab }} 31 | {{#tab name="DOM" }} 32 | 33 | ```rust,ignore 34 | use floating_ui_dom::{compute_position, ComputePositionConfig, Offset, OffsetOptions}; 35 | 36 | compute_position( 37 | reference_el, 38 | floating_el, 39 | ComputePositionConfig::default() 40 | .middleware(vec![ 41 | Box::new(Offset::new(OffsetOptions::default())), 42 | ]), 43 | ); 44 | ``` 45 | 46 | {{#endtab }} 47 | {{#tab name="Leptos" }} 48 | 49 | ```rust,ignore 50 | use floating_ui_leptos::{use_floating, Offset, OffsetOptions, UseFloatingOptions}; 51 | 52 | use_floating( 53 | reference_el, 54 | floating_el, 55 | UseFloatingOptions::default() 56 | .middleware(vec![ 57 | Box::new(Offset::new(OffsetOptions::default())), 58 | ].into()), 59 | ); 60 | ``` 61 | 62 | {{#endtab }} 63 | {{#tab name="Yew" }} 64 | 65 | ```rust,ignore 66 | use floating_ui_yew::{use_floating, Offset, OffsetOptions, UseFloatingOptions}; 67 | 68 | use_floating( 69 | reference_el, 70 | floating_el, 71 | UseFloatingOptions::default() 72 | .middleware(vec![ 73 | Box::new(Offset::new(OffsetOptions::default())), 74 | ]), 75 | ); 76 | ``` 77 | 78 | {{#endtab }} 79 | {{#endtabs }} 80 | 81 | The value(s) passed are [logical](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_logical_properties_and_values), meaning their effect on the physical result is dependent on the placement, writing direction (e.g. RTL), or alignment. 82 | 83 | ## Order 84 | 85 | `Offset` should generally be placed at the beginning of your middleware vector. 86 | 87 | ## Options 88 | 89 | These are the options you can pass to `Offset`. 90 | 91 | ```rust,ignore 92 | pub enum OffsetOptions { 93 | Value(f64), 94 | Values(OffsetOptionsValues), 95 | } 96 | 97 | pub struct OffsetOptionsValues { 98 | pub main_axis: Option, 99 | pub cross_axis: Option, 100 | pub alignment_axis: Option, 101 | } 102 | ``` 103 | 104 | A single number represents the distance (gutter or margin) between the floating element and the reference element. This is shorthand for `main_axis`. 105 | 106 | ```rust,ignore 107 | Offset::new(OffsetOptions::Value(10.0)) 108 | ``` 109 | 110 | A struct instance can also be passed, which enables you to individually configure each axis. 111 | 112 | ### `main_axis` 113 | 114 | Default: `0.0` 115 | 116 | The axis that runs along the side of the floating element. Represents the distance (gutter or margin) between the floating element and the reference element. 117 | 118 | ```rust,ignore 119 | Offset::new(OffsetOptions::Values( 120 | OffsetOptionsValues::default().main_axis(10.0) 121 | )) 122 | ``` 123 | 124 | 125 | 126 | ### `cross_axis` 127 | 128 | Default: `0.0` 129 | 130 | The axis that runs along the alignment of the floating element. Represents the skidding between the floating element and the reference element. 131 | 132 | ```rust,ignore 133 | Offset::new(OffsetOptions::Values( 134 | OffsetOptionsValues::default().cross_axis(20.0) 135 | )) 136 | ``` 137 | 138 | 139 | 140 | ### `alignment_axis` 141 | 142 | Default: `None` 143 | 144 | The same axis as `cross_axis` but applies only to aligned placements and inverts the `End` alignment. When set to a number, it overrides the `cross_axis` value. 145 | 146 | A positive number will move the floating element in the direction of the opposite edge to the one that is aligned, while a negative number the reverse. 147 | 148 | ```rust,ignore 149 | Offset::new(OffsetOptions::Values( 150 | OffsetOptionsValues::default().alignment_axis(20.0) 151 | )) 152 | ``` 153 | 154 | 155 | 156 | 165 | 166 | ## See Also 167 | 168 | - [Floating UI documentation](https://floating-ui.com/docs/offset) 169 | -------------------------------------------------------------------------------- /book/src/middleware/size.md: -------------------------------------------------------------------------------- 1 | # Size 2 | 3 | Provides data to change the size of a floating element. 4 | 5 | This is useful to ensure the floating element isn't too big to fit in the viewport (or more specifically, its clipping context), especially when a maximum size isn't specified. It also allows matching the width/height of the reference element. 6 | 7 | TODO 8 | 9 | ## See Also 10 | 11 | - [Floating UI documentation](https://floating-ui.com/docs/size) 12 | -------------------------------------------------------------------------------- /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="Leptos" }} 45 | 46 | ```rust,ignore 47 | use_floating(virtual_el.into(), floating_el, UseFloatingOptions::default()) 48 | ``` 49 | 50 | {{#endtab }} 51 | {{#tab name="Yew" }} 52 | 53 | ```rust,ignore 54 | use_floating(virtual_el.into(), floating_el, UseFloatingOptions::default()) 55 | ``` 56 | 57 | {{#endtab }} 58 | {{#endtabs }} 59 | 60 | ### `get_bounding_client_rect` 61 | 62 | The most basic virtual element is a plain object that has a `get_bounding_client_rect` method, which mimics a real element's one: 63 | 64 | ```rust,ignore 65 | // A virtual element that is 20 x 20 px starting from (0, 0) 66 | let virtual_el: Box> = Box::new( 67 | DefaultVirtualElement::new(Rc::new(|| { 68 | ClientRectObject { 69 | x: 0.0, 70 | y: 0.0, 71 | top: 0.0, 72 | left: 0.0, 73 | bottom: 20.0, 74 | right: 20.0, 75 | width: 20.0, 76 | height: 20.0, 77 | } 78 | })) 79 | ); 80 | ``` 81 | 82 | 83 | 84 | 85 | ### `context_element` 86 | 87 | 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. 88 | 89 | ```rust,ignore 90 | let virtual_el: Box> = Box::new( 91 | DefaultVirtualElement::new(get_bounding_client_rect) 92 | .context_element( 93 | web_sys::window() 94 | .expext("Window should exist.") 95 | .document() 96 | .expect("Document should exist.") 97 | .query_selector("#context") 98 | .expect("Document should be queried.") 99 | .expect("Element should exist."), 100 | ), 101 | ); 102 | ``` 103 | 104 | ### `get_client_rects` 105 | 106 | This option is useful when using [range selections](https://developer.mozilla.org/en-US/docs/Web/API/Range) and the `Inline` middleware. 107 | 108 | ```rust,ignore 109 | let virtual_el: Box> = Box::new( 110 | DefaultVirtualElement::new(|| range.get_bounding_client_rect().into()) 111 | .get_client_rects(|| ClientRectObject::from_dom_rect_list( 112 | range.get_client_rects().expect("Range should have client rects."), 113 | )), 114 | ); 115 | ``` 116 | 117 | ## See Also 118 | 119 | - [Floating UI documentation](https://floating-ui.com/docs/virtual-elements) 120 | -------------------------------------------------------------------------------- /book/theme/tabs.css: -------------------------------------------------------------------------------- 1 | .mdbook-tabs { 2 | display: flex; 3 | } 4 | 5 | .mdbook-tab { 6 | background-color: var(--table-alternate-bg); 7 | padding: 0.5rem 1rem; 8 | cursor: pointer; 9 | border: none; 10 | font-size: 1.6rem; 11 | line-height: 1.45em; 12 | } 13 | 14 | .mdbook-tab.active { 15 | background-color: var(--table-header-bg); 16 | font-weight: bold; 17 | } 18 | 19 | .mdbook-tab-content { 20 | padding: 1rem 0rem; 21 | } 22 | 23 | .mdbook-tab-content table { 24 | margin: unset; 25 | } 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /book/theme/theme.css: -------------------------------------------------------------------------------- 1 | table { 2 | margin: unset; 3 | } 4 | -------------------------------------------------------------------------------- /book/theme/theme.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('message', (event) => { 2 | if (!event.data.mdbookTrunk) { 3 | return; 4 | } 5 | 6 | const data = event.data.mdbookTrunk; 7 | const iframe = Array.from(document.getElementsByTagName('iframe')).find( 8 | (iframe) => iframe.contentWindow === event.source 9 | ); 10 | if (iframe) { 11 | iframe.style.height = `${data.height}px`; 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /book/theme/trunk.css: -------------------------------------------------------------------------------- 1 | .mdbook-trunk-iframe { 2 | display: block; 3 | width: 100%; 4 | border: 0.1em solid var(--quote-border); 5 | } 6 | 7 | .mdbook-trunk-files-container { 8 | width: 100%; 9 | border: 0.1em solid var(--quote-border); 10 | border-top: 0em; 11 | background-color: var(--quote-border); 12 | } 13 | 14 | .mdbook-trunk-files { 15 | display: flex; 16 | } 17 | 18 | .mdbook-trunk-file { 19 | background-color: var(--table-alternate-bg); 20 | padding: 0.5rem 1rem; 21 | cursor: pointer; 22 | border: none; 23 | font-size: 1.6rem; 24 | line-height: 1.45em; 25 | } 26 | 27 | .mdbook-trunk-file.active { 28 | background-color: var(--table-header-bg); 29 | font-weight: bold; 30 | } 31 | 32 | .mdbook-trunk-file-content > pre { 33 | margin: 0rem; 34 | } 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rustforweb/floating-ui", 3 | "description": "Rust port of Floating UI.", 4 | "author": "Rust for Web ", 5 | "repository": "github:RustForWeb/floating-ui", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": {}, 9 | "devDependencies": { 10 | "prettier": "^3.3.3", 11 | "prettier-plugin-tailwindcss": "^0.6.6", 12 | "tailwindcss": "^3.4.10" 13 | }, 14 | "engines": { 15 | "node": ">=18" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floating-ui-core" 3 | description = "Rust port of Floating UI. Positioning library for floating elements: tooltips, popovers, dropdowns, and more." 4 | homepage = "https://floating-ui.rustforweb.org" 5 | 6 | authors.workspace = true 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | version.workspace = true 11 | 12 | [dependencies] 13 | dyn_derive.workspace = true 14 | dyn_std.workspace = true 15 | floating-ui-utils.workspace = true 16 | serde.workspace = true 17 | serde_json.workspace = true 18 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Rust Floating UI Logo 4 | 5 |

6 | 7 |

floating-ui-core

8 | 9 | This is the platform-agnostic core of Floating UI, exposing the main `compute_position` function but no platform interface logic. 10 | 11 | [Rust Floating UI](https://github.com/RustForWeb/floating-ui) is a Rust port of [Floating UI](https://floating-ui.com). 12 | 13 | ## Documentation 14 | 15 | See [the Rust Floating UI book](https://floating-ui.rustforweb.org/) for documentation. 16 | 17 | ## Rust for Web 18 | 19 | The Rust Floating UI project is part of [Rust for Web](https://github.com/RustForWeb). 20 | 21 | [Rust for Web](https://github.com/RustForWeb) creates and ports web UI libraries for Rust. All projects are free and open source. 22 | -------------------------------------------------------------------------------- /packages/core/src/compute_coords_from_placement.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::{ 2 | Alignment, Axis, Coords, ElementRects, Placement, Side, get_alignment, get_alignment_axis, 3 | get_axis_length, get_side, get_side_axis, 4 | }; 5 | 6 | /// Computes the `x` and `y` coordinates that will place the floating element next to a given reference element based on a `placement`. 7 | pub fn compute_coords_from_placement( 8 | ElementRects { 9 | reference, 10 | floating, 11 | }: &ElementRects, 12 | placement: Placement, 13 | rtl: Option, 14 | ) -> Coords { 15 | let side_axis = get_side_axis(placement); 16 | let alignment_axis = get_alignment_axis(placement); 17 | let align_length = get_axis_length(alignment_axis); 18 | let side = get_side(placement); 19 | let is_vertical = side_axis == Axis::Y; 20 | 21 | let common_x = reference.x + reference.width / 2.0 - floating.width / 2.0; 22 | let common_y = reference.y + reference.height / 2.0 - floating.height / 2.0; 23 | let common_align = reference.length(align_length) / 2.0 - floating.length(align_length) / 2.0; 24 | 25 | let mut coords = match side { 26 | Side::Top => Coords { 27 | x: common_x, 28 | y: reference.y - floating.height, 29 | }, 30 | Side::Right => Coords { 31 | x: reference.x + reference.width, 32 | y: common_y, 33 | }, 34 | Side::Bottom => Coords { 35 | x: common_x, 36 | y: reference.y + reference.height, 37 | }, 38 | Side::Left => Coords { 39 | x: reference.x - floating.width, 40 | y: common_y, 41 | }, 42 | }; 43 | 44 | let rtl = rtl.unwrap_or(false); 45 | match get_alignment(placement) { 46 | Some(Alignment::Start) => { 47 | coords.update_axis(alignment_axis, |value| { 48 | value - common_align * (if rtl && is_vertical { -1.0 } else { 1.0 }) 49 | }); 50 | } 51 | Some(Alignment::End) => { 52 | coords.update_axis(alignment_axis, |value| { 53 | value + common_align * (if rtl && is_vertical { -1.0 } else { 1.0 }) 54 | }); 55 | } 56 | None => {} 57 | } 58 | 59 | coords 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use floating_ui_utils::Rect; 65 | 66 | use super::*; 67 | 68 | const ELEMENT_RECTS: ElementRects = ElementRects { 69 | reference: Rect { 70 | x: 0.0, 71 | y: 0.0, 72 | width: 100.0, 73 | height: 100.0, 74 | }, 75 | floating: Rect { 76 | x: 0.0, 77 | y: 0.0, 78 | width: 50.0, 79 | height: 50.0, 80 | }, 81 | }; 82 | 83 | #[test] 84 | fn test_top() { 85 | assert_eq!( 86 | compute_coords_from_placement(&ELEMENT_RECTS, Placement::Top, None), 87 | Coords { x: 25.0, y: -50.0 } 88 | ) 89 | } 90 | 91 | #[test] 92 | fn test_top_start() { 93 | assert_eq!( 94 | compute_coords_from_placement(&ELEMENT_RECTS, Placement::TopStart, None), 95 | Coords { x: 0.0, y: -50.0 } 96 | ) 97 | } 98 | 99 | #[test] 100 | fn test_top_end() { 101 | assert_eq!( 102 | compute_coords_from_placement(&ELEMENT_RECTS, Placement::TopEnd, None), 103 | Coords { x: 50.0, y: -50.0 } 104 | ) 105 | } 106 | 107 | #[test] 108 | fn test_right() { 109 | assert_eq!( 110 | compute_coords_from_placement(&ELEMENT_RECTS, Placement::Right, None), 111 | Coords { x: 100.0, y: 25.0 } 112 | ) 113 | } 114 | 115 | #[test] 116 | fn test_right_start() { 117 | assert_eq!( 118 | compute_coords_from_placement(&ELEMENT_RECTS, Placement::RightStart, None), 119 | Coords { x: 100.0, y: 0.0 } 120 | ) 121 | } 122 | 123 | #[test] 124 | fn test_right_end() { 125 | assert_eq!( 126 | compute_coords_from_placement(&ELEMENT_RECTS, Placement::RightEnd, None), 127 | Coords { x: 100.0, y: 50.0 } 128 | ) 129 | } 130 | 131 | #[test] 132 | fn test_bottom() { 133 | assert_eq!( 134 | compute_coords_from_placement(&ELEMENT_RECTS, Placement::Bottom, None), 135 | Coords { x: 25.0, y: 100.0 } 136 | ) 137 | } 138 | 139 | #[test] 140 | fn test_bottom_start() { 141 | assert_eq!( 142 | compute_coords_from_placement(&ELEMENT_RECTS, Placement::BottomStart, None), 143 | Coords { x: 0.0, y: 100.0 } 144 | ) 145 | } 146 | 147 | #[test] 148 | fn test_bottom_end() { 149 | assert_eq!( 150 | compute_coords_from_placement(&ELEMENT_RECTS, Placement::BottomEnd, None), 151 | Coords { x: 50.0, y: 100.0 } 152 | ) 153 | } 154 | 155 | #[test] 156 | fn test_left() { 157 | assert_eq!( 158 | compute_coords_from_placement(&ELEMENT_RECTS, Placement::Left, None), 159 | Coords { x: -50.0, y: 25.0 } 160 | ) 161 | } 162 | 163 | #[test] 164 | fn test_left_start() { 165 | assert_eq!( 166 | compute_coords_from_placement(&ELEMENT_RECTS, Placement::LeftStart, None), 167 | Coords { x: -50.0, y: 0.0 } 168 | ) 169 | } 170 | 171 | #[test] 172 | fn test_left_end() { 173 | assert_eq!( 174 | compute_coords_from_placement(&ELEMENT_RECTS, Placement::LeftEnd, None), 175 | Coords { x: -50.0, y: 50.0 } 176 | ) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /packages/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Rust port of [Floating UI](https://floating-ui.com/). 2 | //! 3 | //! This is the platform-agnostic core of Floating UI, exposing the main [`compute_position`][`crate::compute_position::compute_position()`] function but no platform interface logic. 4 | //! 5 | //! See [the Rust Floating UI book](https://floating-ui.rustforweb.org/) for more documenation. 6 | //! 7 | //! See [@floating-ui/core](https://www.npmjs.com/package/@floating-ui/core) for the original package. 8 | 9 | mod compute_coords_from_placement; 10 | mod compute_position; 11 | mod detect_overflow; 12 | pub mod middleware; 13 | mod types; 14 | 15 | #[cfg(test)] 16 | mod test_utils; 17 | 18 | pub use compute_coords_from_placement::*; 19 | pub use compute_position::*; 20 | pub use detect_overflow::*; 21 | pub use types::*; 22 | -------------------------------------------------------------------------------- /packages/core/src/middleware.rs: -------------------------------------------------------------------------------- 1 | //! Middleware implementations for [`compute_position`][`crate::compute_position::compute_position`]. 2 | //! 3 | //! See [the Rust Floating UI book](https://floating-ui.rustforweb.org/middleware/index.html) for more documentation. 4 | 5 | mod arrow; 6 | mod auto_placement; 7 | mod flip; 8 | mod hide; 9 | mod inline; 10 | mod offset; 11 | mod shift; 12 | mod size; 13 | 14 | pub use arrow::*; 15 | pub use auto_placement::*; 16 | pub use flip::*; 17 | pub use hide::*; 18 | pub use inline::*; 19 | pub use offset::*; 20 | pub use shift::*; 21 | pub use size::*; 22 | -------------------------------------------------------------------------------- /packages/core/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::{Dimensions, ElementRects, Rect}; 2 | 3 | use crate::types::{GetClippingRectArgs, GetElementRectsArgs, Platform}; 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct Element {} 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct Window {} 10 | 11 | pub const REFERENCE: Element = Element {}; 12 | pub const FLOATING: Element = Element {}; 13 | pub const REFERENCE_RECT: Rect = Rect { 14 | x: 0.0, 15 | y: 0.0, 16 | width: 100.0, 17 | height: 100.0, 18 | }; 19 | pub const FLOATING_RECT: Rect = Rect { 20 | x: 0.0, 21 | y: 0.0, 22 | width: 50.0, 23 | height: 50.0, 24 | }; 25 | 26 | #[derive(Debug)] 27 | pub struct TestPlatform {} 28 | 29 | impl Platform for TestPlatform { 30 | fn get_element_rects(&self, _args: GetElementRectsArgs) -> ElementRects { 31 | ElementRects { 32 | reference: REFERENCE_RECT, 33 | floating: FLOATING_RECT, 34 | } 35 | } 36 | 37 | fn get_clipping_rect(&self, _args: GetClippingRectArgs) -> Rect { 38 | Rect { 39 | x: 0.0, 40 | y: 0.0, 41 | width: 1000.0, 42 | height: 1000.0, 43 | } 44 | } 45 | 46 | fn get_dimensions(&self, _element: &Element) -> Dimensions { 47 | Dimensions { 48 | width: 10.0, 49 | height: 10.0, 50 | } 51 | } 52 | } 53 | 54 | pub const PLATFORM: TestPlatform = TestPlatform {}; 55 | -------------------------------------------------------------------------------- /packages/dom/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floating-ui-dom" 3 | description = "Rust port of Floating UI. Floating UI for the web." 4 | homepage = "https://floating-ui.rustforweb.org" 5 | 6 | authors.workspace = true 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | version.workspace = true 11 | 12 | [dependencies] 13 | floating-ui-core.workspace = true 14 | floating-ui-utils = { workspace = true, features = ["dom"] } 15 | web-sys.workspace = true 16 | -------------------------------------------------------------------------------- /packages/dom/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Rust Floating UI Logo 4 | 5 |

6 | 7 |

floating-ui-dom

8 | 9 | This is the library to use Floating UI on the web, wrapping `floating-ui-core` with DOM interface logic. 10 | 11 | [Rust Floating UI](https://github.com/RustForWeb/floating-ui) is a Rust port of [Floating UI](https://floating-ui.com). 12 | 13 | ## Documentation 14 | 15 | See [the Rust Floating UI book](https://floating-ui.rustforweb.org/) for documentation. 16 | 17 | ## Rust for Web 18 | 19 | The Rust Floating UI project is part of [Rust for Web](https://github.com/RustForWeb). 20 | 21 | [Rust for Web](https://github.com/RustForWeb) creates and ports web UI libraries for Rust. All projects are free and open source. 22 | -------------------------------------------------------------------------------- /packages/dom/example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floating-ui-dom-example" 3 | description = "Example for Floating UI DOM." 4 | publish = false 5 | 6 | authors.workspace = true 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | version.workspace = true 11 | 12 | [lib] 13 | crate-type = ["cdylib"] 14 | 15 | [dependencies] 16 | console_error_panic_hook.workspace = true 17 | console_log.workspace = true 18 | floating-ui-dom.workspace = true 19 | log.workspace = true 20 | wasm-bindgen.workspace = true 21 | web-sys.workspace = true 22 | -------------------------------------------------------------------------------- /packages/dom/example/README.md: -------------------------------------------------------------------------------- 1 | # Floating UI DOM Example 2 | 3 | Implementation of the [Floating UI tutorial](https://floating-ui.com/docs/tutorial) with [Floating UI DOM](../). 4 | 5 | ## Installation 6 | 7 | ```sh 8 | # Install Trunk 9 | cargo install trunk 10 | ``` 11 | 12 | ## Development 13 | 14 | ```sh 15 | # Start development server 16 | trunk serve 17 | ``` 18 | -------------------------------------------------------------------------------- /packages/dom/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Floating UI DOM Example 4 | 5 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /packages/dom/example/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use floating_ui_dom::{ 4 | ARROW_NAME, Arrow, ArrowData, ArrowOptions, ComputePositionConfig, ComputePositionReturn, 5 | DetectOverflowOptions, Flip, FlipOptions, Offset, OffsetOptions, Padding, Placement, Shift, 6 | ShiftOptions, Side, compute_position, 7 | }; 8 | use wasm_bindgen::prelude::*; 9 | use web_sys::{Element, HtmlElement}; 10 | 11 | #[wasm_bindgen(start)] 12 | fn run() -> Result<(), JsValue> { 13 | console_log::init_with_level(log::Level::Debug).expect("Console logger should be available"); 14 | console_error_panic_hook::set_once(); 15 | 16 | let window = web_sys::window().expect("Window should exist."); 17 | let document = window.document().expect("Window should have document."); 18 | 19 | let button = Rc::new( 20 | document 21 | .get_element_by_id("button") 22 | .expect("Button should exist.") 23 | .unchecked_into::(), 24 | ); 25 | let tooltip = Rc::new( 26 | document 27 | .get_element_by_id("tooltip") 28 | .expect("Tooltip should exist.") 29 | .unchecked_into::(), 30 | ); 31 | let arrow = Rc::new( 32 | document 33 | .get_element_by_id("arrow") 34 | .expect("Arrow should exist.") 35 | .unchecked_into::(), 36 | ); 37 | 38 | fn update( 39 | button: &HtmlElement, 40 | tooltip: &HtmlElement, 41 | arrow: &HtmlElement, 42 | ) -> Result<(), JsValue> { 43 | let button_element: &Element = button; 44 | 45 | let ComputePositionReturn { 46 | x, 47 | y, 48 | placement, 49 | middleware_data, 50 | .. 51 | } = compute_position( 52 | button_element.into(), 53 | tooltip, 54 | ComputePositionConfig::default() 55 | .placement(Placement::Top) 56 | .middleware(vec![ 57 | Box::new(Offset::new(OffsetOptions::Value(6.0))), 58 | Box::new(Flip::new(FlipOptions::default())), 59 | Box::new(Shift::new(ShiftOptions::default().detect_overflow( 60 | DetectOverflowOptions::default().padding(Padding::All(5.0)), 61 | ))), 62 | Box::new(Arrow::new(ArrowOptions::new(arrow.clone().into()))), 63 | ]), 64 | ); 65 | 66 | let arrow_data: Option = middleware_data.get_as(ARROW_NAME); 67 | if let Some(arrow_data) = arrow_data { 68 | let static_side = placement.side().opposite(); 69 | 70 | let arrow_x = arrow_data.x.map_or(String::new(), |x| format!("{x}px")); 71 | let arrow_y = arrow_data.y.map_or(String::new(), |y| format!("{y}px")); 72 | 73 | let style = arrow.style(); 74 | style.set_property( 75 | "left", 76 | match static_side { 77 | Side::Left => "-4px", 78 | _ => &arrow_x, 79 | }, 80 | )?; 81 | style.set_property( 82 | "top", 83 | match static_side { 84 | Side::Top => "-4px", 85 | _ => &arrow_y, 86 | }, 87 | )?; 88 | style.set_property( 89 | "right", 90 | match static_side { 91 | Side::Right => "-4px", 92 | _ => "", 93 | }, 94 | )?; 95 | style.set_property( 96 | "bottom", 97 | match static_side { 98 | Side::Bottom => "-4px", 99 | _ => "", 100 | }, 101 | )?; 102 | } 103 | 104 | let style = tooltip.style(); 105 | style.set_property("left", &format!("{x}px"))?; 106 | style.set_property("top", &format!("{y}px"))?; 107 | 108 | Ok(()) 109 | } 110 | 111 | { 112 | let show_tooltip = Closure::::new({ 113 | let button = button.clone(); 114 | let tooltip = tooltip.clone(); 115 | let arrow = arrow.clone(); 116 | 117 | move || { 118 | tooltip.style().set_property("display", "block").unwrap(); 119 | update(&button, &tooltip, &arrow).unwrap(); 120 | } 121 | }); 122 | 123 | button.add_event_listener_with_callback( 124 | "mouseenter", 125 | show_tooltip.as_ref().unchecked_ref(), 126 | )?; 127 | button.add_event_listener_with_callback("focus", show_tooltip.as_ref().unchecked_ref())?; 128 | 129 | show_tooltip.forget(); 130 | } 131 | 132 | { 133 | let hide_tooltip = Closure::::new({ 134 | let tooltip = tooltip.clone(); 135 | 136 | move || { 137 | tooltip.style().set_property("display", "none").unwrap(); 138 | } 139 | }); 140 | 141 | button.add_event_listener_with_callback( 142 | "mouseleave", 143 | hide_tooltip.as_ref().unchecked_ref(), 144 | )?; 145 | button.add_event_listener_with_callback("blur", hide_tooltip.as_ref().unchecked_ref())?; 146 | 147 | hide_tooltip.forget(); 148 | } 149 | 150 | Ok(()) 151 | } 152 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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, Some(true)) 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 | -------------------------------------------------------------------------------- /packages/dom/src/platform/get_client_length.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::Length; 2 | use web_sys::Element; 3 | 4 | pub fn get_client_length(element: &Element, length: Length) -> f64 { 5 | match length { 6 | Length::Width => element.client_width() as f64, 7 | Length::Height => element.client_height() as f64, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/dom/src/platform/get_client_rects.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::ClientRectObject; 2 | 3 | use crate::types::ElementOrVirtual; 4 | 5 | pub fn get_client_rects(element: ElementOrVirtual) -> Vec { 6 | match element { 7 | ElementOrVirtual::Element(element) => { 8 | ClientRectObject::from_dom_rect_list(element.get_client_rects()) 9 | } 10 | ElementOrVirtual::VirtualElement(virtual_element) => virtual_element 11 | .get_client_rects() 12 | .expect("Virtual element must implement `get_client_rects`."), 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/dom/src/platform/get_dimensions.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::Dimensions; 2 | use web_sys::Element; 3 | 4 | use crate::utils::get_css_dimensions::{CssDimensions, get_css_dimensions}; 5 | 6 | pub fn get_dimensions(element: &Element) -> Dimensions { 7 | let CssDimensions { dimensions, .. } = get_css_dimensions(element); 8 | dimensions 9 | } 10 | -------------------------------------------------------------------------------- /packages/dom/src/platform/get_element_rects.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_core::{GetElementRectsArgs, Platform as CorePlatform}; 2 | use floating_ui_utils::{ElementOrWindow, ElementRects, Rect}; 3 | use web_sys::{Element, Window}; 4 | 5 | use crate::{ 6 | platform::Platform, 7 | utils::get_rect_relative_to_offset_parent::get_rect_relative_to_offset_parent, 8 | }; 9 | 10 | pub fn get_element_rects(platform: &Platform, args: GetElementRectsArgs) -> ElementRects { 11 | let offset_parent = platform 12 | .get_offset_parent(args.floating) 13 | .expect("Platform implements get_offset_parent."); 14 | let dimensions = platform.get_dimensions(args.floating); 15 | 16 | let offset_parent_ref: ElementOrWindow = (&offset_parent).into(); 17 | 18 | ElementRects { 19 | reference: get_rect_relative_to_offset_parent( 20 | args.reference, 21 | offset_parent_ref.into(), 22 | args.strategy, 23 | ), 24 | floating: Rect { 25 | x: 0.0, 26 | y: 0.0, 27 | width: dimensions.width, 28 | height: dimensions.height, 29 | }, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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/dom/src/platform/get_scale.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::Coords; 2 | 3 | use crate::{ 4 | types::ElementOrVirtual, 5 | utils::get_css_dimensions::{CssDimensions, get_css_dimensions}, 6 | }; 7 | 8 | pub fn get_scale(element_or_virtual: ElementOrVirtual) -> Coords { 9 | let dom_element = element_or_virtual.resolve(); 10 | 11 | if let Some(dom_element) = dom_element { 12 | let rect = dom_element.get_bounding_client_rect(); 13 | let CssDimensions { 14 | dimensions, 15 | should_fallback, 16 | } = get_css_dimensions(&dom_element); 17 | let mut x = if should_fallback { 18 | rect.width().round() 19 | } else { 20 | rect.width() 21 | } / dimensions.width; 22 | let mut y = if should_fallback { 23 | rect.height().round() 24 | } else { 25 | rect.height() 26 | } / dimensions.height; 27 | 28 | if x == 0.0 || x.is_nan() || x.is_infinite() { 29 | x = 1.0; 30 | } 31 | 32 | if y == 0.0 || y.is_nan() || y.is_infinite() { 33 | y = 1.0; 34 | } 35 | 36 | Coords { x, y } 37 | } else { 38 | Coords::new(1.0) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/dom/src/platform/is_rtl.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::dom::get_computed_style; 2 | use web_sys::Element; 3 | 4 | pub fn is_rtl(element: &Element) -> bool { 5 | get_computed_style(element) 6 | .get_property_value("direction") 7 | .unwrap_or_default() 8 | == "rtl" 9 | } 10 | -------------------------------------------------------------------------------- /packages/dom/src/types.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_core::{Boundary as CoreBoundary, Middleware}; 2 | use floating_ui_utils::{ 3 | DefaultVirtualElement as CoreDefaultVirtualElement, ElementOrVirtual as CoreElementOrVirtual, 4 | OwnedElementOrVirtual as CoreOwnedElementOrVirtual, 5 | }; 6 | use web_sys::{Element, Window}; 7 | 8 | pub type Boundary = CoreBoundary; 9 | 10 | pub type DefaultVirtualElement = CoreDefaultVirtualElement; 11 | pub type ElementOrVirtual<'a> = CoreElementOrVirtual<'a, Element>; 12 | pub type OwnedElementOrVirtual = CoreOwnedElementOrVirtual; 13 | 14 | /// Vector of middleware used in [`ComputePositionConfig`][`crate::ComputePositionConfig`]. 15 | pub type MiddlewareVec = Vec>>; 16 | -------------------------------------------------------------------------------- /packages/dom/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub mod get_bounding_client_rect; 2 | pub mod get_css_dimensions; 3 | pub mod get_document_rect; 4 | pub mod get_html_offset; 5 | pub mod get_rect_relative_to_offset_parent; 6 | pub mod get_viewport_rect; 7 | pub mod get_visual_offsets; 8 | pub mod get_window_scroll_bar_x; 9 | pub mod is_static_positioned; 10 | pub mod rects_are_equal; 11 | -------------------------------------------------------------------------------- /packages/dom/src/utils/get_bounding_client_rect.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::{ 2 | ClientRectObject, Coords, Rect, 3 | dom::{DomElementOrWindow, get_computed_style, get_frame_element, get_window}, 4 | rect_to_client_rect, 5 | }; 6 | 7 | use crate::{ 8 | platform::get_scale::get_scale, 9 | types::ElementOrVirtual, 10 | utils::get_visual_offsets::{get_visual_offsets, should_add_visual_offsets}, 11 | }; 12 | 13 | pub fn get_bounding_client_rect( 14 | element_or_virtual: ElementOrVirtual, 15 | include_scale: bool, 16 | is_fixed_strategy: bool, 17 | offset_parent: Option, 18 | ) -> ClientRectObject { 19 | let client_rect = match &element_or_virtual { 20 | ElementOrVirtual::Element(element) => element.get_bounding_client_rect().into(), 21 | ElementOrVirtual::VirtualElement(virtual_element) => { 22 | virtual_element.get_bounding_client_rect() 23 | } 24 | }; 25 | let dom_element = element_or_virtual.clone().resolve(); 26 | 27 | let scale = if include_scale { 28 | match &offset_parent { 29 | Some(offset_parent) => match offset_parent { 30 | DomElementOrWindow::Element(element) => get_scale((*element).into()), 31 | DomElementOrWindow::Window(_) => Coords::new(1.0), 32 | }, 33 | None => get_scale(element_or_virtual), 34 | } 35 | } else { 36 | Coords::new(1.0) 37 | }; 38 | 39 | let visual_offsets = if should_add_visual_offsets( 40 | dom_element.as_ref(), 41 | is_fixed_strategy, 42 | offset_parent.clone(), 43 | ) { 44 | get_visual_offsets(dom_element.as_ref()) 45 | } else { 46 | Coords::new(0.0) 47 | }; 48 | 49 | let mut x = (client_rect.left + visual_offsets.x) / scale.x; 50 | let mut y = (client_rect.top + visual_offsets.y) / scale.y; 51 | let mut width = client_rect.width / scale.x; 52 | let mut height = client_rect.height / scale.y; 53 | 54 | if let Some(dom_element) = dom_element { 55 | let window = get_window(Some(&dom_element)); 56 | let offset_window = match offset_parent { 57 | Some(DomElementOrWindow::Element(element)) => Some(get_window(Some(element))), 58 | Some(DomElementOrWindow::Window(window)) => Some(window.clone()), 59 | None => None, 60 | }; 61 | 62 | if offset_parent.is_some() { 63 | let mut current_window = window; 64 | loop { 65 | let current_iframe = get_frame_element(¤t_window); 66 | 67 | if let Some(current_iframe) = current_iframe.as_ref() { 68 | if offset_window 69 | .as_ref() 70 | .is_some_and(|offset_window| offset_window != ¤t_window) 71 | { 72 | let iframe_scale = get_scale(current_iframe.into()); 73 | let iframe_rect = current_iframe.get_bounding_client_rect(); 74 | let css = get_computed_style(current_iframe); 75 | let padding_left = css 76 | .get_property_value("padding-left") 77 | .expect("Computed style should have padding left.") 78 | .parse::() 79 | .expect("Padding left should be a number."); 80 | let padding_top = css 81 | .get_property_value("padding-right") 82 | .expect("Computed style should have padding right.") 83 | .parse::() 84 | .expect("Padding right should be a number."); 85 | 86 | let left = iframe_rect.left() 87 | + (current_iframe.client_left() as f64 + padding_left) * iframe_scale.x; 88 | let top = iframe_rect.top() 89 | + (current_iframe.client_top() as f64 + padding_top) * iframe_scale.y; 90 | 91 | x *= iframe_scale.x; 92 | y *= iframe_scale.y; 93 | width *= iframe_scale.x; 94 | height *= iframe_scale.y; 95 | 96 | x += left; 97 | y += top; 98 | 99 | current_window = get_window(Some(current_iframe)); 100 | } else { 101 | break; 102 | } 103 | } else { 104 | break; 105 | } 106 | } 107 | } 108 | } 109 | 110 | rect_to_client_rect(Rect { 111 | x, 112 | y, 113 | width, 114 | height, 115 | }) 116 | } 117 | -------------------------------------------------------------------------------- /packages/dom/src/utils/get_css_dimensions.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::{ 2 | Dimensions, 3 | dom::{get_computed_style, is_html_element}, 4 | }; 5 | use web_sys::{Element, HtmlElement, wasm_bindgen::JsCast}; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct CssDimensions { 9 | pub dimensions: Dimensions, 10 | pub should_fallback: bool, 11 | } 12 | 13 | pub fn get_css_dimensions(element: &Element) -> CssDimensions { 14 | let css = get_computed_style(element); 15 | 16 | let width = css 17 | .get_property_value("width") 18 | .expect("Computed style should have width.") 19 | .replace("px", "") 20 | .parse::() 21 | .unwrap_or(0.0); 22 | let height = css 23 | .get_property_value("height") 24 | .expect("Computed style should have height.") 25 | .replace("px", "") 26 | .parse::() 27 | .unwrap_or(0.0); 28 | 29 | let offset_width; 30 | let offset_height; 31 | if is_html_element(element) { 32 | let element = element.unchecked_ref::(); 33 | offset_width = element.offset_width() as f64; 34 | offset_height = element.offset_height() as f64; 35 | } else { 36 | offset_width = width; 37 | offset_height = height; 38 | } 39 | let should_fallback = width.round() != offset_width || height.round() != offset_height; 40 | 41 | CssDimensions { 42 | dimensions: if should_fallback { 43 | Dimensions { 44 | width: offset_width, 45 | height: offset_height, 46 | } 47 | } else { 48 | Dimensions { width, height } 49 | }, 50 | should_fallback, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/dom/src/utils/get_document_rect.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::{ 2 | Rect, 3 | dom::{get_document_element, get_node_scroll}, 4 | }; 5 | use web_sys::Element; 6 | 7 | use crate::platform::is_rtl::is_rtl; 8 | 9 | use super::get_window_scroll_bar_x::get_window_scroll_bar_x; 10 | 11 | /// Gets the entire size of the scrollable document area, even extending outside of the `` and `` rect bounds if horizontally scrollable. 12 | pub fn get_document_rect(element: &Element) -> Rect { 13 | let html = get_document_element(Some(element.into())); 14 | let scroll = get_node_scroll(element.into()); 15 | let body = element 16 | .owner_document() 17 | .expect("Element should have owner document.") 18 | .body() 19 | .expect("Document should have body."); 20 | 21 | let width = [ 22 | html.scroll_width(), 23 | html.client_width(), 24 | body.scroll_width(), 25 | body.client_width(), 26 | ] 27 | .into_iter() 28 | .max() 29 | .expect("Iterator is not empty.") as f64; 30 | let height = [ 31 | html.scroll_height(), 32 | html.client_height(), 33 | body.scroll_height(), 34 | body.client_height(), 35 | ] 36 | .into_iter() 37 | .max() 38 | .expect("Iterator is not empty.") as f64; 39 | 40 | let mut x = -scroll.scroll_left + get_window_scroll_bar_x(element, None); 41 | let y = -scroll.scroll_top; 42 | 43 | if is_rtl(&body) { 44 | x += html.client_width().max(body.client_width()) as f64 - width; 45 | } 46 | 47 | Rect { 48 | x, 49 | y, 50 | width, 51 | height, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/dom/src/utils/get_html_offset.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::{Coords, dom::NodeScroll}; 2 | use web_sys::Element; 3 | 4 | use crate::utils::get_window_scroll_bar_x::get_window_scroll_bar_x; 5 | 6 | pub fn get_html_offset( 7 | document_element: &Element, 8 | scroll: &NodeScroll, 9 | ignore_scrollbar_x: Option, 10 | ) -> Coords { 11 | let ignore_scrollbar_x = ignore_scrollbar_x.unwrap_or(false); 12 | 13 | let html_rect = document_element.get_bounding_client_rect(); 14 | let x = html_rect.left() + scroll.scroll_left 15 | - if ignore_scrollbar_x { 16 | 0.0 17 | } else { 18 | // RTL scrollbar. 19 | get_window_scroll_bar_x(document_element, Some(&html_rect)) 20 | }; 21 | let y = html_rect.top() + scroll.scroll_top; 22 | 23 | Coords { x, y } 24 | } 25 | -------------------------------------------------------------------------------- /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, None) 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/utils/get_viewport_rect.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::{Rect, Strategy, dom::get_document_element}; 2 | use web_sys::Element; 3 | 4 | pub fn get_viewport_rect(element: &Element, _strategy: Strategy) -> Rect { 5 | // let window = get_window(Some(element)); 6 | let html = get_document_element(Some(element.into())); 7 | // TODO 8 | // let visual_viewport = window.visual_viewport; 9 | 10 | let x = 0.0; 11 | let y = 0.0; 12 | let width = html.client_width() as f64; 13 | let height = html.client_height() as f64; 14 | 15 | // TODO: visual viewport 16 | 17 | Rect { 18 | x, 19 | y, 20 | width, 21 | height, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/dom/src/utils/get_visual_offsets.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::{ 2 | Coords, 3 | dom::{DomElementOrWindow, get_window}, 4 | }; 5 | use web_sys::Element; 6 | 7 | pub fn get_visual_offsets(_element: Option<&Element>) -> Coords { 8 | // TODO: web-sys does not support VisualViewport 9 | 10 | // let window = get_window(element.map(|element| element.as_ref())); 11 | 12 | // if !is_web_kit() || !window.visual_viewport { 13 | // Coords::new(0.0) 14 | // } else { 15 | // Coords { 16 | // x: todo!(), 17 | // y: todo!(), 18 | // } 19 | // } 20 | 21 | Coords::new(0.0) 22 | } 23 | 24 | pub fn should_add_visual_offsets( 25 | element: Option<&Element>, 26 | is_fixed: bool, 27 | floating_offset_parent: Option, 28 | ) -> bool { 29 | match floating_offset_parent { 30 | Some(DomElementOrWindow::Window(floating_offset_parent)) => { 31 | if is_fixed 32 | && *floating_offset_parent != get_window(element.map(|element| element.as_ref())) 33 | { 34 | false 35 | } else { 36 | is_fixed 37 | } 38 | } 39 | _ => false, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/dom/src/utils/get_window_scroll_bar_x.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::dom::{get_document_element, get_node_scroll}; 2 | use web_sys::{DomRect, Element}; 3 | 4 | use crate::utils::get_bounding_client_rect::get_bounding_client_rect; 5 | 6 | // If has a CSS width greater than the viewport, then this will be incorrect for RTL. 7 | pub fn get_window_scroll_bar_x(element: &Element, rect: Option<&DomRect>) -> f64 { 8 | let left_scroll = get_node_scroll(element.into()).scroll_left; 9 | 10 | if let Some(rect) = rect { 11 | rect.left() + left_scroll 12 | } else { 13 | get_bounding_client_rect( 14 | (&get_document_element(Some(element.into()))).into(), 15 | false, 16 | false, 17 | None, 18 | ) 19 | .left 20 | + left_scroll 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/dom/src/utils/is_static_positioned.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::dom::get_computed_style; 2 | use web_sys::Element; 3 | 4 | pub fn is_static_positioned(element: &Element) -> bool { 5 | get_computed_style(element) 6 | .get_property_value("position") 7 | .expect("Computed style should have position.") 8 | == "static" 9 | } 10 | -------------------------------------------------------------------------------- /packages/dom/src/utils/rects_are_equal.rs: -------------------------------------------------------------------------------- 1 | use floating_ui_utils::ClientRectObject; 2 | 3 | pub fn rects_are_equal(a: &ClientRectObject, b: &ClientRectObject) -> bool { 4 | a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height 5 | } 6 | -------------------------------------------------------------------------------- /packages/leptos/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floating-ui-leptos" 3 | description = "Floating UI for Leptos." 4 | homepage = "https://floating-ui.rustforweb.org/frameworks/leptos.html" 5 | 6 | authors.workspace = true 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | version.workspace = true 11 | 12 | [dependencies] 13 | dyn_derive.workspace = true 14 | dyn_std.workspace = true 15 | floating-ui-dom.workspace = true 16 | leptos.workspace = true 17 | leptos-node-ref.workspace = true 18 | send_wrapper.workspace = true 19 | web-sys.workspace = true 20 | 21 | [dev-dependencies] 22 | wasm-bindgen-test.workspace = true 23 | -------------------------------------------------------------------------------- /packages/leptos/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Rust Floating UI Logo 4 | 5 |

6 | 7 |

floating-ui-leptos

8 | 9 | This is the library to use Floating UI with Leptos. 10 | 11 | [Rust Floating UI](https://github.com/RustForWeb/floating-ui) is a Rust port of [Floating UI](https://floating-ui.com). 12 | 13 | ## Documentation 14 | 15 | See [the Rust Floating UI book](https://floating-ui.rustforweb.org/) for documentation. 16 | 17 | ## Rust for Web 18 | 19 | The Rust Floating UI project is part of [Rust for Web](https://github.com/RustForWeb). 20 | 21 | [Rust for Web](https://github.com/RustForWeb) creates and ports web UI libraries for Rust. All projects are free and open source. 22 | -------------------------------------------------------------------------------- /packages/leptos/example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floating-ui-leptos-example" 3 | description = "Example for Floating UI Leptos." 4 | publish = false 5 | 6 | authors.workspace = true 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | version.workspace = true 11 | 12 | [dependencies] 13 | console_error_panic_hook.workspace = true 14 | console_log.workspace = true 15 | floating-ui-leptos.workspace = true 16 | leptos = { workspace = true, features = ["csr"] } 17 | leptos-node-ref.workspace = true 18 | log.workspace = true 19 | send_wrapper.workspace = true 20 | wasm-bindgen.workspace = true 21 | web-sys.workspace = true 22 | -------------------------------------------------------------------------------- /packages/leptos/example/README.md: -------------------------------------------------------------------------------- 1 | # Floating UI Leptos Example 2 | 3 | Implementation of the [Floating UI tutorial](https://floating-ui.com/docs/tutorial) with [Floating UI Leptos](../). 4 | 5 | ## Installation 6 | 7 | ```sh 8 | # Install Trunk 9 | cargo install trunk 10 | ``` 11 | 12 | ## Development 13 | 14 | ```sh 15 | # Start development server 16 | trunk serve 17 | ``` 18 | -------------------------------------------------------------------------------- /packages/leptos/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /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.into()) 37 | .placement(Placement::Top.into()) 38 | .middleware(SendWrapper::new(middleware).into()) 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 | 68 | 69 |