├── .config ├── .prettierignore ├── babel.config.js ├── eslint.config.js ├── jest-puppeteer.config.js ├── jest.config.js └── rollup.config.js ├── .editorconfig ├── .env ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ ├── cd.yml │ └── ci.yml ├── .gitignore ├── LICENSE ├── MIGRATION_GUIDE.md ├── README.md ├── build ├── animations │ ├── perspective-extreme.js │ ├── perspective-subtle.js │ ├── perspective.js │ ├── scale-extreme.js │ ├── scale-subtle.js │ ├── scale.js │ ├── shift-away-extreme.js │ ├── shift-away-subtle.js │ ├── shift-away.js │ ├── shift-toward-extreme.js │ ├── shift-toward-subtle.js │ └── shift-toward.js ├── base-umd.js ├── base.js ├── bundle-umd.js ├── css │ ├── backdrop.js │ ├── border.js │ ├── svg-arrow.js │ └── tippy.js ├── headless-umd.js ├── headless.js ├── index.js └── themes │ ├── light-border.js │ ├── light.js │ ├── material.js │ └── translucent.js ├── headless └── package.json ├── index.test-d.ts ├── logo.png ├── package.json ├── src ├── _babel.d.ts ├── addons │ ├── createSingleton.ts │ └── delegate.ts ├── bindGlobalEventListeners.ts ├── browser.ts ├── constants.ts ├── createTippy.ts ├── css.ts ├── dom-utils.ts ├── index.ts ├── plugins │ ├── animateFill.ts │ ├── followCursor.ts │ ├── inlinePositioning.ts │ └── sticky.ts ├── props.ts ├── scss │ ├── _mixins.scss │ ├── _vars.scss │ ├── animations │ │ ├── fade.scss │ │ ├── perspective-extreme.scss │ │ ├── perspective-subtle.scss │ │ ├── perspective.scss │ │ ├── scale-extreme.scss │ │ ├── scale-subtle.scss │ │ ├── scale.scss │ │ ├── shift-away-extreme.scss │ │ ├── shift-away-subtle.scss │ │ ├── shift-away.scss │ │ ├── shift-toward-extreme.scss │ │ ├── shift-toward-subtle.scss │ │ └── shift-toward.scss │ ├── backdrop.scss │ ├── border.scss │ ├── index.scss │ ├── svg-arrow.scss │ └── themes │ │ ├── light-border.scss │ │ ├── light.scss │ │ ├── material.scss │ │ └── translucent.scss ├── template.ts ├── types-internal.ts ├── types.ts ├── utils.ts └── validation.ts ├── test ├── functional │ ├── __image_snapshots__ │ │ ├── border-test-js-border-borders-are-correctly-inherited-and-svg-styles-are-correct-1-snap.png │ │ ├── follow-cursor-test-js-follow-cursor-false-does-not-follow-the-cursor-at-all-1-snap.png │ │ ├── follow-cursor-test-js-follow-cursor-horizontal-follows-the-cursor-only-on-the-horizontal-axis-1-snap.png │ │ ├── follow-cursor-test-js-follow-cursor-initial-follows-the-cursor-only-initially-1-snap.png │ │ ├── follow-cursor-test-js-follow-cursor-stays-at-cursor-when-content-changes-1-snap.png │ │ ├── follow-cursor-test-js-follow-cursor-true-follows-the-cursor-on-both-axes-1-snap.png │ │ ├── follow-cursor-test-js-follow-cursor-vertical-follows-the-cursor-only-on-the-vertical-axis-1-snap.png │ │ ├── inline-positioning-test-js-inline-positioning-aligns-correctly-1-snap.png │ │ ├── sticky-test-js-sticky-stays-stuck-to-the-reference-element-when-it-moves-1-snap.png │ │ └── themes-test-js-themes-all-themes-are-correct-1-snap.png │ ├── border.test.js │ ├── followCursor.test.js │ ├── inlinePositioning.test.js │ ├── sticky.test.js │ └── themes.test.js ├── image-reporter.js ├── integration │ ├── __snapshots__ │ │ ├── createTippy.test.js.snap │ │ └── props.test.js.snap │ ├── addons │ │ ├── createSingleton.test.js │ │ └── delegate.test.js │ ├── bindGlobalEventListeners.test.js │ ├── createTippy.test.js │ ├── plugins │ │ ├── __snapshots__ │ │ │ └── inlinePositioning.test.js.snap │ │ ├── animateFill.test.js │ │ ├── followCursor.test.js │ │ └── inlinePositioning.test.js │ └── props.test.js ├── setup.js ├── unit │ ├── __snapshots__ │ │ ├── props.test.js.snap │ │ └── tippy.test.js.snap │ ├── css.test.js │ ├── dom-utils.test.js │ ├── props.test.js │ ├── tippy.test.js │ ├── utils.test.js │ └── validation.test.js ├── utils.js └── visual │ ├── index.css │ ├── index.html │ ├── index.js │ └── tests.js ├── tsconfig.json ├── website ├── .eslintignore ├── .gitignore ├── LICENSE ├── README.md ├── gatsby-config.js ├── gatsby-node.js ├── package.json ├── scripts │ ├── should-deploy-docs.js │ └── should-deploy-docs.sh ├── src │ ├── components │ │ ├── ElasticScroll.js │ │ ├── Footer.js │ │ ├── Framework.js │ │ ├── Header.js │ │ ├── Icon.js │ │ ├── Image.js │ │ ├── Layout.js │ │ ├── Main.js │ │ ├── MiniHeader.js │ │ ├── Nav.js │ │ ├── NavButtons.js │ │ ├── PluginIcon.js │ │ ├── RenderIcon.js │ │ ├── SEO.js │ │ ├── TextGradient.js │ │ ├── Tippy.js │ │ ├── TippyTransition.js │ │ └── examples │ │ │ ├── Ajax.js │ │ │ ├── ContextMenu.js │ │ │ ├── Dropdown.js │ │ │ ├── EventDelegation.js │ │ │ ├── ImageTransition.js │ │ │ ├── Nesting.js │ │ │ ├── Singleton.js │ │ │ ├── TextTransition.js │ │ │ ├── TriggerTarget.js │ │ │ └── mouseRestPlugin.js │ ├── css │ │ ├── index.js │ │ └── theme.js │ ├── favicon.png │ ├── hooks │ │ └── index.js │ ├── images │ │ ├── brain.svg │ │ ├── browser-devtools-tippy-element.jpg │ │ ├── browser.svg │ │ ├── bubbles.svg │ │ ├── gatsby-astronaut.png │ │ ├── gatsby-icon.png │ │ ├── lightning.svg │ │ ├── logo.svg │ │ ├── paintbrush.svg │ │ ├── plugin.svg │ │ ├── pointer.svg │ │ ├── render.svg │ │ ├── typescript.svg │ │ └── wheelchair.svg │ ├── pages │ │ ├── .prettierrc.json │ │ ├── 404.js │ │ ├── index.mdx │ │ ├── v5 │ │ │ ├── accessibility.mdx │ │ │ ├── addons.mdx │ │ │ ├── ajax.mdx │ │ │ ├── all-props.mdx │ │ │ ├── animations.mdx │ │ │ ├── creating-tooltips.mdx │ │ │ ├── customizing-tooltips.mdx │ │ │ ├── faq.mdx │ │ │ ├── getting-started.mdx │ │ │ ├── html-content.mdx │ │ │ ├── lifecycle-hooks.mdx │ │ │ ├── methods.mdx │ │ │ ├── misc.mdx │ │ │ ├── motivation.mdx │ │ │ ├── plugins.mdx │ │ │ ├── themes.mdx │ │ │ └── tippy-instance.mdx │ │ └── v6 │ │ │ ├── accessibility.mdx │ │ │ ├── addons.mdx │ │ │ ├── ajax.mdx │ │ │ ├── all-props.mdx │ │ │ ├── animations.mdx │ │ │ ├── browser-support.mdx │ │ │ ├── constructor.mdx │ │ │ ├── customization.mdx │ │ │ ├── faq.mdx │ │ │ ├── getting-started.mdx │ │ │ ├── headless-tippy.mdx │ │ │ ├── html-content.mdx │ │ │ ├── methods.mdx │ │ │ ├── misc.mdx │ │ │ ├── motivation.mdx │ │ │ ├── plugins.mdx │ │ │ ├── themes.mdx │ │ │ └── tippy-instance.mdx │ └── utils.js └── yarn.lock └── yarn.lock /.config/.prettierignore: -------------------------------------------------------------------------------- 1 | ../build 2 | ../dist 3 | ../headless 4 | ../website/public 5 | ../website/.cache 6 | ../animations 7 | ../themes -------------------------------------------------------------------------------- /.config/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/env', {loose: true, useBuiltIns: 'entry', corejs: 3}], 4 | '@babel/typescript', 5 | ], 6 | plugins: ['dev-expression'], 7 | env: { 8 | test: { 9 | presets: [ 10 | ['@babel/env', {targets: {node: 'current'}}], 11 | '@babel/typescript', 12 | ], 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /.config/eslint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | node: true, 5 | jest: true, 6 | es6: true, 7 | }, 8 | globals: { 9 | __DEV__: true, 10 | }, 11 | extends: [ 12 | 'eslint:recommended', 13 | 'plugin:@typescript-eslint/recommended', 14 | 'prettier', 15 | 'prettier/@typescript-eslint', 16 | ], 17 | plugins: ['@typescript-eslint'], 18 | parser: '@typescript-eslint/parser', 19 | parserOptions: { 20 | project: './tsconfig.json', 21 | }, 22 | rules: { 23 | 'no-prototype-builtins': 'off', 24 | '@typescript-eslint/no-use-before-define': ['error', {functions: false}], 25 | '@typescript-eslint/no-non-null-assertion': 'off', 26 | '@typescript-eslint/no-explicit-any': 'off', 27 | '@typescript-eslint/array-type': 'off', 28 | '@typescript-eslint/no-empty-function': 'off', 29 | '@typescript-eslint/ban-ts-ignore': 'off', 30 | '@typescript-eslint/ban-ts-comment': 'off', 31 | '@typescript-eslint/explicit-module-boundary-types': 'off', 32 | }, 33 | ignorePatterns: [ 34 | 'node_modules', 35 | 'build', 36 | 'animations', 37 | 'themes', 38 | 'test', 39 | 'headless', 40 | 'website', 41 | 'dist', 42 | 'coverage', 43 | ], 44 | }; 45 | -------------------------------------------------------------------------------- /.config/jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const getConfig = require('jest-puppeteer-docker/lib/config'); 4 | const baseConfig = getConfig(); 5 | 6 | module.exports = { 7 | browser: 'chromium', 8 | launch: { 9 | dumpio: false, 10 | headless: process.env.HEADLESS !== 'false', 11 | args: ['--no-sandbox', '--disable-setuid-sandbox'], 12 | }, 13 | server: { 14 | command: 'yarn build:visual && yarn serve', 15 | port: 5000, 16 | launchTimeout: 20000, 17 | }, 18 | ...baseConfig, 19 | }; 20 | -------------------------------------------------------------------------------- /.config/jest.config.js: -------------------------------------------------------------------------------- 1 | // https://github.com/smooth-code/jest-puppeteer/issues/160#issuecomment-491975158 2 | process.env.JEST_PUPPETEER_CONFIG = require.resolve( 3 | './jest-puppeteer.config.js' 4 | ); 5 | 6 | const jestPuppeteerDocker = require('jest-puppeteer-docker/jest-preset'); 7 | 8 | module.exports = { 9 | testMatch: ['/test/**/*.test.js'], 10 | testTimeout: 30000, 11 | globals: { 12 | __DEV__: true, 13 | }, 14 | setupFiles: ['dotenv/config'], 15 | reporters: ['default', require.resolve('../test/image-reporter.js')], 16 | ...jestPuppeteerDocker, 17 | testEnvironment: 'jest-environment-jsdom-fourteen', 18 | setupFilesAfterEnv: [ 19 | require.resolve('../test/setup.js'), 20 | ...jestPuppeteerDocker.setupFilesAfterEnv, 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PUPPETEER_BROWSER=chromium 2 | HEADLESS=true 3 | DEV_PORT=5000 -------------------------------------------------------------------------------- /.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/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐞 Bug report 3 | about: Something is broken 4 | title: '' 5 | labels: "\U0001F41B bug, \U0001F6A7 unconfirmed" 6 | assignees: '' 7 | --- 8 | 9 | ## Bug description 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | ## Reproduction 14 | 15 | 16 | 17 | CodePen link: https://codepen.io/atomiks/pen/yvwQyZ 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 👋 Stack Overflow 4 | url: https://stackoverflow.com/questions/tagged/tippyjs 5 | about: Having trouble with Tippy? Try asking on Stack Overflow! 6 | - name: 💬 Discussions 7 | url: https://github.com/atomiks/tippyjs/discussions 8 | about: Talk with others about Tippy, its usage and future! 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature request 3 | about: Suggest a feature 4 | title: '' 5 | labels: "\U0001F48E enhancement" 6 | assignees: '' 7 | --- 8 | 9 | ## Problem 10 | 11 | A clear and concise description of what the problem is. 12 | 13 | ## Solution 14 | 15 | A clear and concise description of what you want to happen. 16 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | on: [push] 3 | env: 4 | CI: true 5 | 6 | jobs: 7 | publish: 8 | if: 9 | ${{ startsWith(github.event.commits[0].message, 'docs:') || 10 | startsWith(github.event.commits[0].message, 'release:') }} 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | working-directory: ./website 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | - name: Build 19 | run: | 20 | npm install 21 | npm run clean 22 | npm run build:ci 23 | - name: Deploy to GitHub Pages 24 | uses: crazy-max/ghaction-github-pages@v2 25 | with: 26 | target_branch: gh-pages 27 | build_dir: ./website/public 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | env: 4 | CI: true 5 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true 6 | 7 | jobs: 8 | checks: 9 | name: Linting and Type checking 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | - run: npm install 15 | - run: npm run lint 16 | - run: npm run test:types 17 | 18 | dom-tests: 19 | name: Unit and Integration 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: actions/setup-node@v1 24 | - uses: mujo-code/puppeteer-headful@master 25 | - run: npm install 26 | - run: npm run test:dom 27 | 28 | functional-tests: 29 | name: Chromium Functional 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v2 33 | - uses: actions/setup-node@v1 34 | - uses: mujo-code/puppeteer-headful@master 35 | - run: npm install 36 | - run: npm run test:functional 37 | env: 38 | PUPPETEER_BROWSER: chromium 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | coverage/ 3 | .devserver/ 4 | node_modules/ 5 | dist/ 6 | /themes 7 | /animations 8 | index.d.ts 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present 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 |
2 | Tippy.js logo 3 |
4 | 5 |
6 |

