├── .changeset ├── README.md └── config.json ├── .config └── tsconfig.json ├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── update-snapshots.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── packages ├── core │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── index.d.ts │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── _babel.d.ts │ │ ├── computeCoordsFromPlacement.ts │ │ ├── computePosition.ts │ │ ├── detectOverflow.ts │ │ ├── enums.ts │ │ ├── index.ts │ │ ├── middleware │ │ │ ├── arrow.ts │ │ │ ├── autoPlacement.ts │ │ │ ├── flip.ts │ │ │ ├── hide.ts │ │ │ ├── offset.ts │ │ │ ├── shift.ts │ │ │ └── size.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── expandPaddingObject.ts │ │ │ ├── getAlignment.ts │ │ │ ├── getAlignmentSides.ts │ │ │ ├── getBasePlacement.ts │ │ │ ├── getCrossAxis.ts │ │ │ ├── getExpandedPlacements.ts │ │ │ ├── getLengthFromAxis.ts │ │ │ ├── getMainAxisFromPlacement.ts │ │ │ ├── getOppositeAlignmentPlacement.ts │ │ │ ├── getOppositePlacement.ts │ │ │ ├── getPaddingObject.ts │ │ │ ├── rectToClientRect.ts │ │ │ └── within.ts │ └── test │ │ ├── computeCoordsFromPlacement.test.ts │ │ ├── computePosition.test.ts │ │ └── middleware │ │ └── autoPlacement.test.ts ├── dom │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── index.d.ts │ ├── package.json │ ├── playwright.config.ts │ ├── rollup.config.js │ ├── src │ │ ├── index.ts │ │ ├── platform.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── contains.ts │ │ │ ├── convertOffsetParentRelativeRectToViewportRelativeRect.ts │ │ │ ├── getBoundingClientRect.ts │ │ │ ├── getClippingClientRect.ts │ │ │ ├── getComputedStyle.ts │ │ │ ├── getDimensions.ts │ │ │ ├── getDocumentElement.ts │ │ │ ├── getDocumentRect.ts │ │ │ ├── getNodeName.ts │ │ │ ├── getNodeScroll.ts │ │ │ ├── getOffsetParent.ts │ │ │ ├── getParentNode.ts │ │ │ ├── getRectRelativeToOffsetParent.ts │ │ │ ├── getScrollParent.ts │ │ │ ├── getScrollParents.ts │ │ │ ├── getViewportRect.ts │ │ │ ├── getWindowScrollBarX.ts │ │ │ ├── is.ts │ │ │ └── window.ts │ └── test │ │ ├── functional │ │ ├── base.test.ts │ │ ├── base.test.ts-snapshots │ │ │ └── base-linux.png │ │ ├── relative.test.ts │ │ ├── relative.test.ts-snapshots │ │ │ ├── relative-body-linux.png │ │ │ └── relative-html-linux.png │ │ ├── scrollbars.test.ts │ │ ├── scrollbars.test.ts-snapshots │ │ │ ├── scroll-border-linux.png │ │ │ ├── scroll-border-right-linux.png │ │ │ └── scroll-border-rtl-linux.png │ │ ├── size.test.js │ │ ├── size.test.js-snapshots │ │ │ └── same-width-linux.png │ │ ├── transform.test.ts │ │ └── transform.test.ts-snapshots │ │ │ └── transform-reference-scaled-linux.png │ │ └── visual │ │ ├── spec │ │ ├── base.html │ │ ├── clipping-border.html │ │ ├── middleware │ │ │ ├── arrow │ │ │ │ ├── center-offset.html │ │ │ │ ├── main.html │ │ │ │ ├── overflow.html │ │ │ │ └── shift.html │ │ │ ├── autoPlacement │ │ │ │ ├── virtual.html │ │ │ │ ├── x-axis.html │ │ │ │ └── y-axis.html │ │ │ ├── flip │ │ │ │ ├── fallbackPlacements.html │ │ │ │ ├── fallbackStrategy-bestFit.html │ │ │ │ ├── fallbackStrategy-initialPlacement.html │ │ │ │ ├── flipAlignment-false.html │ │ │ │ ├── flipAlignment-true.html │ │ │ │ └── main.html │ │ │ ├── hide │ │ │ │ └── main.html │ │ │ ├── offset │ │ │ │ ├── both.html │ │ │ │ ├── crossAxis.html │ │ │ │ ├── mainAxis.html │ │ │ │ └── number.html │ │ │ ├── shift │ │ │ │ ├── padding.html │ │ │ │ ├── x-axis.html │ │ │ │ └── y-axis.html │ │ │ └── size │ │ │ │ ├── main.html │ │ │ │ └── same-width.html │ │ ├── relative-body.html │ │ ├── relative-html.html │ │ ├── rtl.html │ │ ├── scroll-border-right.html │ │ ├── scroll-border-rtl.html │ │ ├── scroll-border.html │ │ ├── scrolling-absolute │ │ │ ├── base.html │ │ │ ├── different-scrolling-containers.html │ │ │ ├── nested-scrolling-containers-alt.html │ │ │ ├── nested-scrolling-containers.html │ │ │ ├── popper-offset-parent.html │ │ │ ├── reference-offset-parent.html │ │ │ ├── same-offset-parent.html │ │ │ └── window.html │ │ ├── scrolling-fixed │ │ │ ├── base.html │ │ │ ├── different-scrolling-containers.html │ │ │ ├── nested-scrolling-containers-alt.html │ │ │ ├── nested-scrolling-containers.html │ │ │ ├── popper-offset-parent.html │ │ │ ├── reference-offset-parent.html │ │ │ ├── same-offset-parent.html │ │ │ └── window.html │ │ └── transform │ │ │ ├── floating-scaled.html │ │ │ ├── parent-scaled.html │ │ │ └── reference-scaled.html │ │ ├── styles.css │ │ └── utils.mjs ├── react-dom │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── index.d.ts │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils │ │ │ └── useLatestRef.ts │ └── test │ │ ├── index.test.tsx │ │ └── visual │ │ ├── index.html │ │ └── index.js └── react-native │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── index.d.ts │ ├── package.json │ ├── rollup.config.js │ └── src │ ├── createPlatform.ts │ ├── index.ts │ ├── types.ts │ └── utils │ └── useLatestRef.ts ├── tsconfig.json └── website ├── CHANGELOG.md ├── assets ├── global.css ├── logo.png ├── logo.svg └── moonlight-ii.json ├── components ├── Chrome.js ├── Code.js ├── Collapsible.js ├── DropdownExample.js ├── Floating.js ├── Layout.js ├── Navigation.js ├── ReactTippy.js ├── Tutorial.js └── Warning.js ├── next.config.js ├── package.json ├── pages ├── _app.js ├── _document.js ├── docs │ ├── arrow.mdx │ ├── autoPlacement.mdx │ ├── computePosition.mdx │ ├── detectOverflow.mdx │ ├── flip.mdx │ ├── getting-started.mdx │ ├── hide.mdx │ ├── middleware.mdx │ ├── misc.mdx │ ├── motivation.mdx │ ├── offset.mdx │ ├── platform.mdx │ ├── popover.mdx │ ├── react-dom.mdx │ ├── react-native.mdx │ ├── shift.mdx │ ├── size.mdx │ ├── tooltip.mdx │ ├── tutorial.mdx │ └── virtual-elements.mdx └── index.js ├── postcss.config.js ├── public ├── favicon.ico ├── floating-ui.jpg ├── orbs.jpg └── orbs.svg └── tailwind.config.js /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.3/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "restricted", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } 11 | -------------------------------------------------------------------------------- /.config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["../packages/**/src/**/*.ts"], 3 | "exclude": ["../**/*.test.ts"], 4 | "compilerOptions": { 5 | "target": "es2015", 6 | "lib": ["dom", "es2017"], 7 | "moduleResolution": "node", 8 | "module": "es2015", 9 | "removeComments": false, 10 | "skipLibCheck": true, 11 | "declaration": true, 12 | "emitDeclarationOnly": true, 13 | "outDir": "../packages" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | parser: '@typescript-eslint/parser', 8 | plugins: ['@typescript-eslint', 'react-hooks'], 9 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 10 | rules: { 11 | '@typescript-eslint/ban-ts-comment': 'off', 12 | '@typescript-eslint/no-explicit-any': 'off', 13 | '@typescript-eslint/no-var-requires': 'off', 14 | 'react-hooks/rules-of-hooks': 'error', 15 | 'react-hooks/exhaustive-deps': 'warn', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [atomiks] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | checks: 6 | name: Linting and type checking 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: '17' 13 | - uses: bahmutov/npm-install@v1 14 | - run: npm run lint 15 | - run: npm run test:types 16 | 17 | unit-tests: 18 | name: Unit tests 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-node@v2 23 | with: 24 | node-version: '17' 25 | - uses: bahmutov/npm-install@v1 26 | - run: npm run build 27 | - run: npm -w packages/core test 28 | - run: npm -w packages/react-dom test 29 | 30 | functional-tests: 31 | name: Functional tests 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v2 35 | - uses: actions/setup-node@v2 36 | with: 37 | node-version: '17' 38 | - run: npx playwright install-deps 39 | - uses: bahmutov/npm-install@v1 40 | - run: npx playwright install 41 | - run: npm run build 42 | - run: npm -w packages/dom run test:functional 43 | -------------------------------------------------------------------------------- /.github/workflows/update-snapshots.yml: -------------------------------------------------------------------------------- 1 | name: Update Visual Snapshots 2 | on: [workflow_dispatch] 3 | env: 4 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 # Skip downloading during yarn install 5 | PLAYWRIGHT_BROWSERS_PATH: 0 # Places binaries to node_modules/@playwright/test 6 | 7 | jobs: 8 | functional-tests: 9 | name: Functional 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: '17' 16 | - run: npx playwright install-deps 17 | - uses: bahmutov/npm-install@v1 18 | - run: npx playwright install 19 | - run: npm run build 20 | - run: npm -w packages/dom run test:functional:update 21 | - uses: EndBug/add-and-commit@v7 22 | with: 23 | add: '.' 24 | author_name: 'GitHub Actions' 25 | author_email: 'github-actions@github.com' 26 | message: 'test: update visual snapshots' 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .tmp 4 | .chrome 5 | *.log 6 | .jekyll-metadata 7 | dist 8 | .idea/ 9 | .vscode/ 10 | coverage.info 11 | coverage 12 | __diff_output__ 13 | stats 14 | .next 15 | .parcel-cache 16 | test-results 17 | out -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 atomiks 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, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Floating UI has moved to the original Popper repo and is now its new brand. Go there! 2 | 3 | Link: https://github.com/floating-ui/floating-ui/ 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@floating-ui/monorepo", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Floating UI monorepo", 6 | "workspaces": [ 7 | "./packages/*", 8 | "./website" 9 | ], 10 | "scripts": { 11 | "dev": "concurrently 'npm -w packages/core run dev' 'npm -w packages/dom run dev'", 12 | "build": "npm -w packages/core run build && npm -w packages/dom run build && npm -w packages/react-dom run build && npm -w packages/react-native run build", 13 | "build:typescript": "tsc --project .config/tsconfig.json", 14 | "clean": "rm -rf packages/**/src/[^_]*.d.ts packages/**/src/**/[^_]*.d.ts packages/**/dist", 15 | "lint": "eslint . --ext .js,.ts", 16 | "test:types": "tsc", 17 | "dev:website": "npm run build && npm -w website run dev" 18 | }, 19 | "author": "", 20 | "license": "MIT", 21 | "prettier": { 22 | "singleQuote": true, 23 | "bracketSpacing": false, 24 | "proseWrap": "always" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^27.0.3", 28 | "@types/node": "^16.11.10", 29 | "@typescript-eslint/eslint-plugin": "^5.4.0", 30 | "@typescript-eslint/parser": "^5.4.0", 31 | "eslint": "^8.3.0", 32 | "eslint-plugin-react-hooks": "^4.3.0", 33 | "prettier": "^2.5.0", 34 | "shiki": "^0.9.14", 35 | "typescript": "^4.5.2" 36 | }, 37 | "dependencies": { 38 | "@changesets/cli": "^2.18.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @floating-ui/core 2 | 3 | ## 0.2.0 4 | 5 | ### Minor Changes 6 | 7 | feat(size): pass rects to `apply` 8 | 9 | ## 0.1.5 10 | 11 | ### Patch Changes 12 | 13 | - fix: revert back to ES module 14 | - fix(core): specify `0` default option in `offset` middleware 15 | - fix(shift): improve limitShift.offset type 16 | 17 | ## 0.1.4 18 | 19 | ### Patch Changes 20 | 21 | - fix(core): limitShift type 22 | 23 | ## 0.1.3 24 | 25 | ### Patch Changes 26 | 27 | - fix: VirtualElement type 28 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @floating-ui/core 2 | 3 | This is the platform-agnostic core of Floating UI, exposing the main 4 | `computePosition` function but no platform interface logic. 5 | -------------------------------------------------------------------------------- /packages/core/babel.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | presets: [['@babel/env', {loose: true}], '@babel/typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/core/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/types'; 2 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | export default { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | globals: { 6 | __DEV__: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@floating-ui/core", 3 | "version": "0.2.0", 4 | "description": "Positioning library for floating elements: tooltips, popovers, dropdowns, and more", 5 | "main": "dist/floating-ui.core.js", 6 | "module": "dist/floating-ui.core.esm.js", 7 | "unpkg": "dist/floating-ui.core.min.js", 8 | "type": "module", 9 | "exports": { 10 | "import": "./dist/floating-ui.core.esm.js", 11 | "require": "./dist/floating-ui.core.js" 12 | }, 13 | "sideEffects": false, 14 | "files": [ 15 | "dist/", 16 | "index.d.ts", 17 | "src/**/*.d.ts" 18 | ], 19 | "browserslist": "> 0.5%, not dead, not IE 11", 20 | "scripts": { 21 | "test": "jest test", 22 | "build": "NODE_ENV=build rollup -c", 23 | "dev": "rollup -c -w" 24 | }, 25 | "author": "atomiks", 26 | "license": "MIT", 27 | "bugs": "https://github.com/atomiks/floating-ui", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/atomiks/floating-ui.git", 31 | "directory": "packages/core" 32 | }, 33 | "homepage": "https://floating-ui.com", 34 | "keywords": [ 35 | "tooltip", 36 | "popover", 37 | "dropdown", 38 | "menu", 39 | "popup", 40 | "positioning" 41 | ], 42 | "devDependencies": { 43 | "@atomico/rollup-plugin-sizes": "^1.1.4", 44 | "@babel/preset-env": "^7.16.4", 45 | "@babel/preset-typescript": "^7.16.0", 46 | "@rollup/plugin-babel": "^5.3.0", 47 | "@rollup/plugin-node-resolve": "^13.0.6", 48 | "@rollup/plugin-replace": "^3.0.0", 49 | "@types/jest": "^27.0.3", 50 | "jest": "^27.3.1", 51 | "rollup": "^2.60.1", 52 | "rollup-plugin-terser": "^7.0.2", 53 | "ts-jest": "^27.0.7", 54 | "typescript": "^4.5.2" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import {babel} from '@rollup/plugin-babel'; 3 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 4 | import replace from '@rollup/plugin-replace'; 5 | import {terser} from 'rollup-plugin-terser'; 6 | import bundleSize from '@atomico/rollup-plugin-sizes'; 7 | 8 | const input = path.join(__dirname, 'src/index.ts'); 9 | 10 | const bundles = [ 11 | { 12 | input, 13 | output: { 14 | file: path.join(__dirname, 'dist/floating-ui.core.esm.js'), 15 | format: 'esm', 16 | }, 17 | }, 18 | { 19 | input, 20 | output: { 21 | file: path.join(__dirname, 'dist/floating-ui.core.esm.min.js'), 22 | format: 'esm', 23 | }, 24 | }, 25 | { 26 | input, 27 | output: { 28 | name: 'FloatingUICore', 29 | file: path.join(__dirname, 'dist/floating-ui.core.js'), 30 | format: 'umd', 31 | }, 32 | }, 33 | { 34 | input, 35 | output: { 36 | name: 'FloatingUICore', 37 | file: path.join(__dirname, 'dist/floating-ui.core.min.js'), 38 | format: 'umd', 39 | }, 40 | }, 41 | ]; 42 | 43 | const buildExport = bundles.map(({input, output}) => ({ 44 | input, 45 | output, 46 | plugins: [ 47 | nodeResolve({extensions: ['.ts']}), 48 | babel({babelHelpers: 'bundled', extensions: ['.ts']}), 49 | replace({ 50 | __DEV__: output.file.includes('.min.') 51 | ? 'false' 52 | : 'process.env.NODE_ENV !== "production"', 53 | preventAssignment: true, 54 | }), 55 | output.file.includes('.min.') && terser(), 56 | bundleSize(), 57 | ], 58 | })); 59 | 60 | const devExport = { 61 | input: path.join(__dirname, 'src/index.ts'), 62 | output: { 63 | file: path.join(__dirname, `dist/floating-ui.core.esm.js`), 64 | format: 'esm', 65 | }, 66 | plugins: [ 67 | nodeResolve({extensions: ['.ts']}), 68 | babel({babelHelpers: 'bundled', extensions: ['.ts']}), 69 | replace({ 70 | __DEV__: 'true', 71 | preventAssignment: true, 72 | }), 73 | ], 74 | }; 75 | 76 | export default process.env.NODE_ENV === 'build' ? buildExport : devExport; 77 | -------------------------------------------------------------------------------- /packages/core/src/_babel.d.ts: -------------------------------------------------------------------------------- 1 | declare const __DEV__: boolean; 2 | -------------------------------------------------------------------------------- /packages/core/src/computeCoordsFromPlacement.ts: -------------------------------------------------------------------------------- 1 | import type {Placement, ElementRects, Coords} from './types'; 2 | import {getBasePlacement} from './utils/getBasePlacement'; 3 | import {getAlignment} from './utils/getAlignment'; 4 | import {getMainAxisFromPlacement} from './utils/getMainAxisFromPlacement'; 5 | import {getLengthFromAxis} from './utils/getLengthFromAxis'; 6 | 7 | export function computeCoordsFromPlacement({ 8 | reference, 9 | floating, 10 | placement, 11 | }: ElementRects & {placement: Placement}): Coords { 12 | const commonX = reference.x + reference.width / 2 - floating.width / 2; 13 | const commonY = reference.y + reference.height / 2 - floating.height / 2; 14 | 15 | let coords; 16 | switch (getBasePlacement(placement)) { 17 | case 'top': 18 | coords = {x: commonX, y: reference.y - floating.height}; 19 | break; 20 | case 'bottom': 21 | coords = {x: commonX, y: reference.y + reference.height}; 22 | break; 23 | case 'right': 24 | coords = {x: reference.x + reference.width, y: commonY}; 25 | break; 26 | case 'left': 27 | coords = {x: reference.x - floating.width, y: commonY}; 28 | break; 29 | default: 30 | coords = {x: reference.x, y: reference.y}; 31 | } 32 | 33 | const mainAxis = getMainAxisFromPlacement(placement); 34 | const length = getLengthFromAxis(mainAxis); 35 | 36 | switch (getAlignment(placement)) { 37 | case 'start': 38 | coords[mainAxis] = 39 | coords[mainAxis] - (reference[length] / 2 - floating[length] / 2); 40 | break; 41 | case 'end': 42 | coords[mainAxis] = 43 | coords[mainAxis] + (reference[length] / 2 - floating[length] / 2); 44 | break; 45 | default: 46 | } 47 | 48 | return coords; 49 | } 50 | -------------------------------------------------------------------------------- /packages/core/src/computePosition.ts: -------------------------------------------------------------------------------- 1 | import type {ComputePosition, ComputePositionReturn} from './types'; 2 | import {computeCoordsFromPlacement} from './computeCoordsFromPlacement'; 3 | 4 | export const computePosition: ComputePosition = async ( 5 | reference, 6 | floating, 7 | config 8 | ): Promise => { 9 | const { 10 | placement = 'bottom', 11 | strategy = 'absolute', 12 | middleware = [], 13 | platform, 14 | } = config; 15 | 16 | if (__DEV__) { 17 | if (platform == null) { 18 | throw new Error( 19 | ['Floating UI Core: `platform` property was not passed.'].join(' ') 20 | ); 21 | } 22 | 23 | if ( 24 | middleware.filter(({name}) => name === 'autoPlacement' || name === 'flip') 25 | .length > 1 26 | ) { 27 | throw new Error( 28 | [ 29 | 'Floating UI: duplicate `flip` and/or `autoPlacement`', 30 | 'middleware detected. This will lead to an infinite loop. Ensure only', 31 | 'one of either has been passed to the `middleware` array.', 32 | ].join(' ') 33 | ); 34 | } 35 | } 36 | 37 | let rects = await platform.getElementRects({reference, floating, strategy}); 38 | 39 | let {x, y} = computeCoordsFromPlacement({...rects, placement}); 40 | 41 | let statefulPlacement = placement; 42 | let middlewareData = {}; 43 | 44 | let _debug_loop_count_ = 0; 45 | for (let i = 0; i < middleware.length; i++) { 46 | if (__DEV__) { 47 | _debug_loop_count_++; 48 | if (_debug_loop_count_ > 100) { 49 | throw new Error( 50 | [ 51 | 'Floating UI: The middleware lifecycle appears to be', 52 | 'running in an infinite loop. This is usually caused by a `reset`', 53 | 'continually being returned without a break condition.', 54 | ].join(' ') 55 | ); 56 | } 57 | } 58 | 59 | if (i === 0) { 60 | ({x, y} = computeCoordsFromPlacement({ 61 | ...rects, 62 | placement: statefulPlacement, 63 | })); 64 | } 65 | 66 | const {name, fn} = middleware[i]; 67 | const { 68 | x: nextX, 69 | y: nextY, 70 | data, 71 | reset, 72 | } = await fn({ 73 | x, 74 | y, 75 | initialPlacement: placement, 76 | placement: statefulPlacement, 77 | strategy, 78 | middlewareData, 79 | rects, 80 | platform, 81 | elements: {reference, floating}, 82 | }); 83 | 84 | x = nextX ?? x; 85 | y = nextY ?? y; 86 | 87 | middlewareData = {...middlewareData, [name]: data ?? {}}; 88 | 89 | if (reset) { 90 | if (typeof reset === 'object' && reset.placement) { 91 | statefulPlacement = reset.placement; 92 | } 93 | 94 | rects = await platform.getElementRects({reference, floating, strategy}); 95 | 96 | i = -1; 97 | continue; 98 | } 99 | } 100 | 101 | return { 102 | x, 103 | y, 104 | placement: statefulPlacement, 105 | strategy, 106 | middlewareData, 107 | }; 108 | }; 109 | -------------------------------------------------------------------------------- /packages/core/src/detectOverflow.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SideObject, 3 | Padding, 4 | Boundary, 5 | RootBoundary, 6 | ElementContext, 7 | MiddlewareArguments, 8 | } from './types'; 9 | import {getSideObjectFromPadding} from './utils/getPaddingObject'; 10 | import {rectToClientRect} from './utils/rectToClientRect'; 11 | 12 | export type Options = { 13 | boundary: Boundary; 14 | rootBoundary: RootBoundary; 15 | elementContext: ElementContext; 16 | altBoundary: boolean; 17 | padding: Padding; 18 | }; 19 | 20 | export async function detectOverflow( 21 | middlewareArguments: MiddlewareArguments, 22 | options: Partial = {} 23 | ): Promise { 24 | const {x, y, platform, rects, elements, strategy} = middlewareArguments; 25 | 26 | const { 27 | boundary = 'clippingParents', 28 | rootBoundary = 'viewport', 29 | elementContext = 'floating', 30 | altBoundary = false, 31 | padding = 0, 32 | } = options; 33 | 34 | const paddingObject = getSideObjectFromPadding(padding); 35 | const altContext = elementContext === 'floating' ? 'reference' : 'floating'; 36 | const element = elements[altBoundary ? altContext : elementContext]; 37 | 38 | const clippingClientRect = await platform.getClippingClientRect({ 39 | element: (await platform.isElement(element)) 40 | ? element 41 | : element.contextElement || 42 | (await platform.getDocumentElement({element: elements.floating})), 43 | boundary, 44 | rootBoundary, 45 | }); 46 | 47 | const elementClientRect = rectToClientRect( 48 | await platform.convertOffsetParentRelativeRectToViewportRelativeRect({ 49 | rect: 50 | elementContext === 'floating' 51 | ? {...rects.floating, x, y} 52 | : rects.reference, 53 | offsetParent: await platform.getOffsetParent({ 54 | element: elements.floating, 55 | }), 56 | strategy, 57 | }) 58 | ); 59 | 60 | // Debug the client rects: 61 | // draw(clippingClientRect, 'cyan', 1); 62 | // draw(elementClientRect, 'yellow', 2); 63 | 64 | // positive = overflowing the clipping rect 65 | // 0 or negative = within the clipping rect 66 | return { 67 | top: clippingClientRect.top - elementClientRect.top + paddingObject.top, 68 | bottom: 69 | elementClientRect.bottom - 70 | clippingClientRect.bottom + 71 | paddingObject.bottom, 72 | left: clippingClientRect.left - elementClientRect.left + paddingObject.left, 73 | right: 74 | elementClientRect.right - clippingClientRect.right + paddingObject.right, 75 | }; 76 | } 77 | 78 | // function draw(rect, color, id) { 79 | // const lastDraw = document.querySelector(`#draw-${id}`); 80 | 81 | // if (document.body.contains(lastDraw)) { 82 | // document.body.removeChild(lastDraw); 83 | // } 84 | 85 | // const div = document.createElement('div'); 86 | // div.id = `draw-${id}`; 87 | 88 | // Object.assign(div.style, { 89 | // position: 'absolute', 90 | // left: `${rect.x}px`, 91 | // top: `${rect.y}px`, 92 | // width: `${rect.width}px`, 93 | // height: `${rect.height}px`, 94 | // backgroundColor: color, 95 | // opacity: '0.5', 96 | // }); 97 | 98 | // document.body.append(div); 99 | // } 100 | -------------------------------------------------------------------------------- /packages/core/src/enums.ts: -------------------------------------------------------------------------------- 1 | import {Placement} from '..'; 2 | import {BasePlacement} from './types'; 3 | 4 | export const basePlacements: BasePlacement[] = [ 5 | 'top', 6 | 'right', 7 | 'bottom', 8 | 'left', 9 | ]; 10 | export const allPlacements = basePlacements.reduce( 11 | (acc: Placement[], basePlacement) => 12 | acc.concat(basePlacement, `${basePlacement}-start`, `${basePlacement}-end`), 13 | [] 14 | ); 15 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export {computePosition} from './computePosition'; 2 | export {detectOverflow} from './detectOverflow'; 3 | 4 | export {arrow} from './middleware/arrow'; 5 | export {autoPlacement} from './middleware/autoPlacement'; 6 | export {flip} from './middleware/flip'; 7 | export {hide} from './middleware/hide'; 8 | export {offset} from './middleware/offset'; 9 | export {shift, limitShift} from './middleware/shift'; 10 | export {size} from './middleware/size'; 11 | 12 | export {rectToClientRect} from './utils/rectToClientRect'; 13 | -------------------------------------------------------------------------------- /packages/core/src/middleware/arrow.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware, MiddlewareArguments, Padding} from '../types'; 2 | import {getBasePlacement} from '../utils/getBasePlacement'; 3 | import {getLengthFromAxis} from '../utils/getLengthFromAxis'; 4 | import {getMainAxisFromPlacement} from '../utils/getMainAxisFromPlacement'; 5 | import {getSideObjectFromPadding} from '../utils/getPaddingObject'; 6 | import {within} from '../utils/within'; 7 | 8 | export type Options = { 9 | element: any; 10 | padding?: Padding; 11 | }; 12 | 13 | export const arrow = (options: Options): Middleware => ({ 14 | name: 'arrow', 15 | async fn(middlewareArguments: MiddlewareArguments) { 16 | // Since `element` is required, we don't Partial<> the type 17 | const {element, padding = 0} = options ?? {}; 18 | const {x, y, placement, rects, platform} = middlewareArguments; 19 | 20 | if (element == null) { 21 | if (__DEV__) { 22 | console.warn( 23 | 'Floating UI: No `element` was passed to the `arrow` middleware.' 24 | ); 25 | } 26 | return {}; 27 | } 28 | 29 | const paddingObject = getSideObjectFromPadding(padding); 30 | const coords = {x, y}; 31 | const basePlacement = getBasePlacement(placement); 32 | const axis = getMainAxisFromPlacement(basePlacement); 33 | const length = getLengthFromAxis(axis); 34 | const arrowDimensions = await platform.getDimensions({element}); 35 | const minProp = axis === 'y' ? 'top' : 'left'; 36 | const maxProp = axis === 'y' ? 'bottom' : 'right'; 37 | 38 | const endDiff = 39 | rects.reference[length] + 40 | rects.reference[axis] - 41 | coords[axis] - 42 | rects.floating[length]; 43 | const startDiff = coords[axis] - rects.reference[axis]; 44 | 45 | const arrowOffsetParent = await platform.getOffsetParent({element}); 46 | const clientSize = arrowOffsetParent 47 | ? axis === 'y' 48 | ? // @ts-ignore - fallback to 0 49 | arrowOffsetParent.clientHeight || 0 50 | : // @ts-ignore - fallback to 0 51 | arrowOffsetParent.clientWidth || 0 52 | : 0; 53 | 54 | const centerToReference = endDiff / 2 - startDiff / 2; 55 | 56 | // Make sure the arrow doesn't overflow the floating element if the center 57 | // point is outside of the floating element's bounds 58 | const min = paddingObject[minProp]; 59 | const max = clientSize - arrowDimensions[length] - paddingObject[maxProp]; 60 | const center = 61 | clientSize / 2 - arrowDimensions[length] / 2 + centerToReference; 62 | const offset = within(min, center, max); 63 | 64 | return { 65 | data: { 66 | [axis]: offset, 67 | centerOffset: center - offset, 68 | }, 69 | }; 70 | }, 71 | }); 72 | -------------------------------------------------------------------------------- /packages/core/src/middleware/autoPlacement.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Middleware, 3 | MiddlewareArguments, 4 | Placement, 5 | Alignment, 6 | } from '../types'; 7 | import { 8 | detectOverflow, 9 | Options as DetectOverflowOptions, 10 | } from '../detectOverflow'; 11 | import {getBasePlacement} from '../utils/getBasePlacement'; 12 | import {getAlignment} from '../utils/getAlignment'; 13 | import {getAlignmentSides} from '../utils/getAlignmentSides'; 14 | import {getOppositeAlignmentPlacement} from '../utils/getOppositeAlignmentPlacement'; 15 | import {allPlacements} from '../enums'; 16 | 17 | export function getPlacementList( 18 | alignment: Alignment | null, 19 | autoAlignment: boolean, 20 | allowedPlacements: Array 21 | ) { 22 | return allowedPlacements.filter((placement) => { 23 | if (alignment) { 24 | return ( 25 | getAlignment(placement) === alignment || 26 | (autoAlignment 27 | ? getOppositeAlignmentPlacement(placement) !== placement 28 | : false) 29 | ); 30 | } 31 | 32 | return getBasePlacement(placement) === placement; 33 | }); 34 | } 35 | 36 | export type Options = DetectOverflowOptions & { 37 | alignment: Alignment | null; 38 | crossAxis: boolean; 39 | allowedPlacements: Array; 40 | autoAlignment: boolean; 41 | }; 42 | 43 | export const autoPlacement = (options: Partial = {}): Middleware => ({ 44 | name: 'autoPlacement', 45 | async fn(middlewareArguments: MiddlewareArguments) { 46 | const {x, y, rects, middlewareData, placement} = middlewareArguments; 47 | 48 | const { 49 | alignment = null, 50 | crossAxis = false, 51 | allowedPlacements = allPlacements, 52 | autoAlignment = true, 53 | ...detectOverflowOptions 54 | } = options; 55 | 56 | if (middlewareData.autoPlacement?.skip) { 57 | return {}; 58 | } 59 | 60 | const placements = getPlacementList( 61 | alignment, 62 | autoAlignment, 63 | allowedPlacements 64 | ); 65 | 66 | const overflow = await detectOverflow( 67 | middlewareArguments, 68 | detectOverflowOptions 69 | ); 70 | 71 | const currentIndex = middlewareData.autoPlacement?.index ?? 0; 72 | const currentPlacement = placements[currentIndex]; 73 | const {main, cross} = getAlignmentSides(currentPlacement, rects); 74 | 75 | // Make `computeCoords` start from the right place 76 | if (placement !== currentPlacement) { 77 | return { 78 | x, 79 | y, 80 | reset: { 81 | placement: placements[0], 82 | }, 83 | }; 84 | } 85 | 86 | const currentOverflows = [ 87 | overflow[getBasePlacement(currentPlacement)], 88 | overflow[main], 89 | overflow[cross], 90 | ]; 91 | 92 | const allOverflows = [ 93 | ...(middlewareData.autoPlacement?.overflows ?? []), 94 | {placement: currentPlacement, overflows: currentOverflows}, 95 | ]; 96 | 97 | const nextPlacement = placements[currentIndex + 1]; 98 | 99 | // There are more placements to check 100 | if (nextPlacement) { 101 | return { 102 | data: { 103 | index: currentIndex + 1, 104 | overflows: allOverflows, 105 | }, 106 | reset: { 107 | placement: nextPlacement, 108 | }, 109 | }; 110 | } 111 | 112 | const placementsSortedByLeastOverflow = allOverflows 113 | .slice() 114 | .sort( 115 | crossAxis || (autoAlignment && getAlignment(placement)) 116 | ? (a, b) => 117 | a.overflows.reduce((acc, overflow) => acc + overflow, 0) - 118 | b.overflows.reduce((acc, overflow) => acc + overflow, 0) 119 | : (a, b) => a.overflows[0] - b.overflows[0] 120 | ); 121 | const placementThatFitsOnAllSides = placementsSortedByLeastOverflow.find( 122 | ({overflows}) => overflows.every((overflow) => overflow <= 0) 123 | )?.placement; 124 | 125 | return { 126 | data: { 127 | skip: true, 128 | }, 129 | reset: { 130 | placement: 131 | placementThatFitsOnAllSides ?? 132 | placementsSortedByLeastOverflow[0].placement, 133 | }, 134 | }; 135 | }, 136 | }); 137 | -------------------------------------------------------------------------------- /packages/core/src/middleware/flip.ts: -------------------------------------------------------------------------------- 1 | import type {Placement, Middleware, MiddlewareArguments} from '../types'; 2 | import {getOppositePlacement} from '../utils/getOppositePlacement'; 3 | import {getBasePlacement} from '../utils/getBasePlacement'; 4 | import { 5 | detectOverflow, 6 | Options as DetectOverflowOptions, 7 | } from '../detectOverflow'; 8 | import {getAlignmentSides} from '../utils/getAlignmentSides'; 9 | import {getExpandedPlacements} from '../utils/getExpandedPlacements'; 10 | 11 | export type Options = DetectOverflowOptions & { 12 | mainAxis: boolean; 13 | crossAxis: boolean; 14 | fallbackPlacements: Array; 15 | fallbackStrategy: 'bestFit' | 'initialPlacement'; 16 | flipAlignment: boolean; 17 | }; 18 | 19 | export const flip = (options: Partial = {}): Middleware => ({ 20 | name: 'flip', 21 | async fn(middlewareArguments: MiddlewareArguments) { 22 | const {placement, middlewareData, rects, initialPlacement} = 23 | middlewareArguments; 24 | 25 | if (middlewareData.flip?.skip) { 26 | return {}; 27 | } 28 | 29 | const { 30 | mainAxis: checkMainAxis = true, 31 | crossAxis: checkCrossAxis = true, 32 | fallbackPlacements: specifiedFallbackPlacements, 33 | fallbackStrategy = 'bestFit', 34 | flipAlignment = true, 35 | ...detectOverflowOptions 36 | } = options; 37 | 38 | const basePlacement = getBasePlacement(placement); 39 | const isBasePlacement = basePlacement === initialPlacement; 40 | 41 | const fallbackPlacements = 42 | specifiedFallbackPlacements || 43 | (isBasePlacement || !flipAlignment 44 | ? [getOppositePlacement(initialPlacement)] 45 | : getExpandedPlacements(initialPlacement)); 46 | 47 | const placements = [initialPlacement, ...fallbackPlacements]; 48 | 49 | const overflow = await detectOverflow( 50 | middlewareArguments, 51 | detectOverflowOptions 52 | ); 53 | 54 | const overflows = []; 55 | let overflowsData = middlewareData.flip?.overflows || []; 56 | 57 | if (checkMainAxis) { 58 | overflows.push(overflow[basePlacement]); 59 | } 60 | 61 | if (checkCrossAxis) { 62 | const {main, cross} = getAlignmentSides(placement, rects); 63 | overflows.push(overflow[main], overflow[cross]); 64 | } 65 | 66 | overflowsData = [...overflowsData, {placement, overflows}]; 67 | 68 | // One or more sides is overflowing 69 | if (!overflows.every((side) => side <= 0)) { 70 | const nextIndex = (middlewareData.flip?.index ?? 0) + 1; 71 | const nextPlacement = placements[nextIndex]; 72 | 73 | if (nextPlacement) { 74 | // Try next placement and re-run the lifecycle 75 | return { 76 | data: { 77 | index: nextIndex, 78 | overflows: overflowsData, 79 | }, 80 | reset: { 81 | placement: nextPlacement, 82 | }, 83 | }; 84 | } 85 | 86 | let resetPlacement: Placement = 'bottom'; 87 | switch (fallbackStrategy) { 88 | case 'bestFit': { 89 | const placement = overflowsData 90 | .slice() 91 | .sort( 92 | (a, b) => 93 | a.overflows 94 | .filter((overflow) => overflow > 0) 95 | .reduce((acc, overflow) => acc + overflow, 0) - 96 | b.overflows 97 | .filter((overflow) => overflow > 0) 98 | .reduce((acc, overflow) => acc + overflow, 0) 99 | )[0]?.placement; 100 | if (placement) { 101 | resetPlacement = placement; 102 | } 103 | break; 104 | } 105 | case 'initialPlacement': 106 | resetPlacement = initialPlacement; 107 | break; 108 | default: 109 | } 110 | 111 | return { 112 | data: { 113 | skip: true, 114 | }, 115 | reset: { 116 | placement: resetPlacement, 117 | }, 118 | }; 119 | } 120 | 121 | return {}; 122 | }, 123 | }); 124 | -------------------------------------------------------------------------------- /packages/core/src/middleware/hide.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware, MiddlewareArguments, Rect, SideObject} from '../types'; 2 | import {basePlacements} from '../enums'; 3 | import {detectOverflow} from '../detectOverflow'; 4 | 5 | function getSideOffsets(overflow: SideObject, rect: Rect) { 6 | return { 7 | top: overflow.top - rect.height, 8 | right: overflow.right - rect.width, 9 | bottom: overflow.bottom - rect.height, 10 | left: overflow.left - rect.width, 11 | }; 12 | } 13 | 14 | function isAnySideFullyClipped(overflow: SideObject) { 15 | return basePlacements.some((side) => overflow[side] >= 0); 16 | } 17 | 18 | export const hide = (): Middleware => ({ 19 | name: 'hide', 20 | async fn(modifierArguments: MiddlewareArguments) { 21 | const referenceOverflow = await detectOverflow(modifierArguments, { 22 | elementContext: 'reference', 23 | }); 24 | const floatingAltOverflow = await detectOverflow(modifierArguments, { 25 | altBoundary: true, 26 | }); 27 | 28 | const referenceHiddenOffsets = getSideOffsets( 29 | referenceOverflow, 30 | modifierArguments.rects.reference 31 | ); 32 | const escapedOffsets = getSideOffsets( 33 | floatingAltOverflow, 34 | modifierArguments.rects.floating 35 | ); 36 | const referenceHidden = isAnySideFullyClipped(referenceHiddenOffsets); 37 | const escaped = isAnySideFullyClipped(escapedOffsets); 38 | 39 | return { 40 | data: { 41 | referenceHidden, 42 | referenceHiddenOffsets, 43 | escaped, 44 | escapedOffsets, 45 | }, 46 | }; 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /packages/core/src/middleware/offset.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Placement, 3 | Rect, 4 | Coords, 5 | Middleware, 6 | MiddlewareArguments, 7 | } from '../types'; 8 | import {getBasePlacement} from '../utils/getBasePlacement'; 9 | import {getMainAxisFromPlacement} from '../utils/getMainAxisFromPlacement'; 10 | 11 | type OffsetValue = number | {mainAxis?: number; crossAxis?: number}; 12 | type OffsetFunction = (args: { 13 | floating: Rect; 14 | reference: Rect; 15 | placement: Placement; 16 | }) => OffsetValue; 17 | 18 | export type Offset = OffsetValue | OffsetFunction; 19 | 20 | export function convertValueToCoords({ 21 | placement, 22 | rects, 23 | value, 24 | }: { 25 | placement: Placement; 26 | rects: {floating: Rect; reference: Rect}; 27 | value: Offset; 28 | }): Coords { 29 | const basePlacement = getBasePlacement(placement); 30 | const multiplier = ['left', 'top'].includes(basePlacement) ? -1 : 1; 31 | 32 | const rawValue = 33 | typeof value === 'function' ? value({...rects, placement}) : value; 34 | const {mainAxis, crossAxis} = 35 | typeof rawValue === 'number' 36 | ? {mainAxis: rawValue, crossAxis: 0} 37 | : {mainAxis: 0, crossAxis: 0, ...rawValue}; 38 | 39 | return getMainAxisFromPlacement(basePlacement) === 'x' 40 | ? {x: crossAxis, y: mainAxis * multiplier} 41 | : {x: mainAxis * multiplier, y: crossAxis}; 42 | } 43 | 44 | export const offset = (value: Offset = 0): Middleware => ({ 45 | name: 'offset', 46 | fn(middlewareArguments: MiddlewareArguments) { 47 | const {x, y, placement, rects} = middlewareArguments; 48 | const diffCoords = convertValueToCoords({placement, rects, value}); 49 | return { 50 | x: x + diffCoords.x, 51 | y: y + diffCoords.y, 52 | data: diffCoords, 53 | }; 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /packages/core/src/middleware/size.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Dimensions, 3 | ElementRects, 4 | Middleware, 5 | MiddlewareArguments, 6 | } from '../types'; 7 | import { 8 | detectOverflow, 9 | Options as DetectOverflowOptions, 10 | } from '../detectOverflow'; 11 | import {getBasePlacement} from '../utils/getBasePlacement'; 12 | import {getAlignment} from '../utils/getAlignment'; 13 | 14 | export type Options = DetectOverflowOptions & { 15 | apply(args: Dimensions & ElementRects): void; 16 | }; 17 | 18 | export const size = (options: Partial = {}): Middleware => ({ 19 | name: 'size', 20 | async fn(middlewareArguments: MiddlewareArguments) { 21 | const {placement, rects, middlewareData} = middlewareArguments; 22 | const {apply, ...detectOverflowOptions} = options; 23 | 24 | const overflow = await detectOverflow( 25 | middlewareArguments, 26 | detectOverflowOptions 27 | ); 28 | const basePlacement = getBasePlacement(placement); 29 | const isEnd = getAlignment(placement) === 'end'; 30 | 31 | let heightSide: 'top' | 'bottom'; 32 | let widthSide: 'left' | 'right'; 33 | 34 | if (basePlacement === 'top' || basePlacement === 'bottom') { 35 | heightSide = basePlacement; 36 | widthSide = isEnd ? 'left' : 'right'; 37 | } else { 38 | widthSide = basePlacement; 39 | heightSide = isEnd ? 'top' : 'bottom'; 40 | } 41 | 42 | const dimensions = { 43 | height: rects.floating.height - overflow[heightSide], 44 | width: rects.floating.width - overflow[widthSide], 45 | }; 46 | 47 | if (middlewareData.size?.skip) { 48 | return {}; 49 | } 50 | 51 | apply?.({...dimensions, ...rects}); 52 | 53 | return { 54 | data: { 55 | skip: true, 56 | }, 57 | reset: true, 58 | }; 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /packages/core/src/utils/expandPaddingObject.ts: -------------------------------------------------------------------------------- 1 | import {SideObject} from '../types'; 2 | 3 | export function expandPaddingObject(padding: Partial): SideObject { 4 | return {top: 0, right: 0, bottom: 0, left: 0, ...padding}; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/utils/getAlignment.ts: -------------------------------------------------------------------------------- 1 | import type {Alignment} from '../types'; 2 | 3 | export function getAlignment(placement: T): Alignment { 4 | return placement.split('-')[1] as Alignment; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/utils/getAlignmentSides.ts: -------------------------------------------------------------------------------- 1 | import type {ElementRects, Placement, BasePlacement} from '../types'; 2 | import {getLengthFromAxis} from './getLengthFromAxis'; 3 | import {getMainAxisFromPlacement} from './getMainAxisFromPlacement'; 4 | import {getOppositePlacement} from './getOppositePlacement'; 5 | import {getAlignment} from './getAlignment'; 6 | 7 | export function getAlignmentSides( 8 | placement: Placement, 9 | rects: ElementRects 10 | ): { 11 | main: BasePlacement; 12 | cross: BasePlacement; 13 | } { 14 | const isStart = getAlignment(placement) === 'start'; 15 | const mainAxis = getMainAxisFromPlacement(placement); 16 | const length = getLengthFromAxis(mainAxis); 17 | 18 | let mainAlignmentSide: BasePlacement = 19 | mainAxis === 'x' 20 | ? isStart 21 | ? 'right' 22 | : 'left' 23 | : isStart 24 | ? 'bottom' 25 | : 'top'; 26 | 27 | if (rects.reference[length] > rects.floating[length]) { 28 | mainAlignmentSide = getOppositePlacement(mainAlignmentSide); 29 | } 30 | 31 | return { 32 | main: mainAlignmentSide, 33 | cross: getOppositePlacement(mainAlignmentSide), 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/utils/getBasePlacement.ts: -------------------------------------------------------------------------------- 1 | import {BasePlacement, Placement} from '../types'; 2 | 3 | export function getBasePlacement(placement: Placement): BasePlacement { 4 | return placement.split('-')[0] as BasePlacement; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/utils/getCrossAxis.ts: -------------------------------------------------------------------------------- 1 | import {Axis} from '../types'; 2 | 3 | export function getCrossAxis(axis: Axis): Axis { 4 | return axis === 'x' ? 'y' : 'x'; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/utils/getExpandedPlacements.ts: -------------------------------------------------------------------------------- 1 | import type {Placement} from '../types'; 2 | import {getOppositePlacement} from './getOppositePlacement'; 3 | import {getOppositeAlignmentPlacement} from './getOppositeAlignmentPlacement'; 4 | 5 | export function getExpandedPlacements(placement: Placement): Array { 6 | const oppositePlacement = getOppositePlacement(placement); 7 | 8 | return [ 9 | getOppositeAlignmentPlacement(placement), 10 | oppositePlacement, 11 | getOppositeAlignmentPlacement(oppositePlacement), 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/utils/getLengthFromAxis.ts: -------------------------------------------------------------------------------- 1 | import {Axis, Length} from '../types'; 2 | 3 | export function getLengthFromAxis(axis: Axis): Length { 4 | return axis === 'y' ? 'height' : 'width'; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/utils/getMainAxisFromPlacement.ts: -------------------------------------------------------------------------------- 1 | import {Axis, Placement} from '../types'; 2 | import {getBasePlacement} from './getBasePlacement'; 3 | 4 | export function getMainAxisFromPlacement(placement: Placement): Axis { 5 | return ['top', 'bottom'].includes(getBasePlacement(placement)) ? 'x' : 'y'; 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/src/utils/getOppositeAlignmentPlacement.ts: -------------------------------------------------------------------------------- 1 | import type {Placement} from '../types'; 2 | 3 | const hash = {start: 'end', end: 'start'}; 4 | 5 | export function getOppositeAlignmentPlacement(placement: Placement): Placement { 6 | return placement.replace( 7 | /start|end/g, 8 | (matched) => (hash as any)[matched] 9 | ) as Placement; 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/utils/getOppositePlacement.ts: -------------------------------------------------------------------------------- 1 | const hash = {left: 'right', right: 'left', bottom: 'top', top: 'bottom'}; 2 | 3 | export function getOppositePlacement(placement: T): T { 4 | return placement.replace( 5 | /left|right|bottom|top/g, 6 | (matched) => (hash as any)[matched] 7 | ) as T; 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/utils/getPaddingObject.ts: -------------------------------------------------------------------------------- 1 | import {Padding, SideObject} from '../types'; 2 | import {expandPaddingObject} from './expandPaddingObject'; 3 | 4 | export function getSideObjectFromPadding(padding: Padding): SideObject { 5 | return typeof padding !== 'number' 6 | ? expandPaddingObject(padding) 7 | : {top: padding, right: padding, bottom: padding, left: padding}; 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/utils/rectToClientRect.ts: -------------------------------------------------------------------------------- 1 | import {Rect, ClientRectObject} from '../types'; 2 | 3 | export function rectToClientRect(rect: Rect): ClientRectObject { 4 | return { 5 | ...rect, 6 | top: rect.y, 7 | left: rect.x, 8 | right: rect.x + rect.width, 9 | bottom: rect.y + rect.height, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/utils/within.ts: -------------------------------------------------------------------------------- 1 | export function within(min: number, value: number, max: number): number { 2 | return Math.max(min, Math.min(value, max)); 3 | } 4 | -------------------------------------------------------------------------------- /packages/core/test/computeCoordsFromPlacement.test.ts: -------------------------------------------------------------------------------- 1 | import {computeCoordsFromPlacement} from '../src/computeCoordsFromPlacement'; 2 | 3 | const reference = {x: 0, y: 0, width: 100, height: 100}; 4 | const floating = {x: 0, y: 0, width: 50, height: 50}; 5 | 6 | test('bottom', () => { 7 | expect( 8 | computeCoordsFromPlacement({reference, floating, placement: 'bottom'}) 9 | ).toEqual({x: 25, y: 100}); 10 | }); 11 | 12 | test('bottom-start', () => { 13 | expect( 14 | computeCoordsFromPlacement({reference, floating, placement: 'bottom-start'}) 15 | ).toEqual({x: 0, y: 100}); 16 | }); 17 | 18 | test('bottom-end', () => { 19 | expect( 20 | computeCoordsFromPlacement({reference, floating, placement: 'bottom-end'}) 21 | ).toEqual({x: 50, y: 100}); 22 | }); 23 | 24 | test('top', () => { 25 | expect( 26 | computeCoordsFromPlacement({reference, floating, placement: 'top'}) 27 | ).toEqual({x: 25, y: -50}); 28 | }); 29 | 30 | test('top-start', () => { 31 | expect( 32 | computeCoordsFromPlacement({reference, floating, placement: 'top-start'}) 33 | ).toEqual({x: 0, y: -50}); 34 | }); 35 | 36 | test('top-end', () => { 37 | expect( 38 | computeCoordsFromPlacement({reference, floating, placement: 'top-end'}) 39 | ).toEqual({x: 50, y: -50}); 40 | }); 41 | 42 | test('right', () => { 43 | expect( 44 | computeCoordsFromPlacement({reference, floating, placement: 'right'}) 45 | ).toEqual({x: 100, y: 25}); 46 | }); 47 | 48 | test('right-start', () => { 49 | expect( 50 | computeCoordsFromPlacement({reference, floating, placement: 'right-start'}) 51 | ).toEqual({x: 100, y: 0}); 52 | }); 53 | 54 | test('right-end', () => { 55 | expect( 56 | computeCoordsFromPlacement({reference, floating, placement: 'right-end'}) 57 | ).toEqual({x: 100, y: 50}); 58 | }); 59 | 60 | test('left', () => { 61 | expect( 62 | computeCoordsFromPlacement({reference, floating, placement: 'left'}) 63 | ).toEqual({x: -50, y: 25}); 64 | }); 65 | 66 | test('left-start', () => { 67 | expect( 68 | computeCoordsFromPlacement({reference, floating, placement: 'left-start'}) 69 | ).toEqual({x: -50, y: 0}); 70 | }); 71 | 72 | test('left-end', () => { 73 | expect( 74 | computeCoordsFromPlacement({reference, floating, placement: 'left-end'}) 75 | ).toEqual({x: -50, y: 50}); 76 | }); 77 | -------------------------------------------------------------------------------- /packages/core/test/computePosition.test.ts: -------------------------------------------------------------------------------- 1 | import {computePosition} from '../src'; 2 | 3 | const reference = {}; 4 | const floating = {}; 5 | const referenceRect = {x: 0, y: 0, width: 100, height: 100}; 6 | const floatingRect = {x: 0, y: 0, width: 50, height: 50}; 7 | const platform = { 8 | getElementRects: () => 9 | Promise.resolve({ 10 | reference: referenceRect, 11 | floating: floatingRect, 12 | }), 13 | }; 14 | 15 | test('returned data', async () => { 16 | const {x, y, placement, strategy, middlewareData} = await computePosition( 17 | reference, 18 | floating, 19 | { 20 | placement: 'top', 21 | middleware: [{name: 'custom', fn: () => ({data: {property: true}})}], 22 | // @ts-ignore - computePosition() only uses this property 23 | platform, 24 | } 25 | ); 26 | 27 | expect(placement).toBe('top'); 28 | expect(strategy).toBe('absolute'); 29 | expect(x).toBe(25); 30 | expect(y).toBe(-50); 31 | expect(middlewareData).toEqual({ 32 | custom: { 33 | property: true, 34 | }, 35 | }); 36 | }); 37 | 38 | test('middleware', async () => { 39 | const {x, y} = await computePosition(reference, floating, { 40 | // @ts-ignore - computePosition() only uses this property 41 | platform, 42 | }); 43 | 44 | const {x: x2, y: y2} = await computePosition(reference, floating, { 45 | // @ts-ignore - computePosition() only uses this property 46 | platform, 47 | middleware: [ 48 | { 49 | name: 'test', 50 | fn: ({x, y}) => ({x: x + 1, y: y + 1}), 51 | }, 52 | ], 53 | }); 54 | 55 | expect([x2, y2]).toEqual([x + 1, y + 1]); 56 | }); 57 | 58 | test('middlewareData', async () => { 59 | const {middlewareData} = await computePosition(reference, floating, { 60 | // @ts-ignore - computePosition() only uses this property 61 | platform, 62 | middleware: [ 63 | { 64 | name: 'test', 65 | fn: () => ({ 66 | data: { 67 | hello: true, 68 | }, 69 | }), 70 | }, 71 | ], 72 | }); 73 | 74 | expect(middlewareData.test).toEqual({hello: true}); 75 | }); 76 | -------------------------------------------------------------------------------- /packages/core/test/middleware/autoPlacement.test.ts: -------------------------------------------------------------------------------- 1 | import {getPlacementList} from '../../src/middleware/autoPlacement'; 2 | 3 | test('base placement', () => { 4 | expect( 5 | getPlacementList(null, false, [ 6 | 'top', 7 | 'bottom', 8 | 'left', 9 | 'right', 10 | 'top-start', 11 | 'right-end', 12 | ]) 13 | ).toEqual(['top', 'bottom', 'left', 'right']); 14 | }); 15 | 16 | test('start alignment without auto alignment', () => { 17 | expect( 18 | getPlacementList('start', false, [ 19 | 'top', 20 | 'bottom', 21 | 'left', 22 | 'right', 23 | 'top-start', 24 | 'right-end', 25 | 'left-start', 26 | ]) 27 | ).toEqual(['top-start', 'left-start']); 28 | }); 29 | 30 | test('start alignment with auto alignment', () => { 31 | expect( 32 | getPlacementList('start', true, [ 33 | 'top', 34 | 'bottom', 35 | 'left', 36 | 'right', 37 | 'top-start', 38 | 'right-end', 39 | 'left-start', 40 | ]) 41 | ).toEqual(['top-start', 'right-end', 'left-start']); 42 | }); 43 | 44 | test('end alignment without auto alignment', () => { 45 | expect( 46 | getPlacementList('end', false, [ 47 | 'top', 48 | 'bottom', 49 | 'left', 50 | 'right', 51 | 'top-start', 52 | 'right-end', 53 | 'left-start', 54 | ]) 55 | ).toEqual(['right-end']); 56 | }); 57 | 58 | test('end alignment with auto alignment', () => { 59 | expect( 60 | getPlacementList('end', true, [ 61 | 'top', 62 | 'bottom', 63 | 'left', 64 | 'right', 65 | 'top-start', 66 | 'right-end', 67 | 'left-start', 68 | ]) 69 | ).toEqual(['top-start', 'right-end', 'left-start']); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/dom/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @floating-ui/dom 2 | 3 | ## 0.1.6 4 | 5 | ### Patch Changes 6 | 7 | - test 8 | - Updated dependencies 9 | - @floating-ui/core@0.2.0 10 | 11 | ## 0.1.5 12 | 13 | ### Patch Changes 14 | 15 | - Updated dependencies 16 | - @floating-ui/core@0.1.5 17 | 18 | ## 0.1.4 19 | 20 | ### Patch Changes 21 | 22 | - fix(core): limitShift type 23 | - Updated dependencies 24 | - @floating-ui/core@0.1.4 25 | 26 | ## 0.1.3 27 | 28 | ### Patch Changes 29 | 30 | - chore: upgrade @floating-ui/core to @0.1.3 31 | -------------------------------------------------------------------------------- /packages/dom/README.md: -------------------------------------------------------------------------------- 1 | # @floating-ui/dom 2 | 3 | This is the library to use Floating UI on the web, wrapping `@floating-ui/core` 4 | with DOM interface logic. 5 | -------------------------------------------------------------------------------- /packages/dom/babel.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | presets: [['@babel/env', {loose: true}], '@babel/typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/dom/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/types'; 2 | -------------------------------------------------------------------------------- /packages/dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@floating-ui/dom", 3 | "version": "0.1.6", 4 | "description": "Floating UI for the web", 5 | "main": "dist/floating-ui.dom.js", 6 | "module": "dist/floating-ui.dom.esm.js", 7 | "unpkg": "dist/floating-ui.dom.min.js", 8 | "type": "module", 9 | "exports": { 10 | "import": "./dist/floating-ui.dom.esm.js", 11 | "require": "./dist/floating-ui.dom.js" 12 | }, 13 | "sideEffects": false, 14 | "files": [ 15 | "dist/", 16 | "index.d.ts", 17 | "src/**/*.d.ts" 18 | ], 19 | "browserslist": "> 0.5%, not dead, not IE 11", 20 | "scripts": { 21 | "dev": "concurrently 'rollup -c -w' 'serve -l 1234 test/visual'", 22 | "build": "NODE_ENV=build rollup -c", 23 | "test": "jest test", 24 | "test:functional": "rollup -c && npx playwright test", 25 | "test:functional:update": "rollup -c && npx playwright test -u" 26 | }, 27 | "author": "atomiks", 28 | "license": "MIT", 29 | "bugs": "https://github.com/atomiks/floating-ui", 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/atomiks/floating-ui.git", 33 | "directory": "packages/dom" 34 | }, 35 | "homepage": "https://floating-ui.com", 36 | "keywords": [ 37 | "tooltip", 38 | "popover", 39 | "dropdown", 40 | "menu", 41 | "popup", 42 | "positioning" 43 | ], 44 | "dependencies": { 45 | "@floating-ui/core": "^0.2.0" 46 | }, 47 | "devDependencies": { 48 | "@babel/preset-env": "^7.16.4", 49 | "@babel/preset-typescript": "^7.16.0", 50 | "@playwright/test": "^1.16.3", 51 | "@rollup/plugin-replace": "^3.0.0", 52 | "@types/jest": "^27.0.3", 53 | "concurrently": "^6.4.0", 54 | "jest": "^27.3.1", 55 | "rollup-plugin-terser": "^7.0.2", 56 | "serve": "^13.0.2", 57 | "ts-jest": "^27.0.7", 58 | "typescript": "^4.5.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/dom/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type {PlaywrightTestConfig} from '@playwright/test'; 2 | 3 | const config: PlaywrightTestConfig = { 4 | use: { 5 | launchOptions: { 6 | ignoreDefaultArgs: ['--hide-scrollbars'], 7 | }, 8 | }, 9 | webServer: { 10 | command: 'serve -l 1234 test/visual', 11 | port: 1234, 12 | timeout: 120 * 1000, 13 | reuseExistingServer: !process.env.CI, 14 | }, 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/dom/rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import {babel} from '@rollup/plugin-babel'; 3 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 4 | import replace from '@rollup/plugin-replace'; 5 | import {terser} from 'rollup-plugin-terser'; 6 | 7 | const input = path.join(__dirname, 'src/index.ts'); 8 | 9 | const bundles = [ 10 | { 11 | input, 12 | output: { 13 | file: path.join(__dirname, 'dist/floating-ui.dom.esm.js'), 14 | format: 'esm', 15 | }, 16 | }, 17 | { 18 | input, 19 | output: { 20 | file: path.join(__dirname, 'dist/floating-ui.dom.esm.min.js'), 21 | format: 'esm', 22 | }, 23 | }, 24 | { 25 | input, 26 | output: { 27 | name: 'FloatingUIDOM', 28 | file: path.join(__dirname, 'dist/floating-ui.dom.js'), 29 | format: 'umd', 30 | globals: { 31 | '@floating-ui/core': 'FloatingUICore', 32 | }, 33 | }, 34 | }, 35 | { 36 | input, 37 | output: { 38 | name: 'FloatingUIDOM', 39 | file: path.join(__dirname, 'dist/floating-ui.dom.min.js'), 40 | format: 'umd', 41 | globals: { 42 | '@floating-ui/core': 'FloatingUICore', 43 | }, 44 | }, 45 | }, 46 | ]; 47 | 48 | const buildExport = bundles.map(({input, output}) => ({ 49 | input, 50 | output, 51 | external: ['@floating-ui/core'], 52 | plugins: [ 53 | nodeResolve({extensions: ['.ts']}), 54 | babel({babelHelpers: 'bundled', extensions: ['.ts']}), 55 | replace({ 56 | __DEV__: output.file.includes('.min.') 57 | ? 'false' 58 | : 'process.env.NODE_ENV !== "production"', 59 | preventAssignment: true, 60 | }), 61 | output.file.includes('.min.') && terser(), 62 | ], 63 | })); 64 | 65 | const devExport = { 66 | input: path.join(__dirname, 'src/index.ts'), 67 | output: { 68 | file: path.join(__dirname, `test/visual/dist/index.mjs`), 69 | format: 'esm', 70 | }, 71 | plugins: [ 72 | nodeResolve({extensions: ['.ts']}), 73 | babel({babelHelpers: 'bundled', extensions: ['.ts']}), 74 | replace({ 75 | 'process.env.NODE_ENV': '"development"', 76 | __DEV__: 'true', 77 | preventAssignment: true, 78 | }), 79 | ], 80 | }; 81 | 82 | export default process.env.NODE_ENV === 'build' ? buildExport : devExport; 83 | -------------------------------------------------------------------------------- /packages/dom/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | computePosition as computePositionCore, 3 | ComputePositionConfig, 4 | VirtualElement, 5 | } from '@floating-ui/core'; 6 | import {platform} from './platform'; 7 | 8 | export const computePosition = ( 9 | reference: Element | VirtualElement, 10 | floating: HTMLElement, 11 | options: Partial 12 | ) => computePositionCore(reference, floating, {platform, ...options}); 13 | 14 | export { 15 | arrow, 16 | autoPlacement, 17 | flip, 18 | hide, 19 | offset, 20 | shift, 21 | limitShift, 22 | size, 23 | detectOverflow, 24 | } from '@floating-ui/core'; 25 | 26 | export {getScrollParents} from './utils/getScrollParents'; 27 | -------------------------------------------------------------------------------- /packages/dom/src/platform.ts: -------------------------------------------------------------------------------- 1 | import type {Platform} from '@floating-ui/core'; 2 | import {getRectRelativeToOffsetParent} from './utils/getRectRelativeToOffsetParent'; 3 | import {getOffsetParent} from './utils/getOffsetParent'; 4 | import {getDimensions} from './utils/getDimensions'; 5 | import {convertOffsetParentRelativeRectToViewportRelativeRect} from './utils/convertOffsetParentRelativeRectToViewportRelativeRect'; 6 | import {isElement} from './utils/is'; 7 | import {getDocumentElement} from './utils/getDocumentElement'; 8 | import {getClippingClientRect} from './utils/getClippingClientRect'; 9 | 10 | export const platform: Platform = { 11 | getElementRects: ({reference, floating, strategy}) => 12 | Promise.resolve({ 13 | reference: getRectRelativeToOffsetParent( 14 | reference, 15 | getOffsetParent(floating), 16 | strategy 17 | ), 18 | floating: {...getDimensions(floating), x: 0, y: 0}, 19 | }), 20 | convertOffsetParentRelativeRectToViewportRelativeRect: (args) => 21 | Promise.resolve( 22 | convertOffsetParentRelativeRectToViewportRelativeRect(args) 23 | ), 24 | getOffsetParent: ({element}) => Promise.resolve(getOffsetParent(element)), 25 | isElement: (value) => Promise.resolve(isElement(value)), 26 | getDocumentElement: ({element}) => 27 | Promise.resolve(getDocumentElement(element)), 28 | getClippingClientRect: (args) => Promise.resolve(getClippingClientRect(args)), 29 | getDimensions: ({element}) => Promise.resolve(getDimensions(element)), 30 | }; 31 | -------------------------------------------------------------------------------- /packages/dom/src/types.ts: -------------------------------------------------------------------------------- 1 | export type NodeScroll = { 2 | scrollLeft: number; 3 | scrollTop: number; 4 | }; 5 | 6 | export { 7 | arrow, 8 | autoPlacement, 9 | flip, 10 | hide, 11 | offset, 12 | shift, 13 | limitShift, 14 | size, 15 | detectOverflow, 16 | } from '@floating-ui/core'; 17 | 18 | export {computePosition} from './'; 19 | export {getScrollParents} from './utils/getScrollParents'; 20 | -------------------------------------------------------------------------------- /packages/dom/src/utils/contains.ts: -------------------------------------------------------------------------------- 1 | import {isShadowRoot} from './is'; 2 | 3 | export function contains(parent: Element, child: Element): boolean { 4 | const rootNode = child.getRootNode?.(); 5 | 6 | // First, attempt with faster native method 7 | if (parent.contains(child)) { 8 | return true; 9 | } 10 | // then fallback to custom implementation with Shadow DOM support 11 | else if (rootNode && isShadowRoot(rootNode)) { 12 | let next = child; 13 | do { 14 | if (next && parent.isSameNode(next)) { 15 | return true; 16 | } 17 | // @ts-ignore: need a better way to handle this... 18 | next = next.parentNode || next.host; 19 | } while (next); 20 | } 21 | 22 | return false; 23 | } 24 | -------------------------------------------------------------------------------- /packages/dom/src/utils/convertOffsetParentRelativeRectToViewportRelativeRect.ts: -------------------------------------------------------------------------------- 1 | import type {Rect, Strategy} from '@floating-ui/core'; 2 | import {getBoundingClientRect} from './getBoundingClientRect'; 3 | import {getNodeScroll} from './getNodeScroll'; 4 | import {getNodeName} from './getNodeName'; 5 | import {getDocumentElement} from './getDocumentElement'; 6 | import {isHTMLElement, isScrollParent} from './is'; 7 | 8 | export function convertOffsetParentRelativeRectToViewportRelativeRect({ 9 | rect, 10 | offsetParent, 11 | strategy, 12 | }: { 13 | rect: Rect; 14 | offsetParent: Element | Window; 15 | strategy: Strategy; 16 | }): Rect { 17 | const isOffsetParentAnElement = isHTMLElement(offsetParent); 18 | const documentElement = getDocumentElement(offsetParent); 19 | 20 | if (offsetParent === documentElement) { 21 | return rect; 22 | } 23 | 24 | let scroll = {scrollLeft: 0, scrollTop: 0}; 25 | let offsets = {x: 0, y: 0}; 26 | 27 | if ( 28 | isOffsetParentAnElement || 29 | (!isOffsetParentAnElement && strategy !== 'fixed') 30 | ) { 31 | if ( 32 | getNodeName(offsetParent) !== 'body' || 33 | isScrollParent(documentElement) 34 | ) { 35 | scroll = getNodeScroll(offsetParent); 36 | } 37 | 38 | if (isHTMLElement(offsetParent)) { 39 | offsets = getBoundingClientRect(offsetParent); 40 | offsets.x += offsetParent.clientLeft; 41 | offsets.y += offsetParent.clientTop; 42 | } 43 | // This doesn't appear to be need to be negated. 44 | // else if (documentElement) { 45 | // offsets.x = getWindowScrollBarX(documentElement); 46 | // } 47 | } 48 | 49 | return { 50 | ...rect, 51 | x: rect.x - scroll.scrollLeft + offsets.x, 52 | y: rect.y - scroll.scrollTop + offsets.y, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /packages/dom/src/utils/getBoundingClientRect.ts: -------------------------------------------------------------------------------- 1 | import type {ClientRectObject, VirtualElement} from '@floating-ui/core'; 2 | 3 | export function getBoundingClientRect( 4 | element: Element | VirtualElement 5 | ): ClientRectObject { 6 | const clientRect = element.getBoundingClientRect(); 7 | return { 8 | width: clientRect.width, 9 | height: clientRect.height, 10 | top: clientRect.top, 11 | right: clientRect.right, 12 | bottom: clientRect.bottom, 13 | left: clientRect.left, 14 | x: clientRect.left, 15 | y: clientRect.top, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/dom/src/utils/getClippingClientRect.ts: -------------------------------------------------------------------------------- 1 | import { 2 | rectToClientRect, 3 | ClientRectObject, 4 | Boundary, 5 | RootBoundary, 6 | } from '@floating-ui/core'; 7 | import {getViewportRect} from './getViewportRect'; 8 | import {getDocumentRect} from './getDocumentRect'; 9 | import {getScrollParents} from './getScrollParents'; 10 | import {getOffsetParent} from './getOffsetParent'; 11 | import {getDocumentElement} from './getDocumentElement'; 12 | import {getComputedStyle} from './getComputedStyle'; 13 | import {isElement, isHTMLElement} from './is'; 14 | import {getBoundingClientRect} from './getBoundingClientRect'; 15 | import {getParentNode} from './getParentNode'; 16 | import {contains} from './contains'; 17 | import {getNodeName} from './getNodeName'; 18 | 19 | function getInnerBoundingClientRect(element: Element): ClientRectObject { 20 | const clientRect = getBoundingClientRect(element); 21 | const top = clientRect.top + element.clientTop; 22 | const left = clientRect.left + element.clientLeft; 23 | return { 24 | top, 25 | left, 26 | x: left, 27 | y: top, 28 | right: left + element.clientWidth, 29 | bottom: top + element.clientHeight, 30 | width: element.clientWidth, 31 | height: element.clientHeight, 32 | }; 33 | } 34 | 35 | function getClientRectFromClippingParent( 36 | element: Element, 37 | clippingParent: Element | RootBoundary 38 | ): ClientRectObject { 39 | if (clippingParent === 'viewport') { 40 | return rectToClientRect(getViewportRect(element)); 41 | } 42 | 43 | if (isElement(clippingParent)) { 44 | return getInnerBoundingClientRect(clippingParent); 45 | } 46 | 47 | return rectToClientRect(getDocumentRect(getDocumentElement(element))); 48 | } 49 | 50 | // A "clipping parent" is an overflowable container with the characteristic of 51 | // clipping (or hiding) overflowing elements with a position different from 52 | // `initial` 53 | function getClippingParents(element: Element): Array { 54 | const clippingParents = getScrollParents(getParentNode(element)); 55 | const canEscapeClipping = ['absolute', 'fixed'].includes( 56 | getComputedStyle(element).position 57 | ); 58 | const clipperElement = 59 | canEscapeClipping && isHTMLElement(element) 60 | ? getOffsetParent(element) 61 | : element; 62 | 63 | if (!isElement(clipperElement)) { 64 | return []; 65 | } 66 | 67 | // @ts-ignore isElement check ensures we return Array 68 | return clippingParents.filter( 69 | (clippingParent) => 70 | isElement(clippingParent) && 71 | contains(clippingParent, clipperElement) && 72 | getNodeName(clippingParent) !== 'body' && 73 | (canEscapeClipping 74 | ? getComputedStyle(clippingParent).position !== 'static' 75 | : true) 76 | ); 77 | } 78 | 79 | // Gets the maximum area that the element is visible in due to any number of 80 | // clipping parents 81 | export function getClippingClientRect({ 82 | element, 83 | boundary, 84 | rootBoundary, 85 | }: { 86 | element: Element; 87 | boundary: Boundary; 88 | rootBoundary: RootBoundary; 89 | }): ClientRectObject { 90 | const mainClippingParents = 91 | boundary === 'clippingParents' 92 | ? getClippingParents(element) 93 | : [].concat(boundary); 94 | const clippingParents = [...mainClippingParents, rootBoundary]; 95 | const firstClippingParent = clippingParents[0]; 96 | 97 | const clippingRect = clippingParents.reduce((accRect, clippingParent) => { 98 | const rect = getClientRectFromClippingParent(element, clippingParent); 99 | 100 | accRect.top = Math.max(rect.top, accRect.top); 101 | accRect.right = Math.min(rect.right, accRect.right); 102 | accRect.bottom = Math.min(rect.bottom, accRect.bottom); 103 | accRect.left = Math.max(rect.left, accRect.left); 104 | 105 | return accRect; 106 | }, getClientRectFromClippingParent(element, firstClippingParent)); 107 | 108 | clippingRect.width = clippingRect.right - clippingRect.left; 109 | clippingRect.height = clippingRect.bottom - clippingRect.top; 110 | clippingRect.x = clippingRect.left; 111 | clippingRect.y = clippingRect.top; 112 | 113 | return clippingRect; 114 | } 115 | -------------------------------------------------------------------------------- /packages/dom/src/utils/getComputedStyle.ts: -------------------------------------------------------------------------------- 1 | import {getWindow} from './window'; 2 | 3 | export function getComputedStyle(element: Element) { 4 | return getWindow(element).getComputedStyle(element); 5 | } 6 | -------------------------------------------------------------------------------- /packages/dom/src/utils/getDimensions.ts: -------------------------------------------------------------------------------- 1 | import type {Dimensions} from '@floating-ui/core'; 2 | 3 | export function getDimensions(element: HTMLElement): Dimensions { 4 | return { 5 | width: element.offsetWidth, 6 | height: element.offsetHeight, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /packages/dom/src/utils/getDocumentElement.ts: -------------------------------------------------------------------------------- 1 | import {isNode} from './is'; 2 | 3 | export function getDocumentElement(node: Node | Window): HTMLElement { 4 | return ( 5 | (isNode(node) ? node.ownerDocument : node.document) || window.document 6 | ).documentElement; 7 | } 8 | -------------------------------------------------------------------------------- /packages/dom/src/utils/getDocumentRect.ts: -------------------------------------------------------------------------------- 1 | import type {Rect} from '@floating-ui/core'; 2 | import {getDocumentElement} from './getDocumentElement'; 3 | import {getComputedStyle} from './getComputedStyle'; 4 | import getWindowScrollBarX from './getWindowScrollBarX'; 5 | import {getNodeScroll} from './getNodeScroll'; 6 | 7 | // Gets the entire size of the scrollable document area, even extending outside 8 | // of the `` and `` rect bounds if horizontally scrollable 9 | export function getDocumentRect(element: HTMLElement): Rect { 10 | const html = getDocumentElement(element); 11 | const scroll = getNodeScroll(element); 12 | const body = element.ownerDocument?.body; 13 | 14 | const width = Math.max( 15 | html.scrollWidth, 16 | html.clientWidth, 17 | body ? body.scrollWidth : 0, 18 | body ? body.clientWidth : 0 19 | ); 20 | const height = Math.max( 21 | html.scrollHeight, 22 | html.clientHeight, 23 | body ? body.scrollHeight : 0, 24 | body ? body.clientHeight : 0 25 | ); 26 | 27 | let x = -scroll.scrollLeft + getWindowScrollBarX(element); 28 | const y = -scroll.scrollTop; 29 | 30 | if (getComputedStyle(body || html).direction === 'rtl') { 31 | x += Math.max(html.clientWidth, body ? body.clientWidth : 0) - width; 32 | } 33 | 34 | return {width, height, x, y}; 35 | } 36 | -------------------------------------------------------------------------------- /packages/dom/src/utils/getNodeName.ts: -------------------------------------------------------------------------------- 1 | import {isWindow} from './window'; 2 | 3 | export function getNodeName(node: Node | Window): string { 4 | return isWindow(node) ? '' : node ? (node.nodeName || '').toLowerCase() : ''; 5 | } 6 | -------------------------------------------------------------------------------- /packages/dom/src/utils/getNodeScroll.ts: -------------------------------------------------------------------------------- 1 | import {NodeScroll} from '../types'; 2 | import {isWindow} from './window'; 3 | 4 | export function getNodeScroll(element: Element | Window): NodeScroll { 5 | if (isWindow(element)) { 6 | return { 7 | scrollLeft: element.pageXOffset, 8 | scrollTop: element.pageYOffset, 9 | }; 10 | } 11 | 12 | return { 13 | scrollLeft: element.scrollLeft, 14 | scrollTop: element.scrollTop, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/dom/src/utils/getOffsetParent.ts: -------------------------------------------------------------------------------- 1 | import {getNodeName} from './getNodeName'; 2 | import {getParentNode} from './getParentNode'; 3 | import {getWindow} from './window'; 4 | import {isHTMLElement, isTableElement} from './is'; 5 | 6 | function getTrueOffsetParent(element: Element): Element | null { 7 | if ( 8 | !isHTMLElement(element) || 9 | getComputedStyle(element).position === 'fixed' 10 | ) { 11 | return null; 12 | } 13 | 14 | return element.offsetParent; 15 | } 16 | 17 | function getContainingBlock(element: Element) { 18 | // TODO: Try and use feature detection here instead 19 | const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); 20 | 21 | let currentNode: Node | null = getParentNode(element); 22 | 23 | while ( 24 | isHTMLElement(currentNode) && 25 | !['html', 'body'].includes(getNodeName(currentNode)) 26 | ) { 27 | const css = getComputedStyle(currentNode); 28 | 29 | // This is non-exhaustive but covers the most common CSS properties that 30 | // create a containing block. 31 | // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block 32 | if ( 33 | css.transform !== 'none' || 34 | css.perspective !== 'none' || 35 | css.contain === 'paint' || 36 | ['transform', 'perspective'].includes(css.willChange) || 37 | (isFirefox && css.willChange === 'filter') || 38 | (isFirefox && css.filter && css.filter !== 'none') 39 | ) { 40 | return currentNode; 41 | } else { 42 | currentNode = currentNode.parentNode; 43 | } 44 | } 45 | 46 | return null; 47 | } 48 | 49 | // Gets the closest ancestor positioned element. Handles some edge cases, 50 | // such as table ancestors and cross browser bugs. 51 | export function getOffsetParent(element: Element): Element | Window { 52 | const window = getWindow(element); 53 | 54 | let offsetParent = getTrueOffsetParent(element); 55 | 56 | while ( 57 | offsetParent && 58 | isTableElement(offsetParent) && 59 | getComputedStyle(offsetParent).position === 'static' 60 | ) { 61 | offsetParent = getTrueOffsetParent(offsetParent); 62 | } 63 | 64 | if ( 65 | offsetParent && 66 | (getNodeName(offsetParent) === 'html' || 67 | (getNodeName(offsetParent) === 'body' && 68 | getComputedStyle(offsetParent).position === 'static')) 69 | ) { 70 | return window; 71 | } 72 | 73 | return offsetParent || getContainingBlock(element) || window; 74 | } 75 | -------------------------------------------------------------------------------- /packages/dom/src/utils/getParentNode.ts: -------------------------------------------------------------------------------- 1 | import {getNodeName} from './getNodeName'; 2 | import {getDocumentElement} from './getDocumentElement'; 3 | import {isShadowRoot} from './is'; 4 | 5 | export function getParentNode(node: Node): Node { 6 | if (getNodeName(node) === 'html') { 7 | return node; 8 | } 9 | 10 | return ( 11 | // this is a quicker (but less type safe) way to save quite some bytes from the bundle 12 | // @ts-ignore 13 | node.assignedSlot || // step into the shadow DOM of the parent of a slotted node 14 | node.parentNode || // DOM Element detected 15 | (isShadowRoot(node) ? node.host : null) || // ShadowRoot detected 16 | getDocumentElement(node) // fallback 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /packages/dom/src/utils/getRectRelativeToOffsetParent.ts: -------------------------------------------------------------------------------- 1 | import type {Rect, Strategy, VirtualElement} from '@floating-ui/core'; 2 | import {getBoundingClientRect} from './getBoundingClientRect'; 3 | import {getDocumentElement} from './getDocumentElement'; 4 | import {getNodeName} from './getNodeName'; 5 | import {getNodeScroll} from './getNodeScroll'; 6 | import getWindowScrollBarX from './getWindowScrollBarX'; 7 | import {isHTMLElement, isScrollParent} from './is'; 8 | 9 | export function getRectRelativeToOffsetParent( 10 | element: Element | VirtualElement, 11 | offsetParent: Element | Window, 12 | strategy: Strategy 13 | ): Rect { 14 | const isOffsetParentAnElement = isHTMLElement(offsetParent); 15 | const documentElement = getDocumentElement(offsetParent); 16 | const rect = getBoundingClientRect(element); 17 | 18 | let scroll = {scrollLeft: 0, scrollTop: 0}; 19 | let offsets = {x: 0, y: 0}; 20 | 21 | if ( 22 | isOffsetParentAnElement || 23 | (!isOffsetParentAnElement && strategy !== 'fixed') 24 | ) { 25 | if ( 26 | getNodeName(offsetParent) !== 'body' || 27 | isScrollParent(documentElement) 28 | ) { 29 | scroll = getNodeScroll(offsetParent); 30 | } 31 | 32 | if (isHTMLElement(offsetParent)) { 33 | offsets = getBoundingClientRect(offsetParent); 34 | offsets.x += offsetParent.clientLeft; 35 | offsets.y += offsetParent.clientTop; 36 | } else if (documentElement) { 37 | offsets.x = getWindowScrollBarX(documentElement); 38 | } 39 | } 40 | 41 | return { 42 | x: rect.left + scroll.scrollLeft - offsets.x, 43 | y: rect.top + scroll.scrollTop - offsets.y, 44 | width: rect.width, 45 | height: rect.height, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /packages/dom/src/utils/getScrollParent.ts: -------------------------------------------------------------------------------- 1 | import {getParentNode} from './getParentNode'; 2 | import {getNodeName} from './getNodeName'; 3 | import {isScrollParent, isHTMLElement} from './is'; 4 | 5 | export function getScrollParent(node: Node): HTMLElement { 6 | if (['html', 'body', '#document'].includes(getNodeName(node))) { 7 | // @ts-ignore assume body is always available 8 | return node.ownerDocument.body; 9 | } 10 | 11 | if (isHTMLElement(node) && isScrollParent(node)) { 12 | return node; 13 | } 14 | 15 | return getScrollParent(getParentNode(node)); 16 | } 17 | -------------------------------------------------------------------------------- /packages/dom/src/utils/getScrollParents.ts: -------------------------------------------------------------------------------- 1 | import {getScrollParent} from './getScrollParent'; 2 | import {getParentNode} from './getParentNode'; 3 | import {getWindow} from './window'; 4 | import {isScrollParent} from './is'; 5 | 6 | export function getScrollParents( 7 | node: Node, 8 | list: Array = [] 9 | ): Array { 10 | const scrollParent = getScrollParent(node); 11 | const isBody = scrollParent === node.ownerDocument?.body; 12 | const win = getWindow(scrollParent); 13 | const target = isBody 14 | ? ([win] as any).concat( 15 | win.visualViewport || [], 16 | isScrollParent(scrollParent) ? scrollParent : [] 17 | ) 18 | : scrollParent; 19 | const updatedList = list.concat(target); 20 | 21 | return isBody 22 | ? updatedList 23 | : // @ts-ignore: isBody tells us target will be an HTMLElement here 24 | updatedList.concat(getScrollParents(getParentNode(target))); 25 | } 26 | -------------------------------------------------------------------------------- /packages/dom/src/utils/getViewportRect.ts: -------------------------------------------------------------------------------- 1 | import type {Rect} from '@floating-ui/core'; 2 | import {getWindow} from './window'; 3 | import {getDocumentElement} from './getDocumentElement'; 4 | 5 | export function getViewportRect(element: Element): Rect { 6 | const win = getWindow(element); 7 | const html = getDocumentElement(element); 8 | const visualViewport = win.visualViewport; 9 | 10 | let width = html.clientWidth; 11 | let height = html.clientHeight; 12 | let x = 0; 13 | let y = 0; 14 | 15 | if (visualViewport) { 16 | width = visualViewport.width; 17 | height = visualViewport.height; 18 | 19 | // Uses Layout Viewport (like Chrome; Safari does not currently) 20 | // In Chrome, it returns a value very close to 0 (+/-) but contains rounding 21 | // errors due to floating point numbers, so we need to check precision. 22 | // Safari returns a number <= 0, usually < -1 when pinch-zoomed 23 | if ( 24 | Math.abs(win.innerWidth / visualViewport.scale - visualViewport.width) < 25 | 0.001 26 | ) { 27 | x = visualViewport.offsetLeft; 28 | y = visualViewport.offsetTop; 29 | } 30 | } 31 | 32 | return {width, height, x, y}; 33 | } 34 | -------------------------------------------------------------------------------- /packages/dom/src/utils/getWindowScrollBarX.ts: -------------------------------------------------------------------------------- 1 | import {getBoundingClientRect} from './getBoundingClientRect'; 2 | import {getDocumentElement} from './getDocumentElement'; 3 | import {getNodeScroll} from './getNodeScroll'; 4 | 5 | export default function getWindowScrollBarX(element: Element): number { 6 | // If has a CSS width greater than the viewport, then this will be 7 | // incorrect for RTL. 8 | return ( 9 | getBoundingClientRect(getDocumentElement(element)).left + 10 | getNodeScroll(element).scrollLeft 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/dom/src/utils/is.ts: -------------------------------------------------------------------------------- 1 | import {getNodeName} from './getNodeName'; 2 | import {getWindow} from './window'; 3 | 4 | declare global { 5 | interface Window { 6 | HTMLElement: any; 7 | Element: any; 8 | Node: any; 9 | ShadowRoot: any; 10 | } 11 | } 12 | 13 | export function isHTMLElement(value: any): value is HTMLElement { 14 | return value instanceof getWindow(value).HTMLElement; 15 | } 16 | 17 | export function isElement(value: any): value is Element { 18 | return value instanceof getWindow(value).Element; 19 | } 20 | 21 | export function isNode(value: any): value is Node { 22 | return value instanceof getWindow(value).Node; 23 | } 24 | 25 | export function isShadowRoot(node: Node): node is ShadowRoot { 26 | const OwnElement = getWindow(node).ShadowRoot; 27 | return node instanceof OwnElement || node instanceof ShadowRoot; 28 | } 29 | 30 | export function isScrollParent(element: HTMLElement): boolean { 31 | // Firefox wants us to check `-x` and `-y` variations as well 32 | const {overflow, overflowX, overflowY} = getComputedStyle(element); 33 | return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX); 34 | } 35 | 36 | export function isTableElement(element: Element): boolean { 37 | return ['table', 'td', 'th'].includes(getNodeName(element)); 38 | } 39 | -------------------------------------------------------------------------------- /packages/dom/src/utils/window.ts: -------------------------------------------------------------------------------- 1 | export function isWindow(value: any): value is Window { 2 | return value?.toString() === '[object Window]'; 3 | } 4 | 5 | export function getWindow(node: Node | Window): Window { 6 | if (node == null) { 7 | return window; 8 | } 9 | 10 | if (!isWindow(node)) { 11 | const ownerDocument = node.ownerDocument; 12 | return ownerDocument ? ownerDocument.defaultView || window : window; 13 | } 14 | 15 | return node; 16 | } 17 | -------------------------------------------------------------------------------- /packages/dom/test/functional/base.test.ts: -------------------------------------------------------------------------------- 1 | import {test, expect} from '@playwright/test'; 2 | 3 | test('positioned on the right', async ({page}) => { 4 | await page.goto('http://localhost:1234/spec/base'); 5 | expect(await page.screenshot()).toMatchSnapshot('base.png'); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/dom/test/functional/base.test.ts-snapshots/base-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/floating-ui/fabae175bdcec7a95af6855e671bd5df1f4b3388/packages/dom/test/functional/base.test.ts-snapshots/base-linux.png -------------------------------------------------------------------------------- /packages/dom/test/functional/relative.test.ts: -------------------------------------------------------------------------------- 1 | import {test, expect} from '@playwright/test'; 2 | 3 | test('positioned on the right when html is relative', async ({page}) => { 4 | await page.goto('http://localhost:1234/spec/relative-html'); 5 | expect(await page.screenshot()).toMatchSnapshot('relative-html.png'); 6 | }); 7 | 8 | test('positioned on the right when body is relative', async ({page}) => { 9 | await page.goto('http://localhost:1234/spec/relative-body'); 10 | expect(await page.screenshot()).toMatchSnapshot('relative-body.png'); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/dom/test/functional/relative.test.ts-snapshots/relative-body-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/floating-ui/fabae175bdcec7a95af6855e671bd5df1f4b3388/packages/dom/test/functional/relative.test.ts-snapshots/relative-body-linux.png -------------------------------------------------------------------------------- /packages/dom/test/functional/relative.test.ts-snapshots/relative-html-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/floating-ui/fabae175bdcec7a95af6855e671bd5df1f4b3388/packages/dom/test/functional/relative.test.ts-snapshots/relative-html-linux.png -------------------------------------------------------------------------------- /packages/dom/test/functional/scrollbars.test.ts: -------------------------------------------------------------------------------- 1 | import {test, expect} from '@playwright/test'; 2 | 3 | test('not overflowing clipping container on the bottom', async ({page}) => { 4 | await page.goto('http://localhost:1234/spec/scroll-border'); 5 | expect(await page.screenshot()).toMatchSnapshot('scroll-border.png'); 6 | }); 7 | 8 | test('not overflowing clipping container on the right', async ({page}) => { 9 | await page.goto('http://localhost:1234/spec/scroll-border-right'); 10 | expect(await page.screenshot()).toMatchSnapshot('scroll-border-right.png'); 11 | }); 12 | 13 | test('not overflowing clipping container on the left (RTL)', async ({page}) => { 14 | await page.goto('http://localhost:1234/spec/scroll-border-rtl'); 15 | expect(await page.screenshot()).toMatchSnapshot('scroll-border-rtl.png'); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/dom/test/functional/scrollbars.test.ts-snapshots/scroll-border-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/floating-ui/fabae175bdcec7a95af6855e671bd5df1f4b3388/packages/dom/test/functional/scrollbars.test.ts-snapshots/scroll-border-linux.png -------------------------------------------------------------------------------- /packages/dom/test/functional/scrollbars.test.ts-snapshots/scroll-border-right-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/floating-ui/fabae175bdcec7a95af6855e671bd5df1f4b3388/packages/dom/test/functional/scrollbars.test.ts-snapshots/scroll-border-right-linux.png -------------------------------------------------------------------------------- /packages/dom/test/functional/scrollbars.test.ts-snapshots/scroll-border-rtl-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/floating-ui/fabae175bdcec7a95af6855e671bd5df1f4b3388/packages/dom/test/functional/scrollbars.test.ts-snapshots/scroll-border-rtl-linux.png -------------------------------------------------------------------------------- /packages/dom/test/functional/size.test.js: -------------------------------------------------------------------------------- 1 | import {test, expect} from '@playwright/test'; 2 | 3 | test('same width as reference', async ({page}) => { 4 | await page.goto('http://localhost:1234/spec/middleware/size/same-width'); 5 | expect(await page.screenshot()).toMatchSnapshot('same-width.png'); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/dom/test/functional/size.test.js-snapshots/same-width-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/floating-ui/fabae175bdcec7a95af6855e671bd5df1f4b3388/packages/dom/test/functional/size.test.js-snapshots/same-width-linux.png -------------------------------------------------------------------------------- /packages/dom/test/functional/transform.test.ts: -------------------------------------------------------------------------------- 1 | import {test, expect} from '@playwright/test'; 2 | 3 | test('positioned on the right', async ({page}) => { 4 | await page.goto('http://localhost:1234/spec/transform/reference-scaled'); 5 | expect(await page.screenshot()).toMatchSnapshot( 6 | 'transform-reference-scaled.png' 7 | ); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/dom/test/functional/transform.test.ts-snapshots/transform-reference-scaled-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/floating-ui/fabae175bdcec7a95af6855e671bd5df1f4b3388/packages/dom/test/functional/transform.test.ts-snapshots/transform-reference-scaled-linux.png -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
Ref
6 |
Float
7 | 8 | 16 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/clipping-border.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 |
16 |
Ref
17 |
Float
18 |
19 | 20 | 29 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/arrow/center-offset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
13 | Pop 14 |
15 |
16 | 17 | 31 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/arrow/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
13 | Pop 14 |
15 |
16 | 17 | 26 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/arrow/overflow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
13 | Pop 14 |
15 |
16 | 17 | 31 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/arrow/shift.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
13 | Pop 14 |
15 |
16 | 17 | 26 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/autoPlacement/virtual.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 |
24 |
25 |
26 |
Float
27 |
28 | 29 | 52 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/autoPlacement/x-axis.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
Float
13 | 14 | 22 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/autoPlacement/y-axis.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
Float
13 | 14 | 22 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/flip/fallbackPlacements.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
Float
13 | 14 | 23 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/flip/fallbackStrategy-bestFit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 |
17 |
Ref
18 |
Float
19 |
20 | 21 | 30 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/flip/fallbackStrategy-initialPlacement.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 |
17 |
Ref
18 |
Float
19 |
20 | 21 | 30 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/flip/flipAlignment-false.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
Float
13 | 14 | 23 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/flip/flipAlignment-true.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
Float
13 | 14 | 23 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/flip/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
Float
13 | 14 | 23 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/hide/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
Ref
7 |
Float
8 |
9 | 10 | 21 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/offset/both.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
Ref
6 |
Float
7 | 8 | 17 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/offset/crossAxis.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
Ref
6 |
Float
7 | 8 | 17 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/offset/mainAxis.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
Ref
6 |
Float
7 | 8 | 17 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/offset/number.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
Ref
6 |
Float
7 | 8 | 17 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/shift/padding.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
Float
13 | 14 | 25 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/shift/x-axis.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
Float
13 | 14 | 23 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/shift/y-axis.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
Float
13 | 14 | 23 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/size/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 |
13 |
Ref
14 |
Pop
15 |
16 | 17 | 37 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/middleware/size/same-width.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
Ref
6 |
Pop
7 | 8 | 27 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/relative-body.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
Float
13 | 14 | 22 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/relative-html.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
Float
13 | 14 | 22 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/rtl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 22 | 23 |
Ref
24 |
Float
25 | 26 | 37 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scroll-border-right.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 |
22 |
Ref
23 |
Float
24 |
25 | 26 | 27 | 50 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scroll-border-rtl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 |
23 |
Ref
24 |
Float
25 |
26 | 27 | 38 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scroll-border.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 25 | 26 |
27 |
Ref
28 |
Float
29 |
30 | 31 | 42 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-absolute/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
Ref
7 |
Float
8 |
9 | 10 | 20 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-absolute/different-scrolling-containers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
12 |
Ref
13 |
14 | 15 |
16 |
Float
17 |
18 | 19 | 29 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-absolute/nested-scrolling-containers-alt.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 |
Ref
8 |
9 |
Float
10 |
11 | 12 | 22 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-absolute/nested-scrolling-containers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 |
Ref
8 |
Float
9 |
10 |
11 | 12 | 22 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-absolute/popper-offset-parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
Float
7 |
8 | 9 |
Ref
10 | 11 | 21 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-absolute/reference-offset-parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
Ref
7 |
8 | 9 |
Float
10 | 11 | 21 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-absolute/same-offset-parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
Ref
7 |
Float
8 |
9 | 10 | 20 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-absolute/window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 |
Ref
14 |
Float
15 | 16 | 26 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-fixed/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
Ref
7 |
Float
8 |
9 | 10 | 21 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-fixed/different-scrolling-containers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
12 |
Ref
13 |
14 | 15 |
16 |
Float
17 |
18 | 19 | 30 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-fixed/nested-scrolling-containers-alt.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 |
Ref
8 |
9 |
Float
10 |
11 | 12 | 23 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-fixed/nested-scrolling-containers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 |
Ref
8 |
Float
9 |
10 |
11 | 12 | 23 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-fixed/popper-offset-parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
Float
7 |
8 | 9 |
Ref
10 | 11 | 22 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-fixed/reference-offset-parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
Ref
7 |
8 | 9 |
Float
10 | 11 | 22 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-fixed/same-offset-parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
Ref
7 |
Float
8 |
9 | 10 | 21 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/scrolling-fixed/window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 |
Ref
14 |
Float
15 | 16 | 27 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/transform/floating-scaled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 |
Ref
14 |
Float
15 | 16 | 24 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/transform/parent-scaled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
12 |
Ref
13 |
Float
14 |
15 | 16 | 24 | -------------------------------------------------------------------------------- /packages/dom/test/visual/spec/transform/reference-scaled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 |
Ref
12 |
Float
13 | 14 | 22 | -------------------------------------------------------------------------------- /packages/dom/test/visual/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial !important; 3 | } 4 | 5 | #scroll { 6 | overflow: scroll; 7 | background-color: #ccc; 8 | width: 400px; 9 | height: 400px; 10 | } 11 | 12 | #scroll::before { 13 | content: ''; 14 | display: block; 15 | width: 1px; 16 | height: 400px; 17 | } 18 | 19 | #scroll::after { 20 | content: ''; 21 | display: block; 22 | width: 1px; 23 | height: 400px; 24 | } 25 | 26 | #scroll[data-nested] { 27 | width: 350px; 28 | height: 350px; 29 | background-color: #eee; 30 | margin: 0 auto; 31 | } 32 | 33 | #reference { 34 | display: grid; 35 | place-items: center; 36 | width: 200px; 37 | height: 200px; 38 | background-color: white; 39 | border: 4px solid; 40 | color: black; 41 | font-weight: bold; 42 | } 43 | 44 | #floating { 45 | display: grid; 46 | place-items: center; 47 | position: absolute; 48 | width: 100px; 49 | height: 100px; 50 | background-color: white; 51 | border: 4px solid; 52 | color: #0082e1; 53 | font-weight: bold; 54 | } 55 | 56 | #floating[data-fixed] { 57 | position: fixed; 58 | } 59 | 60 | #arrowElement { 61 | position: absolute; 62 | width: 20px; 63 | height: 20px; 64 | background-color: #0082e1; 65 | } 66 | 67 | #clip { 68 | position: relative; 69 | overflow: hidden; 70 | border: 2px solid red; 71 | width: 500px; 72 | height: 300px; 73 | } 74 | -------------------------------------------------------------------------------- /packages/dom/test/visual/utils.mjs: -------------------------------------------------------------------------------- 1 | import {getScrollParents} from './dist/index.mjs'; 2 | 3 | const reference = document.querySelector('#reference'); 4 | const floating = document.querySelector('#floating'); 5 | const arrowElement = document.querySelector('#arrowElement'); 6 | 7 | export function position(data) { 8 | Object.assign(floating.style, { 9 | position: data.strategy, 10 | left: `${data.x}px`, 11 | top: `${data.y}px`, 12 | opacity: data.middlewareData.hide?.escaped ? '0.5' : '1', 13 | visibility: data.middlewareData.hide?.referenceHidden 14 | ? 'hidden' 15 | : 'visible', 16 | }); 17 | 18 | if (arrowElement) { 19 | Object.assign(arrowElement.style, { 20 | position: 'absolute', 21 | left: 22 | data.middlewareData.arrow?.x != null 23 | ? `${data.middlewareData.arrow?.x}px` 24 | : '', 25 | top: 26 | data.middlewareData.arrow?.y != null 27 | ? `${data.middlewareData.arrow?.y}px` 28 | : '', 29 | [{top: 'bottom', bottom: 'top', left: 'right', right: 'left'}[ 30 | data.placement.split('-')[0] 31 | ]]: `-${arrowElement.getBoundingClientRect().height}px`, 32 | opacity: data.middlewareData.arrow?.centerOffset !== 0 ? '0.5' : '1', 33 | }); 34 | } 35 | } 36 | 37 | export function addEventListeners(callback) { 38 | [...getScrollParents(reference), ...getScrollParents(floating)].forEach( 39 | (element) => { 40 | element.addEventListener('scroll', callback); 41 | element.addEventListener('resize', callback); 42 | } 43 | ); 44 | 45 | callback(); 46 | } 47 | -------------------------------------------------------------------------------- /packages/react-dom/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @floating-ui/react-dom 2 | 3 | ## 0.3.2 4 | 5 | ### Patch Changes 6 | 7 | - revert stale middleware position fix 8 | 9 | ## 0.3.1 10 | 11 | ### Patch Changes 12 | 13 | - fix: stale position after middleware change 14 | - fix: error if arrow ref is null 15 | - Updated dependencies 16 | - @floating-ui/dom@0.1.6 17 | 18 | ## 0.3.0 19 | 20 | ### Minor Changes 21 | 22 | - feat: allow a `ref` to be passed to `arrow` middleware element 23 | 24 | ### Patch Changes 25 | 26 | - Updated dependencies 27 | - @floating-ui/dom@0.1.5 28 | 29 | ## 0.2.1 30 | 31 | ### Patch Changes 32 | 33 | - fix(core): limitShift type 34 | - Updated dependencies 35 | - @floating-ui/dom@0.1.4 36 | 37 | ## 0.2.0 38 | 39 | ### Minor Changes 40 | 41 | - feat: remove need to memo middleware array 42 | -------------------------------------------------------------------------------- /packages/react-dom/README.md: -------------------------------------------------------------------------------- 1 | # @floating-ui/react-dom 2 | 3 | This is the library to use Floating UI with React DOM. 4 | -------------------------------------------------------------------------------- /packages/react-dom/babel.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | presets: [['@babel/env', {loose: true}], '@babel/typescript', '@babel/react'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/react-dom/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/types'; 2 | -------------------------------------------------------------------------------- /packages/react-dom/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | export default { 3 | preset: 'ts-jest', 4 | testEnvironment: 'jsdom', 5 | globals: { 6 | __DEV__: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/react-dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@floating-ui/react-dom", 3 | "version": "0.3.2", 4 | "description": "Floating UI for React DOM", 5 | "main": "dist/floating-ui.react-dom.js", 6 | "module": "dist/floating-ui.react-dom.esm.js", 7 | "unpkg": "dist/floating-ui.react-dom.min.js", 8 | "type": "module", 9 | "exports": { 10 | "import": "./dist/floating-ui.react-dom.esm.js", 11 | "require": "./dist/floating-ui.react-dom.js" 12 | }, 13 | "sideEffects": false, 14 | "files": [ 15 | "dist/", 16 | "index.d.ts", 17 | "src/**/*.d.ts" 18 | ], 19 | "browserslist": "> 0.5%, not dead, not IE 11", 20 | "scripts": { 21 | "test": "jest test", 22 | "build": "NODE_ENV=build rollup -c", 23 | "dev": "parcel test/visual/index.html --dist-dir test/visual/dist" 24 | }, 25 | "author": "atomiks", 26 | "license": "MIT", 27 | "bugs": "https://github.com/atomiks/floating-ui", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/atomiks/floating-ui.git", 31 | "directory": "packages/react-dom" 32 | }, 33 | "homepage": "https://floating-ui.com/docs/react-dom", 34 | "keywords": [ 35 | "tooltip", 36 | "popover", 37 | "dropdown", 38 | "menu", 39 | "popup", 40 | "positioning", 41 | "react", 42 | "react-dom" 43 | ], 44 | "peerDependencies": { 45 | "react": ">=16.8.0", 46 | "react-dom": ">=16.8.0" 47 | }, 48 | "dependencies": { 49 | "@floating-ui/dom": "^0.1.6", 50 | "use-isomorphic-layout-effect": "^1.1.1" 51 | }, 52 | "devDependencies": { 53 | "@babel/preset-env": "^7.16.4", 54 | "@babel/preset-react": "^7.16.0", 55 | "@babel/preset-typescript": "^7.16.0", 56 | "@rollup/plugin-babel": "^5.3.0", 57 | "@rollup/plugin-commonjs": "^21.0.1", 58 | "@rollup/plugin-node-resolve": "^13.0.6", 59 | "@rollup/plugin-replace": "^3.0.0", 60 | "@testing-library/react": "^12.1.2", 61 | "@testing-library/react-hooks": "^7.0.2", 62 | "@types/jest": "^27.0.3", 63 | "@types/react": "^17.0.37", 64 | "jest": "^27.3.1", 65 | "parcel": "^2.0.1", 66 | "react": "^17.0.2", 67 | "react-dom": "^17.0.2", 68 | "rollup": "^2.60.1", 69 | "rollup-plugin-terser": "^7.0.2", 70 | "ts-jest": "^27.0.7", 71 | "typescript": "^4.5.2" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/react-dom/rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import {babel} from '@rollup/plugin-babel'; 3 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 4 | import replace from '@rollup/plugin-replace'; 5 | import {terser} from 'rollup-plugin-terser'; 6 | import commonjs from '@rollup/plugin-commonjs'; 7 | 8 | const input = path.join(__dirname, 'src/index.ts'); 9 | 10 | const bundles = [ 11 | { 12 | input, 13 | output: { 14 | file: path.join(__dirname, 'dist/floating-ui.react-dom.esm.js'), 15 | format: 'esm', 16 | }, 17 | }, 18 | { 19 | input, 20 | output: { 21 | file: path.join(__dirname, 'dist/floating-ui.react-dom.esm.min.js'), 22 | format: 'esm', 23 | }, 24 | }, 25 | { 26 | input, 27 | output: { 28 | name: 'FloatingUIReactDOM', 29 | file: path.join(__dirname, 'dist/floating-ui.react-dom.js'), 30 | format: 'umd', 31 | globals: { 32 | react: 'React', 33 | 'react-dom': 'ReactDOM', 34 | '@floating-ui/core': 'FloatingUICore', 35 | '@floating-ui/dom': 'FloatingUIDOM', 36 | }, 37 | }, 38 | }, 39 | { 40 | input, 41 | output: { 42 | name: 'FloatingUIReactDOM', 43 | file: path.join(__dirname, 'dist/floating-ui.react-dom.min.js'), 44 | format: 'umd', 45 | globals: { 46 | react: 'React', 47 | 'react-dom': 'ReactDOM', 48 | '@floating-ui/core': 'FloatingUICore', 49 | '@floating-ui/dom': 'FloatingUIDOM', 50 | }, 51 | }, 52 | }, 53 | ]; 54 | 55 | const buildExport = bundles.map(({input, output}) => ({ 56 | input, 57 | output, 58 | external: ['react', 'react-dom', '@floating-ui/core', '@floating-ui/dom'], 59 | plugins: [ 60 | commonjs(), 61 | nodeResolve({extensions: ['.ts']}), 62 | babel({babelHelpers: 'bundled', extensions: ['.ts']}), 63 | replace({ 64 | __DEV__: output.file.includes('.min.') 65 | ? 'false' 66 | : 'process.env.NODE_ENV !== "production"', 67 | preventAssignment: true, 68 | }), 69 | output.file.includes('.min.') && terser(), 70 | ], 71 | })); 72 | 73 | const devExport = { 74 | input: path.join(__dirname, 'src/index.ts'), 75 | output: { 76 | file: path.join(__dirname, `test/visual/dist/index.mjs`), 77 | format: 'esm', 78 | }, 79 | plugins: [ 80 | commonjs(), 81 | nodeResolve({extensions: ['.ts']}), 82 | babel({babelHelpers: 'bundled', extensions: ['.ts']}), 83 | replace({ 84 | __DEV__: 'true', 85 | preventAssignment: true, 86 | }), 87 | ], 88 | }; 89 | 90 | export default process.env.NODE_ENV === 'build' ? buildExport : devExport; 91 | -------------------------------------------------------------------------------- /packages/react-dom/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ComputePositionConfig, 3 | ComputePositionReturn, 4 | Middleware, 5 | SideObject, 6 | VirtualElement, 7 | } from '@floating-ui/core'; 8 | import {computePosition, arrow as arrowCore} from '@floating-ui/dom'; 9 | import {useCallback, useMemo, useState, useRef, MutableRefObject} from 'react'; 10 | import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect'; 11 | import {useLatestRef} from './utils/useLatestRef'; 12 | 13 | export { 14 | autoPlacement, 15 | flip, 16 | hide, 17 | offset, 18 | shift, 19 | limitShift, 20 | size, 21 | getScrollParents, 22 | detectOverflow, 23 | } from '@floating-ui/dom'; 24 | 25 | type Data = Omit & { 26 | x: number | null; 27 | y: number | null; 28 | }; 29 | 30 | type UseFloatingReturn = Data & { 31 | update: () => void; 32 | reference: (node: Element | VirtualElement | null) => void; 33 | floating: (node: HTMLElement | null) => void; 34 | refs: { 35 | reference: MutableRefObject; 36 | floating: MutableRefObject; 37 | }; 38 | }; 39 | 40 | export function useFloating({ 41 | middleware, 42 | placement, 43 | strategy, 44 | }: Omit, 'platform'> = {}): UseFloatingReturn { 45 | const reference = useRef(null); 46 | const floating = useRef(null); 47 | const [data, setData] = useState({ 48 | // Setting these to `null` will allow the consumer to determine if 49 | // `computePosition()` has run yet 50 | x: null, 51 | y: null, 52 | strategy: strategy ?? 'absolute', 53 | placement: 'bottom', 54 | middlewareData: {}, 55 | }); 56 | 57 | // Memoize middleware internally, to remove the requirement of memoization by consumer 58 | const latestMiddleware = useLatestRef(middleware); 59 | 60 | const update = useCallback(() => { 61 | if (!reference.current || !floating.current) { 62 | return; 63 | } 64 | 65 | computePosition(reference.current, floating.current, { 66 | middleware: latestMiddleware.current, 67 | placement, 68 | strategy, 69 | }).then(setData); 70 | }, [latestMiddleware, placement, strategy]); 71 | 72 | useIsomorphicLayoutEffect(update, [update]); 73 | 74 | const setReference = useCallback( 75 | (node) => { 76 | reference.current = node; 77 | update(); 78 | }, 79 | [update] 80 | ); 81 | 82 | const setFloating = useCallback( 83 | (node) => { 84 | floating.current = node; 85 | update(); 86 | }, 87 | [update] 88 | ); 89 | 90 | return useMemo( 91 | () => ({ 92 | ...data, 93 | update, 94 | reference: setReference, 95 | floating: setFloating, 96 | refs: {reference, floating}, 97 | }), 98 | [data, update, setReference, setFloating] 99 | ); 100 | } 101 | 102 | export const arrow = ({ 103 | element, 104 | padding, 105 | }: { 106 | element: MutableRefObject | HTMLElement; 107 | padding?: number | SideObject; 108 | }): Middleware => { 109 | function isRef(value: unknown): value is MutableRefObject { 110 | return Object.prototype.hasOwnProperty.call(value, 'current'); 111 | } 112 | 113 | return { 114 | name: 'arrow', 115 | fn(args) { 116 | if (isRef(element)) { 117 | if (element.current != null) { 118 | return arrowCore({element: element.current, padding}).fn(args); 119 | } 120 | 121 | return {}; 122 | } else if (element) { 123 | return arrowCore({element, padding}).fn(args); 124 | } 125 | 126 | return {}; 127 | }, 128 | }; 129 | }; 130 | -------------------------------------------------------------------------------- /packages/react-dom/src/types.ts: -------------------------------------------------------------------------------- 1 | export {useFloating} from './'; 2 | 3 | export { 4 | arrow, 5 | autoPlacement, 6 | flip, 7 | hide, 8 | offset, 9 | shift, 10 | limitShift, 11 | size, 12 | detectOverflow, 13 | getScrollParents, 14 | } from '@floating-ui/dom'; 15 | -------------------------------------------------------------------------------- /packages/react-dom/src/utils/useLatestRef.ts: -------------------------------------------------------------------------------- 1 | import {useRef} from 'react'; 2 | import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect'; 3 | 4 | /** 5 | * @see https://epicreact.dev/the-latest-ref-pattern-in-react/ 6 | */ 7 | export function useLatestRef(value: Value) { 8 | const ref = useRef(value); 9 | 10 | useIsomorphicLayoutEffect(() => { 11 | ref.current = value; 12 | }); 13 | 14 | return ref; 15 | } 16 | -------------------------------------------------------------------------------- /packages/react-dom/test/index.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import {useFloating} from '../src'; 5 | import {renderHook} from '@testing-library/react-hooks'; 6 | import {render, waitFor} from '@testing-library/react'; 7 | import * as FloatingUIDom from '@floating-ui/dom'; 8 | 9 | test('`x` and `y` are initially `null`', async () => { 10 | const {result} = renderHook(() => useFloating()); 11 | 12 | expect(result.current.x).toBe(null); 13 | expect(result.current.y).toBe(null); 14 | }); 15 | 16 | test('`middleware` is memoized internally', async () => { 17 | function Component() { 18 | const {reference, floating} = useFloating({ 19 | middleware: [ 20 | { 21 | name: 'identity', 22 | fn({x, y}) { 23 | return {x, y}; 24 | }, 25 | }, 26 | ], 27 | }); 28 | 29 | return ( 30 |
31 | 32 |
floating
33 |
34 | ); 35 | } 36 | 37 | const spy = jest.spyOn(FloatingUIDom, 'computePosition'); 38 | 39 | const {rerender} = render(); 40 | 41 | await waitFor(() => { 42 | expect(spy).toHaveBeenCalledTimes(2); 43 | }); 44 | 45 | rerender(); 46 | 47 | await waitFor(() => { 48 | expect(spy).toHaveBeenCalledTimes(2); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/react-dom/test/visual/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 30 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /packages/react-dom/test/visual/index.js: -------------------------------------------------------------------------------- 1 | import {useEffect, useRef} from 'react'; 2 | import {render} from 'react-dom'; 3 | import {useFloating, shift, flip, arrow, getScrollParents} from '../../src'; 4 | 5 | function App() { 6 | const arrowRef = useRef(); 7 | const { 8 | x, 9 | y, 10 | reference, 11 | floating, 12 | update, 13 | middlewareData: {arrow: {x: arrowX, y: arrowY} = {}}, 14 | } = useFloating({ 15 | placement: 'top-end', 16 | middleware: [flip(), shift(), arrow({element: arrowRef})], 17 | }); 18 | 19 | useEffect(() => { 20 | if (!reference.current || !floating.current) { 21 | return; 22 | } 23 | 24 | const nodes = [ 25 | ...getScrollParents(reference.current), 26 | ...getScrollParents(floating.current), 27 | ]; 28 | 29 | nodes.forEach((node) => { 30 | node.addEventListener('scroll', update); 31 | node.addEventListener('resize', update); 32 | }); 33 | 34 | return () => { 35 | nodes.forEach((node) => { 36 | node.removeEventListener('scroll', update); 37 | node.removeEventListener('resize', update); 38 | }); 39 | }; 40 | }, [floating, reference, update]); 41 | 42 | return ( 43 | <> 44 |
45 | Reference 46 |
47 |
56 | Floating 57 |
66 |
67 | 68 | ); 69 | } 70 | 71 | render(, document.getElementById('root')); 72 | -------------------------------------------------------------------------------- /packages/react-native/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @floating-ui/react-native 2 | 3 | ## 0.3.2 4 | 5 | ### Patch Changes 6 | 7 | - revert stale middleware position fix 8 | - fix: missing `update()` function type 9 | 10 | ## 0.3.1 11 | 12 | ### Patch Changes 13 | 14 | - fix: stale position after middleware change 15 | - fix: error if arrow ref is null 16 | - Updated dependencies 17 | - @floating-ui/core@0.2.0 18 | 19 | ## 0.3.0 20 | 21 | ### Minor Changes 22 | 23 | - feat: allow a `ref` to be passed to `arrow` middleware element 24 | 25 | ### Patch Changes 26 | 27 | - Updated dependencies 28 | - @floating-ui/core@0.1.5 29 | 30 | ## 0.2.1 31 | 32 | ### Patch Changes 33 | 34 | - fix(core): limitShift type 35 | - Updated dependencies 36 | - @floating-ui/core@0.1.4 37 | 38 | ## 0.2.0 39 | 40 | ### Minor Changes 41 | 42 | - feat: remove need to memo middleware array 43 | 44 | ### Patch Changes 45 | 46 | - Updated dependencies 47 | - @floating-ui/core@0.1.3 48 | -------------------------------------------------------------------------------- /packages/react-native/README.md: -------------------------------------------------------------------------------- 1 | # @floating-ui/react-native 2 | 3 | This is the library to use Floating UI with React Native. 4 | -------------------------------------------------------------------------------- /packages/react-native/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/env', {loose: true}], '@babel/typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/react-native/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/types'; 2 | -------------------------------------------------------------------------------- /packages/react-native/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@floating-ui/react-native", 3 | "version": "0.3.2", 4 | "description": "Floating UI for React Native", 5 | "main": "dist/floating-ui.react-native.js", 6 | "module": "dist/floating-ui.react-native.esm.js", 7 | "sideEffects": false, 8 | "files": [ 9 | "dist/", 10 | "index.d.ts", 11 | "src/**/*.d.ts" 12 | ], 13 | "browserslist": "> 0.5%, not dead", 14 | "scripts": { 15 | "test": "jest test", 16 | "build": "NODE_ENV=build rollup -c" 17 | }, 18 | "author": "atomiks", 19 | "license": "MIT", 20 | "bugs": "https://github.com/atomiks/floating-ui", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/atomiks/floating-ui.git", 24 | "directory": "packages/react-native" 25 | }, 26 | "homepage": "https://floating-ui.com/docs/react-native", 27 | "keywords": [ 28 | "tooltip", 29 | "popover", 30 | "dropdown", 31 | "menu", 32 | "popup", 33 | "positioning", 34 | "react", 35 | "react-native" 36 | ], 37 | "peerDependencies": { 38 | "react": ">=16.8.0", 39 | "react-native": ">=0.64.0" 40 | }, 41 | "dependencies": { 42 | "@floating-ui/core": "^0.2.0" 43 | }, 44 | "devDependencies": { 45 | "@babel/preset-env": "^7.16.4", 46 | "@babel/preset-typescript": "^7.16.0", 47 | "@rollup/plugin-babel": "^5.3.0", 48 | "@rollup/plugin-commonjs": "^21.0.1", 49 | "@rollup/plugin-node-resolve": "^13.0.6", 50 | "@rollup/plugin-replace": "^3.0.0", 51 | "@types/jest": "^27.0.3", 52 | "@types/react": "^17.0.37", 53 | "@types/react-native": "^0.66.6", 54 | "jest": "^27.3.1", 55 | "react": "^17.0.2", 56 | "react-dom": "^17.0.2", 57 | "rollup": "^2.60.1", 58 | "rollup-plugin-terser": "^7.0.2", 59 | "ts-jest": "^27.0.7", 60 | "typescript": "^4.5.2" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/react-native/rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import {babel} from '@rollup/plugin-babel'; 3 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 4 | import replace from '@rollup/plugin-replace'; 5 | import {terser} from 'rollup-plugin-terser'; 6 | import commonjs from '@rollup/plugin-commonjs'; 7 | 8 | const input = path.join(__dirname, 'src/index.ts'); 9 | 10 | const bundles = [ 11 | { 12 | input, 13 | output: { 14 | file: path.join(__dirname, 'dist/floating-ui.react-native.esm.js'), 15 | format: 'esm', 16 | }, 17 | }, 18 | { 19 | input, 20 | output: { 21 | file: path.join(__dirname, 'dist/floating-ui.react-native.esm.min.js'), 22 | format: 'esm', 23 | }, 24 | }, 25 | { 26 | input, 27 | output: { 28 | name: 'FloatingUIReactNative', 29 | file: path.join(__dirname, 'dist/floating-ui.react-native.js'), 30 | format: 'umd', 31 | globals: { 32 | react: 'React', 33 | 'react-native': 'ReactNative', 34 | '@floating-ui/core': 'FloatingUICore', 35 | }, 36 | }, 37 | }, 38 | { 39 | input, 40 | output: { 41 | name: 'FloatingUIReactNative', 42 | file: path.join(__dirname, 'dist/floating-ui.react-native.min.js'), 43 | format: 'umd', 44 | globals: { 45 | react: 'React', 46 | 'react-native': 'ReactNative', 47 | '@floating-ui/core': 'FloatingUICore', 48 | }, 49 | }, 50 | }, 51 | ]; 52 | 53 | const buildExport = bundles.map(({input, output}) => ({ 54 | input, 55 | output, 56 | external: ['react', 'react-native', '@floating-ui/core'], 57 | plugins: [ 58 | commonjs(), 59 | nodeResolve({extensions: ['.ts']}), 60 | babel({babelHelpers: 'bundled', extensions: ['.ts']}), 61 | replace({ 62 | __DEV__: output.file.includes('.min.') 63 | ? 'false' 64 | : 'process.env.NODE_ENV !== "production"', 65 | preventAssignment: true, 66 | }), 67 | output.file.includes('.min.') && terser(), 68 | ], 69 | })); 70 | 71 | export default buildExport; 72 | -------------------------------------------------------------------------------- /packages/react-native/src/createPlatform.ts: -------------------------------------------------------------------------------- 1 | import {Dimensions} from 'react-native'; 2 | import { 3 | Platform, 4 | Dimensions as DimensionsType, 5 | rectToClientRect, 6 | } from '@floating-ui/core'; 7 | 8 | const ORIGIN = {x: 0, y: 0}; 9 | 10 | export const createPlatform = ({ 11 | offsetParent, 12 | sameScrollView = true, 13 | scrollOffsets = ORIGIN, 14 | }: { 15 | offsetParent: any; 16 | sameScrollView: boolean; 17 | scrollOffsets: { 18 | x: number; 19 | y: number; 20 | }; 21 | }): Platform => ({ 22 | getElementRects({reference, floating}) { 23 | return new Promise((resolve) => { 24 | const onMeasure = (offsetX = 0, offsetY = 0) => { 25 | floating.measure( 26 | (x: number, y: number, width: number, height: number) => { 27 | const floatingRect = {width, height, ...ORIGIN}; 28 | const method = sameScrollView ? 'measure' : 'measureInWindow'; 29 | 30 | reference[method]( 31 | (x: number, y: number, width: number, height: number) => { 32 | const referenceRect = { 33 | width, 34 | height, 35 | x: x - offsetX, 36 | y: y - offsetY, 37 | }; 38 | 39 | resolve({reference: referenceRect, floating: floatingRect}); 40 | } 41 | ); 42 | } 43 | ); 44 | }; 45 | 46 | if (offsetParent.current) { 47 | offsetParent.current.measure(onMeasure); 48 | } else { 49 | onMeasure(); 50 | } 51 | }); 52 | }, 53 | getClippingClientRect() { 54 | const {width, height} = Dimensions.get('window'); 55 | return Promise.resolve( 56 | rectToClientRect({ 57 | width, 58 | height, 59 | ...(sameScrollView ? scrollOffsets : ORIGIN), 60 | }) 61 | ); 62 | }, 63 | convertOffsetParentRelativeRectToViewportRelativeRect({rect}) { 64 | return new Promise((resolve) => { 65 | const onMeasure = (offsetX = 0, offsetY = 0) => { 66 | resolve({...rect, x: rect.x + offsetX, y: rect.y + offsetY}); 67 | }; 68 | 69 | if (offsetParent.current) { 70 | offsetParent.current.measure(onMeasure); 71 | } else { 72 | onMeasure(); 73 | } 74 | }); 75 | }, 76 | getDocumentElement: () => Promise.resolve({}), 77 | // these are the properties accessed on an offsetParent 78 | getOffsetParent: () => 79 | Promise.resolve({ 80 | clientLeft: 0, 81 | clientTop: 0, 82 | clientWidth: 0, 83 | clientHeight: 0, 84 | }), 85 | getDimensions: ({element}) => 86 | new Promise((resolve) => 87 | element.measure(({width, height}: DimensionsType) => 88 | resolve({width, height}) 89 | ) 90 | ), 91 | isElement: () => Promise.resolve(true), 92 | }); 93 | -------------------------------------------------------------------------------- /packages/react-native/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useMemo, 3 | useState, 4 | useLayoutEffect, 5 | useRef, 6 | useCallback, 7 | RefObject, 8 | } from 'react'; 9 | import {computePosition, arrow as arrowCore} from '@floating-ui/core'; 10 | import type { 11 | Placement, 12 | Middleware, 13 | ComputePositionReturn, 14 | SideObject, 15 | } from '@floating-ui/core'; 16 | import {createPlatform} from './createPlatform'; 17 | import {useLatestRef} from './utils/useLatestRef'; 18 | 19 | export { 20 | autoPlacement, 21 | flip, 22 | hide, 23 | limitShift, 24 | offset, 25 | shift, 26 | size, 27 | detectOverflow, 28 | } from '@floating-ui/core'; 29 | 30 | const ORIGIN = {x: 0, y: 0}; 31 | 32 | type UseFloatingReturn = Data & { 33 | update: () => void; 34 | offsetParent: (node: any) => void; 35 | floating: (node: any) => void; 36 | reference: (node: any) => void; 37 | refs: { 38 | reference: RefObject; 39 | floating: RefObject; 40 | offsetParent: RefObject; 41 | }; 42 | scrollProps: { 43 | onScroll: (event: { 44 | nativeEvent: { 45 | contentOffset: {x: number; y: number}; 46 | }; 47 | }) => void; 48 | scrollEventThrottle: 16; 49 | }; 50 | }; 51 | 52 | type Data = Omit & { 53 | x: number | null; 54 | y: number | null; 55 | }; 56 | 57 | export const useFloating = ({ 58 | placement = 'bottom', 59 | middleware, 60 | sameScrollView = true, 61 | }: { 62 | placement?: Placement; 63 | middleware?: Array; 64 | sameScrollView?: boolean; 65 | } = {}): UseFloatingReturn => { 66 | const reference = useRef(); 67 | const floating = useRef(); 68 | const offsetParent = useRef(); 69 | 70 | const [data, setData] = useState({ 71 | x: null, 72 | y: null, 73 | placement, 74 | strategy: 'absolute', 75 | middlewareData: {}, 76 | }); 77 | 78 | const [scrollOffsets, setScrollOffsets] = useState(ORIGIN); 79 | 80 | const platform = useMemo( 81 | () => createPlatform({offsetParent, scrollOffsets, sameScrollView}), 82 | [offsetParent, scrollOffsets, sameScrollView] 83 | ); 84 | 85 | // Memoize middleware internally, to remove the requirement of memoization by consumer 86 | const latestMiddleware = useLatestRef(middleware); 87 | 88 | const update = useCallback(() => { 89 | if (!reference.current || !floating.current) { 90 | return; 91 | } 92 | 93 | computePosition(reference.current, floating.current, { 94 | middleware: latestMiddleware.current, 95 | platform, 96 | placement, 97 | }).then(setData); 98 | }, [latestMiddleware, platform, placement]); 99 | 100 | useLayoutEffect(() => { 101 | requestAnimationFrame(update); 102 | }, [update]); 103 | 104 | const setReference = useCallback( 105 | (node) => { 106 | reference.current = node; 107 | requestAnimationFrame(update); 108 | }, 109 | [update] 110 | ); 111 | 112 | const setFloating = useCallback( 113 | (node) => { 114 | floating.current = node; 115 | requestAnimationFrame(update); 116 | }, 117 | [update] 118 | ); 119 | 120 | const setOffsetParent = useCallback( 121 | (node) => { 122 | offsetParent.current = node; 123 | requestAnimationFrame(update); 124 | }, 125 | [update] 126 | ); 127 | 128 | return useMemo( 129 | () => ({ 130 | ...data, 131 | update, 132 | refs: {reference, floating, offsetParent}, 133 | offsetParent: setOffsetParent, 134 | reference: setReference, 135 | floating: setFloating, 136 | scrollProps: { 137 | onScroll: (event) => setScrollOffsets(event.nativeEvent.contentOffset), 138 | scrollEventThrottle: 16, 139 | }, 140 | }), 141 | [data, setReference, setFloating, setOffsetParent, update] 142 | ); 143 | }; 144 | 145 | export const arrow = ({ 146 | element, 147 | padding, 148 | }: { 149 | element: any; 150 | padding?: number | SideObject; 151 | }): Middleware => { 152 | function isRef(value: unknown) { 153 | return Object.prototype.hasOwnProperty.call(value, 'current'); 154 | } 155 | 156 | return { 157 | name: 'arrow', 158 | fn(args) { 159 | if (isRef(element)) { 160 | if (element.current != null) { 161 | return arrowCore({element: element.current, padding}).fn(args); 162 | } 163 | 164 | return {}; 165 | } else if (element) { 166 | return arrowCore({element, padding}).fn(args); 167 | } 168 | 169 | return {}; 170 | }, 171 | }; 172 | }; 173 | -------------------------------------------------------------------------------- /packages/react-native/src/types.ts: -------------------------------------------------------------------------------- 1 | export {useFloating} from './'; 2 | 3 | export { 4 | arrow, 5 | autoPlacement, 6 | flip, 7 | hide, 8 | offset, 9 | shift, 10 | limitShift, 11 | size, 12 | detectOverflow, 13 | } from '@floating-ui/core'; 14 | -------------------------------------------------------------------------------- /packages/react-native/src/utils/useLatestRef.ts: -------------------------------------------------------------------------------- 1 | import {useLayoutEffect, useRef} from 'react'; 2 | 3 | /** 4 | * @see https://epicreact.dev/the-latest-ref-pattern-in-react/ 5 | */ 6 | export function useLatestRef(value: Value) { 7 | const ref = useRef(value); 8 | 9 | useLayoutEffect(() => { 10 | ref.current = value; 11 | }); 12 | 13 | return ref; 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "es2019"], 4 | "skipLibCheck": true, 5 | "moduleResolution": "node", 6 | "noEmit": true, 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "noUnusedLocals": true, 10 | "types": ["node", "jest"], 11 | "allowJs": true, 12 | "esModuleInterop": true, 13 | "jsx": "react-jsx" 14 | }, 15 | "include": ["packages/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /website/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # website 2 | 3 | ## 1.0.1 4 | ### Patch Changes 5 | 6 | - Updated dependencies 7 | - @floating-ui/react-dom@0.3.0 8 | -------------------------------------------------------------------------------- /website/assets/global.css: -------------------------------------------------------------------------------- 1 | [data-reach-skip-nav-link] { 2 | color: black; 3 | margin-left: 18rem; 4 | } 5 | 6 | @media (max-width: 600px) { 7 | [data-reach-skip-nav-link] { 8 | margin-left: calc(25vw); 9 | } 10 | } 11 | 12 | .shiki { 13 | position: relative; 14 | } 15 | -------------------------------------------------------------------------------- /website/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/floating-ui/fabae175bdcec7a95af6855e671bd5df1f4b3388/website/assets/logo.png -------------------------------------------------------------------------------- /website/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /website/components/Chrome.js: -------------------------------------------------------------------------------- 1 | import {useRef} from 'react'; 2 | import cn from 'classnames'; 3 | import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect'; 4 | 5 | export const Chrome = ({ 6 | children, 7 | // dark, 8 | center, 9 | scrollable, 10 | className, 11 | }) => { 12 | const scrollableRef = useRef(); 13 | 14 | useIsomorphicLayoutEffect(() => { 15 | if (scrollable) { 16 | scrollableRef.current.scrollTop = 250; 17 | } 18 | }, [scrollable]); 19 | 20 | return ( 21 |
27 |
28 |
32 |
36 |
40 |
41 |
49 | {scrollable &&
} 50 | {children} 51 | {scrollable &&
} 52 |
53 |
54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /website/components/Collapsible.js: -------------------------------------------------------------------------------- 1 | import {useState} from 'react'; 2 | import {ChevronDown, ChevronUp} from 'react-feather'; 3 | 4 | export default function Collapsible({children, title}) { 5 | const [collapsed, setCollapsed] = useState(true); 6 | 7 | return ( 8 |
9 | 16 | {collapsed ? null : children} 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /website/components/Floating.js: -------------------------------------------------------------------------------- 1 | import {cloneElement, useEffect, useState} from 'react'; 2 | import {createPortal} from 'react-dom'; 3 | import * as FloatingUI from '@floating-ui/react-dom'; 4 | 5 | export function Floating({ 6 | children, 7 | content, 8 | tooltipStyle = {}, 9 | middleware, 10 | portaled, 11 | minHeight, 12 | ...options 13 | }) { 14 | const [{height}, setDimensions] = useState({ 15 | width: null, 16 | height: null, 17 | }); 18 | const { 19 | x, 20 | y, 21 | reference, 22 | floating, 23 | update, 24 | middlewareData, 25 | refs, 26 | } = FloatingUI.useFloating({ 27 | middleware: 28 | middleware 29 | ?.map(({name, options}) => 30 | name !== 'size' 31 | ? FloatingUI[name]?.(options) 32 | : FloatingUI.size?.({ 33 | ...options, 34 | apply: ({width, height}) => { 35 | setDimensions({ 36 | width, 37 | height: minHeight 38 | ? Math.max(height, minHeight) 39 | : height, 40 | }); 41 | }, 42 | }) 43 | ) 44 | .filter((v) => v) ?? [], 45 | ...options, 46 | }); 47 | 48 | useEffect(() => { 49 | const nodes = [ 50 | ...FloatingUI.getScrollParents(refs.reference.current), 51 | ...FloatingUI.getScrollParents(refs.floating.current), 52 | ]; 53 | 54 | nodes.forEach((node) => { 55 | node.addEventListener('scroll', update); 56 | node.addEventListener('resize', update); 57 | }); 58 | 59 | return () => { 60 | nodes.forEach((node) => { 61 | node.removeEventListener('scroll', update); 62 | node.removeEventListener('resize', update); 63 | }); 64 | }; 65 | }, [reference, floating, update, refs]); 66 | 67 | const tooltipJsx = ( 68 |
89 |
{content ?? 'Floating'}
90 |
91 | ); 92 | 93 | return ( 94 | <> 95 | {cloneElement(children, {ref: reference})} 96 | {portaled && typeof document !== 'undefined' 97 | ? createPortal( 98 | tooltipJsx, 99 | document.getElementById('floating-root') 100 | ) 101 | : tooltipJsx} 102 | 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /website/components/Navigation.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import {ArrowLeft, ArrowRight} from 'react-feather'; 3 | 4 | export default function Navigation({back, next}) { 5 | return ( 6 |
7 | {back ? ( 8 | 9 | 13 | {back.title} 14 | 15 | 16 | ) : ( 17 |
18 | )} 19 | {next && ( 20 | 21 | 25 | {next.title} 26 | 27 | 28 | )} 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /website/components/ReactTippy.js: -------------------------------------------------------------------------------- 1 | import Tippy from '@tippyjs/react/headless'; 2 | import {useState} from 'react'; 3 | 4 | export const VisibleProp = () => { 5 | const [visible, setVisible] = useState(false); 6 | 7 | return ( 8 | ( 10 |
11 | Tooltip 12 |
13 | )} 14 | visible={visible} 15 | animation={false} 16 | > 17 | 23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /website/components/Warning.js: -------------------------------------------------------------------------------- 1 | export default function Warning({children}) { 2 | return ( 3 |
4 | Note 5 | {children} 6 |
7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /website/next.config.js: -------------------------------------------------------------------------------- 1 | const {createRemarkPlugin} = require('@atomiks/mdx-pretty-code'); 2 | const fs = require('fs'); 3 | 4 | const prettyCode = createRemarkPlugin({ 5 | shikiOptions: { 6 | theme: JSON.parse( 7 | fs.readFileSync( 8 | require.resolve('./assets/moonlight-ii.json'), 9 | 'utf-8' 10 | ) 11 | ), 12 | }, 13 | tokensMap: { 14 | objectKey: 'meta.object-literal.key', 15 | function: 'entity.name.function', 16 | param: 'variable.parameter', 17 | const: 'variable.other.constant', 18 | class: 'support.class', 19 | }, 20 | onVisitLine(node) { 21 | Object.assign(node.style, { 22 | display: 'block', 23 | minHeight: '1rem', 24 | margin: '0 -1.5rem', 25 | padding: '0 1.5rem', 26 | }); 27 | }, 28 | onVisitHighlightedLine(node) { 29 | node.className = 'bg-gray-700'; 30 | }, 31 | onVisitHighlightedWord(node) { 32 | Object.assign(node.style, { 33 | backgroundColor: 'rgba(0,0,0,0.25)', 34 | padding: '0.25rem', 35 | borderRadius: '0.25rem', 36 | }); 37 | }, 38 | }); 39 | 40 | module.exports = { 41 | swcMinify: false, 42 | experimental: {esmExternals: true}, 43 | pageExtensions: ['md', 'mdx', 'tsx', 'ts', 'jsx', 'js'], 44 | webpack(config, options) { 45 | config.module.rules.push({ 46 | test: /\.svg$/, 47 | issuer: { 48 | and: [/\.(js|ts)x?$/], 49 | }, 50 | 51 | use: ['@svgr/webpack'], 52 | }); 53 | 54 | config.module.rules.push({ 55 | test: /\.mdx?$/, 56 | use: [ 57 | // The default `babel-loader` used by Next: 58 | options.defaultLoaders.babel, 59 | { 60 | loader: '@mdx-js/loader', 61 | /** @type {import('@mdx-js/loader').Options} */ 62 | options: { 63 | remarkPlugins: [prettyCode], 64 | }, 65 | }, 66 | ], 67 | }); 68 | 69 | return config; 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "private": true, 4 | "version": "1.0.1", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "dev": "next dev", 9 | "build": "next build && next export", 10 | "start": "next start", 11 | "lint": "next lint" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "prettier": { 16 | "printWidth": 65, 17 | "singleQuote": true, 18 | "bracketSpacing": false, 19 | "proseWrap": "always" 20 | }, 21 | "devDependencies": { 22 | "@svgr/webpack": "^5.5.0", 23 | "autoprefixer": "^10.4.0", 24 | "postcss": "^8.3.11", 25 | "tailwindcss": "^2.2.19" 26 | }, 27 | "dependencies": { 28 | "@atomiks/mdx-pretty-code": "^0.0.2", 29 | "@floating-ui/react-dom": "^0.3.2", 30 | "@mdx-js/loader": "^1.6.22", 31 | "@next/mdx": "^12.0.3", 32 | "@reach/skip-nav": "^0.16.0", 33 | "@tailwindcss/typography": "^0.4.1", 34 | "@tippyjs/react": "^4.2.6", 35 | "classnames": "^2.3.1", 36 | "next": "^12.0.4", 37 | "parse-numeric-range": "^1.3.0", 38 | "react": "^17.0.2", 39 | "react-dom": "^17.0.2", 40 | "react-feather": "^2.0.9", 41 | "shiki": "^0.9.14", 42 | "unist-util-visit": "^2.0.3", 43 | "use-isomorphic-layout-effect": "^1.1.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /website/pages/_app.js: -------------------------------------------------------------------------------- 1 | import 'tailwindcss/tailwind.css'; 2 | import '../assets/global.css'; 3 | 4 | function MyApp({Component, pageProps}) { 5 | return ; 6 | } 7 | 8 | export default MyApp; 9 | -------------------------------------------------------------------------------- /website/pages/_document.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document, { 3 | Html, 4 | Head, 5 | Main, 6 | NextScript, 7 | } from 'next/document'; 8 | 9 | class MyDocument extends Document { 10 | render() { 11 | return ( 12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 | ); 21 | } 22 | } 23 | 24 | export default MyDocument; 25 | -------------------------------------------------------------------------------- /website/pages/docs/arrow.mdx: -------------------------------------------------------------------------------- 1 | import Layout from '../../components/Layout'; 2 | export default Layout; 3 | 4 | # arrow 5 | 6 | The `arrow{:.function}` middleware provides data to position an 7 | inner element of the floating element, usually the triangle or 8 | caret which points toward the reference element, so that it is 9 | centered to it. 10 | 11 | If you're using an aligned placement, like `'top-start'{:js}` or 12 | `'right-end'{:js}`, you may not need to use this middleware if 13 | you want the arrow's position to remain "static". 14 | 15 | ## Usage 16 | 17 | Add a new element **inside** your floating element: 18 | 19 | ```html 20 |
21 | Tooltip text 22 |
23 |
24 | ``` 25 | 26 | And ensure it has `position: absolute{:sass}` applied in its CSS. 27 | 28 | ```js 29 | import {computePosition, arrow} from '@floating-ui/dom'; 30 | 31 | computePosition(referenceEl, floatingEl, { 32 | middleware: [arrow({element: arrowElement})], 33 | }).then(({middlewareData}) => { 34 | const {x, y} = middlewareData.arrow; 35 | 36 | Object.assign(arrowElement.style, { 37 | left: x != null ? `${x}px` : '', 38 | top: y != null ? `${y}px` : '', 39 | }); 40 | }); 41 | ``` 42 | 43 | ## Options 44 | 45 | ```ts 46 | type Options = { 47 | element: any; // `HTMLElement` for the DOM 48 | padding: Padding; 49 | }; 50 | ``` 51 | 52 | ### element 53 | 54 | This is the arrow element to be positioned. 55 | 56 | ```js 57 | arrow({ 58 | element: document.querySelector('#arrow'), 59 | }); 60 | ``` 61 | 62 | ### padding 63 | 64 | ```ts 65 | type Padding = number | SideObject; 66 | ``` 67 | 68 | This describes the padding between the arrow and the edges of the 69 | floating element. If your floating element has 70 | `border-radius{:.function}`, this will prevent it from 71 | overflowing the corners. 72 | 73 | ```js 74 | arrow({ 75 | element: document.querySelector('#arrow'), 76 | padding: 5, // stop 5px from the edges of the floating element 77 | }); 78 | ``` 79 | 80 | ## Data 81 | 82 | The following data is available in `middlewareData.arrow{:js}`: 83 | 84 | ```ts 85 | type Data = { 86 | x?: number; 87 | y?: number; 88 | centerOffset: number; 89 | }; 90 | ``` 91 | 92 | ### x 93 | 94 | This property exists if the arrow should be offset on the x-axis. 95 | 96 | ### y 97 | 98 | This property exists if the arrow should be offset on the y-axis. 99 | 100 | ### centerOffset 101 | 102 | This property describes where the arrow actually is relative to 103 | where it could be if it were allowed to overflow the floating 104 | element in order to stay centered to the reference element. 105 | 106 | This enables two useful things: 107 | 108 | - You can hide the arrow if it can't stay centered to the 109 | reference, i.e. `centerOffset !== 0{:js}`. 110 | - You can interpolate the shape of the arrow (e.g. skew it) so it 111 | stays centered as best as possible. 112 | -------------------------------------------------------------------------------- /website/pages/docs/autoPlacement.mdx: -------------------------------------------------------------------------------- 1 | import Layout from '../../components/Layout'; 2 | export default Layout; 3 | 4 | # autoPlacement 5 | 6 | The `autoPlacement{:.function}` middleware chooses the 7 | `placement{:.objectKey}` automatically (the one with most space 8 | available on checked axes). 9 | 10 | ## Usage 11 | 12 | ```js 13 | import {computePosition, autoPlacement} from '@floating-ui/dom'; 14 | 15 | computePosition(referenceEl, floatingEl, { 16 | middleware: [autoPlacement()], 17 | }); 18 | ``` 19 | 20 | ## Options 21 | 22 | ```ts 23 | type Options = DetectOverflowOptions & { 24 | alignment: Alignment | null; 25 | crossAxis: boolean; 26 | allowedPlacements: Array; 27 | autoAlignment: boolean; 28 | }; 29 | ``` 30 | 31 | ### alignment 32 | 33 | Without options, `autoPlacement{:.function}` will choose any of 34 | the `BasePlacement{:.class}`s which fit best, i.e. `'top'{:js}`, 35 | `'right'{:js}`, `'bottom'{:js}`, or `'left'{:js}`. 36 | 37 | By specifying an alignment, it will choose those aligned 38 | placements. 39 | 40 | ```js 41 | autoPlacement({ 42 | // top-start, right-start, bottom-start, left-start 43 | alignment: 'start', 44 | }); 45 | ``` 46 | 47 | ### crossAxis 48 | 49 | Describes whether to check the `crossAxis{:.objectKey}` when 50 | choosing the ideal placement. 51 | 52 | ```js 53 | autoPlacement({ 54 | crossAxis: true, // false by default 55 | }); 56 | ``` 57 | 58 | ### allowedPlacements 59 | 60 | Describes the placements which are allowed to be chosen. 61 | 62 | ```js 63 | autoPlacement({ 64 | // 'right' and 'left' won't be chosen 65 | allowedPlacements: ['top', 'bottom'], 66 | }); 67 | ``` 68 | 69 | ### autoAlignment 70 | 71 | When `alignment{:.objectKey}` is specified, this describes 72 | whether to automatically choose placements with the opposite 73 | alignment if they fit better. 74 | 75 | ```js 76 | autoPlacement({ 77 | alignment: 'start', 78 | // Won't also choose 'end' alignments if those fit better 79 | autoAlignment: false, // true by default 80 | }); 81 | ``` 82 | -------------------------------------------------------------------------------- /website/pages/docs/detectOverflow.mdx: -------------------------------------------------------------------------------- 1 | import Layout from '../../components/Layout'; 2 | export default Layout; 3 | 4 | # detectOverflow 5 | 6 | This function computes the overflow offsets of either the 7 | reference or floating element relative to any clipping 8 | boundaries. 9 | 10 | Almost every middleware provided by the library uses this 11 | function, making it useful for your own custom middleware. 12 | 13 | The function takes two arguments and returns a promise: 14 | 15 | ```js 16 | await detectOverflow(middlewareArguments, options); 17 | ``` 18 | 19 | ## Usage 20 | 21 | Inside your custom middleware, make your `fn{:.function}` 22 | `async{:.keyword}` and `await{:js}` it, passing in the 23 | `middlewareArguments{:.param}`: 24 | 25 | ```js 26 | import {detectOverflow} from '@floating-ui/dom'; 27 | 28 | const middleware = { 29 | name: 'middleware', 30 | async fn(middlewareArguments) { 31 | const overflow = await detectOverflow(middlewareArguments); 32 | return {}; 33 | }, 34 | }; 35 | ``` 36 | 37 | The returned value, `overflow{:.const}`, is a 38 | `SideObject{:.class}` containing side properties with numbers 39 | representing offsets. 40 | 41 | - A positive number means the element is overflowing the clipping 42 | boundary by that number of pixels. 43 | - A negative number means the element has that number of pixels 44 | left before it will overflow the clipping boundary. 45 | - `0{:js}` means the side lies flush with the clipping boundary. 46 | 47 | ## Options 48 | 49 | `detectOverflow{:.function}` takes options as a second argument. 50 | 51 | ```js 52 | await detectOverflow(middlewareArguments, { 53 | // options 54 | }); 55 | ``` 56 | 57 | ### boundary 58 | 59 | ```ts 60 | type Boundary = 'clippingParents' | Element | Array; 61 | ``` 62 | 63 | This describes the clipping element(s) that overflow will be 64 | checked relative to. The default is `'clippingParents'{:js}`, 65 | which are the scrolling containers (including 66 | `overflow: hidden{:sass}` elements) which will cause the 67 | reference or floating element to be clipped. 68 | 69 | ```js 70 | await detectOverflow(middlewareArguments, { 71 | boundary: document.querySelector('#container'), 72 | }); 73 | ``` 74 | 75 | ### rootBoundary 76 | 77 | ```ts 78 | type RootBoundary = 'viewport' | 'document'; 79 | ``` 80 | 81 | This describes the root boundary that the element will be checked 82 | for overflow relative to. The default is `'viewport'{:js}`, which 83 | is the area of the page the user can see on the screen. The other 84 | option is `'document'{:js}`, which is the entire page outside the 85 | viewport. 86 | 87 | ```js 88 | await detectOverflow(middlewareArguments, { 89 | rootBoundary: 'document', 90 | }); 91 | ``` 92 | 93 | ### padding 94 | 95 | ```ts 96 | type Padding = 97 | | number 98 | | Partial<{ 99 | top: number; 100 | right: number; 101 | bottom: number; 102 | left: number; 103 | }>; 104 | ``` 105 | 106 | This describes the virtual padding around the boundary to check 107 | for overflow. 108 | 109 | ```js 110 | await detectOverflow(middlewareArguments, { 111 | // 5px on all sides 112 | padding: 5, 113 | // Unspecified sides are 0 114 | padding: { 115 | top: 5, 116 | left: 20, 117 | }, 118 | }); 119 | ``` 120 | 121 | ### elementContext 122 | 123 | ```ts 124 | type ElementContext = 'reference' | 'floating'; 125 | ``` 126 | 127 | By default, the floating element is the one being checked for 128 | overflow. 129 | 130 | But you can also change the context to `'reference'{:js}` to 131 | instead check its overflow relative to its clipping boundary. 132 | 133 | ```js 134 | await detectOverflow(middlewareArguments, { 135 | elementContext: 'reference', // 'floating' by default 136 | }); 137 | ``` 138 | 139 | ### altBoundary 140 | 141 | This is a boolean value which determines whether to check the 142 | alternate `elementContext{:.objectKey}`'s boundary. 143 | 144 | For instance, if the `elementContext{:.objectKey}` is 145 | `'floating'{:js}`, and you enable this option, then the boundary 146 | in which overflow is checked for is the `'reference'{:js}`'s 147 | boundary. This only applies if you are using the default 148 | `'clippingParents'{:js}` enum as the `boundary{:.objectKey}`. 149 | 150 | ```js 151 | await detectOverflow(middlewareArguments, { 152 | altBoundary: true, // false by default 153 | }); 154 | ``` 155 | -------------------------------------------------------------------------------- /website/pages/docs/flip.mdx: -------------------------------------------------------------------------------- 1 | import Layout from '../../components/Layout'; 2 | export default Layout; 3 | 4 | # flip 5 | 6 | The `flip{:.function}` middleware changes the placement of the 7 | floating element to the opposite one by default in order to 8 | prevent overflow. 9 | 10 |
11 | 12 | 13 |
14 | Scroll 15 |
16 |
17 |
18 |
19 | 20 | It also has the ability to flip to _any_ placement, not just the 21 | opposite one. 22 | 23 | ## Usage 24 | 25 | ```js 26 | import {computePosition, flip} from '@floating-ui/dom'; 27 | 28 | computePosition(referenceEl, floatingEl, { 29 | middleware: [flip()], 30 | }); 31 | ``` 32 | 33 | ## Options 34 | 35 | ```ts 36 | type Options = DetectOverflowOptions & { 37 | mainAxis: boolean; 38 | crossAxis: boolean; 39 | fallbackPlacements: Array; 40 | fallbackStrategy: 'bestFit' | 'initialPlacement'; 41 | flipAlignment: boolean; 42 | }; 43 | ``` 44 | 45 | ### mainAxis 46 | 47 | This is the main axis in which overflow is checked, either `x` or 48 | `y` depending on the placement. 49 | 50 | ```js 51 | flip({mainAxis: false}); // true by default 52 | ``` 53 | 54 | ### crossAxis 55 | 56 | This is the cross axis in which overflow is checked, the opposite 57 | axis of `mainAxis{:.objectKey}`. 58 | 59 | ```js 60 | flip({crossAxis: false}); // true by default 61 | ``` 62 | 63 | ### fallbackPlacements 64 | 65 | This describes the array of placements to try if the preferred 66 | `placement{:.objectKey}` doesn't fully fit. 67 | 68 | ```js 69 | flip({ 70 | fallbackPlacements: ['right', 'bottom'], 71 | }); 72 | ``` 73 | 74 | If the `placement{:.objectKey}` in `computePosition(){:js}` is 75 | set to `'top'{:js}`, but that doesn't fit, then `'right'{:js}` 76 | will be used instead. If `'right'{:js}` also doesn't fit, then 77 | `'bottom'{:js}` will be used. If none of these fit, then the 78 | best-fitting placement will be used. 79 | 80 | ### fallbackStrategy 81 | 82 | When no placements fit, then you'll want to decide what happens. 83 | `'bestFit'{:js}` will use the placement which fits best on the 84 | checked axes. `'initialPlacement'{:js}` will use the initial 85 | `placement{:.objectKey}` specified. 86 | 87 | ```js 88 | flip({ 89 | fallbackStrategy: 'initialPlacement', // 'bestFit' by default 90 | }); 91 | ``` 92 | 93 | ### flipAlignment 94 | 95 | When an alignment specified, e.g. `'top-start'{:js}` instead of 96 | just `'top'{:js}`, this will flip to `'top-end'{:js}` if `start` 97 | doesn't fit. 98 | 99 | ```js 100 | flip({flipAlignment: false}); // true by default 101 | ``` 102 | 103 | When using this with the `shift{:.function}` middleware, ensure 104 | `flip{:.function}` is placed **before** `shift{:.function}` in 105 | your middleware array. This ensures the 106 | `flipAlignment{:.objectKey}` logic can act before shift's does. 107 | 108 | ### ...detectOverflowOptions 109 | 110 | All of [detectOverflow](/docs/detectOverflow#options)'s options 111 | can be passed. For instance: 112 | 113 | ```js 114 | flip({padding: 5}); // 0 by default 115 | ``` 116 | -------------------------------------------------------------------------------- /website/pages/docs/getting-started.mdx: -------------------------------------------------------------------------------- 1 | import Layout from '../../components/Layout'; 2 | export default Layout; 3 | 4 | # Getting Started 5 | 6 | Floating UI is a low-level library for positioning "floating" 7 | elements like tooltips, popovers, dropdowns, menus and more. 8 | Since these types of elements float on top of the UI without 9 | disrupting the flow of content, challenges arise when positioning 10 | them. 11 | 12 | Floating UI exposes primitives which enable a floating element to 13 | be positioned next to a given reference element while appearing 14 | in view for the user as best as possible. Features include 15 | overflow prevention (or collision awareness), placement flipping, 16 | and more. 17 | 18 | - **Tiny**: 600-byte core with highly modular architecture for 19 | tree-shaking 20 | - **Low-level**: Hyper-granular control over positioning behavior 21 | - **Pure**: Predictable, side-effect free behavior 22 | - **Extensible**: Powerful middleware system 23 | - **Platform-agnostic**: Runs on any JavaScript environment which 24 | provides measurement APIs, including the web and 25 | [React Native](/docs/react-native) 26 | 27 | ```shell 28 | npm install @floating-ui/dom 29 | ``` 30 | 31 | ```shell 32 | yarn add @floating-ui/dom 33 | ``` 34 | 35 | ## CDN 36 | 37 | Floating UI can be used via the `unpkg` CDN by adding 38 | ` 42 | 43 | ``` 44 | 45 | > Important: you should open each link to retrieve the latest 46 | > version to lock it to that version. 47 | 48 | All exports will then be available on 49 | `window.FloatingUIDOM{:js}`. 50 | 51 | ## Tutorial 52 | 53 | Get up and running with a real practical example by creating a 54 | [tooltip](/docs/tutorial). 55 | -------------------------------------------------------------------------------- /website/pages/docs/hide.mdx: -------------------------------------------------------------------------------- 1 | import Layout from '../../components/Layout'; 2 | export default Layout; 3 | 4 | # hide 5 | 6 | The `hide{:.function}` middleware provides data to hide the 7 | floating element in applicable situations, usually when it's not 8 | within the same clipping context as the reference element. 9 | 10 |
11 | 12 | 16 |
17 | Scroll up 18 |
19 |
20 |
21 |
22 | 23 | In the above example, the floating element turns red once it has 24 | `escaped{:.objectKey}` the reference element's clipping context. 25 | Once the reference element is hidden, it hides itself. 26 | 27 | ## Usage 28 | 29 | ```js 30 | import {computePosition, hide} from '@floating-ui/dom'; 31 | 32 | computePosition(referenceEl, floatingEl, { 33 | middleware: [hide()], 34 | }).then(({middlewareData}) => { 35 | const {referenceHidden} = middlewareData.hide; 36 | 37 | Object.assign(floatingEl.style, { 38 | visibility: referenceHidden ? 'hidden' : 'visible', 39 | }); 40 | }); 41 | ``` 42 | 43 | ## Data 44 | 45 | ```ts 46 | type Data = { 47 | referenceHidden: boolean; 48 | referenceHiddenOffsets: SideObject; 49 | escaped: boolean; 50 | escapedOffsets: SideObject; 51 | }; 52 | ``` 53 | 54 | ### referenceHidden 55 | 56 | Determines whether the reference element is currently not visible 57 | on screen for the user. 58 | 59 | ### referenceHiddenOffsets 60 | 61 | A side object containing overflow offsets. 62 | 63 | ### escaped 64 | 65 | Determines whether the floating element has "escaped" the 66 | reference's clipping context and appears fully detached from it. 67 | 68 | ### escapedOffsets 69 | 70 | A side object containing overflow offsets. 71 | -------------------------------------------------------------------------------- /website/pages/docs/misc.mdx: -------------------------------------------------------------------------------- 1 | import Layout from '../../components/Layout'; 2 | export default Layout; 3 | 4 | # Misc 5 | 6 | ## Subpixel and accelerated positioning 7 | 8 | Instead of `top{:.objectKey}` and `left{:.objectKey}` as shown 9 | throughout the docs, you can use `transform: translate(){:sass}` 10 | instead to position the floating element for increased 11 | performance. 12 | 13 | ```js 14 | Object.assign(floatingEl.style, { 15 | top: '0', 16 | left: '0', 17 | transform: `translate(${Math.round(x)}px,${Math.round(y)}px)`, 18 | }); 19 | ``` 20 | 21 | `x` and `y` can contain decimals, so unless the 22 | `transform{:.objectKey}` translation is placed evenly on the 23 | device's subpixel grid, then there will be blurring. You can 24 | check `window.devicePixelRatio{:js}` to round by DPR. 25 | 26 | ### 3D transforms 27 | 28 | You can also promote the floating element to its own layer: 29 | 30 | 31 | ```js 32 | Object.assign(floatingEl.style, { 33 | top: '0', 34 | left: '0', 35 | transform: `translate3d(${Math.round(x)}px,${Math.round(y)}px,0)`, 36 | }); 37 | ``` 38 | 39 | If you're animating the location of the floating element, using 40 | `transform{:.function}` will offer smoother animations. 41 | 42 | ## Transforms on elements 43 | 44 | A `transform{:.function}` on a reference element will not cause 45 | any positioning issues. You should be wary of these cases though: 46 | 47 | ### floating 48 | 49 | If the floating element has a `scale` transform, ensure you 50 | specify a `transform-origin{:.function}` depending on its 51 | placement. 52 | 53 | ### parent 54 | 55 | If a parent of the reference and floating element is scaled, the 56 | position will be **incorrect**. To solve this, place the floating 57 | element outside of the scaled parent context. 58 | 59 | ### body 60 | 61 | If `{:html}` is translated, the position will be offset by 62 | that amount. It's possible to create your own custom middleware 63 | to handle these types of cases if necessary, but for most users 64 | it is not relevant. 65 | -------------------------------------------------------------------------------- /website/pages/docs/motivation.mdx: -------------------------------------------------------------------------------- 1 | import Layout from '../../components/Layout'; 2 | export default Layout; 3 | 4 | # Motivation 5 | 6 | So, why a new positioning library? This page aims to explain why 7 | this library was created. 8 | 9 | ## Comparison with Popper 10 | 11 | [Popper](https://popper.js.org) is currently the most popular 12 | open source solution to position floating elements, created 13 | in 2016. Prior to that it was [Tether](https://tether.io/). 14 | 15 | Floating UI was actually going to be Popper v3, however, the API 16 | change was too big, and so I opted to create a new package 17 | entirely. It also aims to be more ambitious by offering 18 | higher-level primitives, not just positioning logic, under the 19 | new package identity (in the future). 20 | 21 | Floating UI is a fork of Popper in this sense, because it 22 | leverages much of its cross-browser bugfix related code, which is 23 | important. 24 | 25 | Being one of the co-authors of Popper v2, I've learned a lot 26 | since then, and various problems popped up regarding its 27 | architecture that Floating UI aims to solve. 28 | 29 | The differences are summarized as follows: 30 | 31 | - **Cross-platform**: Floating UI is cross-platform, while Popper 32 | not. This means it supports React Native and any other JS 33 | environment, not just the web. 34 | - **Smaller size**: The code is smaller and more optimized, and 35 | everything is modular by default, and thus tree-shakeable. 36 | Popper is not tree-shakeable by default, and even when enabling 37 | the tree-shaking format it's not as effective. With Floating 38 | UI, you can even change the DOM platform to be smaller to save 39 | even more size if you don't need all advanced checks. 40 | - **More intuitive API**: Popper's API is highly mutable, which 41 | can be hard to debug. Floating UI is pure with more intuitive 42 | naming of various APIs. Modifying options of middleware (or 43 | modifiers in Popper) takes far fewer characters to write. 44 | - **Inversion of control**: There were many issues opened 45 | surrounding the nature of `computeStyles` and `applyStyles` in 46 | Popper. Leaving the application of styles up to you means you 47 | don't get annoyed when the library is doing something with the 48 | styles you don't want to happen. Use the computed data as you 49 | please. 50 | - **Improved extensibility**: Modifiers in Popper are hard to 51 | write. `requires`, `requiresIfExists`, `phase`, needing to 52 | check for other modifiers' data to correctly position 53 | something, etc. Floating UI removes all of it. It's all based 54 | on the order of the array, and that's up to you. 55 | - **More features**: New features are easier to support thanks to 56 | the new architecture, and Floating UI already offers more, like 57 | the `size{:.function}` middleware. Importantly, new features 58 | are tree-shaken away even if you don't use them, so there's no 59 | size cost. 60 | - **More predictable**: Floating UI doesn't perform any "magic", 61 | like adding event listeners to update the position, or 62 | auto-ordering the modifiers array. This means you use Floating 63 | UI starting at its most fundamental level, without any 64 | middleware enabled already. You add features as you need them, 65 | which makes the library more predictable. 66 | 67 | Floating UI aims to be an ideal solution for component libraries, 68 | like Bootstrap or Material UI, due to its low-level and 69 | unopinionated nature. 70 | -------------------------------------------------------------------------------- /website/pages/docs/offset.mdx: -------------------------------------------------------------------------------- 1 | import Layout from '../../components/Layout'; 2 | export default Layout; 3 | 4 | # offset 5 | 6 | The `offset` middleware displaces the floating element from its 7 | reference element. 8 | 9 | 10 |
11 | 15 |
16 | 0px 17 |
18 |
19 | 23 |
24 | 10px 25 |
26 |
27 |
28 |
29 | 30 | ## Usage 31 | 32 | ```js 33 | import {computePosition, offset} from '@floating-ui/dom'; 34 | 35 | computePosition(referenceEl, floatingEl, { 36 | middleware: [offset(10)], 37 | }); 38 | ``` 39 | 40 | This will offset the floating element 10px from its reference 41 | element. 42 | 43 | ## Options 44 | 45 | ```ts 46 | type Options = 47 | | OffsetValue 48 | | (({ 49 | reference: Rect, 50 | floating: Rect, 51 | placement: Placement, 52 | }) => OffsetValue); 53 | 54 | // Below is the relative type 55 | type OffsetValue = 56 | | number 57 | | {mainAxis?: number; crossAxis?: number}; 58 | ``` 59 | 60 | ```js 61 | // number: distance offset (mainAxis shorthand) 62 | offset(10); 63 | 64 | // object: configure both axes 65 | offset({ 66 | mainAxis: 10, // distance 67 | crossAxis: 10, // skidding 68 | }); 69 | 70 | // function: takes Rects and placement and return a number 71 | offset(({reference}) => reference.height); 72 | 73 | // function: takes Rects and placement and return an object 74 | offset(({floating, placement}) => ({ 75 | crossAxis: 76 | placement === 'top' ? floating.height : floating.width, 77 | })); 78 | ``` 79 | -------------------------------------------------------------------------------- /website/pages/docs/platform.mdx: -------------------------------------------------------------------------------- 1 | import Layout from '../../components/Layout'; 2 | export default Layout; 3 | 4 | # Platform 5 | 6 | Floating UI works on any platform that can run JavaScript, as 7 | long as it has adequate measurement APIs for elements. 8 | 9 | Floating UI works largely with a `Rect{:.class}`, which is an 10 | object of numbers in the form `{width, height, x, y}`. This data 11 | can come from anywhere, and the library will perform the right 12 | computations. `x` and `y` represent the coordinates of the 13 | element relative to another one. 14 | 15 | ```js 16 | import {computePosition} from '@floating-ui/core'; 17 | 18 | computePosition(referenceElement, floatingElement, { 19 | platform: { 20 | // ... 21 | }, 22 | }); 23 | ``` 24 | 25 | ## Methods 26 | 27 | A `platform{:.objectKey}` is a plain object consisting of 7 28 | methods. These methods allow the platform to interface with 29 | Floating UI's logic. 30 | 31 | Each of these methods can be either async or sync. This enables 32 | support of platforms whose measurement APIs are async, like React 33 | Native. 34 | 35 | ### getElementRects 36 | 37 | Takes in the elements and the positioning `strategy{:.objectKey}` 38 | and returns the element `Rect{:.class}` objects. 39 | 40 | ```js 41 | function getElementRects({reference, floating, strategy}) { 42 | return { 43 | reference: {width: 0, height: 0, x: 0, y: 0}, 44 | floating: {width: 0, height: 0, x: 0, y: 0}, 45 | }; 46 | } 47 | ``` 48 | 49 | #### reference 50 | 51 | The `x{:.objectKey}` and `y{:.objectKey}` values of a reference 52 | `Rect{:.class}` should be its coordinates relative to the 53 | floating element's `offsetParent` element, not the viewport. 54 | 55 | #### floating 56 | 57 | Both `x{:.objectKey}` and `y{:.objectKey}` are not relevant 58 | initially, so you can set these both of these to `0{:js}`. 59 | 60 | ### convertOffsetParentRelativeRectToViewportRelativeRect 61 | 62 | This function will take a `Rect{:.class}` that is relative to a 63 | given `offsetParent{:.param}` element and convert its 64 | `x{:.objectKey}` and `y{:.objectKey}` values such that it is 65 | instead relative to the viewport. 66 | 67 | ```js 68 | function convertOffsetParentRelativeRectToViewportRelativeRect({ 69 | rect, 70 | offsetParent, 71 | strategy, 72 | }) { 73 | return rect; 74 | } 75 | ``` 76 | 77 | ### getOffsetParent 78 | 79 | Returns the `offsetParent` of a given element. The following four 80 | properties are what is accessed on an `offsetParent`. 81 | 82 | ```js 83 | function getOffsetParent({element}) { 84 | return { 85 | clientWidth: 0, 86 | clientHeight: 0, 87 | clientLeft: 0, 88 | clientTop: 0, 89 | }; 90 | } 91 | ``` 92 | 93 | ### isElement 94 | 95 | Determines if the current value is an element. 96 | 97 | ```js 98 | function isElement(value) { 99 | return true; 100 | } 101 | ``` 102 | 103 | ### getDocumentElement 104 | 105 | Returns the document element. 106 | 107 | ```js 108 | function getDocumentElement({element}) { 109 | return {}; 110 | } 111 | ``` 112 | 113 | ### getDimensions 114 | 115 | Return the dimensions of an element. 116 | 117 | ```js 118 | function getDimensions({element}) { 119 | return {width: 0, height: 0}; 120 | } 121 | ``` 122 | 123 | ### getClippingClientRect 124 | 125 | Return the `clientRect` (**relative to the viewport**) whose 126 | outside bounds will clip the given element. For instance, the 127 | viewport. 128 | 129 | ```js 130 | function getClippingClientRect({ 131 | element, 132 | boundary, 133 | rootBoundary, 134 | }) { 135 | return { 136 | width: 0, 137 | height: 0, 138 | top: 0, 139 | right: 0, 140 | bottom: 0, 141 | left: 0, 142 | x: 0, 143 | y: 0, 144 | }; 145 | } 146 | ``` 147 | 148 | ## Usage 149 | 150 | All these methods are passed to `platform{:.objectKey}`: 151 | 152 | ```js 153 | import {computePosition} from '@floating-ui/core'; 154 | 155 | computePosition(referenceEl, floatingEl, { 156 | platform: { 157 | getElementRects, 158 | convertOffsetParentRelativeRectToViewportRelativeRect, 159 | getOffsetParent, 160 | isElement, 161 | getDocumentElement, 162 | getDimensions, 163 | getClippingClientRect, 164 | }, 165 | }); 166 | ``` 167 | -------------------------------------------------------------------------------- /website/pages/docs/react-native.mdx: -------------------------------------------------------------------------------- 1 | import Layout from '../../components/Layout'; 2 | export default Layout; 3 | 4 | # React Native 5 | 6 | **Status: Experimental** 7 | 8 | Support for React Native is new, and as such the API is not final 9 | and might not cover various use cases. It also might be buggy so 10 | consider this a very early alpha. 11 | 12 | Please visit the 13 | [GitHub](https://github.com/atomiks/floating-ui/) and contribute 14 | to the discussions! 15 | 16 | ```shell 17 | npm install @floating-ui/react-native 18 | ``` 19 | 20 | ```shell 21 | yarn add @floating-ui/react-native 22 | ``` 23 | 24 | ## Usage 25 | 26 | The `useFloating(){:js}` hook accepts all of 27 | [computePosition's options](/docs/computePosition#options). 28 | 29 | ```jsx 30 | import {View, Text} from 'react-native'; 31 | import {useFloating, shift} from '@floating-ui/react-native'; 32 | 33 | function App() { 34 | const {x, y, reference, floating} = useFloating({ 35 | placement: 'right', 36 | middleware: [shift()], 37 | }); 38 | 39 | return ( 40 | 41 | 42 | Reference 43 | 44 | 52 | Floating 53 | 54 | 55 | ); 56 | } 57 | ``` 58 | 59 | `x{:.const}` and `y{:.const}` will be `null` initially, before 60 | the layout effect has fired. 61 | 62 | ## ScrollView 63 | 64 | When your floating element is portaled to the app root, while the 65 | reference element is inside a `{:jsx}`, you pass 66 | the `sameScrollView{:.objectKey}` option, and spread 67 | `scrollProps{:.const}` to the component: 68 | 69 | ```jsx /scrollProps/ {7} 70 | import {View, Text, ScrollView} from 'react-native'; 71 | import {useFloating} from '@floating-ui/react-native'; 72 | 73 | function App() { 74 | const {x, y, reference, floating, scrollProps} = useFloating({ 75 | placement: 'right', 76 | sameScrollView: false, 77 | }); 78 | 79 | return ( 80 | 81 | 82 | 83 | Reference 84 | 85 | 86 | 87 | 95 | Floating 96 | 97 | 98 | ); 99 | } 100 | ``` 101 | 102 | ## offsetParent 103 | 104 | Pass this to the floating element's `offsetParent{:.const}`, if 105 | required: 106 | 107 | ```jsx /offsetParent/ 108 | import {View, Text, ScrollView} from 'react-native'; 109 | import {useFloating} from '@floating-ui/react-native'; 110 | 111 | function App() { 112 | const {x, y, reference, floating, offsetParent} = useFloating({ 113 | placement: 'right', 114 | sameScrollView: false, 115 | }); 116 | 117 | return ( 118 | 119 | 120 | 121 | Reference 122 | 123 | 124 | 125 | 126 | 134 | Floating 135 | 136 | 137 | 138 | ); 139 | } 140 | ``` 141 | 142 | ## Updating 143 | 144 | The hook returns an `update(){:js}` function to update the 145 | position: 146 | 147 | ```js 148 | const {update} = useFloating(); 149 | ``` 150 | 151 | In addition, the `refs{:.const}` containing the actual elements 152 | are passed back: 153 | 154 | ```js 155 | const {refs} = useFloating(); 156 | 157 | // .current will be filled with the element inside an effect 158 | // refs.reference.current 159 | // refs.floating.current 160 | // refs.offsetParent.current 161 | ``` 162 | 163 | ## Arrow 164 | 165 | A `ref{:.const}` can be passed as the `element{:.objectKey}`: 166 | 167 | ```jsx /arrowRef/ {11} 168 | import {useRef} from 'react'; 169 | import {useFloating, arrow} from '@floating-ui/react-native'; 170 | 171 | function App() { 172 | const arrowRef = useRef(); 173 | const { 174 | x, 175 | y, 176 | middlewareData: {arrow: {x: arrowX, y: arrowY} = {}}, 177 | } = useFloating({ 178 | middleware: [arrow({element: arrowRef})], 179 | }); 180 | 181 | // Pass the `arrowRef` to the element 182 | } 183 | ``` 184 | 185 | ## Updating middleware 186 | 187 | If you intend to make `middleware{:.objectKey}` dynamic, ensure 188 | you call `update(){:js}` whenever the state changes to compute 189 | the new position. 190 | -------------------------------------------------------------------------------- /website/pages/docs/shift.mdx: -------------------------------------------------------------------------------- 1 | import Layout from '../../components/Layout'; 2 | export default Layout; 3 | 4 | # shift 5 | 6 | The `shift{:.function}` middleware shifts the floating element in 7 | order to keep it in view (prevents overflow). 8 | 9 |
10 | 11 | 14 |
[]
15 |
16 |
17 | 18 | 19 | 23 |
[shift()]
24 |
25 |
26 |
27 | 28 | ## Usage 29 | 30 | ```js 31 | import {computePosition, shift} from '@floating-ui/dom'; 32 | 33 | computePosition(referenceEl, floatingEl, { 34 | middleware: [shift()], 35 | }); 36 | ``` 37 | 38 | ## Options 39 | 40 | ```ts 41 | type Options = DetectOverflowOptions & { 42 | mainAxis: boolean; 43 | crossAxis: boolean; 44 | limiter: (middlewareArguments: MiddlewareArguments) => Coords; 45 | }; 46 | ``` 47 | 48 | ### mainAxis 49 | 50 | This is the main axis in which shifting is applied, either `x` or 51 | `y` depending on the placement. 52 | 53 | ```js 54 | shift({mainAxis: false}); // true by default 55 | ``` 56 | 57 | ### crossAxis 58 | 59 | This is the cross axis in which shifting is applied, the opposite 60 | axis of `mainAxis{:.objectKey}`. Enabling this can lead to the 61 | floating element **overlapping** the reference element, which is 62 | generally not desired and is replaced by the `flip{:.function}` 63 | middleware. 64 | 65 | ```js 66 | shift({crossAxis: true}); // false by default 67 | ``` 68 | 69 | ### limiter 70 | 71 | This accepts a function that **limits** the shifting done, in 72 | order to prevent detachment or "overly-eager" behavior. The 73 | behavior is to stop shifting once the opposite edges of the 74 | elements are aligned. 75 | 76 | ```js 77 | import {shift, limitShift} from '@floating-ui/dom'; 78 | 79 | shift({limiter: limitShift()}); 80 | ``` 81 | 82 | This function itself takes options. 83 | 84 | #### limitShift.mainAxis 85 | 86 | Whether to apply limiting on the mainAxis. 87 | 88 | ```js 89 | shift({ 90 | limiter: limitShift({mainAxis: false}), // true by default 91 | }); 92 | ``` 93 | 94 | #### limitShift.crossAxis 95 | 96 | Whether to apply limiting to the crossAxis. 97 | 98 | ```js 99 | shift({ 100 | limiter: limitShift({crossAxis: false}), // true by default 101 | }); 102 | ``` 103 | 104 | #### limitShift.offset 105 | 106 | This will offset when the limiting starts. A positive number will 107 | start limiting earlier, while negative later. 108 | 109 | ```js 110 | shift({ 111 | limiter: limitShift({ 112 | // Start limiting 5px earlier 113 | offset: 5, 114 | }), 115 | }); 116 | ``` 117 | 118 | This can also take a function, which provides the 119 | `Rect{:.class}`s of each element to read their dimensions: 120 | 121 | ```js 122 | shift({ 123 | limiter: limitShift({ 124 | // Start limiting by the reference's width earlier 125 | offset: ({reference, floating, placement}) => 126 | reference.width, 127 | }), 128 | }); 129 | ``` 130 | 131 | You may also pass an object to configure both axes: 132 | 133 | ```js 134 | shift({ 135 | limiter: limitShift({ 136 | // object 137 | offset: { 138 | mainAxis: 10, 139 | crossAxis: 5, 140 | }, 141 | // or a function which returns one 142 | offset: ({reference, floating, placement}) => ({ 143 | mainAxis: reference.height, 144 | crossAxis: floating.width, 145 | }), 146 | }), 147 | }); 148 | ``` 149 | 150 | ### ...detectOverflowOptions 151 | 152 | All of [detectOverflow](/docs/detectOverflow#options)'s options 153 | can be passed. For instance: 154 | 155 | ```js 156 | shift({padding: 5}); // 0 by default 157 | ``` 158 | -------------------------------------------------------------------------------- /website/pages/docs/virtual-elements.mdx: -------------------------------------------------------------------------------- 1 | import Layout from '../../components/Layout'; 2 | export default Layout; 3 | 4 | # Virtual Elements 5 | 6 | You can position a floating element relative to a virtual element 7 | instead of a real one. This enables things like positioning 8 | context menus or following the cursor. 9 | 10 | ## Usage 11 | 12 | A virtual element is a plain object that has a 13 | `getBoundingClientRect{:.function}` property, which mimics a real 14 | element's one: 15 | 16 | ```js 17 | // A virtual element which is 20 x 20 px, starting from (0, 0) 18 | const virtualEl = { 19 | getBoundingClientRect() { 20 | return { 21 | top: 0, 22 | bottom: 20, 23 | left: 0, 24 | right: 20, 25 | width: 20, 26 | height: 20, 27 | }; 28 | }, 29 | }; 30 | 31 | computePosition(virtualEl, floatingEl); 32 | ``` 33 | 34 | A point reference, such as a mouse event, is one such use case: 35 | 36 | ```js 37 | function onClick({clientX, clientY}) { 38 | const virtualEl = { 39 | getBoundingClientRect() { 40 | return { 41 | width: 0, 42 | height: 0, 43 | left: clientX, 44 | top: clientY, 45 | right: clientX, 46 | bottom: clientY, 47 | }; 48 | }, 49 | }; 50 | 51 | computePosition(virtualEl, floatingEl).then(({x, y}) => { 52 | // Position the floating element relative to the click 53 | }); 54 | } 55 | 56 | document.addEventListener('click', onClick); 57 | ``` 58 | 59 | ## contextElement 60 | 61 | An extra property can be specified on a virtual element: 62 | 63 | ```js 64 | const virtualEl = { 65 | getBoundingClientRect() { 66 | return { 67 | // ... 68 | }; 69 | }, 70 | contextElement: document.querySelector('#context'), 71 | }; 72 | ``` 73 | 74 | This may be required if your virtual element's 75 | `getBoundingClientRect{:.function}` data is **derived** from a 76 | real element's one. For instance, you're modifying a real 77 | reference element's `getBoundingClientRect{:.function}` in some 78 | way, and using a virtual element enables this. 79 | -------------------------------------------------------------------------------- /website/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/floating-ui/fabae175bdcec7a95af6855e671bd5df1f4b3388/website/public/favicon.ico -------------------------------------------------------------------------------- /website/public/floating-ui.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/floating-ui/fabae175bdcec7a95af6855e671bd5df1f4b3388/website/public/floating-ui.jpg -------------------------------------------------------------------------------- /website/public/orbs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/floating-ui/fabae175bdcec7a95af6855e671bd5df1f4b3388/website/public/orbs.jpg -------------------------------------------------------------------------------- /website/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('tailwindcss/colors'); 2 | 3 | module.exports = { 4 | plugins: [require('@tailwindcss/typography')], 5 | purge: [ 6 | './pages/**/*.{js,ts,jsx,tsx,md,mdx}', 7 | './components/**/*.{js,ts,jsx,tsx}', 8 | ], 9 | darkMode: false, 10 | theme: { 11 | extend: { 12 | typography: { 13 | lg: { 14 | css: { 15 | pre: { 16 | '@media (max-width: 500px)': { 17 | paddingLeft: '5%', 18 | paddingRight: '5%', 19 | }, 20 | lineHeight: '2.1', 21 | marginBottom: '0', 22 | ' > code': { 23 | display: 'flex', 24 | flexDirection: 'column', 25 | }, 26 | }, 27 | h1: { 28 | lineHeight: '1.1', 29 | whiteSpace: 'nowrap', 30 | overflow: 'hidden', 31 | textOverflow: 'ellipsis', 32 | '@media (max-width: 500px)': { 33 | fontSize: '2.25rem', 34 | }, 35 | }, 36 | }, 37 | }, 38 | DEFAULT: { 39 | css: { 40 | maxWidth: '70ch', 41 | color: '#BFC3D9', 42 | blockquote: { 43 | color: '#BFC3D9', 44 | }, 45 | strong: { 46 | color: '#fff', 47 | }, 48 | pre: { 49 | color: '#cddbf7', 50 | }, 51 | h1: { 52 | backgroundClip: 'text', 53 | color: 'transparent', 54 | backgroundImage: `linear-gradient(to right, ${colors.yellow['300']}, ${colors.pink['400']})`, 55 | }, 56 | h2: { 57 | color: '#fff', 58 | wordBreak: 'break-word', 59 | }, 60 | h3: { 61 | color: '#BFC3D9', 62 | wordBreak: 'break-word', 63 | }, 64 | 'h2 a': { 65 | color: 'inherit', 66 | }, 67 | 'h3 a': { 68 | color: 'inherit', 69 | }, 70 | code: { 71 | color: '#93a4b5', 72 | borderRadius: '4px', 73 | padding: '2px 4px', 74 | fontWeight: '500', 75 | background: '#272935', 76 | '&::before': { 77 | display: 'none', 78 | }, 79 | '&::after': { 80 | display: 'none', 81 | }, 82 | }, 83 | a: { 84 | color: '#87e1fc', 85 | fontSize: '', 86 | textDecoration: 'none', 87 | borderBottom: '1px solid transparent', 88 | '&:hover': { 89 | borderBottomColor: 'inherit', 90 | }, 91 | '&:active': { 92 | borderBottomStyle: 'dashed', 93 | }, 94 | }, 95 | 'a code': { 96 | color: '#fff', 97 | }, 98 | 'blockquote p:first-of-type::before': { 99 | display: 'none', 100 | }, 101 | }, 102 | }, 103 | }, 104 | colors: { 105 | gray: { 106 | 1000: '#1c1d24', 107 | 900: '#1F2028', 108 | 800: '#272935', 109 | 700: '#353849', 110 | 600: '#575969', 111 | 112 | 200: '#BFC3D9', 113 | 100: '#dcdfec', 114 | 50: '#FFF', 115 | }, 116 | }, 117 | zIndex: { 118 | '-1': '-1', 119 | }, 120 | backgroundImage: { 121 | 'gradient-radial': 122 | 'radial-gradient(circle at 50% 10%, var(--tw-gradient-stops))', 123 | }, 124 | inset: { 125 | '-32': '-128px', 126 | }, 127 | width: { 128 | '1200px': '1200px', 129 | }, 130 | height: { 131 | 128: '32rem', 132 | }, 133 | lineHeight: { 134 | 'gradient-heading': '1.1 !important', 135 | }, 136 | }, 137 | }, 138 | variants: { 139 | extend: { 140 | filter: ['hover'], 141 | saturate: ['hover'], 142 | brightness: ['hover'], 143 | }, 144 | }, 145 | }; 146 | --------------------------------------------------------------------------------