Repository: atomiks/tippyjs Branch: master Commit: ad85f6feb79c Files: 199 Total size: 532.8 KB Directory structure: gitextract_flaxrdjs/ ├── .config/ │ ├── .prettierignore │ ├── babel.config.js │ ├── eslint.config.js │ ├── jest-puppeteer.config.js │ ├── jest.config.js │ └── rollup.config.js ├── .editorconfig ├── .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 ├── 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/ │ │ ├── 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 ├── hooks/ │ └── index.js ├── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .config/.prettierignore ================================================ ../build ../dist ../headless ../website/public ../website/.cache ../animations ../themes ================================================ FILE: .config/babel.config.js ================================================ module.exports = { presets: [ ['@babel/env', {loose: true, useBuiltIns: 'entry', corejs: 3}], '@babel/typescript', ], plugins: ['dev-expression'], env: { test: { presets: [ ['@babel/env', {targets: {node: 'current'}}], '@babel/typescript', ], }, }, }; ================================================ FILE: .config/eslint.config.js ================================================ module.exports = { env: { browser: true, node: true, jest: true, es6: true, }, globals: { __DEV__: true, }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier', 'prettier/@typescript-eslint', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', parserOptions: { project: './tsconfig.json', }, rules: { 'no-prototype-builtins': 'off', '@typescript-eslint/no-use-before-define': ['error', {functions: false}], '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/array-type': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/ban-ts-ignore': 'off', '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', }, ignorePatterns: [ 'node_modules', 'build', 'animations', 'themes', 'test', 'headless', 'website', 'dist', 'coverage', ], }; ================================================ FILE: .config/jest-puppeteer.config.js ================================================ require('dotenv').config(); const getConfig = require('jest-puppeteer-docker/lib/config'); const baseConfig = getConfig(); module.exports = { browser: 'chromium', launch: { dumpio: false, headless: process.env.HEADLESS !== 'false', args: ['--no-sandbox', '--disable-setuid-sandbox'], }, server: { command: 'yarn build:visual && yarn serve', port: 5000, launchTimeout: 20000, }, ...baseConfig, }; ================================================ FILE: .config/jest.config.js ================================================ // https://github.com/smooth-code/jest-puppeteer/issues/160#issuecomment-491975158 process.env.JEST_PUPPETEER_CONFIG = require.resolve( './jest-puppeteer.config.js' ); const jestPuppeteerDocker = require('jest-puppeteer-docker/jest-preset'); module.exports = { testMatch: ['/test/**/*.test.js'], testTimeout: 30000, globals: { __DEV__: true, }, setupFiles: ['dotenv/config'], reporters: ['default', require.resolve('../test/image-reporter.js')], ...jestPuppeteerDocker, testEnvironment: 'jest-environment-jsdom-fourteen', setupFilesAfterEnv: [ require.resolve('../test/setup.js'), ...jestPuppeteerDocker.setupFilesAfterEnv, ], }; ================================================ FILE: .config/rollup.config.js ================================================ import babel from 'rollup-plugin-babel'; import json from 'rollup-plugin-json'; import resolve from 'rollup-plugin-node-resolve'; import cssOnly from 'rollup-plugin-css-only'; import replace from 'rollup-plugin-replace'; import sass from 'rollup-plugin-sass'; import serve from 'rollup-plugin-serve'; import livereload from 'rollup-plugin-livereload'; import {terser} from 'rollup-plugin-terser'; import pkg from '../package.json'; const NAMESPACE_PREFIX = process.env.NAMESPACE || 'tippy'; const plugins = { babel: babel({extensions: ['.js', '.ts']}), replaceNamespace: replace({ __NAMESPACE_PREFIX__: NAMESPACE_PREFIX, }), replaceEnvProduction: replace({ 'process.env.NODE_ENV': JSON.stringify('production'), }), replaceEnvDevelopment: replace({ 'process.env.NODE_ENV': JSON.stringify('development'), }), minify: terser(), resolve: resolve({extensions: ['.js', '.ts']}), css: cssOnly({output: false}), json: json(), }; const prodCommonPlugins = [ plugins.replaceNamespace, plugins.resolve, plugins.json, ]; const pluginConfigs = { base: [plugins.babel, ...prodCommonPlugins], bundle: [plugins.babel, ...prodCommonPlugins, plugins.css], umdBase: [plugins.babel, plugins.replaceEnvDevelopment, ...prodCommonPlugins], umdBaseMin: [ plugins.babel, plugins.replaceEnvProduction, ...prodCommonPlugins, plugins.minify, ], umdBundle: [ plugins.babel, plugins.replaceEnvDevelopment, ...prodCommonPlugins, plugins.css, ], umdBundleMin: [ plugins.babel, plugins.replaceEnvProduction, ...prodCommonPlugins, plugins.minify, plugins.css, ], }; const banner = `/**! * tippy.js v${pkg.version} * (c) 2017-${new Date().getFullYear()} atomiks * MIT License */`; const commonUMDOutputOptions = { globals: {'@popperjs/core': 'Popper'}, format: 'umd', name: 'tippy', sourcemap: true, }; const prodConfig = [ { input: 'build/base-umd.js', plugins: pluginConfigs.umdBase, external: ['@popperjs/core'], output: { ...commonUMDOutputOptions, file: 'dist/tippy.umd.js', banner, }, }, { input: 'build/bundle-umd.js', plugins: pluginConfigs.umdBundle, external: ['@popperjs/core'], output: { ...commonUMDOutputOptions, file: 'dist/tippy-bundle.umd.js', banner, }, }, { input: 'build/base-umd.js', plugins: pluginConfigs.umdBaseMin, external: ['@popperjs/core'], output: { ...commonUMDOutputOptions, file: 'dist/tippy.umd.min.js', }, }, { input: 'build/bundle-umd.js', plugins: pluginConfigs.umdBundleMin, external: ['@popperjs/core'], output: { ...commonUMDOutputOptions, file: 'dist/tippy-bundle.umd.min.js', }, }, { input: 'build/base.js', plugins: pluginConfigs.bundle, external: ['@popperjs/core'], output: { file: 'dist/tippy.esm.js', format: 'esm', banner, sourcemap: true, }, }, { input: 'build/headless.js', plugins: pluginConfigs.base, external: ['@popperjs/core'], output: { file: 'headless/dist/tippy-headless.esm.js', format: 'esm', banner, sourcemap: true, }, }, { input: 'build/base.js', plugins: pluginConfigs.bundle, external: ['@popperjs/core'], output: { file: 'dist/tippy.cjs.js', format: 'cjs', exports: 'named', banner, sourcemap: true, }, }, { input: 'build/headless.js', plugins: pluginConfigs.base, external: ['@popperjs/core'], output: { file: 'headless/dist/tippy-headless.cjs.js', format: 'cjs', exports: 'named', banner, sourcemap: true, }, }, { input: 'build/headless-umd.js', plugins: pluginConfigs.umdBase, external: ['@popperjs/core'], output: { ...commonUMDOutputOptions, file: 'headless/dist/tippy-headless.umd.js', }, }, { input: 'build/headless-umd.js', plugins: pluginConfigs.umdBaseMin, external: ['@popperjs/core'], output: { ...commonUMDOutputOptions, file: 'headless/dist/tippy-headless.umd.min.js', }, }, ]; // Calling the `serve()` plugin causes the process to hang, so we need to delay // its evaluation const configs = { dev: () => ({ input: 'test/visual/tests.js', plugins: [ plugins.babel, plugins.json, plugins.resolve, replace({__DEV__: 'true'}), plugins.replaceEnvDevelopment, sass({output: true}), serve({ contentBase: 'test/visual', port: 1234, }), livereload(), ], output: { file: 'test/visual/dist/bundle.js', format: 'iife', }, }), test: () => ({ input: 'test/visual/tests.js', plugins: [ plugins.babel, plugins.json, plugins.resolve, replace({__DEV__: 'true'}), plugins.replaceEnvDevelopment, sass({output: true}), ], output: { file: 'test/visual/dist/bundle.js', format: 'iife', }, }), }; const func = configs[process.env.NODE_ENV]; export default func ? func() : prodConfig; ================================================ FILE: .editorconfig ================================================ # https://editorconfig.org root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true indent_style = space indent_size = 2 ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [atomiks] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: 🐞 Bug report about: Something is broken title: '' labels: "\U0001F41B bug, \U0001F6A7 unconfirmed" assignees: '' --- ## Bug description A clear and concise description of what the bug is. ## Reproduction CodePen link: https://codepen.io/atomiks/pen/yvwQyZ ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: 👋 Stack Overflow url: https://stackoverflow.com/questions/tagged/tippyjs about: Having trouble with Tippy? Try asking on Stack Overflow! - name: 💬 Discussions url: https://github.com/atomiks/tippyjs/discussions about: Talk with others about Tippy, its usage and future! ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: ✨ Feature request about: Suggest a feature title: '' labels: "\U0001F48E enhancement" assignees: '' --- ## Problem A clear and concise description of what the problem is. ## Solution A clear and concise description of what you want to happen. ================================================ FILE: .github/workflows/cd.yml ================================================ name: CD on: [push] env: CI: true jobs: publish: if: ${{ startsWith(github.event.commits[0].message, 'docs:') || startsWith(github.event.commits[0].message, 'release:') }} runs-on: ubuntu-latest defaults: run: working-directory: ./website steps: - name: Checkout uses: actions/checkout@v2 - name: Build run: | npm install npm run clean npm run build:ci - name: Deploy to GitHub Pages uses: crazy-max/ghaction-github-pages@v2 with: target_branch: gh-pages build_dir: ./website/public env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: [push, pull_request] env: CI: true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true jobs: checks: name: Linting and Type checking runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 - run: npm install - run: npm run lint - run: npm run test:types dom-tests: name: Unit and Integration runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 - uses: mujo-code/puppeteer-headful@master - run: npm install - run: npm run test:dom functional-tests: name: Chromium Functional runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 - uses: mujo-code/puppeteer-headful@master - run: npm install - run: npm run test:functional env: PUPPETEER_BROWSER: chromium ================================================ FILE: .gitignore ================================================ .DS_Store coverage/ .devserver/ node_modules/ dist/ /themes /animations index.d.ts ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017-present atomiks Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MIGRATION_GUIDE.md ================================================ # Migration Guide - [5.x to 6.x](#5x-to-6x) - [4.x to 5.x](#4x-to-5x) --- # 5.x to 6.x Popper.js was updated to v2. The `instance.popperInstance` and `popperOptions` APIs have changed. You can [view its documentation here](https://popper.js.org/docs/v2/). ## Imports `iife` was replaced with `umd` (Rollup chunking has been removed). ## HTML Content To protect against XSS by default, `allowHTML` is now `false` by default. If you're passing strings of HTML to `content`, you must set `allowHTML: true`. ## Themes ### If you were creating custom themes
View details `.tippy-tooltip` has become `.tippy-box`, and theming is done via an attribute now, to match the other props. The following: ```css .tippy-tooltip.tomato-theme { background-color: tomato; } ``` Has become: ```css .tippy-box[data-theme~='tomato'] { background-color: tomato; } ``` > The `~=` attribute operator allows you to specify mutliple theme names like > before with class names. For `.tippy-arrow`, you'll need to specify its color on the `::before` pseudo-element. The following: ```css .tippy-tooltip.tomato-theme[data-placement^='top'] > .tippy-arrow { border-top-color: tomato; } ``` Has become: ```css .tippy-box[data-theme~='tomato'][data-placement^='top'] > .tippy-arrow::before { border-top-color: tomato; } ``` In addition, if you were altering the pixel values, it may need to be adjusted. In addition, Popper 2 changed some attribute names. This: ```css .tippy-tooltip[data-out-of-boundaries] { visibility: hidden; } ``` Has become: ```css .tippy-box[data-reference-hidden] { visibility: hidden; } ```
### If you were targeting `.tippy-popper`
View details `.tippy-popper` is no longer a selector, and is considered an implementation detail for the most part now. `[data-tippy-root]` attribute selector replaces it if necessary.
## Instance ### If you were using `instance.popperChildren`
View details This no longer exists due to the user's ability to specify any structured DOM with `render()` (Headless Tippy). To access the `.tippy-box` element with the default render function (`.tippy-tooltip` in v5): ```js const box = instance.popper.firstElementChild; ```
## Methods ### If you were using `.show()` or `.hide()` with a duration argument
View details These no longer take a duration argument. Instead, use `.setProps({duration: ...})` before calling them if necessary. To replicate `.hide(0)`: ```js instance.unmount(); ```
## Props ### If you were using `boundary`
View details Often, this was to solve a problem in Popper 1, where you set `boundary: "window"`. This is no longer necessary. If you'd like to change it anyway, you can set it in `popperOptions`: ```js tippy(targets, { popperOptions: { modifiers: [ { name: 'preventOverflow', options: { // equivalent to boundary: 'window' in v1, usually NOT necessary in v2 rootBoundary: 'document', }, }, ], }, }); ```
### If you were using `distance` or `offset`
View details These have been merged into a single `offset` prop, to match Popper 2's new API. The following: ```js tippy(targets, { offset: 5, distance: 10, }); ``` Has become: ```js tippy(targets, { offset: [5, 10], }); ``` This tuple also directly replaces `offset: "5, 10"`.
### If you were using `flip`, `flipBehavior`, or `flipOnUpdate`
View details All of these have been removed. To configure these, specify them in `popperOptions`: ```js tippy(targets, { placement: 'bottom', popperOptions: { modifiers: [ { name: 'flip', // flip: false enabled: false, options: { // flipBehavior: ['bottom', 'right', 'top'] fallbackPlacements: ['right', 'top'], }, }, ], }, }); ``` `flipOnUpdate` has no replacement yet. It's always `true`.
### If you were using `aria`
View details This has become an object to allow for better configurability. By default Tippy will infer what to use (`auto`), but this can be overridden. Types: ```ts interface Props { // ... aria: { content?: 'auto' | 'describedby' | 'labelledby' | null; expanded?: 'auto' | boolean; }; } ``` ```js tippy(targets, { aria: { // `null` when interactive is enabled content: 'auto', // `aria-*` attribute // `true` when interactive is enabled expanded: 'auto', // `aria-expanded` attribute }, }); ```
### If you were using `multiple` or relying on its behavior
View details Due to static typing issues, it's been removed. Calling `tippy()` again on the same element will now always create a new tippy for it. Avoid calling `tippy()` multiple times on the same reference if you don't want multiple tippies created for it.
### If you were using `updateDuration`
View details It's now `moveTransition`, which is a whole transition string. This allows you to specify the easing function. ```js tippy(targets, { moveTransition: 'transform 0.2s ease-out', }); ```
### If you were using `lazy`
View details It's been removed. The `popperInstance` is now created and destroyed on mount/unmount. If you were using this for ReferenceObjects, see below. The following: ```js tippy(targets, { lazy: false, onCreate(instance) { instance.popperInstance.reference = { clientWidth: 0, clientHeight: 0, getBoundingClientRect() { return { // ... }; }, }; }, }); ``` Has become a single prop: ```js tippy(targets, { getReferenceClientRect: () => ({ // ... }), }); ``` This implements Popper 2's [Virtual Elements API](https://popper.js.org/docs/v2/virtual-elements/).
## IE11 IE11 is not supported by default anymore, but can be polyfilled. View the Browser Support page on the documentation for details. --- # 4.x to 5.x ### Node Make sure you have DEV warnings enabled by setting `NODE_ENV=development` and ensuring your bundler replaces `process.env.NODE_ENV` with the string `"development"`. - **webpack**: via `mode` option - **Rollup**: via `rollup-plugin-replace` - **Parcel**: automatic - **Browserify/Gulp/Grunt/others**: [View details](https://vuejs.org/v2/guide/deployment.html#With-Build-Tools) ### Browser ```html ``` ## Imports Previously, the default import injected the CSS stylesheet into ``: ```js import tippy from 'tippy.js'; ``` In v5, this import is now side-effect free to work better with dependencies when users have CSP enabled or using frameworks that control the ``. You should import the CSS separately: ```js import tippy from 'tippy.js'; import 'tippy.js/dist/tippy.css'; ``` You can however opt-in to use the injected CSS version, just like v4: ```js // Just like v4 import tippy from 'tippy.js/dist/tippy-bundle.esm'; // Or CommonJS: const tippy = require('tippy.js/dist/tippy-bundle.cjs'); ``` ## `data-tippy` attribute This technique of auto initialization was removed to keep the import side-effect free. Initializing via the `tippy()` constructor is required. ## Animations ### If you want the `animateFill` effect back (it's no longer default)
View details Node: ```js import tippy, {animateFill} from 'tippy.js'; import 'tippy.js/dist/tippy.css'; // These stylesheets are required for it to work import 'tippy.js/dist/backdrop.css'; import 'tippy.js/animations/shift-away.css'; tippy(targets, { animateFill: true, plugins: [animateFill], }); ``` Browser: ```html ```
### If you were using default animations or creating custom animations
View details - Make sure your `visible` state has no translation (of 0px, instead of 10px like before). - `shift-away`, `shift-toward`, `scale` and `perspective` need to be imported separately now. Node: ```js import 'tippy.js/animations/scale.css'; ``` Browser: ```html ```
## Props ### If you were using `interactive: true`
View details When using `interactive: true`, the tippy may be invisible or appear cut off if your reference element is in a container with: - `position` (e.g. fixed, absolute, sticky) - `overflow: hidden` To fix add the following prop (recommended): ```js tippy(reference, { // ... popperOptions: { positionFixed: true, }, }); ``` Or, if the above causes issues: ```js tippy(reference, { // ... appendTo: document.body, }); ``` ⚠️ For the latter, you need to be employing focus mangement for accessibility.
### If you were using `arrowType: 'round'`
View details Import the `svg-arrow` CSS, and the `roundArrow` string, and use the `arrow` prop instead. Node: ```js import {roundArrow} from 'tippy.js'; import 'tippy.js/dist/svg-arrow.css'; tippy(targets, { arrow: roundArrow, }); ``` Browser: ```html ```
### If you were using `followCursor`
View details Node: ```js import tippy, {followCursor} from 'tippy.js'; tippy('button', { followCursor: true, plugins: [followCursor], }); ``` Browser: (Works as before.)
### If you were using `sticky`
View details Node: ```js import tippy, {sticky} from 'tippy.js'; tippy('button', { sticky: true, plugins: [sticky], }); ``` Browser: (Works as before.)
### If you were using `target`
View details Use `delegate()`. Node: ```js import tippy, {delegate} from 'tippy.js'; delegate('#parent', {target: 'button'}); ``` Browser: ```html ```
### If you were using `showOnInit`
View details It's now named `showOnCreate`, to match the `onCreate` lifecycle hook
### If you were using `size`
View details It's been removed, as it's more flexible to just use a theme and specify the `font-size` and `padding` properties.
### If you were using `touchHold`
View details Use `touch: "hold"` instead.
### If you were using `a11y`
View details Ensure non-focusable elements have `tabindex="0"` added to them. Otherwise, use natively focusable elements everywhere possible.
### If you were using `wait`
View details Use the `onTrigger` and `onUntrigger` lifecycles and temporarily disable the instance. ```js tippy(targets, { onTrigger(instance) { instance.disable(); // Make your async call... // Once finished: instance.enable(); instance.show(); }, onUntrigger(instance) { // Re-enable the instance here depending on the async cancellation logic instance.enable(); }, }); ```
## Instances ### If you were using `instance.set()`
View details ```diff - instance.set({}); + instance.setProps({}); ```
## Static methods ### If you were using `tippy.setDefaults()`
View details ```diff - tippy.defaults; + tippy.defaultProps; ``` ```diff - tippy.setDefaults({}); + tippy.setDefaultProps({}); ```
### If you were using `tippy.hideAll()`
View details In ESM/CJS contexts, it's no longer attached to `tippy` Node: ```js import {hideAll} from 'tippy.js'; hideAll(); ``` Browser: (Works as before.)
### If you were using `tippy.group()`
View details Use `createSingleton()`. Node: ```js import tippy, {createSingleton} from 'tippy.js'; createSingleton(tippy('button'), {delay: 1000}); ``` Browser: ```html ```
## Themes ### If you were using the included themes
View details - `google` is now `material`
### If you were creating custom themes
View details - `[x-placement]` attribute is now `[data-placement]` - `[x-out-of-boundaries]` is now `[data-out-of-boundaries]` - `.tippy-roundarrow` is now `.tippy-svg-arrow` - `.tippy-tooltip` no longer has `padding` on it, rather the `.tippy-content` selector does. - `.tippy-tooltip` no longer has `text-align: center`
## Other ### If you were using virtual reference objects
View details Set `instance.popperInstance.reference = ReferenceObject` in the `onTrigger` lifecycle, or `onCreate` with `lazy: false`.
## Types
View details - `Props` is not `Partial` anymore, it's `Required` - `Options` removed (use `Partial`) - `BasicPlacement` renamed to `BasePlacement`
================================================ FILE: README.md ================================================
Tippy.js logo

Tippy.js

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

npm Downloads per Month MIT License

## Demo and Documentation ➡️ **[View the latest demo & docs here](https://atomiks.github.io/tippyjs/)** [Migration Guide](https://github.com/atomiks/tippyjs/blob/master/MIGRATION_GUIDE.md) ## Installation ### Package Managers ```bash # npm npm i tippy.js # Yarn yarn add tippy.js ``` Import the `tippy` constructor and the core CSS: ```js import tippy from 'tippy.js'; import 'tippy.js/dist/tippy.css'; ``` ### CDN ```html ``` The core CSS comes bundled with the default unpkg import. ## Usage For detailed usage information, [visit the docs](https://atomiks.github.io/tippyjs/v6/getting-started/). ## Component Wrappers - React: [@tippyjs/react](https://github.com/atomiks/tippyjs-react) (official) - Ember: [ember-tippy](https://github.com/nag5000/ember-tippy) (unofficial) ## License MIT ================================================ FILE: build/animations/perspective-extreme.js ================================================ import '../../src/scss/animations/perspective-extreme.scss'; ================================================ FILE: build/animations/perspective-subtle.js ================================================ import '../../src/scss/animations/perspective-subtle.scss'; ================================================ FILE: build/animations/perspective.js ================================================ import '../../src/scss/animations/perspective.scss'; ================================================ FILE: build/animations/scale-extreme.js ================================================ import '../../src/scss/animations/scale-extreme.scss'; ================================================ FILE: build/animations/scale-subtle.js ================================================ import '../../src/scss/animations/scale-subtle.scss'; ================================================ FILE: build/animations/scale.js ================================================ import '../../src/scss/animations/scale.scss'; ================================================ FILE: build/animations/shift-away-extreme.js ================================================ import '../../src/scss/animations/shift-away-extreme.scss'; ================================================ FILE: build/animations/shift-away-subtle.js ================================================ import '../../src/scss/animations/shift-away-subtle.scss'; ================================================ FILE: build/animations/shift-away.js ================================================ import '../../src/scss/animations/shift-away.scss'; ================================================ FILE: build/animations/shift-toward-extreme.js ================================================ import '../../src/scss/animations/shift-toward-extreme.scss'; ================================================ FILE: build/animations/shift-toward-subtle.js ================================================ import '../../src/scss/animations/shift-toward-subtle.scss'; ================================================ FILE: build/animations/shift-toward.js ================================================ import '../../src/scss/animations/shift-toward.scss'; ================================================ FILE: build/base-umd.js ================================================ import tippy, {hideAll} from '../src'; import createSingleton from '../src/addons/createSingleton'; import delegate from '../src/addons/delegate'; import animateFill from '../src/plugins/animateFill'; import followCursor from '../src/plugins/followCursor'; import inlinePositioning from '../src/plugins/inlinePositioning'; import sticky from '../src/plugins/sticky'; import {ROUND_ARROW} from '../src/constants'; import {render} from '../src/template'; tippy.setDefaultProps({ plugins: [animateFill, followCursor, inlinePositioning, sticky], render, }); tippy.createSingleton = createSingleton; tippy.delegate = delegate; tippy.hideAll = hideAll; tippy.roundArrow = ROUND_ARROW; export default tippy; ================================================ FILE: build/base.js ================================================ import tippy from '../src'; import {render} from '../src/template'; tippy.setDefaultProps({render}); export {default, hideAll} from '../src'; export {default as createSingleton} from '../src/addons/createSingleton'; export {default as delegate} from '../src/addons/delegate'; export {default as animateFill} from '../src/plugins/animateFill'; export {default as followCursor} from '../src/plugins/followCursor'; export {default as inlinePositioning} from '../src/plugins/inlinePositioning'; export {default as sticky} from '../src/plugins/sticky'; export {ROUND_ARROW as roundArrow} from '../src/constants'; ================================================ FILE: build/bundle-umd.js ================================================ import css from '../dist/tippy.css'; import {injectCSS} from '../src/css'; import {isBrowser} from '../src/browser'; import tippy, {hideAll} from '../src'; import createSingleton from '../src/addons/createSingleton'; import delegate from '../src/addons/delegate'; import animateFill from '../src/plugins/animateFill'; import followCursor from '../src/plugins/followCursor'; import inlinePositioning from '../src/plugins/inlinePositioning'; import sticky from '../src/plugins/sticky'; import {ROUND_ARROW} from '../src/constants'; import {render} from '../src/template'; if (isBrowser) { injectCSS(css); } tippy.setDefaultProps({ plugins: [animateFill, followCursor, inlinePositioning, sticky], render, }); tippy.createSingleton = createSingleton; tippy.delegate = delegate; tippy.hideAll = hideAll; tippy.roundArrow = ROUND_ARROW; export default tippy; ================================================ FILE: build/css/backdrop.js ================================================ import '../../src/scss/backdrop.scss'; ================================================ FILE: build/css/border.js ================================================ import '../../src/scss/border.scss'; ================================================ FILE: build/css/svg-arrow.js ================================================ import '../../src/scss/svg-arrow.scss'; ================================================ FILE: build/css/tippy.js ================================================ import '../../src/scss/index.scss'; ================================================ FILE: build/headless-umd.js ================================================ import tippy, {hideAll} from '../src'; import createSingleton from '../src/addons/createSingleton'; import delegate from '../src/addons/delegate'; import animateFill from '../src/plugins/animateFill'; import followCursor from '../src/plugins/followCursor'; import inlinePositioning from '../src/plugins/inlinePositioning'; import sticky from '../src/plugins/sticky'; import {ROUND_ARROW} from '../src/constants'; tippy.setDefaultProps({ plugins: [animateFill, followCursor, inlinePositioning, sticky], animation: false, }); tippy.createSingleton = createSingleton; tippy.delegate = delegate; tippy.hideAll = hideAll; tippy.roundArrow = ROUND_ARROW; export default tippy; ================================================ FILE: build/headless.js ================================================ import tippy from '../src'; export {hideAll} from '../src'; export {default as createSingleton} from '../src/addons/createSingleton'; export {default as delegate} from '../src/addons/delegate'; export {default as animateFill} from '../src/plugins/animateFill'; export {default as followCursor} from '../src/plugins/followCursor'; export {default as inlinePositioning} from '../src/plugins/inlinePositioning'; export {default as sticky} from '../src/plugins/sticky'; export {ROUND_ARROW as roundArrow} from '../src/constants'; tippy.setDefaultProps({animation: false}); export default tippy; ================================================ FILE: build/index.js ================================================ // This file builds the CSS dist files. The main `rollup.config.js` builds the // JS dist files. /* eslint-disable @typescript-eslint/no-var-requires */ const fs = require('fs'); const {rollup} = require('rollup'); const babel = require('rollup-plugin-babel'); const sass = require('rollup-plugin-sass'); const postcss = require('postcss'); const autoprefixer = require('autoprefixer'); const cssnano = require('cssnano'); const resolve = require('rollup-plugin-node-resolve'); const json = require('rollup-plugin-json'); const cssOnly = require('rollup-plugin-css-only'); const replace = require('rollup-plugin-replace'); const NAMESPACE_PREFIX = process.env.NAMESPACE || 'tippy'; const THEME = process.env.THEME; const BASE_OUTPUT_CONFIG = { name: 'tippy', globals: {'popper.js': 'Popper'}, sourcemap: true, }; const PLUGINS = { babel: babel({ exclude: 'node_modules/**', extensions: ['.js', '.ts'], }), replaceNamespace: replace({ __NAMESPACE_PREFIX__: NAMESPACE_PREFIX, }), resolve: resolve({extensions: ['.js', '.ts']}), css: cssOnly({output: false}), json: json(), }; const PLUGIN_CONFIG = [ PLUGINS.babel, PLUGINS.replaceNamespace, PLUGINS.resolve, PLUGINS.json, PLUGINS.css, ]; function createPluginSCSS(output, shouldInjectNodeEnvTheme = false) { let data = `$namespace-prefix: ${NAMESPACE_PREFIX};`; if (shouldInjectNodeEnvTheme && THEME) { data += `@import './themes/${THEME}.scss';`; } return sass({ output, options: {data}, processor(css) { return postcss([autoprefixer, cssnano]) .process(css, {from: undefined}) .then((result) => result.css); }, }); } function createRollupConfig(inputFile, plugins) { return { input: `./build/${inputFile}`, external: ['popper.js'], plugins, }; } async function build() { // Create `index.d.ts` file from `src/types.ts` fs.copyFileSync('./src/types.ts', './index.d.ts'); // Create base CSS files for (const filename of fs.readdirSync(`./build/css`)) { const cssConfig = createRollupConfig( `css/${filename}`, PLUGIN_CONFIG.concat( createPluginSCSS(`./dist/${filename.replace('.js', '.css')}`, true) ) ); const cssBundle = await rollup(cssConfig); await cssBundle.write({ ...BASE_OUTPUT_CONFIG, sourcemap: false, format: 'umd', file: './index.js', }); } // Themes + animations for (const folder of ['themes', 'animations']) { for (const filename of fs.readdirSync(`./build/${folder}`)) { const filenameWithCSSExtension = filename.replace('.js', '.css'); const outputFile = `./${folder}/${filenameWithCSSExtension}`; const config = createRollupConfig( `${folder}/${filename}`, PLUGIN_CONFIG.concat(createPluginSCSS(outputFile)) ); const bundle = await rollup(config); await bundle.write({ ...BASE_OUTPUT_CONFIG, format: 'umd', sourcemap: false, file: 'index.js', }); } } fs.unlinkSync('./index.js'); } build(); ================================================ FILE: build/themes/light-border.js ================================================ import '../../src/scss/themes/light-border.scss'; ================================================ FILE: build/themes/light.js ================================================ import '../../src/scss/themes/light.scss'; ================================================ FILE: build/themes/material.js ================================================ import '../../src/scss/themes/material.scss'; ================================================ FILE: build/themes/translucent.js ================================================ import '../../src/scss/themes/translucent.scss'; ================================================ FILE: headless/package.json ================================================ { "name": "tippy-headless", "private": true, "version": "0.1.0", "description": "Headless rendering for Tippy.js", "types": "../index.d.ts", "main": "dist/tippy-headless.cjs.js", "module": "dist/tippy-headless.esm.js", "unpkg": "dist/tippy-headless.umd.min.js", "sideEffects": false, "files": [ "dist/" ], "author": "atomiks", "license": "MIT" } ================================================ FILE: index.test-d.ts ================================================ import {expectType} from 'tsd'; import tippy, { Instance, Props, Tippy, LifecycleHooks, delegate, DelegateInstance, createSingleton, Plugin, animateFill, followCursor, inlinePositioning, sticky, hideAll, roundArrow, CreateSingletonInstance, } from './src/types'; interface CustomProps { custom: number; } type FilteredProps = CustomProps & Omit; type ExtendedProps = FilteredProps & LifecycleHooks; declare const tippyExtended: Tippy; const singleTarget = document.createElement('div'); const mulitpleTargets = document.querySelectorAll('div'); expectType(tippy(singleTarget)); expectType(tippy(singleTarget, {content: 'hello'})); expectType(tippy(mulitpleTargets)); expectType(tippy(mulitpleTargets, {content: 'hello'})); expectType(delegate(singleTarget, {target: '.child'})); expectType( delegate(singleTarget, {target: '.child', content: 'hello'}) ); expectType(delegate(mulitpleTargets, {target: '.child'})); expectType( delegate(mulitpleTargets, {target: '.child', content: 'hello'}) ); const tippyInstances = [tippy(singleTarget), tippy(singleTarget)]; const singleton = createSingleton(tippyInstances); expectType(createSingleton(tippyInstances)); expectType( createSingleton(tippyInstances, {content: 'hello'}) ); expectType( createSingleton(tippyInstances, {overrides: ['content']}) ); expectType<(instances: Instance[]) => void>(singleton.setInstances); // TODO: I want to assert that these *don't* error, but `tsd` does not provide // such a function(?) createSingleton(tippyExtended('button')); singleton.setInstances(tippyExtended('button')); expectType( tippy(singleTarget, { plugins: [animateFill, followCursor, inlinePositioning, sticky], }) ); const customPlugin: Plugin = { name: 'custom', defaultValue: 42, fn(instance) { expectType>(instance); expectType(instance.props.custom); return {}; }, }; expectType>( tippyExtended(singleTarget, { custom: 42, plugins: [customPlugin], }) ); expectType(hideAll({duration: 50, exclude: tippy(singleTarget)})); expectType(roundArrow); ================================================ FILE: package.json ================================================ { "name": "tippy.js", "version": "6.3.7", "description": "The complete tooltip, popover, dropdown, and menu solution for the web", "main": "dist/tippy.cjs.js", "module": "dist/tippy.esm.js", "unpkg": "dist/tippy-bundle.umd.min.js", "types": "index.d.ts", "sideEffects": [ "**/*.css" ], "author": "atomiks", "contributors": [ "Brett Zamir" ], "license": "MIT", "bugs": "https://github.com/atomiks/tippyjs/issues", "homepage": "https://atomiks.github.io/tippyjs/", "keywords": [ "tooltip", "popover", "popper", "dropdown", "popup", "tippy", "tippy.js" ], "files": [ "dist/", "animations/", "themes/", "headless/", "index.d.ts" ], "repository": { "type": "git", "url": "git+https://github.com/atomiks/tippyjs.git" }, "scripts": { "dev": "cross-env NODE_ENV=dev rollup -c .config/rollup.config.js --watch", "build": "node build/index.js && rollup -c .config/rollup.config.js && bundlesize", "build:visual": "cross-env NODE_ENV=test rollup -c .config/rollup.config.js", "serve": "serve test/visual", "test:dom": "jest unit integration --coverage", "test:functional": "jest functional", "test:types": "tsc && tsd", "test": "yarn test:types && yarn test:dom && yarn test:functional", "lint": "eslint . --ext .ts,.js", "format": "prettier --write \"**/*.{js,ts,json,md,mdx,scss,css}\" --ignore-path .config/.prettierignore", "clean": "rimraf dist/ themes/ animations/ coverage/ .devserver/ .cache/ ./index.d.ts", "prepare": "yarn clean && yarn build" }, "babel": { "extends": "./.config/babel.config" }, "jest": { "preset": "./.config/jest.config" }, "eslintConfig": { "extends": "./.config/eslint.config" }, "prettier": { "singleQuote": true, "bracketSpacing": false, "proseWrap": "always" }, "browserslist": [ "> 0.5%", "not dead", "not safari < 8" ], "bundlesize": [ { "path": "dist/tippy-bundle.umd.min.js", "maxSize": "10 kB" }, { "path": "headless/dist/tippy-headless.umd.min.js", "maxSize": "10 kB" }, { "path": "dist/tippy.umd.min.js", "maxSize": "10 kB" }, { "path": "dist/tippy.css", "maxSize": "5 kB" } ], "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "src/**/*.ts": [ "jest --findRelatedTests", "eslint . --ext .ts,.js", "git add" ], "{build,src,test,website/src}/**/*.{ts,js,json,css,scss,md}": [ "prettier --write", "git add" ] }, "devDependencies": { "@babel/core": "^7.8.3", "@babel/preset-env": "^7.8.3", "@babel/preset-typescript": "^7.13.0", "@testing-library/dom": "^6.11.0", "@types/node": "^12.12.25", "@typescript-eslint/eslint-plugin": "^4.16.1", "@typescript-eslint/parser": "^4.16.1", "autoprefixer": "^9.7.4", "babel-jest": "^25.3.0", "babel-plugin-dev-expression": "^0.2.2", "bundlesize": "^0.18.0", "colorette": "^1.1.0", "core-js": "^3.6.4", "cross-env": "^7.0.0", "cssnano": "^4.1.10", "dotenv": "^8.2.0", "eslint": "^6.8.0", "eslint-config-prettier": "^6.9.0", "husky": "^3.1.0", "jest": "^25.3.0", "jest-environment-jsdom-fourteen": "^1.0.1", "jest-image-snapshot": "^2.12.0", "jest-puppeteer": "^4.4.0", "jest-puppeteer-docker": "^1.3.2", "lint-staged": "^9.5.0", "postcss": "^7.0.26", "poster": "0.0.9", "prettier": "^2.0.1", "promise": "^8.0.3", "puppeteer": "^2.1.1", "rimraf": "^3.0.0", "rollup": "^1.29.1", "rollup-plugin-babel": "^4.3.3", "rollup-plugin-commonjs": "^10.0.2", "rollup-plugin-css-only": "^1.0.0", "rollup-plugin-json": "^4.0.0", "rollup-plugin-livereload": "^1.0.4", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-replace": "^2.2.0", "rollup-plugin-sass": "^1.2.2", "rollup-plugin-serve": "^1.0.1", "rollup-plugin-terser": "^5.2.0", "sass": "^1.25.0", "serve": "^11.3.0", "tsd": "^0.14.0", "typescript": "^4.2.2" }, "dependencies": { "@popperjs/core": "^2.9.0" } } ================================================ FILE: src/_babel.d.ts ================================================ declare const __DEV__: boolean; ================================================ FILE: src/addons/createSingleton.ts ================================================ import tippy from '..'; import {div} from '../dom-utils'; import { CreateSingleton, Plugin, CreateSingletonProps, ReferenceElement, CreateSingletonInstance, Instance, Props, } from '../types'; import {normalizeToArray, removeProperties} from '../utils'; import {errorWhen} from '../validation'; import {applyStyles, Modifier} from '@popperjs/core'; // The default `applyStyles` modifier has a cleanup function that gets called // every time the popper is destroyed (i.e. a new target), removing the styles // and causing transitions to break for singletons when the console is open, but // most notably for non-transform styles being used, `gpuAcceleration: false`. const applyStylesModifier: Modifier<'applyStyles', Record> = { ...applyStyles, effect({state}) { const initialStyles = { popper: { position: state.options.strategy, left: '0', top: '0', margin: '0', }, arrow: { position: 'absolute', }, reference: {}, }; Object.assign(state.elements.popper.style, initialStyles.popper); state.styles = initialStyles; if (state.elements.arrow) { Object.assign(state.elements.arrow.style, initialStyles.arrow); } // intentionally return no cleanup function // return () => { ... } }, }; const createSingleton: CreateSingleton = ( tippyInstances, optionalProps = {} ) => { /* istanbul ignore else */ if (__DEV__) { errorWhen( !Array.isArray(tippyInstances), [ 'The first argument passed to createSingleton() must be an array of', 'tippy instances. The passed value was', String(tippyInstances), ].join(' ') ); } let individualInstances = tippyInstances; let references: Array = []; let triggerTargets: Array = []; let currentTarget: Element | null; let overrides = optionalProps.overrides; let interceptSetPropsCleanups: Array<() => void> = []; let shownOnCreate = false; function setTriggerTargets(): void { triggerTargets = individualInstances .map((instance) => normalizeToArray(instance.props.triggerTarget || instance.reference) ) .reduce((acc, item) => acc.concat(item), []); } function setReferences(): void { references = individualInstances.map((instance) => instance.reference); } function enableInstances(isEnabled: boolean): void { individualInstances.forEach((instance) => { if (isEnabled) { instance.enable(); } else { instance.disable(); } }); } function interceptSetProps(singleton: Instance): Array<() => void> { return individualInstances.map((instance) => { const originalSetProps = instance.setProps; instance.setProps = (props): void => { originalSetProps(props); if (instance.reference === currentTarget) { singleton.setProps(props); } }; return (): void => { instance.setProps = originalSetProps; }; }); } // have to pass singleton, as it maybe undefined on first call function prepareInstance( singleton: Instance, target: ReferenceElement ): void { const index = triggerTargets.indexOf(target); // bail-out if (target === currentTarget) { return; } currentTarget = target; const overrideProps: Partial = (overrides || []) .concat('content') .reduce((acc, prop) => { (acc as any)[prop] = individualInstances[index].props[prop]; return acc; }, {}); singleton.setProps({ ...overrideProps, getReferenceClientRect: typeof overrideProps.getReferenceClientRect === 'function' ? overrideProps.getReferenceClientRect : (): ClientRect => references[index]?.getBoundingClientRect(), }); } enableInstances(false); setReferences(); setTriggerTargets(); const plugin: Plugin = { fn() { return { onDestroy(): void { enableInstances(true); }, onHidden(): void { currentTarget = null; }, onClickOutside(instance): void { if (instance.props.showOnCreate && !shownOnCreate) { shownOnCreate = true; currentTarget = null; } }, onShow(instance): void { if (instance.props.showOnCreate && !shownOnCreate) { shownOnCreate = true; prepareInstance(instance, references[0]); } }, onTrigger(instance, event): void { prepareInstance(instance, event.currentTarget as Element); }, }; }, }; const singleton = tippy(div(), { ...removeProperties(optionalProps, ['overrides']), plugins: [plugin, ...(optionalProps.plugins || [])], triggerTarget: triggerTargets, popperOptions: { ...optionalProps.popperOptions, modifiers: [ ...(optionalProps.popperOptions?.modifiers || []), applyStylesModifier, ], }, }) as CreateSingletonInstance; const originalShow = singleton.show; singleton.show = (target?: ReferenceElement | Instance | number): void => { originalShow(); // first time, showOnCreate or programmatic call with no params // default to showing first instance if (!currentTarget && target == null) { return prepareInstance(singleton, references[0]); } // triggered from event (do nothing as prepareInstance already called by onTrigger) // programmatic call with no params when already visible (do nothing again) if (currentTarget && target == null) { return; } // target is index of instance if (typeof target === 'number') { return ( references[target] && prepareInstance(singleton, references[target]) ); } // target is a child tippy instance if (individualInstances.indexOf(target as Instance) >= 0) { const ref = (target as Instance).reference; return prepareInstance(singleton, ref); } // target is a ReferenceElement if (references.indexOf(target as ReferenceElement) >= 0) { return prepareInstance(singleton, target as ReferenceElement); } }; singleton.showNext = (): void => { const first = references[0]; if (!currentTarget) { return singleton.show(0); } const index = references.indexOf(currentTarget); singleton.show(references[index + 1] || first); }; singleton.showPrevious = (): void => { const last = references[references.length - 1]; if (!currentTarget) { return singleton.show(last); } const index = references.indexOf(currentTarget); const target = references[index - 1] || last; singleton.show(target); }; const originalSetProps = singleton.setProps; singleton.setProps = (props): void => { overrides = props.overrides || overrides; originalSetProps(props); }; singleton.setInstances = (nextInstances): void => { enableInstances(true); interceptSetPropsCleanups.forEach((fn) => fn()); individualInstances = nextInstances; enableInstances(false); setReferences(); setTriggerTargets(); interceptSetPropsCleanups = interceptSetProps(singleton); singleton.setProps({triggerTarget: triggerTargets}); }; interceptSetPropsCleanups = interceptSetProps(singleton); return singleton; }; export default createSingleton; ================================================ FILE: src/addons/delegate.ts ================================================ import tippy from '..'; import {TOUCH_OPTIONS} from '../constants'; import {defaultProps} from '../props'; import {Instance, Props, Targets} from '../types'; import {ListenerObject} from '../types-internal'; import {normalizeToArray, removeProperties} from '../utils'; import {errorWhen} from '../validation'; const BUBBLING_EVENTS_MAP = { mouseover: 'mouseenter', focusin: 'focus', click: 'click', }; /** * Creates a delegate instance that controls the creation of tippy instances * for child elements (`target` CSS selector). */ function delegate( targets: Targets, props: Partial & {target: string} ): Instance | Instance[] { /* istanbul ignore else */ if (__DEV__) { errorWhen( !(props && props.target), [ 'You must specity a `target` prop indicating a CSS selector string matching', 'the target elements that should receive a tippy.', ].join(' ') ); } let listeners: ListenerObject[] = []; let childTippyInstances: Instance[] = []; let disabled = false; const {target} = props; const nativeProps = removeProperties(props, ['target']); const parentProps = {...nativeProps, trigger: 'manual', touch: false}; const childProps = { touch: defaultProps.touch, ...nativeProps, showOnCreate: true, }; const returnValue = tippy(targets, parentProps); const normalizedReturnValue = normalizeToArray(returnValue); function onTrigger(event: Event): void { if (!event.target || disabled) { return; } const targetNode = (event.target as Element).closest(target); if (!targetNode) { return; } // Get relevant trigger with fallbacks: // 1. Check `data-tippy-trigger` attribute on target node // 2. Fallback to `trigger` passed to `delegate()` // 3. Fallback to `defaultProps.trigger` const trigger = targetNode.getAttribute('data-tippy-trigger') || props.trigger || defaultProps.trigger; // @ts-ignore if (targetNode._tippy) { return; } if (event.type === 'touchstart' && typeof childProps.touch === 'boolean') { return; } if ( event.type !== 'touchstart' && trigger.indexOf((BUBBLING_EVENTS_MAP as any)[event.type]) < 0 ) { return; } const instance = tippy(targetNode, childProps); if (instance) { childTippyInstances = childTippyInstances.concat(instance); } } function on( node: Element, eventType: string, handler: EventListener, options: boolean | Record = false ): void { node.addEventListener(eventType, handler, options); listeners.push({node, eventType, handler, options}); } function addEventListeners(instance: Instance): void { const {reference} = instance; on(reference, 'touchstart', onTrigger, TOUCH_OPTIONS); on(reference, 'mouseover', onTrigger); on(reference, 'focusin', onTrigger); on(reference, 'click', onTrigger); } function removeEventListeners(): void { listeners.forEach(({node, eventType, handler, options}: ListenerObject) => { node.removeEventListener(eventType, handler, options); }); listeners = []; } function applyMutations(instance: Instance): void { const originalDestroy = instance.destroy; const originalEnable = instance.enable; const originalDisable = instance.disable; instance.destroy = (shouldDestroyChildInstances = true): void => { if (shouldDestroyChildInstances) { childTippyInstances.forEach((instance) => { instance.destroy(); }); } childTippyInstances = []; removeEventListeners(); originalDestroy(); }; instance.enable = (): void => { originalEnable(); childTippyInstances.forEach((instance) => instance.enable()); disabled = false; }; instance.disable = (): void => { originalDisable(); childTippyInstances.forEach((instance) => instance.disable()); disabled = true; }; addEventListeners(instance); } normalizedReturnValue.forEach(applyMutations); return returnValue; } export default delegate; ================================================ FILE: src/bindGlobalEventListeners.ts ================================================ import {TOUCH_OPTIONS} from './constants'; import {isReferenceElement} from './dom-utils'; export const currentInput = {isTouch: false}; let lastMouseMoveTime = 0; /** * When a `touchstart` event is fired, it's assumed the user is using touch * input. We'll bind a `mousemove` event listener to listen for mouse input in * the future. This way, the `isTouch` property is fully dynamic and will handle * hybrid devices that use a mix of touch + mouse input. */ export function onDocumentTouchStart(): void { if (currentInput.isTouch) { return; } currentInput.isTouch = true; if (window.performance) { document.addEventListener('mousemove', onDocumentMouseMove); } } /** * When two `mousemove` event are fired consecutively within 20ms, it's assumed * the user is using mouse input again. `mousemove` can fire on touch devices as * well, but very rarely that quickly. */ export function onDocumentMouseMove(): void { const now = performance.now(); if (now - lastMouseMoveTime < 20) { currentInput.isTouch = false; document.removeEventListener('mousemove', onDocumentMouseMove); } lastMouseMoveTime = now; } /** * When an element is in focus and has a tippy, leaving the tab/window and * returning causes it to show again. For mouse users this is unexpected, but * for keyboard use it makes sense. * TODO: find a better technique to solve this problem */ export function onWindowBlur(): void { const activeElement = document.activeElement as HTMLElement | null; if (isReferenceElement(activeElement)) { const instance = activeElement._tippy!; if (activeElement.blur && !instance.state.isVisible) { activeElement.blur(); } } } export default function bindGlobalEventListeners(): void { document.addEventListener('touchstart', onDocumentTouchStart, TOUCH_OPTIONS); window.addEventListener('blur', onWindowBlur); } ================================================ FILE: src/browser.ts ================================================ export const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'; export const isIE11 = isBrowser ? // @ts-ignore !!window.msCrypto : false; ================================================ FILE: src/constants.ts ================================================ export const ROUND_ARROW = ''; export const BOX_CLASS = `__NAMESPACE_PREFIX__-box`; export const CONTENT_CLASS = `__NAMESPACE_PREFIX__-content`; export const BACKDROP_CLASS = `__NAMESPACE_PREFIX__-backdrop`; export const ARROW_CLASS = `__NAMESPACE_PREFIX__-arrow`; export const SVG_ARROW_CLASS = `__NAMESPACE_PREFIX__-svg-arrow`; export const TOUCH_OPTIONS = {passive: true, capture: true}; export const TIPPY_DEFAULT_APPEND_TO = () => document.body; ================================================ FILE: src/createTippy.ts ================================================ import {createPopper, StrictModifiers, Modifier} from '@popperjs/core'; import {currentInput} from './bindGlobalEventListeners'; import {isIE11} from './browser'; import {TIPPY_DEFAULT_APPEND_TO, TOUCH_OPTIONS} from './constants'; import { actualContains, div, getOwnerDocument, isCursorOutsideInteractiveBorder, isMouseEvent, setTransitionDuration, setVisibilityState, updateTransitionEndListener, } from './dom-utils'; import {defaultProps, evaluateProps, getExtendedPassedProps} from './props'; import {getChildren} from './template'; import { Content, Instance, LifecycleHooks, PopperElement, Props, ReferenceElement, } from './types'; import {ListenerObject, PopperTreeData, PopperChildren} from './types-internal'; import { arrayFrom, debounce, getValueAtIndexOrReturn, invokeWithArgsOrReturn, normalizeToArray, pushIfUnique, splitBySpaces, unique, removeUndefinedProps, } from './utils'; import {createMemoryLeakWarning, errorWhen, warnWhen} from './validation'; let idCounter = 1; let mouseMoveListeners: ((event: MouseEvent) => void)[] = []; // Used by `hideAll()` export let mountedInstances: Instance[] = []; export default function createTippy( reference: ReferenceElement, passedProps: Partial ): Instance { const props = evaluateProps(reference, { ...defaultProps, ...getExtendedPassedProps(removeUndefinedProps(passedProps)), }); // =========================================================================== // 🔒 Private members // =========================================================================== let showTimeout: any; let hideTimeout: any; let scheduleHideAnimationFrame: number; let isVisibleFromClick = false; let didHideDueToDocumentMouseDown = false; let didTouchMove = false; let ignoreOnFirstUpdate = false; let lastTriggerEvent: Event | undefined; let currentTransitionEndListener: (event: TransitionEvent) => void; let onFirstUpdate: () => void; let listeners: ListenerObject[] = []; let debouncedOnMouseMove = debounce(onMouseMove, props.interactiveDebounce); let currentTarget: Element; // =========================================================================== // 🔑 Public members // =========================================================================== const id = idCounter++; const popperInstance = null; const plugins = unique(props.plugins); const state = { // Is the instance currently enabled? isEnabled: true, // Is the tippy currently showing and not transitioning out? isVisible: false, // Has the instance been destroyed? isDestroyed: false, // Is the tippy currently mounted to the DOM? isMounted: false, // Has the tippy finished transitioning in? isShown: false, }; const instance: Instance = { // properties id, reference, popper: div(), popperInstance, props, state, plugins, // methods clearDelayTimeouts, setProps, setContent, show, hide, hideWithInteractivity, enable, disable, unmount, destroy, }; // TODO: Investigate why this early return causes a TDZ error in the tests — // it doesn't seem to happen in the browser /* istanbul ignore if */ if (!props.render) { if (__DEV__) { errorWhen(true, 'render() function has not been supplied.'); } return instance; } // =========================================================================== // Initial mutations // =========================================================================== const {popper, onUpdate} = props.render(instance); popper.setAttribute('data-__NAMESPACE_PREFIX__-root', ''); popper.id = `__NAMESPACE_PREFIX__-${instance.id}`; instance.popper = popper; reference._tippy = instance; popper._tippy = instance; const pluginsHooks = plugins.map((plugin) => plugin.fn(instance)); const hasAriaExpanded = reference.hasAttribute('aria-expanded'); addListeners(); handleAriaExpandedAttribute(); handleStyles(); invokeHook('onCreate', [instance]); if (props.showOnCreate) { scheduleShow(); } // Prevent a tippy with a delay from hiding if the cursor left then returned // before it started hiding popper.addEventListener('mouseenter', () => { if (instance.props.interactive && instance.state.isVisible) { instance.clearDelayTimeouts(); } }); popper.addEventListener('mouseleave', () => { if ( instance.props.interactive && instance.props.trigger.indexOf('mouseenter') >= 0 ) { getDocument().addEventListener('mousemove', debouncedOnMouseMove); } }); return instance; // =========================================================================== // 🔒 Private methods // =========================================================================== function getNormalizedTouchSettings(): [string | boolean, number] { const {touch} = instance.props; return Array.isArray(touch) ? touch : [touch, 0]; } function getIsCustomTouchBehavior(): boolean { return getNormalizedTouchSettings()[0] === 'hold'; } function getIsDefaultRenderFn(): boolean { // @ts-ignore return !!instance.props.render?.$$tippy; } function getCurrentTarget(): Element { return currentTarget || reference; } function getDocument(): Document { const parent = getCurrentTarget().parentNode as Element; return parent ? getOwnerDocument(parent) : document; } function getDefaultTemplateChildren(): PopperChildren { return getChildren(popper); } function getDelay(isShow: boolean): number { // For touch or keyboard input, force `0` delay for UX reasons // Also if the instance is mounted but not visible (transitioning out), // ignore delay if ( (instance.state.isMounted && !instance.state.isVisible) || currentInput.isTouch || (lastTriggerEvent && lastTriggerEvent.type === 'focus') ) { return 0; } return getValueAtIndexOrReturn( instance.props.delay, isShow ? 0 : 1, defaultProps.delay ); } function handleStyles(fromHide = false): void { popper.style.pointerEvents = instance.props.interactive && !fromHide ? '' : 'none'; popper.style.zIndex = `${instance.props.zIndex}`; } function invokeHook( hook: keyof LifecycleHooks, args: [Instance, any?], shouldInvokePropsHook = true ): void { pluginsHooks.forEach((pluginHooks) => { if (pluginHooks[hook]) { pluginHooks[hook]!(...args); } }); if (shouldInvokePropsHook) { instance.props[hook](...args); } } function handleAriaContentAttribute(): void { const {aria} = instance.props; if (!aria.content) { return; } const attr = `aria-${aria.content}`; const id = popper.id; const nodes = normalizeToArray(instance.props.triggerTarget || reference); nodes.forEach((node) => { const currentValue = node.getAttribute(attr); if (instance.state.isVisible) { node.setAttribute(attr, currentValue ? `${currentValue} ${id}` : id); } else { const nextValue = currentValue && currentValue.replace(id, '').trim(); if (nextValue) { node.setAttribute(attr, nextValue); } else { node.removeAttribute(attr); } } }); } function handleAriaExpandedAttribute(): void { if (hasAriaExpanded || !instance.props.aria.expanded) { return; } const nodes = normalizeToArray(instance.props.triggerTarget || reference); nodes.forEach((node) => { if (instance.props.interactive) { node.setAttribute( 'aria-expanded', instance.state.isVisible && node === getCurrentTarget() ? 'true' : 'false' ); } else { node.removeAttribute('aria-expanded'); } }); } function cleanupInteractiveMouseListeners(): void { getDocument().removeEventListener('mousemove', debouncedOnMouseMove); mouseMoveListeners = mouseMoveListeners.filter( (listener) => listener !== debouncedOnMouseMove ); } function onDocumentPress(event: MouseEvent | TouchEvent): void { // Moved finger to scroll instead of an intentional tap outside if (currentInput.isTouch) { if (didTouchMove || event.type === 'mousedown') { return; } } const actualTarget = (event.composedPath && event.composedPath()[0]) || event.target; // Clicked on interactive popper if ( instance.props.interactive && actualContains(popper, actualTarget as Element) ) { return; } // Clicked on the event listeners target if ( normalizeToArray(instance.props.triggerTarget || reference).some((el) => actualContains(el, actualTarget as Element) ) ) { if (currentInput.isTouch) { return; } if ( instance.state.isVisible && instance.props.trigger.indexOf('click') >= 0 ) { return; } } else { invokeHook('onClickOutside', [instance, event]); } if (instance.props.hideOnClick === true) { instance.clearDelayTimeouts(); instance.hide(); // `mousedown` event is fired right before `focus` if pressing the // currentTarget. This lets a tippy with `focus` trigger know that it // should not show didHideDueToDocumentMouseDown = true; setTimeout(() => { didHideDueToDocumentMouseDown = false; }); // The listener gets added in `scheduleShow()`, but this may be hiding it // before it shows, and hide()'s early bail-out behavior can prevent it // from being cleaned up if (!instance.state.isMounted) { removeDocumentPress(); } } } function onTouchMove(): void { didTouchMove = true; } function onTouchStart(): void { didTouchMove = false; } function addDocumentPress(): void { const doc = getDocument(); doc.addEventListener('mousedown', onDocumentPress, true); doc.addEventListener('touchend', onDocumentPress, TOUCH_OPTIONS); doc.addEventListener('touchstart', onTouchStart, TOUCH_OPTIONS); doc.addEventListener('touchmove', onTouchMove, TOUCH_OPTIONS); } function removeDocumentPress(): void { const doc = getDocument(); doc.removeEventListener('mousedown', onDocumentPress, true); doc.removeEventListener('touchend', onDocumentPress, TOUCH_OPTIONS); doc.removeEventListener('touchstart', onTouchStart, TOUCH_OPTIONS); doc.removeEventListener('touchmove', onTouchMove, TOUCH_OPTIONS); } function onTransitionedOut(duration: number, callback: () => void): void { onTransitionEnd(duration, () => { if ( !instance.state.isVisible && popper.parentNode && popper.parentNode.contains(popper) ) { callback(); } }); } function onTransitionedIn(duration: number, callback: () => void): void { onTransitionEnd(duration, callback); } function onTransitionEnd(duration: number, callback: () => void): void { const box = getDefaultTemplateChildren().box; function listener(event: TransitionEvent): void { if (event.target === box) { updateTransitionEndListener(box, 'remove', listener); callback(); } } // Make callback synchronous if duration is 0 // `transitionend` won't fire otherwise if (duration === 0) { return callback(); } updateTransitionEndListener(box, 'remove', currentTransitionEndListener); updateTransitionEndListener(box, 'add', listener); currentTransitionEndListener = listener; } function on( eventType: string, handler: EventListener, options: boolean | Record = false ): void { const nodes = normalizeToArray(instance.props.triggerTarget || reference); nodes.forEach((node) => { node.addEventListener(eventType, handler, options); listeners.push({node, eventType, handler, options}); }); } function addListeners(): void { if (getIsCustomTouchBehavior()) { on('touchstart', onTrigger, {passive: true}); on('touchend', onMouseLeave as EventListener, {passive: true}); } splitBySpaces(instance.props.trigger).forEach((eventType) => { if (eventType === 'manual') { return; } on(eventType, onTrigger); switch (eventType) { case 'mouseenter': on('mouseleave', onMouseLeave as EventListener); break; case 'focus': on(isIE11 ? 'focusout' : 'blur', onBlurOrFocusOut as EventListener); break; case 'focusin': on('focusout', onBlurOrFocusOut as EventListener); break; } }); } function removeListeners(): void { listeners.forEach(({node, eventType, handler, options}: ListenerObject) => { node.removeEventListener(eventType, handler, options); }); listeners = []; } function onTrigger(event: Event): void { let shouldScheduleClickHide = false; if ( !instance.state.isEnabled || isEventListenerStopped(event) || didHideDueToDocumentMouseDown ) { return; } const wasFocused = lastTriggerEvent?.type === 'focus'; lastTriggerEvent = event; currentTarget = event.currentTarget as Element; handleAriaExpandedAttribute(); if (!instance.state.isVisible && isMouseEvent(event)) { // If scrolling, `mouseenter` events can be fired if the cursor lands // over a new target, but `mousemove` events don't get fired. This // causes interactive tooltips to get stuck open until the cursor is // moved mouseMoveListeners.forEach((listener) => listener(event)); } // Toggle show/hide when clicking click-triggered tooltips if ( event.type === 'click' && (instance.props.trigger.indexOf('mouseenter') < 0 || isVisibleFromClick) && instance.props.hideOnClick !== false && instance.state.isVisible ) { shouldScheduleClickHide = true; } else { scheduleShow(event); } if (event.type === 'click') { isVisibleFromClick = !shouldScheduleClickHide; } if (shouldScheduleClickHide && !wasFocused) { scheduleHide(event); } } function onMouseMove(event: MouseEvent): void { const target = event.target as Node; const isCursorOverReferenceOrPopper = getCurrentTarget().contains(target) || popper.contains(target); if (event.type === 'mousemove' && isCursorOverReferenceOrPopper) { return; } const popperTreeData = getNestedPopperTree() .concat(popper) .map((popper) => { const instance = popper._tippy!; const state = instance.popperInstance?.state; if (state) { return { popperRect: popper.getBoundingClientRect(), popperState: state, props, }; } return null; }) .filter(Boolean) as PopperTreeData[]; if (isCursorOutsideInteractiveBorder(popperTreeData, event)) { cleanupInteractiveMouseListeners(); scheduleHide(event); } } function onMouseLeave(event: MouseEvent): void { const shouldBail = isEventListenerStopped(event) || (instance.props.trigger.indexOf('click') >= 0 && isVisibleFromClick); if (shouldBail) { return; } if (instance.props.interactive) { instance.hideWithInteractivity(event); return; } scheduleHide(event); } function onBlurOrFocusOut(event: FocusEvent): void { if ( instance.props.trigger.indexOf('focusin') < 0 && event.target !== getCurrentTarget() ) { return; } // If focus was moved to within the popper if ( instance.props.interactive && event.relatedTarget && popper.contains(event.relatedTarget as Element) ) { return; } scheduleHide(event); } function isEventListenerStopped(event: Event): boolean { return currentInput.isTouch ? getIsCustomTouchBehavior() !== event.type.indexOf('touch') >= 0 : false; } function createPopperInstance(): void { destroyPopperInstance(); const { popperOptions, placement, offset, getReferenceClientRect, moveTransition, } = instance.props; const arrow = getIsDefaultRenderFn() ? getChildren(popper).arrow : null; const computedReference = getReferenceClientRect ? { getBoundingClientRect: getReferenceClientRect, contextElement: getReferenceClientRect.contextElement || getCurrentTarget(), } : reference; const tippyModifier: Modifier<'$$tippy', Record> = { name: '$$tippy', enabled: true, phase: 'beforeWrite', requires: ['computeStyles'], fn({state}) { if (getIsDefaultRenderFn()) { const {box} = getDefaultTemplateChildren(); ['placement', 'reference-hidden', 'escaped'].forEach((attr) => { if (attr === 'placement') { box.setAttribute('data-placement', state.placement); } else { if (state.attributes.popper[`data-popper-${attr}`]) { box.setAttribute(`data-${attr}`, ''); } else { box.removeAttribute(`data-${attr}`); } } }); state.attributes.popper = {}; } }, }; type TippyModifier = Modifier<'$$tippy', Record>; type ExtendedModifiers = StrictModifiers | Partial; const modifiers: Array = [ { name: 'offset', options: { offset, }, }, { name: 'preventOverflow', options: { padding: { top: 2, bottom: 2, left: 5, right: 5, }, }, }, { name: 'flip', options: { padding: 5, }, }, { name: 'computeStyles', options: { adaptive: !moveTransition, }, }, tippyModifier, ]; if (getIsDefaultRenderFn() && arrow) { modifiers.push({ name: 'arrow', options: { element: arrow, padding: 3, }, }); } modifiers.push(...(popperOptions?.modifiers || [])); instance.popperInstance = createPopper( computedReference, popper, { ...popperOptions, placement, onFirstUpdate, modifiers, } ); } function destroyPopperInstance(): void { if (instance.popperInstance) { instance.popperInstance.destroy(); instance.popperInstance = null; } } function mount(): void { const {appendTo} = instance.props; let parentNode: any; // By default, we'll append the popper to the triggerTargets's parentNode so // it's directly after the reference element so the elements inside the // tippy can be tabbed to // If there are clipping issues, the user can specify a different appendTo // and ensure focus management is handled correctly manually const node = getCurrentTarget(); if ( (instance.props.interactive && appendTo === TIPPY_DEFAULT_APPEND_TO) || appendTo === 'parent' ) { parentNode = node.parentNode; } else { parentNode = invokeWithArgsOrReturn(appendTo, [node]); } // The popper element needs to exist on the DOM before its position can be // updated as Popper needs to read its dimensions if (!parentNode.contains(popper)) { parentNode.appendChild(popper); } instance.state.isMounted = true; createPopperInstance(); /* istanbul ignore else */ if (__DEV__) { // Accessibility check warnWhen( instance.props.interactive && appendTo === defaultProps.appendTo && node.nextElementSibling !== popper, [ 'Interactive tippy element may not be accessible via keyboard', 'navigation because it is not directly after the reference element', 'in the DOM source order.', '\n\n', 'Using a wrapper
or tag around the reference element', 'solves this by creating a new parentNode context.', '\n\n', 'Specifying `appendTo: document.body` silences this warning, but it', 'assumes you are using a focus management solution to handle', 'keyboard navigation.', '\n\n', 'See: https://atomiks.github.io/tippyjs/v6/accessibility/#interactivity', ].join(' ') ); } } function getNestedPopperTree(): PopperElement[] { return arrayFrom( popper.querySelectorAll('[data-__NAMESPACE_PREFIX__-root]') ); } function scheduleShow(event?: Event): void { instance.clearDelayTimeouts(); if (event) { invokeHook('onTrigger', [instance, event]); } addDocumentPress(); let delay = getDelay(true); const [touchValue, touchDelay] = getNormalizedTouchSettings(); if (currentInput.isTouch && touchValue === 'hold' && touchDelay) { delay = touchDelay; } if (delay) { showTimeout = setTimeout(() => { instance.show(); }, delay); } else { instance.show(); } } function scheduleHide(event: Event): void { instance.clearDelayTimeouts(); invokeHook('onUntrigger', [instance, event]); if (!instance.state.isVisible) { removeDocumentPress(); return; } // For interactive tippies, scheduleHide is added to a document.body handler // from onMouseLeave so must intercept scheduled hides from mousemove/leave // events when trigger contains mouseenter and click, and the tip is // currently shown as a result of a click. if ( instance.props.trigger.indexOf('mouseenter') >= 0 && instance.props.trigger.indexOf('click') >= 0 && ['mouseleave', 'mousemove'].indexOf(event.type) >= 0 && isVisibleFromClick ) { return; } const delay = getDelay(false); if (delay) { hideTimeout = setTimeout(() => { if (instance.state.isVisible) { instance.hide(); } }, delay); } else { // Fixes a `transitionend` problem when it fires 1 frame too // late sometimes, we don't want hide() to be called. scheduleHideAnimationFrame = requestAnimationFrame(() => { instance.hide(); }); } } // =========================================================================== // 🔑 Public methods // =========================================================================== function enable(): void { instance.state.isEnabled = true; } function disable(): void { // Disabling the instance should also hide it // https://github.com/atomiks/tippy.js-react/issues/106 instance.hide(); instance.state.isEnabled = false; } function clearDelayTimeouts(): void { clearTimeout(showTimeout); clearTimeout(hideTimeout); cancelAnimationFrame(scheduleHideAnimationFrame); } function setProps(partialProps: Partial): void { /* istanbul ignore else */ if (__DEV__) { warnWhen(instance.state.isDestroyed, createMemoryLeakWarning('setProps')); } if (instance.state.isDestroyed) { return; } invokeHook('onBeforeUpdate', [instance, partialProps]); removeListeners(); const prevProps = instance.props; const nextProps = evaluateProps(reference, { ...prevProps, ...removeUndefinedProps(partialProps), ignoreAttributes: true, }); instance.props = nextProps; addListeners(); if (prevProps.interactiveDebounce !== nextProps.interactiveDebounce) { cleanupInteractiveMouseListeners(); debouncedOnMouseMove = debounce( onMouseMove, nextProps.interactiveDebounce ); } // Ensure stale aria-expanded attributes are removed if (prevProps.triggerTarget && !nextProps.triggerTarget) { normalizeToArray(prevProps.triggerTarget).forEach((node) => { node.removeAttribute('aria-expanded'); }); } else if (nextProps.triggerTarget) { reference.removeAttribute('aria-expanded'); } handleAriaExpandedAttribute(); handleStyles(); if (onUpdate) { onUpdate(prevProps, nextProps); } if (instance.popperInstance) { createPopperInstance(); // Fixes an issue with nested tippies if they are all getting re-rendered, // and the nested ones get re-rendered first. // https://github.com/atomiks/tippyjs-react/issues/177 // TODO: find a cleaner / more efficient solution(!) getNestedPopperTree().forEach((nestedPopper) => { // React (and other UI libs likely) requires a rAF wrapper as it flushes // its work in one requestAnimationFrame(nestedPopper._tippy!.popperInstance!.forceUpdate); }); } invokeHook('onAfterUpdate', [instance, partialProps]); } function setContent(content: Content): void { instance.setProps({content}); } function show(): void { /* istanbul ignore else */ if (__DEV__) { warnWhen(instance.state.isDestroyed, createMemoryLeakWarning('show')); } // Early bail-out const isAlreadyVisible = instance.state.isVisible; const isDestroyed = instance.state.isDestroyed; const isDisabled = !instance.state.isEnabled; const isTouchAndTouchDisabled = currentInput.isTouch && !instance.props.touch; const duration = getValueAtIndexOrReturn( instance.props.duration, 0, defaultProps.duration ); if ( isAlreadyVisible || isDestroyed || isDisabled || isTouchAndTouchDisabled ) { return; } // Normalize `disabled` behavior across browsers. // Firefox allows events on disabled elements, but Chrome doesn't. // Using a wrapper element (i.e. ) is recommended. if (getCurrentTarget().hasAttribute('disabled')) { return; } invokeHook('onShow', [instance], false); if (instance.props.onShow(instance) === false) { return; } instance.state.isVisible = true; if (getIsDefaultRenderFn()) { popper.style.visibility = 'visible'; } handleStyles(); addDocumentPress(); if (!instance.state.isMounted) { popper.style.transition = 'none'; } // If flipping to the opposite side after hiding at least once, the // animation will use the wrong placement without resetting the duration if (getIsDefaultRenderFn()) { const {box, content} = getDefaultTemplateChildren(); setTransitionDuration([box, content], 0); } onFirstUpdate = (): void => { if (!instance.state.isVisible || ignoreOnFirstUpdate) { return; } ignoreOnFirstUpdate = true; // reflow void popper.offsetHeight; popper.style.transition = instance.props.moveTransition; if (getIsDefaultRenderFn() && instance.props.animation) { const {box, content} = getDefaultTemplateChildren(); setTransitionDuration([box, content], duration); setVisibilityState([box, content], 'visible'); } handleAriaContentAttribute(); handleAriaExpandedAttribute(); pushIfUnique(mountedInstances, instance); // certain modifiers (e.g. `maxSize`) require a second update after the // popper has been positioned for the first time instance.popperInstance?.forceUpdate(); invokeHook('onMount', [instance]); if (instance.props.animation && getIsDefaultRenderFn()) { onTransitionedIn(duration, () => { instance.state.isShown = true; invokeHook('onShown', [instance]); }); } }; mount(); } function hide(): void { /* istanbul ignore else */ if (__DEV__) { warnWhen(instance.state.isDestroyed, createMemoryLeakWarning('hide')); } // Early bail-out const isAlreadyHidden = !instance.state.isVisible; const isDestroyed = instance.state.isDestroyed; const isDisabled = !instance.state.isEnabled; const duration = getValueAtIndexOrReturn( instance.props.duration, 1, defaultProps.duration ); if (isAlreadyHidden || isDestroyed || isDisabled) { return; } invokeHook('onHide', [instance], false); if (instance.props.onHide(instance) === false) { return; } instance.state.isVisible = false; instance.state.isShown = false; ignoreOnFirstUpdate = false; isVisibleFromClick = false; if (getIsDefaultRenderFn()) { popper.style.visibility = 'hidden'; } cleanupInteractiveMouseListeners(); removeDocumentPress(); handleStyles(true); if (getIsDefaultRenderFn()) { const {box, content} = getDefaultTemplateChildren(); if (instance.props.animation) { setTransitionDuration([box, content], duration); setVisibilityState([box, content], 'hidden'); } } handleAriaContentAttribute(); handleAriaExpandedAttribute(); if (instance.props.animation) { if (getIsDefaultRenderFn()) { onTransitionedOut(duration, instance.unmount); } } else { instance.unmount(); } } function hideWithInteractivity(event: MouseEvent): void { /* istanbul ignore else */ if (__DEV__) { warnWhen( instance.state.isDestroyed, createMemoryLeakWarning('hideWithInteractivity') ); } getDocument().addEventListener('mousemove', debouncedOnMouseMove); pushIfUnique(mouseMoveListeners, debouncedOnMouseMove); debouncedOnMouseMove(event); } function unmount(): void { /* istanbul ignore else */ if (__DEV__) { warnWhen(instance.state.isDestroyed, createMemoryLeakWarning('unmount')); } if (instance.state.isVisible) { instance.hide(); } if (!instance.state.isMounted) { return; } destroyPopperInstance(); // If a popper is not interactive, it will be appended outside the popper // tree by default. This seems mainly for interactive tippies, but we should // find a workaround if possible getNestedPopperTree().forEach((nestedPopper) => { nestedPopper._tippy!.unmount(); }); if (popper.parentNode) { popper.parentNode.removeChild(popper); } mountedInstances = mountedInstances.filter((i) => i !== instance); instance.state.isMounted = false; invokeHook('onHidden', [instance]); } function destroy(): void { /* istanbul ignore else */ if (__DEV__) { warnWhen(instance.state.isDestroyed, createMemoryLeakWarning('destroy')); } if (instance.state.isDestroyed) { return; } instance.clearDelayTimeouts(); instance.unmount(); removeListeners(); delete reference._tippy; instance.state.isDestroyed = true; invokeHook('onDestroy', [instance]); } } ================================================ FILE: src/css.ts ================================================ export function injectCSS(css: string): void { const style = document.createElement('style'); style.textContent = css; style.setAttribute('data-__NAMESPACE_PREFIX__-stylesheet', ''); const head = document.head; const firstStyleOrLinkTag = document.querySelector('head>style,head>link'); if (firstStyleOrLinkTag) { head.insertBefore(style, firstStyleOrLinkTag); } else { head.appendChild(style); } } ================================================ FILE: src/dom-utils.ts ================================================ import {ReferenceElement, Targets} from './types'; import {PopperTreeData} from './types-internal'; import {arrayFrom, isType, normalizeToArray, getBasePlacement} from './utils'; export function div(): HTMLDivElement { return document.createElement('div'); } export function isElement(value: unknown): value is Element | DocumentFragment { return ['Element', 'Fragment'].some((type) => isType(value, type)); } export function isNodeList(value: unknown): value is NodeList { return isType(value, 'NodeList'); } export function isMouseEvent(value: unknown): value is MouseEvent { return isType(value, 'MouseEvent'); } export function isReferenceElement(value: any): value is ReferenceElement { return !!(value && value._tippy && value._tippy.reference === value); } export function getArrayOfElements(value: Targets): Element[] { if (isElement(value)) { return [value]; } if (isNodeList(value)) { return arrayFrom(value); } if (Array.isArray(value)) { return value; } return arrayFrom(document.querySelectorAll(value)); } export function setTransitionDuration( els: (HTMLDivElement | null)[], value: number ): void { els.forEach((el) => { if (el) { el.style.transitionDuration = `${value}ms`; } }); } export function setVisibilityState( els: (HTMLDivElement | null)[], state: 'visible' | 'hidden' ): void { els.forEach((el) => { if (el) { el.setAttribute('data-state', state); } }); } export function getOwnerDocument( elementOrElements: Element | Element[] ): Document { const [element] = normalizeToArray(elementOrElements); // Elements created via a