Tippy.js

7 |

The complete tooltip, popover, dropdown, and menu solution for the web

8 | 9 | npm Downloads per Month 10 | 11 | 12 | MIT License 13 | 14 |
15 |
16 |
17 | 18 | ## Demo and Documentation 19 | 20 | ➡️ **[View the latest demo & docs here](https://atomiks.github.io/tippyjs/)** 21 | 22 | [Migration Guide](https://github.com/atomiks/tippyjs/blob/master/MIGRATION_GUIDE.md) 23 | 24 | ## Installation 25 | 26 | ### Package Managers 27 | 28 | ```bash 29 | # npm 30 | npm i tippy.js 31 | 32 | # Yarn 33 | yarn add tippy.js 34 | ``` 35 | 36 | Import the `tippy` constructor and the core CSS: 37 | 38 | ```js 39 | import tippy from 'tippy.js'; 40 | import 'tippy.js/dist/tippy.css'; 41 | ``` 42 | 43 | ### CDN 44 | 45 | ```html 46 | 47 | 48 | ``` 49 | 50 | The core CSS comes bundled with the default unpkg import. 51 | 52 | ## Usage 53 | 54 | For detailed usage information, 55 | [visit the docs](https://atomiks.github.io/tippyjs/v6/getting-started/). 56 | 57 | ## Component Wrappers 58 | 59 | - React: [@tippyjs/react](https://github.com/atomiks/tippyjs-react) (official) 60 | - Ember: [ember-tippy](https://github.com/nag5000/ember-tippy) (unofficial) 61 | 62 | ## License 63 | 64 | MIT 65 | -------------------------------------------------------------------------------- /build/animations/perspective-extreme.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/animations/perspective-extreme.scss'; 2 | -------------------------------------------------------------------------------- /build/animations/perspective-subtle.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/animations/perspective-subtle.scss'; 2 | -------------------------------------------------------------------------------- /build/animations/perspective.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/animations/perspective.scss'; 2 | -------------------------------------------------------------------------------- /build/animations/scale-extreme.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/animations/scale-extreme.scss'; 2 | -------------------------------------------------------------------------------- /build/animations/scale-subtle.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/animations/scale-subtle.scss'; 2 | -------------------------------------------------------------------------------- /build/animations/scale.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/animations/scale.scss'; 2 | -------------------------------------------------------------------------------- /build/animations/shift-away-extreme.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/animations/shift-away-extreme.scss'; 2 | -------------------------------------------------------------------------------- /build/animations/shift-away-subtle.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/animations/shift-away-subtle.scss'; 2 | -------------------------------------------------------------------------------- /build/animations/shift-away.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/animations/shift-away.scss'; 2 | -------------------------------------------------------------------------------- /build/animations/shift-toward-extreme.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/animations/shift-toward-extreme.scss'; 2 | -------------------------------------------------------------------------------- /build/animations/shift-toward-subtle.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/animations/shift-toward-subtle.scss'; 2 | -------------------------------------------------------------------------------- /build/animations/shift-toward.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/animations/shift-toward.scss'; 2 | -------------------------------------------------------------------------------- /build/base-umd.js: -------------------------------------------------------------------------------- 1 | import tippy, {hideAll} from '../src'; 2 | import createSingleton from '../src/addons/createSingleton'; 3 | import delegate from '../src/addons/delegate'; 4 | import animateFill from '../src/plugins/animateFill'; 5 | import followCursor from '../src/plugins/followCursor'; 6 | import inlinePositioning from '../src/plugins/inlinePositioning'; 7 | import sticky from '../src/plugins/sticky'; 8 | import {ROUND_ARROW} from '../src/constants'; 9 | import {render} from '../src/template'; 10 | 11 | tippy.setDefaultProps({ 12 | plugins: [animateFill, followCursor, inlinePositioning, sticky], 13 | render, 14 | }); 15 | 16 | tippy.createSingleton = createSingleton; 17 | tippy.delegate = delegate; 18 | tippy.hideAll = hideAll; 19 | tippy.roundArrow = ROUND_ARROW; 20 | 21 | export default tippy; 22 | -------------------------------------------------------------------------------- /build/base.js: -------------------------------------------------------------------------------- 1 | import tippy from '../src'; 2 | import {render} from '../src/template'; 3 | 4 | tippy.setDefaultProps({render}); 5 | 6 | export {default, hideAll} from '../src'; 7 | export {default as createSingleton} from '../src/addons/createSingleton'; 8 | export {default as delegate} from '../src/addons/delegate'; 9 | export {default as animateFill} from '../src/plugins/animateFill'; 10 | export {default as followCursor} from '../src/plugins/followCursor'; 11 | export {default as inlinePositioning} from '../src/plugins/inlinePositioning'; 12 | export {default as sticky} from '../src/plugins/sticky'; 13 | export {ROUND_ARROW as roundArrow} from '../src/constants'; 14 | -------------------------------------------------------------------------------- /build/bundle-umd.js: -------------------------------------------------------------------------------- 1 | import css from '../dist/tippy.css'; 2 | import {injectCSS} from '../src/css'; 3 | import {isBrowser} from '../src/browser'; 4 | import tippy, {hideAll} from '../src'; 5 | import createSingleton from '../src/addons/createSingleton'; 6 | import delegate from '../src/addons/delegate'; 7 | import animateFill from '../src/plugins/animateFill'; 8 | import followCursor from '../src/plugins/followCursor'; 9 | import inlinePositioning from '../src/plugins/inlinePositioning'; 10 | import sticky from '../src/plugins/sticky'; 11 | import {ROUND_ARROW} from '../src/constants'; 12 | import {render} from '../src/template'; 13 | 14 | if (isBrowser) { 15 | injectCSS(css); 16 | } 17 | 18 | tippy.setDefaultProps({ 19 | plugins: [animateFill, followCursor, inlinePositioning, sticky], 20 | render, 21 | }); 22 | 23 | tippy.createSingleton = createSingleton; 24 | tippy.delegate = delegate; 25 | tippy.hideAll = hideAll; 26 | tippy.roundArrow = ROUND_ARROW; 27 | 28 | export default tippy; 29 | -------------------------------------------------------------------------------- /build/css/backdrop.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/backdrop.scss'; 2 | -------------------------------------------------------------------------------- /build/css/border.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/border.scss'; 2 | -------------------------------------------------------------------------------- /build/css/svg-arrow.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/svg-arrow.scss'; 2 | -------------------------------------------------------------------------------- /build/css/tippy.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/index.scss'; 2 | -------------------------------------------------------------------------------- /build/headless-umd.js: -------------------------------------------------------------------------------- 1 | import tippy, {hideAll} from '../src'; 2 | import createSingleton from '../src/addons/createSingleton'; 3 | import delegate from '../src/addons/delegate'; 4 | import animateFill from '../src/plugins/animateFill'; 5 | import followCursor from '../src/plugins/followCursor'; 6 | import inlinePositioning from '../src/plugins/inlinePositioning'; 7 | import sticky from '../src/plugins/sticky'; 8 | import {ROUND_ARROW} from '../src/constants'; 9 | 10 | tippy.setDefaultProps({ 11 | plugins: [animateFill, followCursor, inlinePositioning, sticky], 12 | animation: false, 13 | }); 14 | 15 | tippy.createSingleton = createSingleton; 16 | tippy.delegate = delegate; 17 | tippy.hideAll = hideAll; 18 | tippy.roundArrow = ROUND_ARROW; 19 | 20 | export default tippy; 21 | -------------------------------------------------------------------------------- /build/headless.js: -------------------------------------------------------------------------------- 1 | import tippy from '../src'; 2 | 3 | export {hideAll} from '../src'; 4 | export {default as createSingleton} from '../src/addons/createSingleton'; 5 | export {default as delegate} from '../src/addons/delegate'; 6 | export {default as animateFill} from '../src/plugins/animateFill'; 7 | export {default as followCursor} from '../src/plugins/followCursor'; 8 | export {default as inlinePositioning} from '../src/plugins/inlinePositioning'; 9 | export {default as sticky} from '../src/plugins/sticky'; 10 | export {ROUND_ARROW as roundArrow} from '../src/constants'; 11 | 12 | tippy.setDefaultProps({animation: false}); 13 | 14 | export default tippy; 15 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | // This file builds the CSS dist files. The main `rollup.config.js` builds the 2 | // JS dist files. 3 | 4 | /* eslint-disable @typescript-eslint/no-var-requires */ 5 | const fs = require('fs'); 6 | const {rollup} = require('rollup'); 7 | const babel = require('rollup-plugin-babel'); 8 | const sass = require('rollup-plugin-sass'); 9 | const postcss = require('postcss'); 10 | const autoprefixer = require('autoprefixer'); 11 | const cssnano = require('cssnano'); 12 | const resolve = require('rollup-plugin-node-resolve'); 13 | const json = require('rollup-plugin-json'); 14 | const cssOnly = require('rollup-plugin-css-only'); 15 | const replace = require('rollup-plugin-replace'); 16 | 17 | const NAMESPACE_PREFIX = process.env.NAMESPACE || 'tippy'; 18 | const THEME = process.env.THEME; 19 | 20 | const BASE_OUTPUT_CONFIG = { 21 | name: 'tippy', 22 | globals: {'popper.js': 'Popper'}, 23 | sourcemap: true, 24 | }; 25 | 26 | const PLUGINS = { 27 | babel: babel({ 28 | exclude: 'node_modules/**', 29 | extensions: ['.js', '.ts'], 30 | }), 31 | replaceNamespace: replace({ 32 | __NAMESPACE_PREFIX__: NAMESPACE_PREFIX, 33 | }), 34 | resolve: resolve({extensions: ['.js', '.ts']}), 35 | css: cssOnly({output: false}), 36 | json: json(), 37 | }; 38 | 39 | const PLUGIN_CONFIG = [ 40 | PLUGINS.babel, 41 | PLUGINS.replaceNamespace, 42 | PLUGINS.resolve, 43 | PLUGINS.json, 44 | PLUGINS.css, 45 | ]; 46 | 47 | function createPluginSCSS(output, shouldInjectNodeEnvTheme = false) { 48 | let data = `$namespace-prefix: ${NAMESPACE_PREFIX};`; 49 | 50 | if (shouldInjectNodeEnvTheme && THEME) { 51 | data += `@import './themes/${THEME}.scss';`; 52 | } 53 | 54 | return sass({ 55 | output, 56 | options: {data}, 57 | processor(css) { 58 | return postcss([autoprefixer, cssnano]) 59 | .process(css, {from: undefined}) 60 | .then((result) => result.css); 61 | }, 62 | }); 63 | } 64 | 65 | function createRollupConfig(inputFile, plugins) { 66 | return { 67 | input: `./build/${inputFile}`, 68 | external: ['popper.js'], 69 | plugins, 70 | }; 71 | } 72 | 73 | async function build() { 74 | // Create `index.d.ts` file from `src/types.ts` 75 | fs.copyFileSync('./src/types.ts', './index.d.ts'); 76 | 77 | // Create base CSS files 78 | for (const filename of fs.readdirSync(`./build/css`)) { 79 | const cssConfig = createRollupConfig( 80 | `css/${filename}`, 81 | PLUGIN_CONFIG.concat( 82 | createPluginSCSS(`./dist/${filename.replace('.js', '.css')}`, true) 83 | ) 84 | ); 85 | const cssBundle = await rollup(cssConfig); 86 | await cssBundle.write({ 87 | ...BASE_OUTPUT_CONFIG, 88 | sourcemap: false, 89 | format: 'umd', 90 | file: './index.js', 91 | }); 92 | } 93 | 94 | // Themes + animations 95 | for (const folder of ['themes', 'animations']) { 96 | for (const filename of fs.readdirSync(`./build/${folder}`)) { 97 | const filenameWithCSSExtension = filename.replace('.js', '.css'); 98 | const outputFile = `./${folder}/${filenameWithCSSExtension}`; 99 | 100 | const config = createRollupConfig( 101 | `${folder}/${filename}`, 102 | PLUGIN_CONFIG.concat(createPluginSCSS(outputFile)) 103 | ); 104 | const bundle = await rollup(config); 105 | await bundle.write({ 106 | ...BASE_OUTPUT_CONFIG, 107 | format: 'umd', 108 | sourcemap: false, 109 | file: 'index.js', 110 | }); 111 | } 112 | } 113 | 114 | fs.unlinkSync('./index.js'); 115 | } 116 | 117 | build(); 118 | -------------------------------------------------------------------------------- /build/themes/light-border.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/themes/light-border.scss'; 2 | -------------------------------------------------------------------------------- /build/themes/light.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/themes/light.scss'; 2 | -------------------------------------------------------------------------------- /build/themes/material.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/themes/material.scss'; 2 | -------------------------------------------------------------------------------- /build/themes/translucent.js: -------------------------------------------------------------------------------- 1 | import '../../src/scss/themes/translucent.scss'; 2 | -------------------------------------------------------------------------------- /headless/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tippy-headless", 3 | "private": true, 4 | "version": "0.1.0", 5 | "description": "Headless rendering for Tippy.js", 6 | "types": "../index.d.ts", 7 | "main": "dist/tippy-headless.cjs.js", 8 | "module": "dist/tippy-headless.esm.js", 9 | "unpkg": "dist/tippy-headless.umd.min.js", 10 | "sideEffects": false, 11 | "files": [ 12 | "dist/" 13 | ], 14 | "author": "atomiks", 15 | "license": "MIT" 16 | } 17 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd'; 2 | import tippy, { 3 | Instance, 4 | Props, 5 | Tippy, 6 | LifecycleHooks, 7 | delegate, 8 | DelegateInstance, 9 | createSingleton, 10 | Plugin, 11 | animateFill, 12 | followCursor, 13 | inlinePositioning, 14 | sticky, 15 | hideAll, 16 | roundArrow, 17 | CreateSingletonInstance, 18 | } from './src/types'; 19 | 20 | interface CustomProps { 21 | custom: number; 22 | } 23 | 24 | type FilteredProps = CustomProps & 25 | Omit; 26 | 27 | type ExtendedProps = FilteredProps & LifecycleHooks; 28 | 29 | declare const tippyExtended: Tippy; 30 | 31 | const singleTarget = document.createElement('div'); 32 | const mulitpleTargets = document.querySelectorAll('div'); 33 | 34 | expectType(tippy(singleTarget)); 35 | expectType(tippy(singleTarget, {content: 'hello'})); 36 | 37 | expectType(tippy(mulitpleTargets)); 38 | expectType(tippy(mulitpleTargets, {content: 'hello'})); 39 | 40 | expectType(delegate(singleTarget, {target: '.child'})); 41 | expectType( 42 | delegate(singleTarget, {target: '.child', content: 'hello'}) 43 | ); 44 | 45 | expectType(delegate(mulitpleTargets, {target: '.child'})); 46 | expectType( 47 | delegate(mulitpleTargets, {target: '.child', content: 'hello'}) 48 | ); 49 | 50 | const tippyInstances = [tippy(singleTarget), tippy(singleTarget)]; 51 | const singleton = createSingleton(tippyInstances); 52 | 53 | expectType(createSingleton(tippyInstances)); 54 | expectType( 55 | createSingleton(tippyInstances, {content: 'hello'}) 56 | ); 57 | expectType( 58 | createSingleton(tippyInstances, {overrides: ['content']}) 59 | ); 60 | expectType<(instances: Instance[]) => void>(singleton.setInstances); 61 | 62 | // TODO: I want to assert that these *don't* error, but `tsd` does not provide 63 | // such a function(?) 64 | createSingleton(tippyExtended('button')); 65 | singleton.setInstances(tippyExtended('button')); 66 | 67 | expectType( 68 | tippy(singleTarget, { 69 | plugins: [animateFill, followCursor, inlinePositioning, sticky], 70 | }) 71 | ); 72 | 73 | const customPlugin: Plugin = { 74 | name: 'custom', 75 | defaultValue: 42, 76 | fn(instance) { 77 | expectType>(instance); 78 | expectType(instance.props.custom); 79 | 80 | return {}; 81 | }, 82 | }; 83 | 84 | expectType>( 85 | tippyExtended(singleTarget, { 86 | custom: 42, 87 | plugins: [customPlugin], 88 | }) 89 | ); 90 | 91 | expectType(hideAll({duration: 50, exclude: tippy(singleTarget)})); 92 | 93 | expectType(roundArrow); 94 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/tippyjs/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/logo.png -------------------------------------------------------------------------------- /src/_babel.d.ts: -------------------------------------------------------------------------------- 1 | declare const __DEV__: boolean; 2 | -------------------------------------------------------------------------------- /src/bindGlobalEventListeners.ts: -------------------------------------------------------------------------------- 1 | import {TOUCH_OPTIONS} from './constants'; 2 | import {isReferenceElement} from './dom-utils'; 3 | 4 | export const currentInput = {isTouch: false}; 5 | let lastMouseMoveTime = 0; 6 | 7 | /** 8 | * When a `touchstart` event is fired, it's assumed the user is using touch 9 | * input. We'll bind a `mousemove` event listener to listen for mouse input in 10 | * the future. This way, the `isTouch` property is fully dynamic and will handle 11 | * hybrid devices that use a mix of touch + mouse input. 12 | */ 13 | export function onDocumentTouchStart(): void { 14 | if (currentInput.isTouch) { 15 | return; 16 | } 17 | 18 | currentInput.isTouch = true; 19 | 20 | if (window.performance) { 21 | document.addEventListener('mousemove', onDocumentMouseMove); 22 | } 23 | } 24 | 25 | /** 26 | * When two `mousemove` event are fired consecutively within 20ms, it's assumed 27 | * the user is using mouse input again. `mousemove` can fire on touch devices as 28 | * well, but very rarely that quickly. 29 | */ 30 | export function onDocumentMouseMove(): void { 31 | const now = performance.now(); 32 | 33 | if (now - lastMouseMoveTime < 20) { 34 | currentInput.isTouch = false; 35 | 36 | document.removeEventListener('mousemove', onDocumentMouseMove); 37 | } 38 | 39 | lastMouseMoveTime = now; 40 | } 41 | 42 | /** 43 | * When an element is in focus and has a tippy, leaving the tab/window and 44 | * returning causes it to show again. For mouse users this is unexpected, but 45 | * for keyboard use it makes sense. 46 | * TODO: find a better technique to solve this problem 47 | */ 48 | export function onWindowBlur(): void { 49 | const activeElement = document.activeElement as HTMLElement | null; 50 | 51 | if (isReferenceElement(activeElement)) { 52 | const instance = activeElement._tippy!; 53 | 54 | if (activeElement.blur && !instance.state.isVisible) { 55 | activeElement.blur(); 56 | } 57 | } 58 | } 59 | 60 | export default function bindGlobalEventListeners(): void { 61 | document.addEventListener('touchstart', onDocumentTouchStart, TOUCH_OPTIONS); 62 | window.addEventListener('blur', onWindowBlur); 63 | } 64 | -------------------------------------------------------------------------------- /src/browser.ts: -------------------------------------------------------------------------------- 1 | export const isBrowser = 2 | typeof window !== 'undefined' && typeof document !== 'undefined'; 3 | 4 | export const isIE11 = isBrowser 5 | ? // @ts-ignore 6 | !!window.msCrypto 7 | : false; 8 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ROUND_ARROW = 2 | ''; 3 | 4 | export const BOX_CLASS = `__NAMESPACE_PREFIX__-box`; 5 | export const CONTENT_CLASS = `__NAMESPACE_PREFIX__-content`; 6 | export const BACKDROP_CLASS = `__NAMESPACE_PREFIX__-backdrop`; 7 | export const ARROW_CLASS = `__NAMESPACE_PREFIX__-arrow`; 8 | export const SVG_ARROW_CLASS = `__NAMESPACE_PREFIX__-svg-arrow`; 9 | 10 | export const TOUCH_OPTIONS = {passive: true, capture: true}; 11 | 12 | export const TIPPY_DEFAULT_APPEND_TO = () => document.body; 13 | -------------------------------------------------------------------------------- /src/css.ts: -------------------------------------------------------------------------------- 1 | export function injectCSS(css: string): void { 2 | const style = document.createElement('style'); 3 | style.textContent = css; 4 | style.setAttribute('data-__NAMESPACE_PREFIX__-stylesheet', ''); 5 | const head = document.head; 6 | const firstStyleOrLinkTag = document.querySelector('head>style,head>link'); 7 | 8 | if (firstStyleOrLinkTag) { 9 | head.insertBefore(style, firstStyleOrLinkTag); 10 | } else { 11 | head.appendChild(style); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import bindGlobalEventListeners, { 2 | currentInput, 3 | } from './bindGlobalEventListeners'; 4 | import createTippy, {mountedInstances} from './createTippy'; 5 | import {getArrayOfElements, isElement, isReferenceElement} from './dom-utils'; 6 | import {defaultProps, setDefaultProps, validateProps} from './props'; 7 | import {HideAll, HideAllOptions, Instance, Props, Targets} from './types'; 8 | import {validateTargets, warnWhen} from './validation'; 9 | 10 | function tippy( 11 | targets: Targets, 12 | optionalProps: Partial = {} 13 | ): Instance | Instance[] { 14 | const plugins = defaultProps.plugins.concat(optionalProps.plugins || []); 15 | 16 | /* istanbul ignore else */ 17 | if (__DEV__) { 18 | validateTargets(targets); 19 | validateProps(optionalProps, plugins); 20 | } 21 | 22 | bindGlobalEventListeners(); 23 | 24 | const passedProps: Partial = {...optionalProps, plugins}; 25 | 26 | const elements = getArrayOfElements(targets); 27 | 28 | /* istanbul ignore else */ 29 | if (__DEV__) { 30 | const isSingleContentElement = isElement(passedProps.content); 31 | const isMoreThanOneReferenceElement = elements.length > 1; 32 | warnWhen( 33 | isSingleContentElement && isMoreThanOneReferenceElement, 34 | [ 35 | 'tippy() was passed an Element as the `content` prop, but more than', 36 | 'one tippy instance was created by this invocation. This means the', 37 | 'content element will only be appended to the last tippy instance.', 38 | '\n\n', 39 | 'Instead, pass the .innerHTML of the element, or use a function that', 40 | 'returns a cloned version of the element instead.', 41 | '\n\n', 42 | '1) content: element.innerHTML\n', 43 | '2) content: () => element.cloneNode(true)', 44 | ].join(' ') 45 | ); 46 | } 47 | 48 | const instances = elements.reduce( 49 | (acc, reference): Instance[] => { 50 | const instance = reference && createTippy(reference, passedProps); 51 | 52 | if (instance) { 53 | acc.push(instance); 54 | } 55 | 56 | return acc; 57 | }, 58 | [] 59 | ); 60 | 61 | return isElement(targets) ? instances[0] : instances; 62 | } 63 | 64 | tippy.defaultProps = defaultProps; 65 | tippy.setDefaultProps = setDefaultProps; 66 | tippy.currentInput = currentInput; 67 | 68 | export default tippy; 69 | 70 | export const hideAll: HideAll = ({ 71 | exclude: excludedReferenceOrInstance, 72 | duration, 73 | }: HideAllOptions = {}) => { 74 | mountedInstances.forEach((instance) => { 75 | let isExcluded = false; 76 | 77 | if (excludedReferenceOrInstance) { 78 | isExcluded = isReferenceElement(excludedReferenceOrInstance) 79 | ? instance.reference === excludedReferenceOrInstance 80 | : instance.popper === (excludedReferenceOrInstance as Instance).popper; 81 | } 82 | 83 | if (!isExcluded) { 84 | const originalDuration = instance.props.duration; 85 | 86 | instance.setProps({duration}); 87 | instance.hide(); 88 | 89 | if (!instance.state.isDestroyed) { 90 | instance.setProps({duration: originalDuration}); 91 | } 92 | } 93 | }); 94 | }; 95 | -------------------------------------------------------------------------------- /src/plugins/animateFill.ts: -------------------------------------------------------------------------------- 1 | import {BACKDROP_CLASS} from '../constants'; 2 | import {div, setVisibilityState} from '../dom-utils'; 3 | import {getChildren} from '../template'; 4 | import {AnimateFill} from '../types'; 5 | import {errorWhen} from '../validation'; 6 | 7 | const animateFill: AnimateFill = { 8 | name: 'animateFill', 9 | defaultValue: false, 10 | fn(instance) { 11 | // @ts-ignore 12 | if (!instance.props.render?.$$tippy) { 13 | if (__DEV__) { 14 | errorWhen( 15 | instance.props.animateFill, 16 | 'The `animateFill` plugin requires the default render function.' 17 | ); 18 | } 19 | 20 | return {}; 21 | } 22 | 23 | const {box, content} = getChildren(instance.popper); 24 | 25 | const backdrop = instance.props.animateFill 26 | ? createBackdropElement() 27 | : null; 28 | 29 | return { 30 | onCreate(): void { 31 | if (backdrop) { 32 | box.insertBefore(backdrop, box.firstElementChild!); 33 | box.setAttribute('data-animatefill', ''); 34 | box.style.overflow = 'hidden'; 35 | 36 | instance.setProps({arrow: false, animation: 'shift-away'}); 37 | } 38 | }, 39 | onMount(): void { 40 | if (backdrop) { 41 | const {transitionDuration} = box.style; 42 | const duration = Number(transitionDuration.replace('ms', '')); 43 | 44 | // The content should fade in after the backdrop has mostly filled the 45 | // tooltip element. `clip-path` is the other alternative but is not 46 | // well-supported and is buggy on some devices. 47 | content.style.transitionDelay = `${Math.round(duration / 10)}ms`; 48 | 49 | backdrop.style.transitionDuration = transitionDuration; 50 | setVisibilityState([backdrop], 'visible'); 51 | } 52 | }, 53 | onShow(): void { 54 | if (backdrop) { 55 | backdrop.style.transitionDuration = '0ms'; 56 | } 57 | }, 58 | onHide(): void { 59 | if (backdrop) { 60 | setVisibilityState([backdrop], 'hidden'); 61 | } 62 | }, 63 | }; 64 | }, 65 | }; 66 | 67 | export default animateFill; 68 | 69 | function createBackdropElement(): HTMLDivElement { 70 | const backdrop = div(); 71 | backdrop.className = BACKDROP_CLASS; 72 | setVisibilityState([backdrop], 'hidden'); 73 | return backdrop; 74 | } 75 | -------------------------------------------------------------------------------- /src/plugins/sticky.ts: -------------------------------------------------------------------------------- 1 | import {VirtualElement} from '@popperjs/core'; 2 | import {ReferenceElement, Sticky} from '../types'; 3 | 4 | const sticky: Sticky = { 5 | name: 'sticky', 6 | defaultValue: false, 7 | fn(instance) { 8 | const {reference, popper} = instance; 9 | 10 | function getReference(): ReferenceElement | VirtualElement { 11 | return instance.popperInstance 12 | ? instance.popperInstance.state.elements.reference 13 | : reference; 14 | } 15 | 16 | function shouldCheck(value: 'reference' | 'popper'): boolean { 17 | return instance.props.sticky === true || instance.props.sticky === value; 18 | } 19 | 20 | let prevRefRect: ClientRect | null = null; 21 | let prevPopRect: ClientRect | null = null; 22 | 23 | function updatePosition(): void { 24 | const currentRefRect = shouldCheck('reference') 25 | ? getReference().getBoundingClientRect() 26 | : null; 27 | const currentPopRect = shouldCheck('popper') 28 | ? popper.getBoundingClientRect() 29 | : null; 30 | 31 | if ( 32 | (currentRefRect && areRectsDifferent(prevRefRect, currentRefRect)) || 33 | (currentPopRect && areRectsDifferent(prevPopRect, currentPopRect)) 34 | ) { 35 | if (instance.popperInstance) { 36 | instance.popperInstance.update(); 37 | } 38 | } 39 | 40 | prevRefRect = currentRefRect; 41 | prevPopRect = currentPopRect; 42 | 43 | if (instance.state.isMounted) { 44 | requestAnimationFrame(updatePosition); 45 | } 46 | } 47 | 48 | return { 49 | onMount(): void { 50 | if (instance.props.sticky) { 51 | updatePosition(); 52 | } 53 | }, 54 | }; 55 | }, 56 | }; 57 | 58 | export default sticky; 59 | 60 | function areRectsDifferent( 61 | rectA: ClientRect | null, 62 | rectB: ClientRect | null 63 | ): boolean { 64 | if (rectA && rectB) { 65 | return ( 66 | rectA.top !== rectB.top || 67 | rectA.right !== rectB.right || 68 | rectA.bottom !== rectB.bottom || 69 | rectA.left !== rectB.left 70 | ); 71 | } 72 | 73 | return true; 74 | } 75 | -------------------------------------------------------------------------------- /src/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin backdrop-transform-enter($placement) { 2 | $scale: 1; 3 | @if ($placement == 'top') { 4 | transform: scale($scale) translate(-50%, -55%); 5 | } @else if ($placement == 'bottom') { 6 | transform: scale($scale) translate(-50%, -45%); 7 | } @else if ($placement == 'left') { 8 | transform: scale($scale) translate(-50%, -50%); 9 | } @else if ($placement == 'right') { 10 | transform: scale($scale) translate(-50%, -50%); 11 | } 12 | } 13 | 14 | @mixin backdrop-transform-leave($placement) { 15 | $scale: 0.2; 16 | @if ($placement == 'top') { 17 | transform: scale($scale) translate(-50%, -45%); 18 | } @else if ($placement == 'bottom') { 19 | transform: scale($scale) translate(-50%, 0); 20 | } @else if ($placement == 'left') { 21 | transform: scale($scale) translate(-75%, -50%); 22 | } @else if ($placement == 'right') { 23 | transform: scale($scale) translate(-25%, -50%); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/scss/_vars.scss: -------------------------------------------------------------------------------- 1 | $namespace-prefix: '__NAMESPACE_PREFIX__' !default; 2 | $placements: 'top', 'bottom', 'left', 'right'; 3 | $origins: bottom, top, right, left; 4 | $backdrop-origins: 0% 25%, 0% -50%, 50% 0%, -50% 0%; 5 | $backdrop-border-radii: 40% 40% 0 0, 0 0 30% 30%, 50% 0 0 50%, 0 50% 50% 0; 6 | $arrow-size: 16px; 7 | -------------------------------------------------------------------------------- /src/scss/animations/fade.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | .#{$namespace-prefix}-box { 5 | &[data-animation='fade'][data-state='hidden'] { 6 | opacity: 0; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/scss/animations/perspective-extreme.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | @mixin visible-transform($placement) { 5 | @if ($placement == 'top') { 6 | transform: perspective(700px); 7 | } @else if ($placement == 'bottom') { 8 | transform: perspective(700px); 9 | } @else if ($placement == 'left') { 10 | transform: perspective(700px); 11 | } @else if ($placement == 'right') { 12 | transform: perspective(700px); 13 | } 14 | } 15 | 16 | @mixin hidden-transform($placement) { 17 | @if ($placement == 'top') { 18 | transform: perspective(700px) translateY(10px) rotateX(90deg); 19 | } @else if ($placement == 'bottom') { 20 | transform: perspective(700px) translateY(-10px) rotateX(-90deg); 21 | } @else if ($placement == 'left') { 22 | transform: perspective(700px) translateX(10px) rotateY(-90deg); 23 | } @else if ($placement == 'right') { 24 | transform: perspective(700px) translateX(-10px) rotateY(90deg); 25 | } 26 | } 27 | 28 | .#{$namespace-prefix}-box { 29 | &[data-animation='perspective-extreme'] { 30 | @each $placement in $placements { 31 | &[data-placement^='#{$placement}'] { 32 | transform-origin: nth($origins, index($placements, $placement)); 33 | 34 | &[data-state='visible'] { 35 | @include visible-transform($placement); 36 | } 37 | 38 | &[data-state='hidden'] { 39 | @include hidden-transform($placement); 40 | } 41 | } 42 | } 43 | 44 | &[data-state='hidden'] { 45 | opacity: 0.5; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/scss/animations/perspective-subtle.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | @mixin visible-transform($placement) { 5 | @if ($placement == 'top') { 6 | transform: perspective(700px); 7 | } @else if ($placement == 'bottom') { 8 | transform: perspective(700px); 9 | } @else if ($placement == 'left') { 10 | transform: perspective(700px); 11 | } @else if ($placement == 'right') { 12 | transform: perspective(700px); 13 | } 14 | } 15 | 16 | @mixin hidden-transform($placement) { 17 | @if ($placement == 'top') { 18 | transform: perspective(700px) translateY(5px) rotateX(30deg); 19 | } @else if ($placement == 'bottom') { 20 | transform: perspective(700px) translateY(-5px) rotateX(-30deg); 21 | } @else if ($placement == 'left') { 22 | transform: perspective(700px) translateX(5px) rotateY(-30deg); 23 | } @else if ($placement == 'right') { 24 | transform: perspective(700px) translateX(-5px) rotateY(30deg); 25 | } 26 | } 27 | 28 | .#{$namespace-prefix}-box { 29 | &[data-animation='perspective-subtle'] { 30 | @each $placement in $placements { 31 | &[data-placement^='#{$placement}'] { 32 | transform-origin: nth($origins, index($placements, $placement)); 33 | 34 | &[data-state='visible'] { 35 | @include visible-transform($placement); 36 | } 37 | 38 | &[data-state='hidden'] { 39 | @include hidden-transform($placement); 40 | } 41 | } 42 | } 43 | 44 | &[data-state='hidden'] { 45 | opacity: 0; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/scss/animations/perspective.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | @mixin visible-transform($placement) { 5 | @if ($placement == 'top') { 6 | transform: perspective(700px); 7 | } @else if ($placement == 'bottom') { 8 | transform: perspective(700px); 9 | } @else if ($placement == 'left') { 10 | transform: perspective(700px); 11 | } @else if ($placement == 'right') { 12 | transform: perspective(700px); 13 | } 14 | } 15 | 16 | @mixin hidden-transform($placement) { 17 | @if ($placement == 'top') { 18 | transform: perspective(700px) translateY(8px) rotateX(60deg); 19 | } @else if ($placement == 'bottom') { 20 | transform: perspective(700px) translateY(-8px) rotateX(-60deg); 21 | } @else if ($placement == 'left') { 22 | transform: perspective(700px) translateX(8px) rotateY(-60deg); 23 | } @else if ($placement == 'right') { 24 | transform: perspective(700px) translateX(-8px) rotateY(60deg); 25 | } 26 | } 27 | 28 | .#{$namespace-prefix}-box { 29 | &[data-animation='perspective'] { 30 | @each $placement in $placements { 31 | &[data-placement^='#{$placement}'] { 32 | transform-origin: nth($origins, index($placements, $placement)); 33 | 34 | &[data-state='visible'] { 35 | @include visible-transform($placement); 36 | } 37 | 38 | &[data-state='hidden'] { 39 | @include hidden-transform($placement); 40 | } 41 | } 42 | } 43 | 44 | &[data-state='hidden'] { 45 | opacity: 0; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/scss/animations/scale-extreme.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | .#{$namespace-prefix}-box { 5 | &[data-animation='scale-extreme'] { 6 | @each $placement in $placements { 7 | &[data-placement^='#{$placement}'] { 8 | transform-origin: nth($origins, index($placements, $placement)); 9 | } 10 | } 11 | 12 | &[data-state='hidden'] { 13 | transform: scale(0); 14 | opacity: 0.25; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/scss/animations/scale-subtle.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | .#{$namespace-prefix}-box { 5 | &[data-animation='scale-subtle'] { 6 | @each $placement in $placements { 7 | &[data-placement^='#{$placement}'] { 8 | transform-origin: nth($origins, index($placements, $placement)); 9 | } 10 | } 11 | 12 | &[data-state='hidden'] { 13 | transform: scale(0.8); 14 | opacity: 0; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/scss/animations/scale.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | .#{$namespace-prefix}-box { 5 | &[data-animation='scale'] { 6 | @each $placement in $placements { 7 | &[data-placement^='#{$placement}'] { 8 | transform-origin: nth($origins, index($placements, $placement)); 9 | } 10 | } 11 | 12 | &[data-state='hidden'] { 13 | transform: scale(0.5); 14 | opacity: 0; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/scss/animations/shift-away-extreme.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | @mixin hidden-transform($placement) { 5 | @if ($placement == 'top') { 6 | transform: translateY(20px); 7 | } @else if ($placement == 'bottom') { 8 | transform: translateY(-20px); 9 | } @else if ($placement == 'left') { 10 | transform: translateX(20px); 11 | } @else if ($placement == 'right') { 12 | transform: translateX(-20px); 13 | } 14 | } 15 | 16 | .#{$namespace-prefix}-box { 17 | &[data-animation='shift-away-extreme'] { 18 | &[data-state='hidden'] { 19 | opacity: 0; 20 | 21 | @each $placement in $placements { 22 | &[data-placement^='#{$placement}'] { 23 | @include hidden-transform($placement); 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/scss/animations/shift-away-subtle.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | @mixin hidden-transform($placement) { 5 | @if ($placement == 'top') { 6 | transform: translateY(5px); 7 | } @else if ($placement == 'bottom') { 8 | transform: translateY(-5px); 9 | } @else if ($placement == 'left') { 10 | transform: translateX(5px); 11 | } @else if ($placement == 'right') { 12 | transform: translateX(-5px); 13 | } 14 | } 15 | 16 | .#{$namespace-prefix}-box { 17 | &[data-animation='shift-away-subtle'] { 18 | &[data-state='hidden'] { 19 | opacity: 0; 20 | 21 | @each $placement in $placements { 22 | &[data-placement^='#{$placement}'] { 23 | @include hidden-transform($placement); 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/scss/animations/shift-away.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | @mixin hidden-transform($placement) { 5 | @if ($placement == 'top') { 6 | transform: translateY(10px); 7 | } @else if ($placement == 'bottom') { 8 | transform: translateY(-10px); 9 | } @else if ($placement == 'left') { 10 | transform: translateX(10px); 11 | } @else if ($placement == 'right') { 12 | transform: translateX(-10px); 13 | } 14 | } 15 | 16 | .#{$namespace-prefix}-box { 17 | &[data-animation='shift-away'][data-state='hidden'] { 18 | opacity: 0; 19 | 20 | @each $placement in $placements { 21 | &[data-placement^='#{$placement}'] { 22 | @include hidden-transform($placement); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/scss/animations/shift-toward-extreme.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | @mixin hidden-transform($placement) { 5 | @if ($placement == 'top') { 6 | transform: translateY(-20px); 7 | } @else if ($placement == 'bottom') { 8 | transform: translateY(20px); 9 | } @else if ($placement == 'left') { 10 | transform: translateX(-20px); 11 | } @else if ($placement == 'right') { 12 | transform: translateX(20px); 13 | } 14 | } 15 | 16 | .#{$namespace-prefix}-box { 17 | &[data-animation='shift-toward-extreme'][data-state='hidden'] { 18 | opacity: 0; 19 | 20 | @each $placement in $placements { 21 | &[data-placement^='#{$placement}'] { 22 | @include hidden-transform($placement); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/scss/animations/shift-toward-subtle.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | @mixin hidden-transform($placement) { 5 | @if ($placement == 'top') { 6 | transform: translateY(-5px); 7 | } @else if ($placement == 'bottom') { 8 | transform: translateY(5px); 9 | } @else if ($placement == 'left') { 10 | transform: translateX(-5px); 11 | } @else if ($placement == 'right') { 12 | transform: translateX(5px); 13 | } 14 | } 15 | 16 | .#{$namespace-prefix}-box { 17 | &[data-animation='shift-toward-subtle'][data-state='hidden'] { 18 | opacity: 0; 19 | 20 | @each $placement in $placements { 21 | &[data-placement^='#{$placement}'] { 22 | &[data-state='hidden'] { 23 | @include hidden-transform($placement); 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/scss/animations/shift-toward.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | @mixin hidden-transform($placement) { 5 | @if ($placement == 'top') { 6 | transform: translateY(-10px); 7 | } @else if ($placement == 'bottom') { 8 | transform: translateY(10px); 9 | } @else if ($placement == 'left') { 10 | transform: translateX(-10px); 11 | } @else if ($placement == 'right') { 12 | transform: translateX(10px); 13 | } 14 | } 15 | 16 | .#{$namespace-prefix}-box { 17 | &[data-animation='shift-toward'][data-state='hidden'] { 18 | opacity: 0; 19 | 20 | @each $placement in $placements { 21 | &[data-placement^='#{$placement}'] { 22 | @include hidden-transform($placement); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/scss/backdrop.scss: -------------------------------------------------------------------------------- 1 | @import './_mixins.scss'; 2 | @import './_vars.scss'; 3 | 4 | .#{$namespace-prefix}-box { 5 | @each $placement in $placements { 6 | &[data-placement^='#{$placement}'] { 7 | > .#{$namespace-prefix}-backdrop { 8 | transform-origin: nth( 9 | $backdrop-origins, 10 | index($placements, $placement) 11 | ); 12 | border-radius: nth( 13 | $backdrop-border-radii, 14 | index($placements, $placement) 15 | ); 16 | 17 | &[data-state='visible'] { 18 | @include backdrop-transform-enter($placement); 19 | } 20 | 21 | &[data-state='hidden'] { 22 | @include backdrop-transform-leave($placement); 23 | } 24 | } 25 | } 26 | } 27 | } 28 | 29 | .#{$namespace-prefix}-box { 30 | &[data-animatefill] { 31 | // Declared with !important so that custom themes don't need to specify 32 | // this property. 33 | background-color: transparent !important; 34 | } 35 | } 36 | 37 | .#{$namespace-prefix}-backdrop { 38 | position: absolute; 39 | background-color: #333; 40 | border-radius: 50%; 41 | width: calc(110% + 32px); 42 | left: 50%; 43 | top: 50%; 44 | z-index: -1; 45 | transition: all cubic-bezier(0.46, 0.1, 0.52, 0.98); 46 | backface-visibility: hidden; 47 | 48 | &[data-state='hidden'] { 49 | opacity: 0; 50 | } 51 | 52 | &::after { 53 | content: ''; 54 | float: left; 55 | padding-top: 100%; 56 | } 57 | } 58 | 59 | .#{$namespace-prefix}-backdrop + .#{$namespace-prefix}-content { 60 | transition-property: opacity; 61 | will-change: opacity; 62 | 63 | &[data-state='hidden'] { 64 | opacity: 0; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/scss/border.scss: -------------------------------------------------------------------------------- 1 | @import './_vars.scss'; 2 | 3 | .#{$namespace-prefix}-box { 4 | border: 1px transparent; 5 | 6 | &[data-placement^='top'] > .#{$namespace-prefix}-arrow { 7 | &::after { 8 | border-top-color: inherit; 9 | border-width: 8px 8px 0; 10 | bottom: -8px; 11 | left: 0; 12 | } 13 | } 14 | 15 | &[data-placement^='bottom'] > .#{$namespace-prefix}-arrow { 16 | &::after { 17 | border-bottom-color: inherit; 18 | border-width: 0 8px 8px; 19 | top: -8px; 20 | left: 0; 21 | } 22 | } 23 | 24 | &[data-placement^='left'] > .#{$namespace-prefix}-arrow { 25 | &::after { 26 | border-left-color: inherit; 27 | border-width: 8px 0 8px 8px; 28 | right: -8px; 29 | top: 0; 30 | } 31 | } 32 | 33 | &[data-placement^='right'] > .#{$namespace-prefix}-arrow { 34 | &::after { 35 | border-width: 8px 8px 8px 0; 36 | left: -8px; 37 | top: 0; 38 | border-right-color: inherit; 39 | } 40 | } 41 | 42 | &[data-placement^='top'] 43 | > .#{$namespace-prefix}-svg-arrow 44 | > svg:first-child:not(:last-child) { 45 | top: 17px; 46 | } 47 | 48 | &[data-placement^='bottom'] 49 | > .#{$namespace-prefix}-svg-arrow 50 | > svg:first-child:not(:last-child) { 51 | bottom: 17px; 52 | } 53 | 54 | &[data-placement^='left'] 55 | > .#{$namespace-prefix}-svg-arrow 56 | > svg:first-child:not(:last-child) { 57 | left: 12px; 58 | } 59 | 60 | &[data-placement^='right'] 61 | > .#{$namespace-prefix}-svg-arrow 62 | > svg:first-child:not(:last-child) { 63 | right: 12px; 64 | } 65 | } 66 | 67 | .#{$namespace-prefix}-arrow { 68 | & { 69 | border-color: inherit; 70 | } 71 | 72 | &::after { 73 | content: ''; 74 | z-index: -1; 75 | position: absolute; 76 | border-color: transparent; 77 | border-style: solid; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/scss/index.scss: -------------------------------------------------------------------------------- 1 | @import './_vars.scss'; 2 | @import './animations/fade.scss'; 3 | 4 | $color: #333; 5 | 6 | [data-#{$namespace-prefix}-root] { 7 | max-width: calc(100vw - 10px); 8 | } 9 | 10 | .#{$namespace-prefix}-box { 11 | position: relative; 12 | background-color: $color; 13 | color: white; 14 | border-radius: 4px; 15 | font-size: 14px; 16 | line-height: 1.4; 17 | white-space: initial; 18 | outline: 0; 19 | transition-property: transform, visibility, opacity; 20 | 21 | &[data-placement^='top'] > .#{$namespace-prefix}-arrow { 22 | bottom: 0; 23 | 24 | &::before { 25 | bottom: -7px; 26 | left: 0; 27 | border-width: 8px 8px 0; 28 | border-top-color: initial; 29 | transform-origin: center top; 30 | } 31 | } 32 | 33 | &[data-placement^='bottom'] > .#{$namespace-prefix}-arrow { 34 | top: 0; 35 | 36 | &::before { 37 | top: -7px; 38 | left: 0; 39 | border-width: 0 8px 8px; 40 | border-bottom-color: initial; 41 | transform-origin: center bottom; 42 | } 43 | } 44 | 45 | &[data-placement^='left'] > .#{$namespace-prefix}-arrow { 46 | right: 0; 47 | 48 | &::before { 49 | border-width: 8px 0 8px 8px; 50 | border-left-color: initial; 51 | right: -7px; 52 | transform-origin: center left; 53 | } 54 | } 55 | 56 | &[data-placement^='right'] > .#{$namespace-prefix}-arrow { 57 | left: 0; 58 | 59 | &::before { 60 | left: -7px; 61 | border-width: 8px 8px 8px 0; 62 | border-right-color: initial; 63 | transform-origin: center right; 64 | } 65 | } 66 | 67 | &[data-inertia][data-state='visible'] { 68 | transition-timing-function: cubic-bezier(0.54, 1.5, 0.38, 1.11); 69 | } 70 | } 71 | 72 | .#{$namespace-prefix}-arrow { 73 | & { 74 | width: $arrow-size; 75 | height: $arrow-size; 76 | color: $color; 77 | } 78 | 79 | &::before { 80 | content: ''; 81 | position: absolute; 82 | border-color: transparent; 83 | border-style: solid; 84 | } 85 | } 86 | 87 | .#{$namespace-prefix}-content { 88 | position: relative; 89 | padding: 5px 9px; 90 | z-index: 1; 91 | } 92 | -------------------------------------------------------------------------------- /src/scss/svg-arrow.scss: -------------------------------------------------------------------------------- 1 | @import './_vars.scss'; 2 | 3 | .#{$namespace-prefix}-box { 4 | &[data-placement^='top'] > .#{$namespace-prefix}-svg-arrow { 5 | bottom: 0; 6 | 7 | &::after, 8 | > svg { 9 | top: 16px; 10 | transform: rotate(180deg); 11 | } 12 | } 13 | 14 | &[data-placement^='bottom'] > .#{$namespace-prefix}-svg-arrow { 15 | top: 0; 16 | 17 | > svg { 18 | bottom: 16px; 19 | } 20 | } 21 | 22 | &[data-placement^='left'] > .#{$namespace-prefix}-svg-arrow { 23 | right: 0; 24 | 25 | &::after, 26 | > svg { 27 | transform: rotate(90deg); 28 | top: calc(50% - 3px); 29 | left: 11px; 30 | } 31 | } 32 | 33 | &[data-placement^='right'] > .#{$namespace-prefix}-svg-arrow { 34 | left: 0; 35 | 36 | &::after, 37 | > svg { 38 | transform: rotate(-90deg); 39 | top: calc(50% - 3px); 40 | right: 11px; 41 | } 42 | } 43 | } 44 | 45 | .#{$namespace-prefix}-svg-arrow { 46 | position: absolute; 47 | width: $arrow-size; 48 | height: $arrow-size; 49 | fill: #333; 50 | text-align: initial; 51 | 52 | > svg { 53 | position: absolute; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/scss/themes/light-border.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | $color: white; 5 | $transparent-light: rgba(0, 8, 16, 0.08); 6 | $transparent-dark: rgba(0, 8, 16, 0.15); 7 | $transparent-darker: rgba(0, 8, 16, 0.2); 8 | 9 | .#{$namespace-prefix}-box[data-theme~='light-border'] { 10 | background-color: $color; 11 | background-clip: padding-box; 12 | border: 1px solid $transparent-dark; 13 | color: #333; 14 | box-shadow: 0 4px 14px -2px $transparent-light; 15 | 16 | > .#{$namespace-prefix}-backdrop { 17 | background-color: $color; 18 | } 19 | 20 | > .#{$namespace-prefix}-arrow, 21 | > .#{$namespace-prefix}-svg-arrow { 22 | &::after { 23 | content: ''; 24 | position: absolute; 25 | z-index: -1; 26 | } 27 | } 28 | 29 | > .#{$namespace-prefix}-arrow::after { 30 | border-color: transparent; 31 | border-style: solid; 32 | } 33 | 34 | &[data-placement^='top'] { 35 | > .#{$namespace-prefix}-arrow { 36 | &::before { 37 | border-top-color: $color; 38 | } 39 | 40 | &::after { 41 | border-top-color: $transparent-darker; 42 | border-width: 7px 7px 0; 43 | top: $arrow-size + 1; 44 | left: 1px; 45 | } 46 | } 47 | 48 | > .#{$namespace-prefix}-svg-arrow { 49 | > svg { 50 | top: $arrow-size; 51 | } 52 | 53 | &::after { 54 | top: $arrow-size + 1; 55 | } 56 | } 57 | } 58 | 59 | &[data-placement^='bottom'] { 60 | > .#{$namespace-prefix}-arrow { 61 | &::before { 62 | border-bottom-color: $color; 63 | bottom: $arrow-size; 64 | } 65 | 66 | &::after { 67 | border-bottom-color: $transparent-darker; 68 | border-width: 0 7px 7px; 69 | bottom: $arrow-size + 1; 70 | left: 1px; 71 | } 72 | } 73 | 74 | > .#{$namespace-prefix}-svg-arrow { 75 | > svg { 76 | bottom: $arrow-size; 77 | } 78 | 79 | &::after { 80 | bottom: $arrow-size + 1; 81 | } 82 | } 83 | } 84 | 85 | &[data-placement^='left'] { 86 | > .#{$namespace-prefix}-arrow { 87 | &::before { 88 | border-left-color: $color; 89 | } 90 | 91 | &::after { 92 | border-left-color: $transparent-darker; 93 | border-width: 7px 0 7px 7px; 94 | left: $arrow-size + 1; 95 | top: 1px; 96 | } 97 | } 98 | 99 | > .#{$namespace-prefix}-svg-arrow { 100 | > svg { 101 | left: 11px; 102 | } 103 | 104 | &::after { 105 | left: 12px; 106 | } 107 | } 108 | } 109 | 110 | &[data-placement^='right'] { 111 | > .#{$namespace-prefix}-arrow { 112 | &::before { 113 | border-right-color: $color; 114 | right: $arrow-size; 115 | } 116 | 117 | &::after { 118 | border-width: 7px 7px 7px 0; 119 | right: $arrow-size + 1; 120 | top: 1px; 121 | border-right-color: $transparent-darker; 122 | } 123 | } 124 | 125 | > .#{$namespace-prefix}-svg-arrow { 126 | > svg { 127 | right: 11px; 128 | } 129 | 130 | &::after { 131 | right: 12px; 132 | } 133 | } 134 | } 135 | 136 | > .#{$namespace-prefix}-svg-arrow { 137 | fill: white; 138 | 139 | &::after { 140 | background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMCA2czEuNzk2LS4wMTMgNC42Ny0zLjYxNUM1Ljg1MS45IDYuOTMuMDA2IDggMGMxLjA3LS4wMDYgMi4xNDguODg3IDMuMzQzIDIuMzg1QzE0LjIzMyA2LjAwNSAxNiA2IDE2IDZIMHoiIGZpbGw9InJnYmEoMCwgOCwgMTYsIDAuMikiIC8+PC9zdmc+); 141 | background-size: $arrow-size 6px; 142 | width: $arrow-size; 143 | height: 6px; 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/scss/themes/light.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | $color: white; 5 | 6 | .#{$namespace-prefix}-box[data-theme~='light'] { 7 | color: #26323d; 8 | box-shadow: 0 0 20px 4px rgba(154, 161, 177, 0.15), 9 | 0 4px 80px -8px rgba(36, 40, 47, 0.25), 10 | 0 4px 4px -2px rgba(91, 94, 105, 0.15); 11 | background-color: $color; 12 | 13 | &[data-placement^='top'] > .#{$namespace-prefix}-arrow::before { 14 | border-top-color: $color; 15 | } 16 | 17 | &[data-placement^='bottom'] > .#{$namespace-prefix}-arrow::before { 18 | border-bottom-color: $color; 19 | } 20 | 21 | &[data-placement^='left'] > .#{$namespace-prefix}-arrow::before { 22 | border-left-color: $color; 23 | } 24 | 25 | &[data-placement^='right'] > .#{$namespace-prefix}-arrow::before { 26 | border-right-color: $color; 27 | } 28 | 29 | > .#{$namespace-prefix}-backdrop { 30 | background-color: $color; 31 | } 32 | 33 | > .#{$namespace-prefix}-svg-arrow { 34 | fill: $color; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/scss/themes/material.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | $color: #505355; 5 | 6 | .#{$namespace-prefix}-box[data-theme~='material'] { 7 | background-color: $color; 8 | font-weight: 600; 9 | 10 | &[data-placement^='top'] > .#{$namespace-prefix}-arrow::before { 11 | border-top-color: $color; 12 | } 13 | 14 | &[data-placement^='bottom'] > .#{$namespace-prefix}-arrow::before { 15 | border-bottom-color: $color; 16 | } 17 | 18 | &[data-placement^='left'] > .#{$namespace-prefix}-arrow::before { 19 | border-left-color: $color; 20 | } 21 | 22 | &[data-placement^='right'] > .#{$namespace-prefix}-arrow::before { 23 | border-right-color: $color; 24 | } 25 | 26 | > .#{$namespace-prefix}-backdrop { 27 | background-color: $color; 28 | } 29 | 30 | > .#{$namespace-prefix}-svg-arrow { 31 | fill: $color; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/scss/themes/translucent.scss: -------------------------------------------------------------------------------- 1 | @import '../_mixins.scss'; 2 | @import '../_vars.scss'; 3 | 4 | $color: rgba(0, 0, 0, 0.7); 5 | 6 | .#{$namespace-prefix}-box[data-theme~='translucent'] { 7 | background-color: $color; 8 | 9 | > .#{$namespace-prefix}-arrow { 10 | width: 14px; 11 | height: 14px; 12 | } 13 | 14 | /** 15 | * We use an 8px arrow with 1px overlap by default, since some browsers (at 16 | * least they used to) caused a 1px gap between the arrow and the popper. 17 | * However, with the translucent theme, this causes darkening since it 18 | * overlaps. 19 | */ 20 | &[data-placement^='top'] > .#{$namespace-prefix}-arrow { 21 | &::before { 22 | border-width: 7px 7px 0; 23 | border-top-color: $color; 24 | } 25 | } 26 | 27 | &[data-placement^='bottom'] > .#{$namespace-prefix}-arrow { 28 | &::before { 29 | border-width: 0 7px 7px; 30 | border-bottom-color: $color; 31 | } 32 | } 33 | 34 | &[data-placement^='left'] > .#{$namespace-prefix}-arrow { 35 | &::before { 36 | border-width: 7px 0 7px 7px; 37 | border-left-color: $color; 38 | } 39 | } 40 | 41 | &[data-placement^='right'] > .#{$namespace-prefix}-arrow { 42 | &::before { 43 | border-width: 7px 7px 7px 0; 44 | border-right-color: $color; 45 | } 46 | } 47 | 48 | > .#{$namespace-prefix}-backdrop { 49 | background-color: $color; 50 | } 51 | 52 | > .#{$namespace-prefix}-svg-arrow { 53 | fill: $color; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/types-internal.ts: -------------------------------------------------------------------------------- 1 | import {State} from '@popperjs/core'; 2 | import {Props} from './types'; 3 | 4 | export interface ListenerObject { 5 | node: Element; 6 | eventType: string; 7 | handler: EventListenerOrEventListenerObject; 8 | options: boolean | Record; 9 | } 10 | 11 | export interface PopperTreeData { 12 | popperRect: ClientRect; 13 | popperState: State; 14 | props: Props; 15 | } 16 | 17 | export interface PopperChildren { 18 | box: HTMLDivElement; 19 | content: HTMLDivElement; 20 | arrow?: HTMLDivElement; 21 | backdrop?: HTMLDivElement; 22 | } 23 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import {BasePlacement, Placement} from './types'; 2 | 3 | export function hasOwnProperty( 4 | obj: Record, 5 | key: string 6 | ): boolean { 7 | return {}.hasOwnProperty.call(obj, key); 8 | } 9 | 10 | export function getValueAtIndexOrReturn( 11 | value: T | [T | null, T | null], 12 | index: number, 13 | defaultValue: T | [T, T] 14 | ): T { 15 | if (Array.isArray(value)) { 16 | const v = value[index]; 17 | return v == null 18 | ? Array.isArray(defaultValue) 19 | ? defaultValue[index] 20 | : defaultValue 21 | : v; 22 | } 23 | 24 | return value; 25 | } 26 | 27 | export function isType(value: any, type: string): boolean { 28 | const str = {}.toString.call(value); 29 | return str.indexOf('[object') === 0 && str.indexOf(`${type}]`) > -1; 30 | } 31 | 32 | export function invokeWithArgsOrReturn(value: any, args: any[]): any { 33 | return typeof value === 'function' ? value(...args) : value; 34 | } 35 | 36 | export function debounce( 37 | fn: (arg: T) => void, 38 | ms: number 39 | ): (arg: T) => void { 40 | // Avoid wrapping in `setTimeout` if ms is 0 anyway 41 | if (ms === 0) { 42 | return fn; 43 | } 44 | 45 | let timeout: any; 46 | 47 | return (arg): void => { 48 | clearTimeout(timeout); 49 | timeout = setTimeout(() => { 50 | fn(arg); 51 | }, ms); 52 | }; 53 | } 54 | 55 | export function removeProperties(obj: T, keys: string[]): Partial { 56 | const clone = {...obj}; 57 | keys.forEach((key) => { 58 | delete (clone as any)[key]; 59 | }); 60 | return clone; 61 | } 62 | 63 | export function splitBySpaces(value: string): string[] { 64 | return value.split(/\s+/).filter(Boolean); 65 | } 66 | 67 | export function normalizeToArray(value: T | T[]): T[] { 68 | return ([] as T[]).concat(value); 69 | } 70 | 71 | export function pushIfUnique(arr: T[], value: T): void { 72 | if (arr.indexOf(value) === -1) { 73 | arr.push(value); 74 | } 75 | } 76 | 77 | export function appendPxIfNumber(value: string | number): string { 78 | return typeof value === 'number' ? `${value}px` : value; 79 | } 80 | 81 | export function unique(arr: T[]): T[] { 82 | return arr.filter((item, index) => arr.indexOf(item) === index); 83 | } 84 | 85 | export function getNumber(value: string | number): number { 86 | return typeof value === 'number' ? value : parseFloat(value); 87 | } 88 | 89 | export function getBasePlacement(placement: Placement): BasePlacement { 90 | return placement.split('-')[0] as BasePlacement; 91 | } 92 | 93 | export function arrayFrom(value: ArrayLike): any[] { 94 | return [].slice.call(value); 95 | } 96 | 97 | export function removeUndefinedProps( 98 | obj: Record 99 | ): Partial> { 100 | return Object.keys(obj).reduce((acc, key) => { 101 | if (obj[key] !== undefined) { 102 | (acc as any)[key] = obj[key]; 103 | } 104 | 105 | return acc; 106 | }, {}); 107 | } 108 | -------------------------------------------------------------------------------- /src/validation.ts: -------------------------------------------------------------------------------- 1 | import {Targets} from './types'; 2 | 3 | export function createMemoryLeakWarning(method: string): string { 4 | const txt = method === 'destroy' ? 'n already-' : ' '; 5 | 6 | return [ 7 | `${method}() was called on a${txt}destroyed instance. This is a no-op but`, 8 | 'indicates a potential memory leak.', 9 | ].join(' '); 10 | } 11 | 12 | export function clean(value: string): string { 13 | const spacesAndTabs = /[ \t]{2,}/g; 14 | const lineStartWithSpaces = /^[ \t]*/gm; 15 | 16 | return value 17 | .replace(spacesAndTabs, ' ') 18 | .replace(lineStartWithSpaces, '') 19 | .trim(); 20 | } 21 | 22 | function getDevMessage(message: string): string { 23 | return clean(` 24 | %ctippy.js 25 | 26 | %c${clean(message)} 27 | 28 | %c👷‍ This is a development-only message. It will be removed in production. 29 | `); 30 | } 31 | 32 | export function getFormattedMessage(message: string): string[] { 33 | return [ 34 | getDevMessage(message), 35 | // title 36 | 'color: #00C584; font-size: 1.3em; font-weight: bold;', 37 | // message 38 | 'line-height: 1.5', 39 | // footer 40 | 'color: #a6a095;', 41 | ]; 42 | } 43 | 44 | // Assume warnings and errors never have the same message 45 | let visitedMessages: Set; 46 | if (__DEV__) { 47 | resetVisitedMessages(); 48 | } 49 | 50 | export function resetVisitedMessages(): void { 51 | visitedMessages = new Set(); 52 | } 53 | 54 | export function warnWhen(condition: boolean, message: string): void { 55 | if (condition && !visitedMessages.has(message)) { 56 | visitedMessages.add(message); 57 | console.warn(...getFormattedMessage(message)); 58 | } 59 | } 60 | 61 | export function errorWhen(condition: boolean, message: string): void { 62 | if (condition && !visitedMessages.has(message)) { 63 | visitedMessages.add(message); 64 | console.error(...getFormattedMessage(message)); 65 | } 66 | } 67 | 68 | export function validateTargets(targets: Targets): void { 69 | const didPassFalsyValue = !targets; 70 | const didPassPlainObject = 71 | Object.prototype.toString.call(targets) === '[object Object]' && 72 | !(targets as any).addEventListener; 73 | 74 | errorWhen( 75 | didPassFalsyValue, 76 | [ 77 | 'tippy() was passed', 78 | '`' + String(targets) + '`', 79 | 'as its targets (first) argument. Valid types are: String, Element,', 80 | 'Element[], or NodeList.', 81 | ].join(' ') 82 | ); 83 | 84 | errorWhen( 85 | didPassPlainObject, 86 | [ 87 | 'tippy() was passed a plain object which is not supported as an argument', 88 | 'for virtual positioning. Use props.getReferenceClientRect instead.', 89 | ].join(' ') 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /test/functional/__image_snapshots__/border-test-js-border-borders-are-correctly-inherited-and-svg-styles-are-correct-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/tippyjs/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/test/functional/__image_snapshots__/border-test-js-border-borders-are-correctly-inherited-and-svg-styles-are-correct-1-snap.png -------------------------------------------------------------------------------- /test/functional/__image_snapshots__/follow-cursor-test-js-follow-cursor-false-does-not-follow-the-cursor-at-all-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/tippyjs/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/test/functional/__image_snapshots__/follow-cursor-test-js-follow-cursor-false-does-not-follow-the-cursor-at-all-1-snap.png -------------------------------------------------------------------------------- /test/functional/__image_snapshots__/follow-cursor-test-js-follow-cursor-horizontal-follows-the-cursor-only-on-the-horizontal-axis-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/tippyjs/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/test/functional/__image_snapshots__/follow-cursor-test-js-follow-cursor-horizontal-follows-the-cursor-only-on-the-horizontal-axis-1-snap.png -------------------------------------------------------------------------------- /test/functional/__image_snapshots__/follow-cursor-test-js-follow-cursor-initial-follows-the-cursor-only-initially-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/tippyjs/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/test/functional/__image_snapshots__/follow-cursor-test-js-follow-cursor-initial-follows-the-cursor-only-initially-1-snap.png -------------------------------------------------------------------------------- /test/functional/__image_snapshots__/follow-cursor-test-js-follow-cursor-stays-at-cursor-when-content-changes-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/tippyjs/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/test/functional/__image_snapshots__/follow-cursor-test-js-follow-cursor-stays-at-cursor-when-content-changes-1-snap.png -------------------------------------------------------------------------------- /test/functional/__image_snapshots__/follow-cursor-test-js-follow-cursor-true-follows-the-cursor-on-both-axes-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/tippyjs/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/test/functional/__image_snapshots__/follow-cursor-test-js-follow-cursor-true-follows-the-cursor-on-both-axes-1-snap.png -------------------------------------------------------------------------------- /test/functional/__image_snapshots__/follow-cursor-test-js-follow-cursor-vertical-follows-the-cursor-only-on-the-vertical-axis-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/tippyjs/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/test/functional/__image_snapshots__/follow-cursor-test-js-follow-cursor-vertical-follows-the-cursor-only-on-the-vertical-axis-1-snap.png -------------------------------------------------------------------------------- /test/functional/__image_snapshots__/inline-positioning-test-js-inline-positioning-aligns-correctly-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/tippyjs/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/test/functional/__image_snapshots__/inline-positioning-test-js-inline-positioning-aligns-correctly-1-snap.png -------------------------------------------------------------------------------- /test/functional/__image_snapshots__/sticky-test-js-sticky-stays-stuck-to-the-reference-element-when-it-moves-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/tippyjs/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/test/functional/__image_snapshots__/sticky-test-js-sticky-stays-stuck-to-the-reference-element-when-it-moves-1-snap.png -------------------------------------------------------------------------------- /test/functional/__image_snapshots__/themes-test-js-themes-all-themes-are-correct-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/tippyjs/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/test/functional/__image_snapshots__/themes-test-js-themes-all-themes-are-correct-1-snap.png -------------------------------------------------------------------------------- /test/functional/border.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment puppeteer 3 | */ 4 | import {navigateToTest, screenshotTest} from '../utils'; 5 | 6 | describe('border', () => { 7 | it('borders are correctly inherited and SVG styles are correct', async () => { 8 | const page = await browser.newPage(); 9 | await page.setViewport({width: 1200, height: 800}); 10 | 11 | await page.goto('http://host.docker.internal:5000'); 12 | await navigateToTest(page, 'border'); 13 | 14 | expect(await screenshotTest(page, 'border')).toMatchImageSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/functional/inlinePositioning.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment puppeteer 3 | */ 4 | import {navigateToTest, screenshotTest} from '../utils'; 5 | 6 | describe('inlinePositioning', () => { 7 | it('aligns correctly', async () => { 8 | const page = await browser.newPage(); 9 | await page.setViewport({width: 1200, height: 800}); 10 | 11 | await page.goto('http://host.docker.internal:5000'); 12 | await navigateToTest(page, 'inlinePositioning'); 13 | 14 | expect( 15 | await screenshotTest(page, 'inlinePositioning') 16 | ).toMatchImageSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/functional/sticky.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment puppeteer 3 | */ 4 | import {navigateToTest, screenshotTest} from '../utils'; 5 | 6 | describe('sticky', () => { 7 | it('stays stuck to the reference element when it moves', async () => { 8 | const page = await browser.newPage(); 9 | await page.setViewport({width: 1200, height: 800}); 10 | 11 | await page.goto('http://host.docker.internal:5000'); 12 | await navigateToTest(page, 'sticky'); 13 | 14 | expect(await screenshotTest(page, 'sticky')).toMatchImageSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/functional/themes.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment puppeteer 3 | */ 4 | import {navigateToTest, screenshotTest} from '../utils'; 5 | 6 | describe('themes', () => { 7 | it('all themes are correct', async () => { 8 | const page = await browser.newPage(); 9 | await page.setViewport({width: 1200, height: 800}); 10 | 11 | await page.goto('http://host.docker.internal:5000'); 12 | await navigateToTest(page, 'themes'); 13 | 14 | expect(await screenshotTest(page, 'themes')).toMatchImageSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/image-reporter.js: -------------------------------------------------------------------------------- 1 | const {red, bold} = require('colorette'); 2 | const fs = require('fs'); 3 | const poster = require('poster'); 4 | 5 | // https://api.anonymousfiles.io/ 6 | 7 | class ImageReporter { 8 | constructor(globalConfig, options) { 9 | this._globalConfig = globalConfig; 10 | this._options = options; 11 | } 12 | 13 | onTestResult(test, testResult, aggregateResults) { 14 | if (process.env.CI !== 'true') { 15 | return; 16 | } 17 | 18 | if ( 19 | testResult.numFailingTests && 20 | testResult.failureMessage.match(/different from snapshot/) 21 | ) { 22 | const files = fs.readdirSync( 23 | './test/functional/__image_snapshots__/__diff_output__/' 24 | ); 25 | files.forEach(async (value) => { 26 | const file = `./test/functional/__image_snapshots__/__diff_output__/${value}`; 27 | 28 | poster.post( 29 | file, 30 | { 31 | uploadUrl: 'https://api.anonymousfiles.io/', 32 | fileId: 'file', 33 | fileContentType: 'image/png', 34 | }, 35 | (err, data) => { 36 | if (err) { 37 | throw err; 38 | } 39 | 40 | console.log( 41 | red(bold(`Uploaded image diff file to ${JSON.parse(data).url}`)) 42 | ); 43 | } 44 | ); 45 | }); 46 | } 47 | } 48 | } 49 | 50 | module.exports = ImageReporter; 51 | -------------------------------------------------------------------------------- /test/integration/__snapshots__/createTippy.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`createTippy returns the instance with expected properties 1`] = ` 4 | Object { 5 | "clearDelayTimeouts": [Function], 6 | "destroy": [Function], 7 | "disable": [Function], 8 | "enable": [Function], 9 | "hide": [Function], 10 | "hideWithInteractivity": [Function], 11 | "id": 1, 12 | "plugins": Array [], 13 | "popper":
18 | , 37 | "popperInstance": null, 38 | "props": Object { 39 | "allowHTML": false, 40 | "animateFill": false, 41 | "animation": "fade", 42 | "appendTo": [Function], 43 | "aria": Object { 44 | "content": "describedby", 45 | "expanded": false, 46 | }, 47 | "arrow": true, 48 | "content": "__DEFAULT_TEST_CONTENT__", 49 | "delay": 0, 50 | "duration": 0, 51 | "followCursor": false, 52 | "getReferenceClientRect": null, 53 | "hideOnClick": true, 54 | "ignoreAttributes": false, 55 | "inertia": false, 56 | "inlinePositioning": false, 57 | "interactive": false, 58 | "interactiveBorder": 2, 59 | "interactiveDebounce": 0, 60 | "maxWidth": 350, 61 | "moveTransition": "", 62 | "offset": Array [ 63 | 0, 64 | 10, 65 | ], 66 | "onAfterUpdate": [Function], 67 | "onBeforeUpdate": [Function], 68 | "onClickOutside": [Function], 69 | "onCreate": [Function], 70 | "onDestroy": [Function], 71 | "onHidden": [Function], 72 | "onHide": [Function], 73 | "onMount": [Function], 74 | "onShow": [Function], 75 | "onShown": [Function], 76 | "onTrigger": [Function], 77 | "onUntrigger": [Function], 78 | "placement": "top", 79 | "plugins": Array [], 80 | "popperOptions": Object {}, 81 | "render": [Function], 82 | "role": "tooltip", 83 | "showOnCreate": false, 84 | "sticky": false, 85 | "theme": "", 86 | "touch": true, 87 | "trigger": "mouseenter focus", 88 | "triggerTarget": null, 89 | "zIndex": 9999, 90 | }, 91 | "reference": 52 | 53 | ); 54 | } 55 | 56 | export default Ajax; 57 | -------------------------------------------------------------------------------- /website/src/components/examples/ContextMenu.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useRef} from 'react'; 2 | import {tippy} from '../Tippy'; 3 | import {css} from '@emotion/core'; 4 | 5 | function ContextMenu() { 6 | const containerRef = useRef(); 7 | 8 | useEffect(() => { 9 | function handleContextMenu(event) { 10 | event.preventDefault(); 11 | 12 | instance.setProps({ 13 | getReferenceClientRect: () => ({ 14 | width: 0, 15 | height: 0, 16 | top: event.clientY, 17 | bottom: event.clientY, 18 | left: event.clientX, 19 | right: event.clientX, 20 | }), 21 | }); 22 | 23 | instance.show(); 24 | } 25 | 26 | function handleScroll() { 27 | instance.hide(); 28 | instance.unmount(); 29 | } 30 | 31 | const container = containerRef.current; 32 | 33 | const instance = tippy(container, { 34 | content: 'Context menu', 35 | offset: [0, 0], 36 | arrow: false, 37 | placement: 'right-start', 38 | interactive: true, 39 | theme: 'light', 40 | trigger: 'manual', 41 | }); 42 | 43 | container.addEventListener('contextmenu', handleContextMenu); 44 | window.addEventListener('scroll', handleScroll); 45 | 46 | return () => { 47 | container.removeEventListener('contextmenu', handleContextMenu); 48 | window.removeEventListener('scroll', handleScroll); 49 | instance.destroy(); 50 | }; 51 | }); 52 | 53 | return ( 54 |
67 | Right click inside 68 |
69 | ); 70 | } 71 | 72 | export default ContextMenu; 73 | -------------------------------------------------------------------------------- /website/src/components/examples/Dropdown.js: -------------------------------------------------------------------------------- 1 | import React, {forwardRef} from 'react'; 2 | import styled from '@emotion/styled'; 3 | import {css} from '@emotion/core'; 4 | import Tippy from '../Tippy'; 5 | import {Button} from '../Framework'; 6 | 7 | const List = styled.div` 8 | margin: 0; 9 | padding-left: 0; 10 | list-style: none; 11 | text-align: left; 12 | `; 13 | 14 | const Reaction = styled.button` 15 | background: none; 16 | border: none; 17 | font-size: 22px; 18 | color: inherit; 19 | transition: transform 0.1s ease-out; 20 | transform: scale(1.0001); 21 | cursor: pointer; 22 | 23 | &:hover, 24 | &:focus { 25 | transform: scale(1.25); 26 | } 27 | `; 28 | 29 | const Text = styled.p` 30 | margin: 6px 0; 31 | color: #777; 32 | `; 33 | 34 | const DropdownTippy = forwardRef((props, ref) => ( 35 | 44 | )); 45 | 46 | function Dropdown({text = 'Dropdown'}) { 47 | return ( 48 | 51 | Pick your reaction 52 |
53 | 54 | 55 | 56 | 👍 57 | 58 | 59 | 60 | 61 | 👎 62 | 63 | 64 | 65 | 66 | ❤️ 67 | 68 | 69 | 70 | 71 | 😂 72 | 73 | 74 | 75 | 76 | 🎉 77 | 78 | 79 | 80 | 81 | } 82 | interactive={true} 83 | arrow={true} 84 | animateFill={false} 85 | offset={[0, 7]} 86 | placement="bottom" 87 | animation="fade" 88 | theme="light-border" 89 | trigger="click" 90 | > 91 | 92 |
93 | ); 94 | } 95 | 96 | export default Dropdown; 97 | -------------------------------------------------------------------------------- /website/src/components/examples/EventDelegation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tippy from '../Tippy'; 3 | import {Button} from '../Framework'; 4 | 5 | function EventDelegation() { 6 | return ( 7 | 8 |
9 | 12 | 19 | 26 |
27 |
28 | ); 29 | } 30 | 31 | export default EventDelegation; 32 | -------------------------------------------------------------------------------- /website/src/components/examples/ImageTransition.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useRef} from 'react'; 2 | import styled from '@emotion/styled'; 3 | import Tippy from '../Tippy'; 4 | import {Button} from '../Framework'; 5 | import TippyTransition from '../TippyTransition'; 6 | 7 | const StyledTippy = styled(Tippy)` 8 | overflow: hidden; 9 | `; 10 | 11 | function DimensionsTransition() { 12 | const [display, setDisplay] = useState('none'); 13 | const [expanded, setExpanded] = useState(false); 14 | const imageContainerRef = useRef(); 15 | 16 | function onClick() { 17 | setExpanded((expanded) => !expanded); 18 | setDisplay((display) => (display === 'none' ? 'block' : 'none')); 19 | } 20 | 21 | function onChange(instance) { 22 | if (!instance.state.isVisible) { 23 | return; 24 | } 25 | 26 | imageContainerRef.current.style.display = 'block'; 27 | } 28 | 29 | return ( 30 | 31 | 34 | 37 |
42 | Starry mountain landscape 52 |
53 | 54 | } 55 | interactive={true} 56 | arrow={false} 57 | trigger="click" 58 | > 59 | 60 |
61 |
62 | ); 63 | } 64 | 65 | export default DimensionsTransition; 66 | -------------------------------------------------------------------------------- /website/src/components/examples/Nesting.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {css} from '@emotion/core'; 3 | import Tippy from '../Tippy'; 4 | import {Button} from '../Framework'; 5 | 6 | const padding = css` 7 | padding: 10px; 8 | `; 9 | 10 | const commonProps = { 11 | onCreate({popper}) { 12 | popper.style.width = 'max-content'; 13 | }, 14 | interactive: true, 15 | theme: 'light-border', 16 | css: padding, 17 | }; 18 | 19 | function Nesting() { 20 | return ( 21 | 36 | 37 | 38 | } 39 | > 40 | 41 |
42 | } 43 | > 44 | 45 | 46 | } 47 | > 48 | 49 | 50 | ); 51 | } 52 | 53 | export default Nesting; 54 | -------------------------------------------------------------------------------- /website/src/components/examples/Singleton.js: -------------------------------------------------------------------------------- 1 | import React, {cloneElement} from 'react'; 2 | import Tippy, {useSingleton} from '../Tippy'; 3 | import {Button} from '../Framework'; 4 | import {Children} from 'react'; 5 | 6 | const array = Array(4).fill(); 7 | 8 | function Singleton({group, transition}) { 9 | const [source, target] = useSingleton(); 10 | const delay = transition ? [100, 500] : 500; 11 | 12 | const children = array.map((_, i) => ( 13 | 14 | 15 | 16 | )); 17 | 18 | const sourceElement = ( 19 | 26 | ); 27 | 28 | if (group) { 29 | return ( 30 | <> 31 | {sourceElement} 32 | {Children.map(children, (child) => 33 | cloneElement(child, {singleton: target}) 34 | )} 35 | 36 | ); 37 | } 38 | 39 | return ( 40 | <> 41 | {sourceElement} 42 | {children} 43 | 44 | ); 45 | } 46 | 47 | export default Singleton; 48 | -------------------------------------------------------------------------------- /website/src/components/examples/TextTransition.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | import styled from '@emotion/styled'; 3 | import Tippy from '../Tippy'; 4 | import {Button} from '../Framework'; 5 | import TippyTransition from '../TippyTransition'; 6 | import {useInstance} from '../../hooks'; 7 | 8 | const StyledTippy = styled(Tippy)` 9 | overflow: hidden; 10 | `; 11 | 12 | const contents = [ 13 | 'Hello there!', 14 | 'This is an example of a simple text transition.', 15 | 'We are using a FLIP library called `react-flip-toolkit`, which allows 60 FPS transitions of element dimensions.', 16 | "The text itself does not transition, just the tippy element's dimensions.", 17 | 'You might want to use an opacity transition for the text itself.', 18 | 'Thanks for reading! (restarting...)', 19 | ]; 20 | 21 | function DimensionsTransition() { 22 | const [content, setContent] = useState(contents[0]); 23 | const component = useInstance({currentIndex: 0}); 24 | 25 | function scheduleNextContent() { 26 | const currentIndex = contents.findIndex((c) => c === content); 27 | const nextIndex = 28 | currentIndex === contents.length - 1 ? 0 : currentIndex + 1; 29 | const nextContent = contents[nextIndex]; 30 | 31 | clearTimeout(component.timeout); 32 | component.timeout = setTimeout(() => { 33 | setContent(nextContent); 34 | scheduleNextContent(); 35 | }, 1000 + contents[currentIndex].length * 50); 36 | } 37 | 38 | useEffect(() => { 39 | if (component.instance.state.isVisible) { 40 | scheduleNextContent(); 41 | } 42 | }); 43 | 44 | function onCreate(instance) { 45 | component.instance = instance; 46 | } 47 | 48 | function onMount() { 49 | scheduleNextContent(); 50 | } 51 | 52 | function onHidden() { 53 | clearTimeout(component.timeout); 54 | } 55 | 56 | return ( 57 | 58 | 67 | 68 | 69 | 70 | ); 71 | } 72 | 73 | export default DimensionsTransition; 74 | -------------------------------------------------------------------------------- /website/src/components/examples/TriggerTarget.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect, useRef} from 'react'; 2 | import Tippy from '../Tippy'; 3 | import styled from '@emotion/styled'; 4 | 5 | const PositioningTarget = styled.span` 6 | background: tomato; 7 | color: white; 8 | padding: 4px 8px; 9 | `; 10 | 11 | function TriggerTarget() { 12 | const [mounted, setMounted] = useState(false); 13 | const ref = useRef(); 14 | 15 | useEffect(() => { 16 | setMounted(true); 17 | }, []); 18 | 19 | return ( 20 |
21 | Trigger target vs{' '} 22 | {mounted && ( 23 | 24 | positioning target 25 | 26 | )} 27 |
28 | ); 29 | } 30 | 31 | export default TriggerTarget; 32 | -------------------------------------------------------------------------------- /website/src/components/examples/mouseRestPlugin.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'mouseRest', 3 | defaultValue: false, 4 | fn(instance) { 5 | const {reference} = instance; 6 | const DEBOUNCE_MS = 80; 7 | 8 | let timeout; 9 | 10 | // If the `trigger` isn't `"mouseenter"`, then this plugin doesn't apply. 11 | function getIsEnabled() { 12 | return ( 13 | instance.props.mouseRest && 14 | instance.props.trigger.indexOf('mouseenter') !== -1 15 | ); 16 | } 17 | 18 | return { 19 | onCreate() { 20 | if (!getIsEnabled()) { 21 | return; 22 | } 23 | 24 | const triggerWithoutMouseEnter = instance.props.trigger 25 | .replace('mouseenter', '') 26 | .trim(); 27 | 28 | instance.setProps({trigger: triggerWithoutMouseEnter}); 29 | 30 | reference.addEventListener('mousemove', () => { 31 | clearTimeout(timeout); 32 | timeout = setTimeout(() => instance.show(), DEBOUNCE_MS); 33 | }); 34 | 35 | reference.addEventListener('mouseleave', () => { 36 | clearTimeout(timeout); 37 | instance.hide(); 38 | }); 39 | }, 40 | onDestroy() { 41 | clearTimeout(timeout); 42 | }, 43 | }; 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /website/src/css/theme.js: -------------------------------------------------------------------------------- 1 | export default { 2 | border: 'rgba(0, 32, 128, 0.12)', 3 | gradient: 'linear-gradient(135deg, #00acff, #6f99fc) no-repeat', 4 | }; 5 | -------------------------------------------------------------------------------- /website/src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/tippyjs/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/website/src/favicon.png -------------------------------------------------------------------------------- /website/src/hooks/index.js: -------------------------------------------------------------------------------- 1 | import {useState} from 'react'; 2 | 3 | export function useInstance(initialValue = {}) { 4 | return useState(initialValue)[0]; 5 | } 6 | -------------------------------------------------------------------------------- /website/src/images/brain.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /website/src/images/browser-devtools-tippy-element.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/tippyjs/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/website/src/images/browser-devtools-tippy-element.jpg -------------------------------------------------------------------------------- /website/src/images/browser.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 10 | 13 | 17 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /website/src/images/gatsby-astronaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/tippyjs/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/website/src/images/gatsby-astronaut.png -------------------------------------------------------------------------------- /website/src/images/gatsby-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomiks/tippyjs/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/website/src/images/gatsby-icon.png -------------------------------------------------------------------------------- /website/src/images/lightning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /website/src/images/plugin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/src/images/pointer.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /website/src/images/render.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/src/images/typescript.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /website/src/images/wheelchair.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 9 | 10 | 11 | 16 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /website/src/pages/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "bracketSpacing": false, 4 | "proseWrap": "always" 5 | } 6 | -------------------------------------------------------------------------------- /website/src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from '../components/Layout'; 3 | import SEO from '../components/SEO'; 4 | 5 | function NotFoundPage({pageContext}) { 6 | const context = {...pageContext, frontmatter: {title: '404: Not Found'}}; 7 | return ( 8 | 9 | 10 |

Unfortunately, the page you were looking for does not exist.

11 |
12 | ); 13 | } 14 | 15 | export default NotFoundPage; 16 | -------------------------------------------------------------------------------- /website/src/pages/v5/accessibility.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Accessibility 3 | path: /v5/accessibility/ 4 | index: 12 5 | --- 6 | 7 | Tooltip and popovers are usually not mouse-only UI elements. If vital 8 | functionality or information is contained within them, they should be accessible 9 | to keyboard and touch inputs so that users who navigate interfaces without using 10 | a mouse are not locked out. This is especially true for people with disabilities 11 | such as low vision who rely on screen reader technology to assist them with 12 | using an application. 13 | 14 | To ensure these users get the best possible experience, Tippy already employs 15 | baked-in defaults to ensure accessibility. However, some special consideration 16 | should be taken into account when using the library so you can be aware of 17 | potential accessibility problems that may arise. 18 | 19 | ### Use natively focusable elements 20 | 21 | Tooltips should only be applied to natively focusable elements like ` 39 | 42 | ``` 43 | 44 | This allows screen reader software to announce the tooltip content describing 45 | the reference element once it's in focus. 46 | 47 | ### Interactivity 48 | 49 | Tippy uses two techniques to ensure interactive popovers are accessible: 50 | 51 | - `aria-expanded` attribute 52 | - `appendTo: "parent"` 53 | 54 | This means once the reference element has focus, the assistive technology will 55 | let the user know it has an expandable popover attached to it. 56 | 57 | Once they open it, elements within the tippy can be tabbed to immediately once 58 | focus has left the reference element. This relies on there being no more 59 | focusable sibling elements after the reference element itself. 60 | 61 | Before opening the popover: 62 | 63 | ```html 64 |
65 | 66 |
67 | ``` 68 | 69 | After opening the popover: 70 | 71 | ```html 72 |
73 | 74 | 77 |
78 | ``` 79 | 80 | You should wrap the reference element in its own parent element (`
` or 81 | ``) if it's not the only focusable sibling element. 82 | 83 | #### Clipping issues 84 | 85 | Sometimes, this behavior won't work for your app due to clipping issues. In this 86 | case, you need to specify a custom `appendTo` element outside of the parent node 87 | context and use a focus management solution to handle keyboard navigation. 88 | [More details here](../faq/#my-tooltip-appears-cut-off-or-is-not-showing-at-all). 89 | -------------------------------------------------------------------------------- /website/src/pages/v5/animations.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Animations 3 | path: /v5/animations/ 4 | index: 7 5 | --- 6 | 7 | import ImageTransition from '../../components/examples/ImageTransition'; 8 | import TextTransition from '../../components/examples/TextTransition'; 9 | 10 | Tippies use an opacity `fade` transition by default. 11 | 12 | ### Included animations 13 | 14 | The package comes with extra animations for you to use: 15 | 16 | - `shift-away` 17 | - `shift-toward` 18 | - `scale` 19 | - `perspective` 20 | 21 | They need to be imported separately. 22 | 23 | ```js 24 | import 'tippy.js/animations/scale.css'; 25 | ``` 26 | 27 | Pass the animation name as the `animation` prop: 28 | 29 | ```js 30 | tippy('button', { 31 | animation: 'scale', 32 | }); 33 | ``` 34 | 35 | Each of these animations also has 3 variants (normal, subtle, and extreme) using 36 | the following format: 37 | 38 | ```js 39 | import 'tippy.js/animations/scale.css'; 40 | import 'tippy.js/animations/scale-subtle.css'; 41 | import 'tippy.js/animations/scale-extreme.css'; 42 | ``` 43 | 44 | ### Custom animations 45 | 46 | To create your own animation: 47 | 48 | - Specify the animation name in the `[data-animation]` attribute selector 49 | - Target the visibility state of the tippy: `[data-state="hidden"]` or 50 | `[data-state="visible"]` 51 | - Depending on the animation, target the placement of the tippy too: e.g. 52 | `[data-placement^="top"]` 53 | 54 | ```css 55 | .tippy-tooltip[data-animation='rotate'][data-state='hidden'] { 56 | opacity: 0; 57 | transform: rotate(90deg); 58 | } 59 | ``` 60 | 61 | ```js 62 | tippy('button', { 63 | animation: 'rotate', 64 | }); 65 | ``` 66 | 67 | ### Inertia 68 | 69 | There's a prop named `inertia` that adds an elastic inertial effect to the 70 | tippy, which is a limited CSS-only way to mimic spring physics. 71 | 72 | ```js 73 | tippy('button', { 74 | inertia: true, 75 | }); 76 | ``` 77 | 78 | You can customize this prop in your CSS: 79 | 80 | ```css 81 | .tippy-tooltip[data-inertia][data-state='visible'] { 82 | transition-timing-function: cubic-bezier(...); 83 | } 84 | ``` 85 | 86 | ### Material filling effect 87 | 88 | Import the `animateFill` plugin, plus `dist/backdrop.css` & 89 | `animations/shift-away.css` stylesheets. 90 | 91 | ```js 92 | import tippy, {animateFill} from 'tippy.js'; 93 | import 'tippy.js/dist/backdrop.css'; 94 | import 'tippy.js/animations/shift-away.css'; 95 | 96 | tippy(targets, { 97 | animateFill: true, 98 | plugins: [animateFill], 99 | }); 100 | ``` 101 | 102 | ### CSS animations 103 | 104 | Maybe plain transitions aren't enough for your use case. You can also use CSS 105 | animations (e.g. `animate.css`): 106 | 107 | ```js 108 | tippy('button', { 109 | onMount(instance) { 110 | const {tooltip} = instance.popperChildren; 111 | requestAnimationFrame(() => { 112 | tooltip.classList.add('animated'); 113 | tooltip.classList.add('wobble'); 114 | }); 115 | }, 116 | onHidden(instance) { 117 | const {tooltip} = instance.popperChildren; 118 | tooltip.classList.remove('animated'); 119 | tooltip.classList.remove('wobble'); 120 | }, 121 | }); 122 | ``` 123 | 124 | You can also use `@keyframes` and add the `animation` property to your animation 125 | selector too. 126 | 127 | ### Dimensions transition 128 | 129 | While a tippy is showing, the content inside of it may change. How do you 130 | smoothly transition its dimensions? By default, it instantly changes size when 131 | the content is updated. It turns out this is quite complex to do, but possible. 132 | 133 | #### Partially dynamic 134 | 135 | View the [CodePen demo](https://codepen.io/atomiks/pen/LgjMbW). 136 | -------------------------------------------------------------------------------- /website/src/pages/v5/creating-tooltips.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Creating Tooltips 3 | path: /v5/creating-tooltips/ 4 | index: 2 5 | --- 6 | 7 | Give elements you would like to give tooltips to a `data-tippy-content` 8 | attribute: 9 | 10 | ```html 11 | 12 | 13 | ``` 14 | 15 | To give them a tippy tooltip, call the `tippy()` function with a CSS selector 16 | matching these elements: 17 | 18 | ```js 19 | tippy('[data-tippy-content]'); 20 | ``` 21 | 22 | The `data-tippy-content` attribute allows you to give different tooltip content 23 | to many different elements, while only needing to initialize once. 24 | 25 | If targeting a single element, you can use the `content` prop instead of the 26 | attribute: 27 | 28 | ```js 29 | tippy('#singleElement', { 30 | content: 'Tooltip', 31 | }); 32 | ``` 33 | 34 | Tippy will create tooltips for elements even if you forget to give them content, 35 | which creates an odd shape with no content, so ensure your CSS selector is 36 | specific enough to guarantee their content. 37 | 38 | ### Content types 39 | 40 | Plain text and HTML (string or element) are allowed. 41 | 42 | If you're passing unknown user data to `content`, disable HTML for safety, 43 | unless explicitly sanitizing it: 44 | 45 | ```js 46 | tippy('#singleElement', { 47 | content: unsafeUserData, 48 | allowHTML: false, 49 | }); 50 | ``` 51 | 52 | ### Target types 53 | 54 | The first argument you pass to `tippy()` is the targets you want to give 55 | tooltips to. This can represent one or many different elements. 56 | 57 | ```js 58 | // String (CSS selector matching elements on the document) 59 | tippy('#id'); 60 | tippy('.class'); 61 | tippy('[data-tippy-content]'); 62 | 63 | // Element 64 | tippy(document.getElementById('my-element')); 65 | 66 | // Element[] 67 | tippy([element1, element2, element3]); 68 | 69 | // NodeList 70 | tippy(document.querySelectorAll('.my-elements')); 71 | ``` 72 | 73 | ### Disabled elements 74 | 75 | If an element is disabled, you will need to use a wrapper element (`` or 76 | `
`) in order for the tippy to work. Elements with the disabled attribute 77 | aren't interactive, meaning users cannot focus, hover, or click them to trigger 78 | a tippy. 79 | 80 | 81 | ```html 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | ``` 90 | 91 | Please note that this has accessibility concerns and should be avoided if 92 | possible. 93 | 94 | ### SVG in IE11 95 | 96 | If you need to support SVG elements in IE11, you will need to include a polyfill 97 | for `SVGElement.prototype.contains`. 98 | 99 | The polyfill is small: 100 | 101 | ```js 102 | if (!SVGElement.prototype.contains) { 103 | SVGElement.prototype.contains = HTMLDivElement.prototype.contains; 104 | } 105 | ``` 106 | -------------------------------------------------------------------------------- /website/src/pages/v5/customizing-tooltips.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Customizing Tooltips 3 | path: /v5/customizing-tooltips/ 4 | index: 3 5 | --- 6 | 7 | As seen in the demo, the `tippy()` function takes an object of optional props as 8 | a second argument to customize the tooltips being created: 9 | 10 | ```js 11 | tippy('button', { 12 | duration: 0, 13 | arrow: false, 14 | delay: [1000, 200], 15 | }); 16 | ``` 17 | 18 | You can also specify props on the element using data attributes: 19 | 20 | ```html 21 | 28 | ``` 29 | 30 | Note that only JSON values are valid in attributes. 31 | 32 | It can be useful to use the function for "global" config and choose attributes 33 | for individual config here and there: 34 | 35 | ```html 36 | 37 | 38 | 39 | ``` 40 | 41 | ```js 42 | // Global config for all