├── .browserslistrc ├── .editorconfig ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── ci.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── .size-snapshot.json ├── .storybook ├── main.js └── preview.js ├── LICENSE ├── README.md ├── babel.config.json ├── codecov.yml ├── examples ├── animation │ ├── .gitignore │ ├── .rescriptsrc.js │ ├── README.md │ ├── package.json │ ├── public │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ └── index.js ├── basic │ ├── .gitignore │ ├── .rescriptsrc.js │ ├── README.md │ ├── package.json │ ├── public │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ └── index.js ├── close-on-esc │ ├── .gitignore │ ├── .rescriptsrc.js │ ├── README.md │ ├── package.json │ ├── public │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ └── index.js ├── controlled │ ├── .gitignore │ ├── .rescriptsrc.js │ ├── README.md │ ├── package.json │ ├── public │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ └── index.js ├── persist-once-mounted │ ├── .gitignore │ ├── .rescriptsrc.js │ ├── README.md │ ├── package.json │ ├── public │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ └── index.js ├── portal │ ├── .gitignore │ ├── .rescriptsrc.js │ ├── README.md │ ├── package.json │ ├── public │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ └── index.js └── render-prop │ ├── .gitignore │ ├── .rescriptsrc.js │ ├── README.md │ ├── package.json │ ├── public │ ├── index.html │ └── manifest.json │ └── src │ └── index.js ├── migrating.md ├── package.json ├── release-notes.md ├── rollup.config.js ├── src ├── index.ts ├── styles.css ├── types.ts ├── usePopperTooltip.ts └── utils.ts ├── stories └── basic.stories.tsx ├── tests └── usePopperTooltip.spec.tsx ├── tsconfig.json └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 0.25% 2 | not dead 3 | not op_mini all 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:prettier/recommended", 7 | "plugin:react/recommended", 8 | "plugin:react-hooks/recommended" 9 | ], 10 | "parserOptions": { 11 | "ecmaVersion": 2020, 12 | "sourceType": "module", 13 | "ecmaFeatures": { 14 | "jsx": true 15 | } 16 | }, 17 | "settings": { 18 | "react": { 19 | "version": "detect" 20 | } 21 | }, 22 | "rules": { 23 | "@typescript-eslint/explicit-function-return-type": "off", 24 | "@typescript-eslint/no-non-null-assertion": "off", 25 | "@typescript-eslint/explicit-module-boundary-types": "off" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Use Node.js v16 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 16 21 | - name: yarn install, build, and test 22 | run: | 23 | yarn --frozen-lockfile 24 | yarn typecheck 25 | yarn build 26 | yarn test --ci --coverage 27 | yarn lint 28 | bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }} 29 | env: 30 | CI: true 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | *.iml 4 | .*.haste_cache.* 5 | .DS_Store 6 | npm-debug.log 7 | yarn-error.log 8 | .idea/ 9 | .vscode/ 10 | node_modules/ 11 | dist/ 12 | .docz/ 13 | coverage/ 14 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn typecheck && yarn build && yarn test && yarn lint-staged && git add .size-snapshot.json 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "react-popper-tooltip.js": { 3 | "bundled": 11498, 4 | "minified": 5496, 5 | "gzipped": 1812, 6 | "treeshaked": { 7 | "rollup": { 8 | "code": 142, 9 | "import_statements": 142 10 | }, 11 | "webpack": { 12 | "code": 1394 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../stories/**/*.stories.mdx", 4 | "../stories/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | layout: 'centered', 3 | }; 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present Mohsin Ul Haq 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-popper-tooltip 2 | 3 | [![npm version](https://img.shields.io/npm/v/react-popper-tooltip.svg?style=flat-square)](https://www.npmjs.com/package/react-popper-tooltip) 4 | [![npm downloads](https://img.shields.io/npm/dm/react-popper-tooltip.svg?style=flat-square)](https://www.npmjs.com/package/react-popper-tooltip) 5 | [![codecov](https://codecov.io/gh/mohsinulhaq/react-popper-tooltip/branch/master/graph/badge.svg)](https://codecov.io/gh/mohsinulhaq/react-popper-tooltip) 6 | 7 | A React hook to effortlessly build smart tooltips. Based on [react-popper](https://github.com/FezVrasta/react-popper) 8 | and [popper.js](https://popper.js.org). 9 | 10 | ## NOTE 11 | 12 | > - This is the documentation for 4.x which introduced the `usePopperTooltip` hook. 13 | > - If you're looking for the render prop version, 14 | see [3.x docs](https://github.com/mohsinulhaq/react-popper-tooltip/blob/v3/README.md). 15 | > - If you're looking to upgrade from 3.x render prop to 4.x hook, please refer to our [migration guide](migrating.md). 16 | 17 | ## Examples 18 | 19 | - Basic usage [Demo](https://codesandbox.io/s/github/mohsinulhaq/react-popper-tooltip/tree/master/examples/basic) ([Source](/examples/basic)) 20 | - Animating appearance with react-spring [Demo](https://codesandbox.io/s/github/mohsinulhaq/react-popper-tooltip/tree/master/examples/animation) ([Source](/examples/animation)) 21 | - Closing tooltip with Esc button [Demo](https://codesandbox.io/s/github/mohsinulhaq/react-popper-tooltip/tree/master/examples/close-on-esc) ([Source](/examples/close-on-esc)) 22 | - Using as a controlled component [Demo](https://codesandbox.io/s/github/mohsinulhaq/react-popper-tooltip/tree/master/examples/controlled) ([Source](/examples/controlled)) 23 | - Persist the tooltip in the DOM once it's mounted [Demo](https://codesandbox.io/s/github/mohsinulhaq/react-popper-tooltip/tree/master/examples/persist-once-mounted) ([Source](/examples/persist-once-mounted)) 24 | - Using with react portal [Demo](https://codesandbox.io/s/github/mohsinulhaq/react-popper-tooltip/tree/master/examples/portal) ([Source](/examples/portal)) 25 | - Implementing render prop (v3) API [Demo](https://codesandbox.io/s/github/mohsinulhaq/react-popper-tooltip/tree/master/examples/render-prop) ([Source](/examples/render-prop)) 26 | 27 | 28 | ## Installation 29 | 30 | You can install **react-popper-tooltip** with [npm](https://www.npmjs.com) or [yarn](https://yarnpkg.com). 31 | 32 | ```bash 33 | npm i react-popper-tooltip 34 | # or 35 | yarn add react-popper-tooltip 36 | ``` 37 | 38 | ## Quick start 39 | 40 | This example illustrates how to create a minimal tooltip with default settings and using our default CSS file. 41 | 42 | ```jsx 43 | import * as React from 'react'; 44 | import { usePopperTooltip } from 'react-popper-tooltip'; 45 | import 'react-popper-tooltip/dist/styles.css'; 46 | 47 | function App() { 48 | const { 49 | getArrowProps, 50 | getTooltipProps, 51 | setTooltipRef, 52 | setTriggerRef, 53 | visible, 54 | } = usePopperTooltip(); 55 | 56 | return ( 57 |
58 | 61 | {visible && ( 62 |
66 |
67 | Tooltip 68 |
69 | )} 70 |
71 | ); 72 | } 73 | 74 | render(, document.getElementById('root')); 75 | ``` 76 | 77 | ## Styling 78 | 79 | With **react-popper-tooltip**, you can use CSS, LESS, SASS, or any CSS-in-JS library you're already using in your 80 | project. However, we do provide a minimal CSS-file file you can use for a quick start or as a reference to create your 81 | own tooltip styles. 82 | 83 | Import `react-popper-tooltip/dist/styles.css` to import it into your project. Add classes 84 | `tooltip-container` and `tooltip-arrow` to the tooltip container and arrow element accordingly. 85 | 86 | While the tooltip is being displayed, you have access to some attributes on the tooltip container. You can use them 87 | in your CSS in specific scenarios. 88 | 89 | - `data-popper-placement`: contains the current tooltip `placement`. You can use it to properly offset and display the 90 | arrow element (e.g., if the tooltip is displayed on the right, the arrow should point to the left and vice versa). 91 | 92 | - `data-popper-reference-hidden`: set to true when the trigger element is fully clipped and hidden from view, which 93 | causes the tooltip to appear to be attached to nothing. Set to false otherwise. 94 | 95 | - `data-popper-escaped`: set to true when the tooltip escapes the trigger element's boundary (and so it appears 96 | detached). Set to false otherwise. 97 | 98 | - `data-popper-interactive`: contains the current `interactive` option value. 99 | 100 | ## API reference 101 | 102 | ### usePopperTooltip 103 | 104 | ```jsx 105 | const { 106 | getArrowProps, 107 | getTooltipProps, 108 | setTooltipRef, 109 | setTriggerRef, 110 | tooltipRef, 111 | triggerRef, 112 | visible, 113 | ...popperProps 114 | } = usePopperTooltip( 115 | { 116 | closeOnOutsideClick, 117 | closeOnTriggerHidden, 118 | defaultVisible, 119 | delayHide, 120 | delayShow, 121 | followCursor, 122 | interactive, 123 | mutationObserverOptions, 124 | offset, 125 | onVisibleChange, 126 | placement, 127 | trigger, 128 | visible, 129 | }, 130 | popperOptions 131 | ); 132 | ``` 133 | 134 | #### Options 135 | 136 | - `closeOnOutsideClick: boolean`, defaults to `true` 137 | 138 | If `true`, closes the tooltip when user clicks outside the trigger element. 139 | 140 | - `closeOnTriggerHidden: boolean`, defaults to `false` 141 | 142 | Whether to close the tooltip when its trigger is out of boundary. 143 | 144 | - `delayHide: number`, defaults to `0` 145 | 146 | Delay in hiding the tooltip (ms). 147 | 148 | - `delayShow: number`, defaults to `0` 149 | 150 | Delay in showing the tooltip (ms). 151 | 152 | - `defaultVisible: boolean`, defaults to `false` 153 | 154 | The initial visibility state of the tooltip when the hook is initialized. 155 | 156 | - `followCursor: boolean`, defaults to `false` 157 | 158 | If `true`, the tooltip will stick to the cursor position. You would probably want to use this option with hover trigger. 159 | 160 | - `mutationObserverOptions: MutationObserverInit | null`, defaults 161 | to `{ attributes: true, childList: true, subtree: true }` 162 | 163 | Options to [MutationObserver 164 | ](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver), used internally for updating tooltip position based on its DOM changes. When the tooltip is 165 | visible and its content changes, it automatically repositions itself. In some cases 166 | you may need to change which parameters to observe or opt-out of tracking the changes at all. 167 | 168 | - `offset: [number, number]`, defaults to `[0, 6]` 169 | 170 | This is a shorthand for `popperOptions.modifiers` offset modifier option. The default value means the tooltip will be 171 | placed 6px away from the trigger element (to reserve enough space for the arrow element). 172 | 173 | We use this default value to match the size of the arrow element from our default CSS file. Feel free to change it if you are using your 174 | own styles. 175 | 176 | See [offset modifier docs](https://popper.js.org/docs/v2/modifiers/offset). 177 | 178 | `popperOptions` takes precedence over this option. 179 | 180 | - `onVisibleChange: (state: boolean) => void` 181 | 182 | Called with the tooltip state, when the visibility of the tooltip changes. 183 | 184 | - `trigger: TriggerType | TriggerType[] | null`, where `TriggerType = 'click' | 'right-click' | 'hover' | 'focus'`, 185 | defaults to `hover` 186 | 187 | Event or events that trigger the tooltip. Use `null` if you want to disable all events. It's useful in cases when 188 | you control the state of the tooltip. 189 | 190 | - `visible: boolean` 191 | 192 | The visibility state of the tooltip. Use this prop if you want to control the state of the tooltip. Note that `delayShow` and `delayHide` are not used if the tooltip is controlled. You have to apply delay on your external state. 193 | 194 | **react-popper-tooltip** manages its own state internally and calls `onVisibleChange` handler with any relevant changes. 195 | 196 | However, if more control is needed, you can pass this prop, and the state becomes controlled. As soon as it's not 197 | undefined, internally, **react-popper-tooltip** will determine its state based on your prop's value rather than its own 198 | internal state. 199 | 200 | - `placement: 'auto' | 'auto-start' | 'auto-end' | 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'right' | 'right-start' | 'right-end' | 'left' | 'left-start' | 'left-end';` 201 | 202 | The preferred placement of the tooltip. This is an alias for `popperOptions.placement` option. 203 | 204 | `popperOptions` takes precedence over this option. 205 | 206 | - `interactive: boolean`, defaults to `false` 207 | 208 | If `true`, hovering the tooltip will keep it open. Normally, if you trigger the tooltip on hover event, the tooltip 209 | closes when the mouse cursor moves out of the trigger element. If it moves to the tooltip element, the tooltip stays 210 | open. It's useful if you want to allow your users to interact with the tooltip's content (select and copy text, click a 211 | link, etc.). In this case you might want to increase `delayHide` value to give the user more time to react. 212 | 213 | - `popperOptions: { placement, modifiers, strategy, onFirstUpdate }` 214 | 215 | These options passed directly to the underlying `usePopper` hook. 216 | See [https://popper.js.org/docs/v2/constructors/#options](https://popper.js.org/docs/v2/constructors/#options). 217 | 218 | Keep in mind, if you set `placement` or _any_ `modifiers` here, it replaces `offset` and `placement` options above. They 219 | won't be merged into the final object. You have to add `offset` modifier along with others here to make it work. 220 | 221 | #### Returns 222 | 223 | - `triggerRef: HTMLElement | null` 224 | 225 | The trigger DOM element ref. 226 | 227 | - `tooltipRef: HTMLElement | null` 228 | 229 | The tooltip DOM element ref. 230 | 231 | - `setTooltipRef: (HTMLElement | null) => void | null` 232 | 233 | A tooltip callback ref. Must be assigned to the tooltip's `ref` prop. 234 | 235 | - `setTriggerRef: (HTMLElement | null) => void | null` 236 | 237 | A trigger callback ref. Must be assigned to the trigger's `ref` prop. 238 | 239 | - `visible: boolean` 240 | 241 | The current visibility state of the tooltip. Use it to display or hide the tooltip. 242 | 243 | - `getArrowProps: (props) => mergedProps` 244 | 245 | This function merges your props and the internal props of the arrow element. We recommend passing all your props to that 246 | function rather than applying them on the element directly to avoid your props being overridden or overriding the 247 | internal props. 248 | 249 | It returns the merged props that you need to pass to the arrow element. 250 | 251 | - `getTooltipProps: (props) => mergedProps` 252 | 253 | This function merges your props and the internal props of the tooltip element. We recommend passing all your props to 254 | that function rather than applying them on the element directly to avoid your props being overridden or overriding the 255 | internal props. 256 | 257 | It returns the merged props that you need to pass to tooltip element. 258 | 259 | - `popperProps: { update, forceUpdate, state }` 260 | 261 | Some props returned by the underlying `usePopper` hook. 262 | See [https://popper.js.org/react-popper/v2/hook](https://popper.js.org/react-popper/v2/hook). 263 | 264 | This doesn't include `styles` and `attributes` props. They are included into `getArrowProps` and `getTooltipProps` prop 265 | getters. 266 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-typescript", 4 | "@babel/preset-react", 5 | ["@babel/preset-env", { "bugfixes": true, "loose": true }] 6 | ], 7 | "plugins": [["@babel/transform-runtime"]] 8 | } 9 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: off 4 | patch: off 5 | -------------------------------------------------------------------------------- /examples/animation/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | yarn.lock 15 | package-lock.json 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /examples/animation/.rescriptsrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const resolveFrom = require('resolve-from') 3 | 4 | const fixLinkedDependencies = config => { 5 | config.resolve = { 6 | ...config.resolve, 7 | alias: { 8 | ...config.resolve.alias, 9 | react$: resolveFrom(path.resolve('node_modules'), 'react'), 10 | 'react-dom$': resolveFrom(path.resolve('node_modules'), 'react-dom'), 11 | }, 12 | } 13 | return config 14 | } 15 | 16 | module.exports = [ 17 | fixLinkedDependencies, 18 | ] 19 | -------------------------------------------------------------------------------- /examples/animation/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run this example: 4 | 5 | - `npm install` or `yarn` 6 | - `npm run start` or `yarn start` 7 | -------------------------------------------------------------------------------- /examples/animation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "^17.0.1", 4 | "react-dom": "^17.0.1", 5 | "react-popper-tooltip": "^4.0.0", 6 | "react-spring": "^8.0.27", 7 | "react-scripts": "3.4.3" 8 | }, 9 | "devDependencies": { 10 | "@rescripts/cli": "^0.0.15" 11 | }, 12 | "scripts": { 13 | "start": "rescripts start", 14 | "build": "rescripts build", 15 | "test": "rescripts test --env=jsdom", 16 | "eject": "rescripts eject" 17 | }, 18 | "browserslist": [ 19 | ">0.2%", 20 | "not dead", 21 | "not ie <= 11", 22 | "not op_mini all" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /examples/animation/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/animation/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/animation/src/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { usePopperTooltip } from 'react-popper-tooltip'; 4 | import { animated, useTransition } from 'react-spring'; 5 | import 'react-popper-tooltip/dist/styles.css'; 6 | 7 | function App() { 8 | return ; 9 | } 10 | 11 | function Example() { 12 | const [controlledVisible, setControlledVisible] = React.useState(false); 13 | 14 | const { 15 | getArrowProps, 16 | getTooltipProps, 17 | setTooltipRef, 18 | setTriggerRef, 19 | } = usePopperTooltip({ 20 | visible: controlledVisible, 21 | onVisibleChange: setControlledVisible, 22 | }); 23 | 24 | const transitions = useTransition(controlledVisible, null, { 25 | from: { opacity: 0 }, 26 | enter: { opacity: 1 }, 27 | leave: { opacity: 0 }, 28 | }); 29 | 30 | return ( 31 |
32 |

react-popper-tooltip

33 |

34 | A show/hide animation example using{' '} 35 | react-spring library. 36 |

37 | 38 | 41 | 42 | {transitions.map( 43 | ({ item, key, props }) => 44 | item && ( 45 | 53 | Tooltip element 54 |
55 | 56 | ) 57 | )} 58 |
59 | ); 60 | } 61 | 62 | const rootElement = document.getElementById('root'); 63 | ReactDOM.render(, rootElement); 64 | -------------------------------------------------------------------------------- /examples/basic/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | yarn.lock 15 | package-lock.json 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /examples/basic/.rescriptsrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const resolveFrom = require('resolve-from') 3 | 4 | const fixLinkedDependencies = config => { 5 | config.resolve = { 6 | ...config.resolve, 7 | alias: { 8 | ...config.resolve.alias, 9 | react$: resolveFrom(path.resolve('node_modules'), 'react'), 10 | 'react-dom$': resolveFrom(path.resolve('node_modules'), 'react-dom'), 11 | }, 12 | } 13 | return config 14 | } 15 | 16 | module.exports = [ 17 | fixLinkedDependencies, 18 | ] 19 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run this example: 4 | 5 | - `npm install` or `yarn` 6 | - `npm run start` or `yarn start` 7 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "^17.0.1", 4 | "react-dom": "^17.0.1", 5 | "react-popper-tooltip": "^4.0.0", 6 | "react-scripts": "3.4.3" 7 | }, 8 | "devDependencies": { 9 | "@rescripts/cli": "^0.0.15" 10 | }, 11 | "scripts": { 12 | "start": "rescripts start", 13 | "build": "rescripts build", 14 | "test": "rescripts test --env=jsdom", 15 | "eject": "rescripts eject" 16 | }, 17 | "browserslist": [ 18 | ">0.2%", 19 | "not dead", 20 | "not ie <= 11", 21 | "not op_mini all" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /examples/basic/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/basic/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/basic/src/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { usePopperTooltip } from 'react-popper-tooltip'; 4 | import 'react-popper-tooltip/dist/styles.css'; 5 | 6 | function App() { 7 | return ; 8 | } 9 | 10 | function Example() { 11 | const { 12 | getArrowProps, 13 | getTooltipProps, 14 | setTooltipRef, 15 | setTriggerRef, 16 | visible, 17 | } = usePopperTooltip(); 18 | 19 | return ( 20 | <> 21 |
22 |

Basic example

23 | 24 | 27 | 28 | {visible && ( 29 |
33 | Tooltip element 34 |
35 |
36 | )} 37 |
38 | 39 | ); 40 | } 41 | 42 | const rootElement = document.getElementById('root'); 43 | ReactDOM.render(, rootElement); 44 | -------------------------------------------------------------------------------- /examples/close-on-esc/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | yarn.lock 15 | package-lock.json 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /examples/close-on-esc/.rescriptsrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const resolveFrom = require('resolve-from') 3 | 4 | const fixLinkedDependencies = config => { 5 | config.resolve = { 6 | ...config.resolve, 7 | alias: { 8 | ...config.resolve.alias, 9 | react$: resolveFrom(path.resolve('node_modules'), 'react'), 10 | 'react-dom$': resolveFrom(path.resolve('node_modules'), 'react-dom'), 11 | }, 12 | } 13 | return config 14 | } 15 | 16 | module.exports = [ 17 | fixLinkedDependencies, 18 | ] 19 | -------------------------------------------------------------------------------- /examples/close-on-esc/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run this example: 4 | 5 | - `npm install` or `yarn` 6 | - `npm run start` or `yarn start` 7 | -------------------------------------------------------------------------------- /examples/close-on-esc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "^17.0.1", 4 | "react-dom": "^17.0.1", 5 | "react-popper-tooltip": "^4.0.0", 6 | "react-scripts": "3.4.3" 7 | }, 8 | "devDependencies": { 9 | "@rescripts/cli": "^0.0.15" 10 | }, 11 | "scripts": { 12 | "start": "rescripts start", 13 | "build": "rescripts build", 14 | "test": "rescripts test --env=jsdom", 15 | "eject": "rescripts eject" 16 | }, 17 | "browserslist": [ 18 | ">0.2%", 19 | "not dead", 20 | "not ie <= 11", 21 | "not op_mini all" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /examples/close-on-esc/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/close-on-esc/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/close-on-esc/src/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { usePopperTooltip } from 'react-popper-tooltip'; 4 | import 'react-popper-tooltip/dist/styles.css'; 5 | 6 | function App() { 7 | return ; 8 | } 9 | 10 | function Example() { 11 | const [controlledVisible, setControlledVisible] = React.useState(false); 12 | 13 | const { 14 | getArrowProps, 15 | getTooltipProps, 16 | setTooltipRef, 17 | setTriggerRef, 18 | visible, 19 | } = usePopperTooltip({ 20 | trigger: 'click', 21 | visible: controlledVisible, 22 | onVisibleChange: setControlledVisible, 23 | }); 24 | 25 | React.useEffect(() => { 26 | const handleKeyDown = ({ key }) => { 27 | if (key === 'Escape') { 28 | setControlledVisible(false); 29 | } 30 | }; 31 | 32 | document.addEventListener('keydown', handleKeyDown); 33 | 34 | return () => { 35 | document.removeEventListener('keydown', handleKeyDown); 36 | }; 37 | }, []); 38 | 39 | return ( 40 |
41 |

react-popper-tooltip

42 |

43 | This is an example of how to close the tooltip pressing the Esc button. 44 |

45 | 46 | 49 | 50 | {visible && ( 51 |
55 | Tooltip element 56 |
57 |
58 | )} 59 |
60 | ); 61 | } 62 | 63 | const rootElement = document.getElementById('root'); 64 | ReactDOM.render(, rootElement); 65 | -------------------------------------------------------------------------------- /examples/controlled/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | yarn.lock 15 | package-lock.json 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /examples/controlled/.rescriptsrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const resolveFrom = require('resolve-from') 3 | 4 | const fixLinkedDependencies = config => { 5 | config.resolve = { 6 | ...config.resolve, 7 | alias: { 8 | ...config.resolve.alias, 9 | react$: resolveFrom(path.resolve('node_modules'), 'react'), 10 | 'react-dom$': resolveFrom(path.resolve('node_modules'), 'react-dom'), 11 | }, 12 | } 13 | return config 14 | } 15 | 16 | module.exports = [ 17 | fixLinkedDependencies, 18 | ] 19 | -------------------------------------------------------------------------------- /examples/controlled/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run this example: 4 | 5 | - `npm install` or `yarn` 6 | - `npm run start` or `yarn start` 7 | -------------------------------------------------------------------------------- /examples/controlled/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "^17.0.1", 4 | "react-dom": "^17.0.1", 5 | "react-popper-tooltip": "^4.0.0", 6 | "react-scripts": "3.4.3" 7 | }, 8 | "devDependencies": { 9 | "@rescripts/cli": "^0.0.15" 10 | }, 11 | "scripts": { 12 | "start": "rescripts start", 13 | "build": "rescripts build", 14 | "test": "rescripts test --env=jsdom", 15 | "eject": "rescripts eject" 16 | }, 17 | "browserslist": [ 18 | ">0.2%", 19 | "not dead", 20 | "not ie <= 11", 21 | "not op_mini all" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /examples/controlled/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/controlled/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/controlled/src/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { usePopperTooltip } from 'react-popper-tooltip'; 4 | import 'react-popper-tooltip/dist/styles.css'; 5 | 6 | function App() { 7 | return ; 8 | } 9 | 10 | function Example() { 11 | const [controlledVisible, setControlledVisible] = React.useState(false); 12 | 13 | const { 14 | getArrowProps, 15 | getTooltipProps, 16 | setTooltipRef, 17 | setTriggerRef, 18 | visible, 19 | } = usePopperTooltip({ 20 | trigger: 'click', 21 | closeOnOutsideClick: false, 22 | visible: controlledVisible, 23 | onVisibleChange: setControlledVisible, 24 | }); 25 | 26 | return ( 27 |
28 |

react-popper-tooltip

29 |

30 | This is an example of using react-popper-tooltip as a controlled 31 | component. 32 |

33 | 34 | 37 | 38 |

39 | External state control - click the button below to show/hide the 40 | tooltip. 41 |

42 | 45 | 46 | {visible && ( 47 |
51 | Tooltip element 52 |
53 |
54 | )} 55 |
56 | ); 57 | } 58 | 59 | const rootElement = document.getElementById('root'); 60 | ReactDOM.render(, rootElement); 61 | -------------------------------------------------------------------------------- /examples/persist-once-mounted/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | yarn.lock 15 | package-lock.json 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /examples/persist-once-mounted/.rescriptsrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const resolveFrom = require('resolve-from') 3 | 4 | const fixLinkedDependencies = config => { 5 | config.resolve = { 6 | ...config.resolve, 7 | alias: { 8 | ...config.resolve.alias, 9 | react$: resolveFrom(path.resolve('node_modules'), 'react'), 10 | 'react-dom$': resolveFrom(path.resolve('node_modules'), 'react-dom'), 11 | }, 12 | } 13 | return config 14 | } 15 | 16 | module.exports = [ 17 | fixLinkedDependencies, 18 | ] 19 | -------------------------------------------------------------------------------- /examples/persist-once-mounted/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run this example: 4 | 5 | - `npm install` or `yarn` 6 | - `npm run start` or `yarn start` 7 | -------------------------------------------------------------------------------- /examples/persist-once-mounted/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "^17.0.1", 4 | "react-dom": "^17.0.1", 5 | "react-popper-tooltip": "^4.0.0", 6 | "react-scripts": "3.4.3" 7 | }, 8 | "devDependencies": { 9 | "@rescripts/cli": "^0.0.15" 10 | }, 11 | "scripts": { 12 | "start": "rescripts start", 13 | "build": "rescripts build", 14 | "test": "rescripts test --env=jsdom", 15 | "eject": "rescripts eject" 16 | }, 17 | "browserslist": [ 18 | ">0.2%", 19 | "not dead", 20 | "not ie <= 11", 21 | "not op_mini all" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /examples/persist-once-mounted/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/persist-once-mounted/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/persist-once-mounted/src/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { usePopperTooltip } from 'react-popper-tooltip'; 4 | import 'react-popper-tooltip/dist/styles.css'; 5 | 6 | function App() { 7 | return ; 8 | } 9 | 10 | function HeavyCalculations() { 11 | const [counter, setCounter] = React.useState(0); 12 | return ( 13 |
14 |

15 | Some heavy calculatins happens here when mounted. The component state 16 | preserved when tooltip shows/hides. 17 |

18 |

19 | 20 | {counter} 21 |

22 |
23 | ); 24 | } 25 | 26 | function Example() { 27 | const [mounted, setMounted] = React.useState(false); 28 | 29 | const { 30 | getArrowProps, 31 | getTooltipProps, 32 | setTooltipRef, 33 | setTriggerRef, 34 | visible, 35 | } = usePopperTooltip({ 36 | trigger: 'click', 37 | interactive: true, 38 | onVisibleChange: setMountedOnceVisible, 39 | }); 40 | 41 | function setMountedOnceVisible(visible) { 42 | if (!mounted && visible) { 43 | setMounted(true); 44 | } 45 | } 46 | 47 | return ( 48 |
49 |

react-popper-tooltip

50 |

51 | In this example, the tooltip stays in the DOM once mounted. It can be 52 | helpful for heavy components to avoid unnecessary mounting/dismounting 53 | whenever tooltip is hidden or shown or when you want to preserve the 54 | tooltip content's state. 55 |

56 |

57 | Mounted: {mounted ? 'yes' : 'no'}, visible: {visible ? 'yes' : 'no'} 58 |

59 | 60 | 63 | 64 | {mounted && ( 65 |
74 |
77 | 78 |
79 | )} 80 |
81 | ); 82 | } 83 | 84 | const rootElement = document.getElementById('root'); 85 | ReactDOM.render(, rootElement); 86 | -------------------------------------------------------------------------------- /examples/portal/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | yarn.lock 15 | package-lock.json 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /examples/portal/.rescriptsrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const resolveFrom = require('resolve-from') 3 | 4 | const fixLinkedDependencies = config => { 5 | config.resolve = { 6 | ...config.resolve, 7 | alias: { 8 | ...config.resolve.alias, 9 | react$: resolveFrom(path.resolve('node_modules'), 'react'), 10 | 'react-dom$': resolveFrom(path.resolve('node_modules'), 'react-dom'), 11 | }, 12 | } 13 | return config 14 | } 15 | 16 | module.exports = [ 17 | fixLinkedDependencies, 18 | ] 19 | -------------------------------------------------------------------------------- /examples/portal/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run this example: 4 | 5 | - `npm install` or `yarn` 6 | - `npm run start` or `yarn start` 7 | -------------------------------------------------------------------------------- /examples/portal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "^17.0.1", 4 | "react-dom": "^17.0.1", 5 | "react-popper-tooltip": "^4.0.0", 6 | "react-scripts": "3.4.3" 7 | }, 8 | "devDependencies": { 9 | "@rescripts/cli": "^0.0.15" 10 | }, 11 | "scripts": { 12 | "start": "rescripts start", 13 | "build": "rescripts build", 14 | "test": "rescripts test --env=jsdom", 15 | "eject": "rescripts eject" 16 | }, 17 | "browserslist": [ 18 | ">0.2%", 19 | "not dead", 20 | "not ie <= 11", 21 | "not op_mini all" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /examples/portal/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/portal/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/portal/src/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { usePopperTooltip } from 'react-popper-tooltip'; 4 | import 'react-popper-tooltip/src/styles.css'; 5 | 6 | function App() { 7 | return ; 8 | } 9 | 10 | function Example() { 11 | const { 12 | getArrowProps, 13 | getTooltipProps, 14 | setTooltipRef, 15 | setTriggerRef, 16 | visible, 17 | } = usePopperTooltip(); 18 | 19 | return ( 20 |
21 |

react-popper-tooltip

22 |

Using a react portal to render a tooltip.

23 | 24 | 27 | 28 | {visible && 29 | ReactDOM.createPortal( 30 |
34 | Tooltip element 35 |
36 |
, 37 | document.body 38 | )} 39 |
40 | ); 41 | } 42 | 43 | const rootElement = document.getElementById('root'); 44 | ReactDOM.render(, rootElement); 45 | -------------------------------------------------------------------------------- /examples/render-prop/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | yarn.lock 15 | package-lock.json 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /examples/render-prop/.rescriptsrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const resolveFrom = require('resolve-from') 3 | 4 | const fixLinkedDependencies = config => { 5 | config.resolve = { 6 | ...config.resolve, 7 | alias: { 8 | ...config.resolve.alias, 9 | react$: resolveFrom(path.resolve('node_modules'), 'react'), 10 | 'react-dom$': resolveFrom(path.resolve('node_modules'), 'react-dom'), 11 | }, 12 | } 13 | return config 14 | } 15 | 16 | module.exports = [ 17 | fixLinkedDependencies, 18 | ] 19 | -------------------------------------------------------------------------------- /examples/render-prop/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run this example: 4 | 5 | - `npm install` or `yarn` 6 | - `npm run start` or `yarn start` 7 | -------------------------------------------------------------------------------- /examples/render-prop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "react": "^17.0.1", 4 | "react-dom": "^17.0.1", 5 | "react-popper-tooltip": "^4.0.0", 6 | "react-scripts": "3.4.3" 7 | }, 8 | "devDependencies": { 9 | "@rescripts/cli": "^0.0.15" 10 | }, 11 | "scripts": { 12 | "start": "rescripts start", 13 | "build": "rescripts build", 14 | "test": "rescripts test --env=jsdom", 15 | "eject": "rescripts eject" 16 | }, 17 | "browserslist": [ 18 | ">0.2%", 19 | "not dead", 20 | "not ie <= 11", 21 | "not op_mini all" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /examples/render-prop/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/render-prop/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/render-prop/src/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ReactDOM, { createPortal } from 'react-dom'; 3 | import { usePopperTooltip } from 'react-popper-tooltip'; 4 | import 'react-popper-tooltip/dist/styles.css'; 5 | 6 | function App() { 7 | return ; 8 | } 9 | 10 | const canUseDOM = Boolean( 11 | typeof window !== 'undefined' && 12 | window.document && 13 | window.document.createElement 14 | ); 15 | 16 | const mutationObserverDefaults = { 17 | childList: true, 18 | subtree: true, 19 | }; 20 | 21 | function TooltipTrigger({ 22 | children, 23 | // Some defaults changed in the hook implementation. 24 | // For backward compatibility we have to override them here. 25 | closeOnReferenceHidden = true, 26 | defaultTooltipShown, 27 | getTriggerRef, 28 | modifiers, 29 | mutationObserverOptions = mutationObserverDefaults, 30 | onVisibilityChange, 31 | placement = 'right', 32 | portalContainer = canUseDOM ? document.body : null, 33 | tooltip, 34 | tooltipShown, 35 | usePortal = canUseDOM, 36 | ...restProps 37 | }) { 38 | const { 39 | triggerRef, 40 | getArrowProps, 41 | getTooltipProps, 42 | setTooltipRef, 43 | setTriggerRef, 44 | visible, 45 | state, 46 | } = usePopperTooltip( 47 | { 48 | // Some props renamed in the hook implementation. 49 | defaultVisible: defaultTooltipShown, 50 | onVisibleChange: onVisibilityChange, 51 | visible: tooltipShown, 52 | closeOnTriggerHidden: closeOnReferenceHidden, 53 | ...restProps, 54 | }, 55 | { 56 | placement, 57 | modifiers, 58 | } 59 | ); 60 | 61 | const reference = children({ 62 | // No longer required, for backward compatibility. 63 | getTriggerProps: (props) => props, 64 | triggerRef: setTriggerRef, 65 | }); 66 | 67 | const popper = tooltip({ 68 | tooltipRef: setTooltipRef, 69 | getArrowProps, 70 | getTooltipProps, 71 | placement: state ? state.placement : undefined, 72 | }); 73 | 74 | React.useEffect(() => { 75 | if (typeof getTriggerRef === 'function') getTriggerRef(triggerRef); 76 | }, [triggerRef, getTriggerRef]); 77 | 78 | return ( 79 | <> 80 | {reference} 81 | {visible 82 | ? usePortal 83 | ? createPortal(popper, portalContainer) 84 | : popper 85 | : null} 86 | 87 | ); 88 | } 89 | 90 | function Example() { 91 | return ( 92 |
93 |

react-popper-tooltip

94 |

Render prop example

95 | 96 | ( 99 |
105 |
110 | Tooltip element 111 |
112 | )} 113 | > 114 | {({ triggerRef }) => ( 115 | 118 | )} 119 | 120 |
121 | ); 122 | } 123 | 124 | const rootElement = document.getElementById('root'); 125 | ReactDOM.render(, rootElement); 126 | -------------------------------------------------------------------------------- /migrating.md: -------------------------------------------------------------------------------- 1 | # Migrating from `TooltipTrigger` to `usePopperTooltip` 2 | 3 | Version 4.x introduced the `usePopperTooltip` hook and dropped the support of the `TooltipTrigger` component utilizing 4 | render prop pattern. 5 | 6 | This guide will provide you with the information to help you upgrade to 4.x hooks. 7 | 8 | Here's an example of using the `TooltipTrigger` component using some common options. 9 | 10 | ```jsx 11 | ( 15 |
21 |
26 | Tooltip element 27 |
28 | )} 29 | > 30 | {({ triggerRef }) => ( 31 | 34 | )} 35 | 36 | ``` 37 | 38 | Here's the same component rewritten using the hook: 39 | 40 | ```js 41 | const { 42 | getArrowProps, 43 | getTooltipProps, 44 | setTooltipRef, 45 | setTriggerRef, 46 | visible, 47 | } = usePopperTooltip({ trigger: 'click', delayHide: 1000 }); 48 | 49 | return ( 50 | <> 51 | 54 | 55 | {visible && ( 56 |
60 | Tooltip element 61 |
62 |
63 | )} 64 | 65 | ); 66 | ``` 67 | 68 | When you use the hook, all props that you previously passed to the `TooltipTrigger` component now are arguments of the 69 | hook itself. Please note, that some props have been renamed, so in some cases you can't just copy-paste them from your 70 | render prop component to the hook. See the [release notes to 4.x](release-notes.md). 71 | 72 | The hook returns an object containing set of properties. `setTooltipRef` and `setTriggerRef` are ref callbacks and have 73 | to be assigned to the tooltip and trigger elements accordingly in order to let the hook have access to the underlying 74 | DOM elements. 75 | 76 | Previously, they called `triggerRef` and `tooltipRef`, and had the same meaning of the ref callbacks. Now the hook 77 | returns properties with these names as well but in the hook version they actually contain the corresponding DOM 78 | elements. You don't need to use `getTriggerRef` to get a ref of the trigger element anymore. 79 | 80 | The `tooltip` and `children` props have now been removed. Now you completely responsible for the composition of your 81 | tooltip. If you, for example, want to have your tooltip rendered through React portal, you have to import react-dom and 82 | use `createPortal` in your code. 83 | 84 | Use `visible` property to show or hide the tooltip. 85 | 86 | If you still have questions, see [examples section](README.md#examples) for more code examples using the hook. 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-popper-tooltip", 3 | "version": "4.4.2", 4 | "description": "React tooltip library built around react-popper", 5 | "author": "Mohsin Ul Haq ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/mohsinulhaq/react-popper-tooltip" 10 | }, 11 | "keywords": [ 12 | "react", 13 | "tooltip", 14 | "popover", 15 | "overlay", 16 | "react-tooltip", 17 | "react-popper" 18 | ], 19 | "main": "dist/cjs/react-popper-tooltip.js", 20 | "module": "dist/esm/react-popper-tooltip.js", 21 | "browser": "dist/esm/react-popper-tooltip.js", 22 | "typings": "dist/index.d.ts", 23 | "style": "dist/styles.css", 24 | "files": [ 25 | "dist" 26 | ], 27 | "sideEffects": [ 28 | "dist/styles.css" 29 | ], 30 | "scripts": { 31 | "build": "rm -rf dist && rollup -c && cp src/styles.css dist && yarn tsc && rm -rf compiled", 32 | "prepare": "husky install && yarn typecheck && yarn build && yarn test && yarn lint-staged", 33 | "prettier": "prettier --write src/**/*.{ts,tsx}", 34 | "typecheck": "tsc --noEmit", 35 | "lint": "eslint \"{src,tests,examples}**/*.{ts,tsx}\"", 36 | "start": "rollup -c -w", 37 | "test": "jest --env=jsdom tests", 38 | "storybook": "start-storybook -p 6006", 39 | "build-storybook": "build-storybook" 40 | }, 41 | "jest": { 42 | "modulePathIgnorePatterns": [ 43 | "/examples/" 44 | ], 45 | "setupFilesAfterEnv": [ 46 | "@testing-library/jest-dom/extend-expect" 47 | ] 48 | }, 49 | "lint-staged": { 50 | "**/*.(ts|tsx)": [ 51 | "yarn lint --fix" 52 | ] 53 | }, 54 | "peerDependencies": { 55 | "react": ">=16.6.0", 56 | "react-dom": ">=16.6.0" 57 | }, 58 | "dependencies": { 59 | "@babel/runtime": "^7.18.3", 60 | "@popperjs/core": "^2.11.5", 61 | "react-popper": "^2.3.0" 62 | }, 63 | "devDependencies": { 64 | "@babel/core": "^7.18.5", 65 | "@babel/plugin-transform-runtime": "^7.18.5", 66 | "@babel/preset-env": "^7.18.2", 67 | "@babel/preset-react": "^7.17.12", 68 | "@babel/preset-typescript": "^7.17.12", 69 | "@storybook/addon-actions": "^6.5.9", 70 | "@storybook/addon-essentials": "^6.5.9", 71 | "@storybook/addon-links": "^6.5.9", 72 | "@storybook/react": "^6.5.9", 73 | "@testing-library/jest-dom": "^5.16.4", 74 | "@testing-library/react": "^13.3.0", 75 | "@testing-library/user-event": "^14.2.1", 76 | "@types/jest": "^28.1.3", 77 | "@types/react": "^18.0.14", 78 | "@types/react-dom": "^18.0.5", 79 | "@typescript-eslint/eslint-plugin": "^5.29.0", 80 | "@typescript-eslint/parser": "^5.29.0", 81 | "babel-loader": "^8.2.5", 82 | "eslint": "^8.18.0", 83 | "eslint-config-prettier": "^8.5.0", 84 | "eslint-plugin-prettier": "^4.0.0", 85 | "eslint-plugin-react": "^7.30.1", 86 | "eslint-plugin-react-hooks": "^4.6.0", 87 | "husky": "^8.0.1", 88 | "jest": "^28.1.1", 89 | "jest-environment-jsdom": "^28.1.1", 90 | "lint-staged": "^13.0.3", 91 | "prettier": "^2.7.1", 92 | "react": "^18.2.0", 93 | "react-dom": "^18.2.0", 94 | "rollup": "^2.75.7", 95 | "rollup-plugin-babel": "^4.4.0", 96 | "rollup-plugin-node-resolve": "^5.2.0", 97 | "rollup-plugin-size-snapshot": "^0.12.0", 98 | "typescript": "^4.7.4" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /release-notes.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | With **react-popper**, used under the hood of **react-popper-tooltip**, introducing the `usePopper` hook in the last 4 | major update, we're now releasing the hook version of our own library as well - `usePopperTooltip`. The hook provides many new features and flexibility 5 | and allows for implementations not possible before. 6 | 7 | ## Breaking changes 8 | 9 | This release onwards, the hook is the only way of creating tooltips. This version drops the support of 10 | the `TooltipTrigger` render prop component. If you want to upgrade and still keep using render prop API, 11 | refer to our example section to implement the legacy API with our new hook. 12 | 13 | We wrote this version from scratch. Although thoroughly tested, it can still possibly contain some regressions. Please, 14 | report any problems using the [issues link](https://github.com/mohsinulhaq/react-popper-tooltip/issues). 15 | 16 | - For the sake of consistency, we made some changes to the props names. 17 | 18 | - `defaultTooltipShown` is renamed to `defaultVisible` 19 | - `tooltipShown` is renamed to `visible` 20 | - `onVisibilityChange` is renamed to `onVisibleChange` 21 | - `closeOnReferenceHidden` is renamed to `closeOnTriggerHidden` and the default value changed from `true` to `false` 22 | 23 | - The default placement is now `bottom` instead of `right`, in line with react-popper defaults. 24 | 25 | - The string value `"none"` for the prop `trigger` is replaced with `null`. 26 | 27 | - The default CSS has a few positioning and naming changes. 28 | 29 | - Previously, when a user hovered the tooltip, it stayed open to allow the user to interact with the tooltip's content. 30 | Now the tooltip closes as soon as the cursor leaves the trigger element. The new option `interactive` has been added to 31 | configure this behavior. 32 | 33 | - `getTriggerProps` and `arrowRef` are no longer needed. 34 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import { sizeSnapshot } from 'rollup-plugin-size-snapshot'; 4 | import pkg from './package.json'; 5 | 6 | const input = 'src/index.ts'; 7 | const external = (id) => !id.startsWith('.') && !id.startsWith('/'); 8 | const getBabelOptions = (useESModules = true) => ({ 9 | extensions: ['.ts', '.tsx'], 10 | runtimeHelpers: true, 11 | plugins: [['@babel/plugin-transform-runtime', { useESModules }]], 12 | }); 13 | 14 | export default [ 15 | { 16 | input, 17 | output: { 18 | file: pkg.main, 19 | exports: 'auto', 20 | format: 'cjs', 21 | interop: false, 22 | sourcemap: true, 23 | }, 24 | external, 25 | plugins: [ 26 | resolve({ extensions: ['.ts', '.tsx'] }), 27 | babel(getBabelOptions(false)), 28 | sizeSnapshot(), 29 | ], 30 | }, 31 | { 32 | input, 33 | output: { 34 | file: pkg.module, 35 | format: 'esm', 36 | sourcemap: true, 37 | }, 38 | external, 39 | plugins: [ 40 | resolve({ extensions: ['.ts', '.tsx'] }), 41 | babel(getBabelOptions()), 42 | sizeSnapshot(), 43 | ], 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { usePopperTooltip } from './usePopperTooltip'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | .tooltip-container { 2 | --tooltipBackground: #fff; 3 | --tooltipBorder: #c0c0c0; 4 | --tooltipColor: #000; 5 | 6 | background-color: var(--tooltipBackground); 7 | border-radius: 3px; 8 | border: 1px solid var(--tooltipBorder); 9 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.18); 10 | color: var(--tooltipColor); 11 | display: flex; 12 | flex-direction: column; 13 | padding: 0.4rem; 14 | transition: opacity 0.3s; 15 | z-index: 9999; 16 | } 17 | 18 | .tooltip-container[data-popper-interactive='false'] { 19 | pointer-events: none; 20 | } 21 | 22 | .tooltip-arrow { 23 | height: 1rem; 24 | position: absolute; 25 | width: 1rem; 26 | pointer-events: none; 27 | } 28 | 29 | .tooltip-arrow::before { 30 | border-style: solid; 31 | content: ''; 32 | display: block; 33 | height: 0; 34 | margin: auto; 35 | width: 0; 36 | } 37 | 38 | .tooltip-arrow::after { 39 | border-style: solid; 40 | content: ''; 41 | display: block; 42 | height: 0; 43 | margin: auto; 44 | position: absolute; 45 | width: 0; 46 | } 47 | 48 | .tooltip-container[data-popper-placement*='bottom'] .tooltip-arrow { 49 | left: 0; 50 | margin-top: -0.3rem; 51 | top: 0; 52 | } 53 | 54 | .tooltip-container[data-popper-placement*='bottom'] .tooltip-arrow::before { 55 | border-color: transparent transparent var(--tooltipBorder) transparent; 56 | border-width: 0 0.5rem 0.4rem 0.5rem; 57 | position: absolute; 58 | top: -1px; 59 | } 60 | 61 | .tooltip-container[data-popper-placement*='bottom'] .tooltip-arrow::after { 62 | border-color: transparent transparent var(--tooltipBackground) transparent; 63 | border-width: 0 0.5rem 0.4rem 0.5rem; 64 | } 65 | 66 | .tooltip-container[data-popper-placement*='top'] .tooltip-arrow { 67 | bottom: 0; 68 | left: 0; 69 | margin-bottom: -1rem; 70 | } 71 | 72 | .tooltip-container[data-popper-placement*='top'] .tooltip-arrow::before { 73 | border-color: var(--tooltipBorder) transparent transparent transparent; 74 | border-width: 0.4rem 0.5rem 0 0.5rem; 75 | position: absolute; 76 | top: 1px; 77 | } 78 | 79 | .tooltip-container[data-popper-placement*='top'] .tooltip-arrow::after { 80 | border-color: var(--tooltipBackground) transparent transparent transparent; 81 | border-width: 0.4rem 0.5rem 0 0.5rem; 82 | } 83 | 84 | .tooltip-container[data-popper-placement*='right'] .tooltip-arrow { 85 | left: 0; 86 | margin-left: -0.7rem; 87 | } 88 | 89 | .tooltip-container[data-popper-placement*='right'] .tooltip-arrow::before { 90 | border-color: transparent var(--tooltipBorder) transparent transparent; 91 | border-width: 0.5rem 0.4rem 0.5rem 0; 92 | } 93 | 94 | .tooltip-container[data-popper-placement*='right'] .tooltip-arrow::after { 95 | border-color: transparent var(--tooltipBackground) transparent transparent; 96 | border-width: 0.5rem 0.4rem 0.5rem 0; 97 | left: 6px; 98 | top: 0; 99 | } 100 | 101 | .tooltip-container[data-popper-placement*='left'] .tooltip-arrow { 102 | margin-right: -0.7rem; 103 | right: 0; 104 | } 105 | 106 | .tooltip-container[data-popper-placement*='left'] .tooltip-arrow::before { 107 | border-color: transparent transparent transparent var(--tooltipBorder); 108 | border-width: 0.5rem 0 0.5rem 0.4rem; 109 | } 110 | 111 | .tooltip-container[data-popper-placement*='left'] .tooltip-arrow::after { 112 | border-color: transparent transparent transparent var(--tooltipBackground); 113 | border-width: 0.5rem 0 0.5rem 0.4rem; 114 | left: 3px; 115 | top: 0; 116 | } 117 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as PopperJS from '@popperjs/core'; 3 | 4 | export type TriggerType = 5 | | 'click' 6 | | 'double-click' 7 | | 'right-click' 8 | | 'hover' 9 | | 'focus'; 10 | 11 | export type Config = { 12 | /** 13 | * Whether to close the tooltip when its trigger is out of boundary 14 | * @default false 15 | */ 16 | closeOnTriggerHidden?: boolean; 17 | /** 18 | * Event or events that trigger the tooltip 19 | * @default hover 20 | */ 21 | trigger?: TriggerType | TriggerType[] | null; 22 | /** 23 | * Delay in hiding the tooltip (ms) 24 | * @default 0 25 | */ 26 | delayHide?: number; 27 | /** 28 | * Delay in showing the tooltip (ms) 29 | * @default 0 30 | */ 31 | delayShow?: number; 32 | /** 33 | * Whether to make the tooltip spawn at cursor position 34 | * @default false 35 | */ 36 | followCursor?: boolean; 37 | /** 38 | * Options to MutationObserver, used internally for updating 39 | * tooltip position based on its DOM changes 40 | * @default { attributes: true, childList: true, subtree: true } 41 | */ 42 | mutationObserverOptions?: MutationObserverInit | null; 43 | /** 44 | * Whether tooltip is shown by default 45 | * @default false 46 | */ 47 | defaultVisible?: boolean; 48 | /** 49 | * Used to create controlled tooltip 50 | */ 51 | visible?: boolean; 52 | /** 53 | * Called when the visibility of the tooltip changes 54 | */ 55 | onVisibleChange?: (state: boolean) => void; 56 | /** 57 | * If `true`, a click outside the trigger element closes the tooltip 58 | * @default true 59 | */ 60 | closeOnOutsideClick?: boolean; 61 | /** 62 | * If `true`, hovering the tooltip will keep it open. Normally tooltip closes when the mouse cursor moves out of 63 | * the trigger element. If it moves to the tooltip element, the tooltip stays open. 64 | * @default false 65 | */ 66 | interactive?: boolean; 67 | /** 68 | * Alias for popper.js placement, see https://popper.js.org/docs/v2/constructors/#placement 69 | */ 70 | placement?: PopperJS.Placement; 71 | /** 72 | * Shorthand for popper.js offset modifier, see https://popper.js.org/docs/v2/modifiers/offset/ 73 | * @default [0, 6] 74 | */ 75 | offset?: [number, number]; 76 | }; 77 | 78 | export type PopperOptions = Partial & { 79 | createPopper?: typeof PopperJS.createPopper; 80 | }; 81 | 82 | export type PropsGetterArgs = { 83 | style?: React.CSSProperties; 84 | [key: string]: unknown; 85 | }; 86 | -------------------------------------------------------------------------------- /src/usePopperTooltip.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import type { VirtualElement } from '@popperjs/core'; 3 | import { usePopper } from 'react-popper'; 4 | import { 5 | useControlledState, 6 | useGetLatest, 7 | generateBoundingClientRect, 8 | } from './utils'; 9 | import { Config, PopperOptions, PropsGetterArgs, TriggerType } from './types'; 10 | 11 | const virtualElement: VirtualElement = { 12 | getBoundingClientRect: generateBoundingClientRect(), 13 | }; 14 | 15 | const defaultConfig: Config = { 16 | closeOnOutsideClick: true, 17 | closeOnTriggerHidden: false, 18 | defaultVisible: false, 19 | delayHide: 0, 20 | delayShow: 0, 21 | followCursor: false, 22 | interactive: false, 23 | mutationObserverOptions: { 24 | attributes: true, 25 | childList: true, 26 | subtree: true, 27 | }, 28 | offset: [0, 6], 29 | trigger: 'hover', 30 | }; 31 | 32 | export function usePopperTooltip( 33 | config: Config = {}, 34 | popperOptions: PopperOptions = {} 35 | ) { 36 | // Merging options with default options. 37 | // Keys with undefined values are replaced with the default ones if any. 38 | // Keys with other values pass through. 39 | const finalConfig = ( 40 | Object.keys(defaultConfig) as Array 41 | ).reduce( 42 | (config, key) => ({ 43 | ...config, 44 | [key]: config[key] !== undefined ? config[key] : defaultConfig[key], 45 | }), 46 | config 47 | ); 48 | 49 | const defaultModifiers = React.useMemo( 50 | () => [{ name: 'offset', options: { offset: finalConfig.offset } }], 51 | // eslint-disable-next-line react-hooks/exhaustive-deps 52 | Array.isArray(finalConfig.offset) ? finalConfig.offset : [] 53 | ); 54 | 55 | const finalPopperOptions = { 56 | ...popperOptions, 57 | placement: popperOptions.placement || finalConfig.placement, 58 | modifiers: popperOptions.modifiers || defaultModifiers, 59 | }; 60 | 61 | const [triggerRef, setTriggerRef] = React.useState(null); 62 | const [tooltipRef, setTooltipRef] = React.useState(null); 63 | const [visible, setVisible] = useControlledState({ 64 | initial: finalConfig.defaultVisible, 65 | value: finalConfig.visible, 66 | onChange: finalConfig.onVisibleChange, 67 | }); 68 | 69 | const timer = React.useRef(); 70 | React.useEffect(() => () => clearTimeout(timer.current), []); 71 | 72 | const { styles, attributes, ...popperProps } = usePopper( 73 | finalConfig.followCursor ? virtualElement : triggerRef, 74 | tooltipRef, 75 | finalPopperOptions 76 | ); 77 | 78 | const update = popperProps.update; 79 | 80 | const getLatest = useGetLatest({ 81 | visible, 82 | triggerRef, 83 | tooltipRef, 84 | finalConfig, 85 | }); 86 | 87 | const isTriggeredBy = React.useCallback( 88 | (trigger: TriggerType) => { 89 | return Array.isArray(finalConfig.trigger) 90 | ? finalConfig.trigger.includes(trigger) 91 | : finalConfig.trigger === trigger; 92 | }, 93 | // eslint-disable-next-line react-hooks/exhaustive-deps 94 | Array.isArray(finalConfig.trigger) 95 | ? finalConfig.trigger 96 | : [finalConfig.trigger] 97 | ); 98 | 99 | const hideTooltip = React.useCallback(() => { 100 | clearTimeout(timer.current); 101 | timer.current = window.setTimeout( 102 | () => setVisible(false), 103 | finalConfig.delayHide 104 | ); 105 | }, [finalConfig.delayHide, setVisible]); 106 | 107 | const showTooltip = React.useCallback(() => { 108 | clearTimeout(timer.current); 109 | timer.current = window.setTimeout( 110 | () => setVisible(true), 111 | finalConfig.delayShow 112 | ); 113 | }, [finalConfig.delayShow, setVisible]); 114 | 115 | const toggleTooltip = React.useCallback(() => { 116 | if (getLatest().visible) { 117 | hideTooltip(); 118 | } else { 119 | showTooltip(); 120 | } 121 | }, [getLatest, hideTooltip, showTooltip]); 122 | 123 | // Handle click outside 124 | React.useEffect(() => { 125 | if (!getLatest().finalConfig.closeOnOutsideClick) return; 126 | 127 | const handleClickOutside: EventListener = (event) => { 128 | const { tooltipRef, triggerRef } = getLatest(); 129 | const target = event.composedPath?.()?.[0] || event.target; 130 | if (target instanceof Node) { 131 | if ( 132 | tooltipRef != null && 133 | triggerRef != null && 134 | !tooltipRef.contains(target) && 135 | !triggerRef.contains(target) 136 | ) { 137 | hideTooltip(); 138 | } 139 | } 140 | }; 141 | document.addEventListener('mousedown', handleClickOutside); 142 | 143 | return () => document.removeEventListener('mousedown', handleClickOutside); 144 | }, [getLatest, hideTooltip]); 145 | 146 | // Trigger: click 147 | React.useEffect(() => { 148 | if (triggerRef == null || !isTriggeredBy('click')) return; 149 | 150 | triggerRef.addEventListener('click', toggleTooltip); 151 | 152 | return () => triggerRef.removeEventListener('click', toggleTooltip); 153 | }, [triggerRef, isTriggeredBy, toggleTooltip]); 154 | 155 | // Trigger: double-click 156 | React.useEffect(() => { 157 | if (triggerRef == null || !isTriggeredBy('double-click')) return; 158 | 159 | triggerRef.addEventListener('dblclick', toggleTooltip); 160 | 161 | return () => triggerRef.removeEventListener('dblclick', toggleTooltip); 162 | }, [triggerRef, isTriggeredBy, toggleTooltip]); 163 | 164 | // Trigger: right-click 165 | React.useEffect(() => { 166 | if (triggerRef == null || !isTriggeredBy('right-click')) return; 167 | 168 | const preventDefaultAndToggle: EventListener = (event) => { 169 | // Don't show the context menu 170 | event.preventDefault(); 171 | toggleTooltip(); 172 | }; 173 | 174 | triggerRef.addEventListener('contextmenu', preventDefaultAndToggle); 175 | return () => 176 | triggerRef.removeEventListener('contextmenu', preventDefaultAndToggle); 177 | }, [triggerRef, isTriggeredBy, toggleTooltip]); 178 | 179 | // Trigger: focus 180 | React.useEffect(() => { 181 | if (triggerRef == null || !isTriggeredBy('focus')) return; 182 | 183 | triggerRef.addEventListener('focus', showTooltip); 184 | triggerRef.addEventListener('blur', hideTooltip); 185 | return () => { 186 | triggerRef.removeEventListener('focus', showTooltip); 187 | triggerRef.removeEventListener('blur', hideTooltip); 188 | }; 189 | }, [triggerRef, isTriggeredBy, showTooltip, hideTooltip]); 190 | 191 | // Trigger: hover on trigger 192 | React.useEffect(() => { 193 | if (triggerRef == null || !isTriggeredBy('hover')) return; 194 | 195 | triggerRef.addEventListener('mouseenter', showTooltip); 196 | triggerRef.addEventListener('mouseleave', hideTooltip); 197 | return () => { 198 | triggerRef.removeEventListener('mouseenter', showTooltip); 199 | triggerRef.removeEventListener('mouseleave', hideTooltip); 200 | }; 201 | }, [triggerRef, isTriggeredBy, showTooltip, hideTooltip]); 202 | 203 | // Trigger: hover on tooltip, keep it open if hovered 204 | React.useEffect(() => { 205 | if ( 206 | tooltipRef == null || 207 | !isTriggeredBy('hover') || 208 | !getLatest().finalConfig.interactive 209 | ) 210 | return; 211 | 212 | tooltipRef.addEventListener('mouseenter', showTooltip); 213 | tooltipRef.addEventListener('mouseleave', hideTooltip); 214 | return () => { 215 | tooltipRef.removeEventListener('mouseenter', showTooltip); 216 | tooltipRef.removeEventListener('mouseleave', hideTooltip); 217 | }; 218 | }, [tooltipRef, isTriggeredBy, showTooltip, hideTooltip, getLatest]); 219 | 220 | // Handle closing tooltip if trigger hidden 221 | const isReferenceHidden = 222 | popperProps?.state?.modifiersData?.hide?.isReferenceHidden; 223 | React.useEffect(() => { 224 | if (finalConfig.closeOnTriggerHidden && isReferenceHidden) hideTooltip(); 225 | }, [finalConfig.closeOnTriggerHidden, hideTooltip, isReferenceHidden]); 226 | 227 | // Handle follow cursor 228 | React.useEffect(() => { 229 | if (!finalConfig.followCursor || triggerRef == null) return; 230 | 231 | function setMousePosition({ 232 | clientX, 233 | clientY, 234 | }: { 235 | clientX: number; 236 | clientY: number; 237 | }) { 238 | virtualElement.getBoundingClientRect = generateBoundingClientRect( 239 | clientX, 240 | clientY 241 | ); 242 | update?.(); 243 | } 244 | 245 | triggerRef.addEventListener('mousemove', setMousePosition); 246 | return () => triggerRef.removeEventListener('mousemove', setMousePosition); 247 | }, [finalConfig.followCursor, triggerRef, update]); 248 | 249 | // Handle tooltip DOM mutation changes (aka mutation observer) 250 | React.useEffect(() => { 251 | if ( 252 | tooltipRef == null || 253 | update == null || 254 | finalConfig.mutationObserverOptions == null 255 | ) 256 | return; 257 | 258 | const observer = new MutationObserver(update); 259 | observer.observe(tooltipRef, finalConfig.mutationObserverOptions); 260 | return () => observer.disconnect(); 261 | }, [finalConfig.mutationObserverOptions, tooltipRef, update]); 262 | 263 | // Tooltip props getter 264 | const getTooltipProps = (args: PropsGetterArgs = {}) => { 265 | return { 266 | ...args, 267 | style: { 268 | ...args.style, 269 | ...styles.popper, 270 | } as React.CSSProperties, 271 | ...attributes.popper, 272 | 'data-popper-interactive': finalConfig.interactive, 273 | }; 274 | }; 275 | 276 | // Arrow props getter 277 | const getArrowProps = (args: PropsGetterArgs = {}) => { 278 | return { 279 | ...args, 280 | ...attributes.arrow, 281 | style: { 282 | ...args.style, 283 | ...styles.arrow, 284 | } as React.CSSProperties, 285 | 'data-popper-arrow': true, 286 | }; 287 | }; 288 | 289 | return { 290 | getArrowProps, 291 | getTooltipProps, 292 | setTooltipRef, 293 | setTriggerRef, 294 | tooltipRef, 295 | triggerRef, 296 | visible, 297 | ...popperProps, 298 | }; 299 | } 300 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | // kudos to @tannerlinsley https://twitter.com/tannerlinsley 4 | export function useGetLatest(val: T) { 5 | const ref = React.useRef(val); 6 | ref.current = val; 7 | return React.useCallback(() => ref.current, []); 8 | } 9 | 10 | const noop = () => { 11 | // do nothing 12 | }; 13 | 14 | export function useControlledState({ 15 | initial, 16 | value, 17 | onChange = noop, 18 | }: { 19 | initial?: T; 20 | value?: T; 21 | onChange?: (state: T) => void; 22 | }): [T, (state: T) => void] { 23 | if (initial === undefined && value === undefined) { 24 | throw new TypeError( 25 | 'Either "value" or "initial" variable must be set. Now both are undefined' 26 | ); 27 | } 28 | 29 | const [state, setState] = React.useState(initial); 30 | 31 | const getLatest = useGetLatest(state); 32 | 33 | const set = React.useCallback( 34 | (updater: T) => { 35 | const state = getLatest(); 36 | 37 | const updatedState = 38 | typeof updater === 'function' ? updater(state) : updater; 39 | 40 | if (typeof updatedState.persist === 'function') updatedState.persist(); 41 | 42 | setState(updatedState); 43 | if (typeof onChange === 'function') onChange(updatedState); 44 | }, 45 | [getLatest, onChange] 46 | ); 47 | 48 | const isControlled = value !== undefined; 49 | 50 | return [isControlled ? value! : state!, isControlled ? onChange : set]; 51 | } 52 | 53 | export function generateBoundingClientRect(x = 0, y = 0): () => DOMRect { 54 | return () => ({ 55 | width: 0, 56 | height: 0, 57 | top: y, 58 | right: x, 59 | bottom: y, 60 | left: x, 61 | x: 0, 62 | y: 0, 63 | toJSON: () => null, 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /stories/basic.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Story, Meta } from '@storybook/react'; 3 | import { usePopperTooltip, Config } from '../src'; 4 | import '../src/styles.css'; 5 | 6 | type ExampleProps = Config & { offsetDistance?: number }; 7 | 8 | export const Example: Story = ({ 9 | offsetDistance, 10 | ...props 11 | }: ExampleProps) => { 12 | const [shown, setShown] = React.useState(false); 13 | const { 14 | visible, 15 | setTriggerRef, 16 | setTooltipRef, 17 | getArrowProps, 18 | getTooltipProps, 19 | } = usePopperTooltip({ 20 | ...props, 21 | offset: [0, offsetDistance], 22 | onVisibleChange: setShown, 23 | visible: shown, 24 | }); 25 | 26 | return ( 27 | <> 28 | 31 | 32 | {visible && ( 33 |
37 |
38 | Tooltip element 39 |
40 | )} 41 | 42 | ); 43 | }; 44 | 45 | Example.argTypes = { 46 | delayHide: { 47 | control: { 48 | type: 'number', 49 | options: { min: 0, step: 1 }, 50 | }, 51 | }, 52 | delayShow: { 53 | control: { 54 | type: 'number', 55 | options: { min: 0, step: 1 }, 56 | }, 57 | }, 58 | followCursor: { 59 | control: { 60 | type: 'boolean', 61 | }, 62 | }, 63 | interactive: { 64 | control: { 65 | type: 'boolean', 66 | }, 67 | }, 68 | offsetDistance: { 69 | control: { 70 | type: 'number', 71 | options: { min: 0, step: 1 }, 72 | }, 73 | defaultValue: 6, 74 | }, 75 | placement: { 76 | control: { 77 | type: 'select', 78 | options: ['top', 'right', 'bottom', 'left'], 79 | }, 80 | }, 81 | trigger: { 82 | control: { 83 | type: 'select', 84 | options: ['hover', 'click', 'right-click', 'focus', null], 85 | }, 86 | }, 87 | }; 88 | 89 | export default { 90 | title: 'usePopperTooltip', 91 | } as Meta; 92 | -------------------------------------------------------------------------------- /tests/usePopperTooltip.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | render, 4 | screen, 5 | waitFor, 6 | fireEvent, 7 | act, 8 | } from '@testing-library/react'; 9 | import userEvent from '@testing-library/user-event'; 10 | import { usePopperTooltip, Config } from '../src'; 11 | 12 | const TriggerText = 'Trigger'; 13 | const TooltipText = 'Tooltip'; 14 | 15 | function Tooltip({ options }: { options: Config }) { 16 | const { 17 | setTriggerRef, 18 | setTooltipRef, 19 | getArrowProps, 20 | getTooltipProps, 21 | visible, 22 | } = usePopperTooltip(options); 23 | 24 | return ( 25 | <> 26 | 27 | 28 | {visible && ( 29 |
30 |
31 | {TooltipText} 32 |
33 | )} 34 | 35 | ); 36 | } 37 | 38 | beforeEach(() => { 39 | jest.useFakeTimers(); 40 | }); 41 | 42 | afterEach(() => { 43 | jest.runOnlyPendingTimers(); 44 | jest.useRealTimers(); 45 | }); 46 | 47 | describe('trigger option', () => { 48 | test('hover trigger', async () => { 49 | render(); 50 | 51 | // tooltip not visible initially 52 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 53 | 54 | // tooltip shown on hover in 55 | userEvent.hover(screen.getByText(TriggerText)); 56 | expect(await screen.findByText(TooltipText)).toBeInTheDocument(); 57 | 58 | // tooltip hidden on hover out 59 | userEvent.unhover(screen.getByText(TriggerText)); 60 | await waitFor(() => { 61 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 62 | }); 63 | }); 64 | 65 | test('click trigger', async () => { 66 | render(); 67 | 68 | // tooltip not visible initially 69 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 70 | 71 | // tooltip shown on click 72 | userEvent.click(screen.getByText(TriggerText)); 73 | expect(await screen.findByText(TooltipText)).toBeInTheDocument(); 74 | 75 | // tooltip hidden on click 76 | userEvent.click(screen.getByText(TriggerText)); 77 | await waitFor(() => { 78 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 79 | }); 80 | }); 81 | 82 | test('right-click trigger', async () => { 83 | render(); 84 | 85 | // tooltip not visible initially 86 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 87 | 88 | // tooltip shown on right-click 89 | fireEvent.contextMenu(screen.getByText(TriggerText)); 90 | expect(await screen.findByText(TooltipText)).toBeInTheDocument(); 91 | 92 | // tooltip hidden on right-click 93 | fireEvent.contextMenu(screen.getByText(TriggerText)); 94 | await waitFor(() => { 95 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 96 | }); 97 | }); 98 | 99 | test('focus trigger', async () => { 100 | render(); 101 | 102 | // tooltip not visible initially 103 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 104 | 105 | // tooltip shown on focus 106 | fireEvent.focus(screen.getByText(TriggerText)); 107 | expect(await screen.findByText(TooltipText)).toBeInTheDocument(); 108 | 109 | // tooltip hidden on blur 110 | fireEvent.blur(screen.getByText(TriggerText)); 111 | await waitFor(() => { 112 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 113 | }); 114 | }); 115 | 116 | test('trigger array', async () => { 117 | render(); 118 | 119 | // tooltip not visible initially 120 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 121 | 122 | // tooltip shown on hover 123 | userEvent.hover(screen.getByText(TriggerText)); 124 | expect(await screen.findByText(TooltipText)).toBeInTheDocument(); 125 | 126 | // tooltip hidden on click 127 | userEvent.click(screen.getByText(TriggerText)); 128 | await waitFor(() => { 129 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 130 | }); 131 | }); 132 | 133 | test('null trigger', async () => { 134 | render(); 135 | 136 | // tooltip not visible initially 137 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 138 | 139 | // Nothing after hover 140 | userEvent.hover(screen.getByText(TriggerText)); 141 | jest.runAllTimers(); 142 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 143 | 144 | // Nothing after click 145 | userEvent.click(screen.getByText(TriggerText)); 146 | jest.runAllTimers(); 147 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 148 | 149 | // Nothing after right-click 150 | fireEvent.contextMenu(screen.getByText(TriggerText)); 151 | jest.runAllTimers(); 152 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 153 | 154 | // Nothing after focus 155 | fireEvent.focus(screen.getByText(TriggerText)); 156 | jest.runAllTimers(); 157 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 158 | }); 159 | }); 160 | 161 | test('closeOnOutsideClick removes tooltip on document.body click', async () => { 162 | render(); 163 | 164 | // Show on click 165 | userEvent.click(screen.getByText(TriggerText)); 166 | expect(await screen.findByText(TooltipText)).toBeInTheDocument(); 167 | 168 | // Hide on body click 169 | userEvent.click(document.body); 170 | await waitFor(() => { 171 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 172 | }); 173 | }); 174 | 175 | test("closeOnOutsideClick doesn't remove tooltip on tooltip click", async () => { 176 | render(); 177 | 178 | // Show on click 179 | userEvent.click(screen.getByText(TriggerText)); 180 | expect(await screen.findByText(TooltipText)).toBeInTheDocument(); 181 | 182 | userEvent.click(screen.getByText(TooltipText)); 183 | await waitFor(() => { 184 | expect(screen.queryByText(TooltipText)).toBeInTheDocument(); 185 | }); 186 | }); 187 | 188 | test('delayShow option renders tooltip after specified delay', async () => { 189 | render(); 190 | 191 | userEvent.hover(screen.getByText(TriggerText)); 192 | // Nothing after a 2000ms 193 | jest.advanceTimersByTime(2000); 194 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 195 | 196 | act(() => { 197 | jest.runAllTimers(); 198 | }); 199 | // It shows up sometime later. Here RTL uses fake timers to await as well, so 200 | // it awaits for the element infinitely, advancing jest fake timer by 50ms 201 | // in an endless loop. And this is why the test passes even if delayShow set 202 | // a way bigger than a default timeout of 1000ms would allow. 203 | expect(await screen.findByText(TooltipText)).toBeInTheDocument(); 204 | }); 205 | 206 | test('delayHide option removes tooltip after specified delay', async () => { 207 | render(); 208 | 209 | userEvent.hover(screen.getByText(TriggerText)); 210 | act(() => { 211 | jest.runAllTimers(); 212 | }); 213 | expect(await screen.findByText(TooltipText)).toBeInTheDocument(); 214 | 215 | userEvent.unhover(screen.getByText(TriggerText)); 216 | // Still present after 2000ms 217 | act(() => { 218 | jest.advanceTimersByTime(2000); 219 | }); 220 | expect(screen.getByText(TooltipText)).toBeInTheDocument(); 221 | act(() => { 222 | jest.runAllTimers(); 223 | }); 224 | // Removed some time later 225 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 226 | }); 227 | 228 | describe('defaultVisible option', () => { 229 | test('with false value renders nothing', async () => { 230 | render(); 231 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 232 | }); 233 | 234 | test('with true value renders tooltip', async () => { 235 | render(); 236 | expect(await screen.findByText(TooltipText)).toBeInTheDocument(); 237 | }); 238 | }); 239 | 240 | test('onVisibleChange option called when state changes', async () => { 241 | const onVisibleChange = jest.fn(); 242 | render(); 243 | 244 | // By default not visible, change visible to true when first time hover 245 | userEvent.hover(screen.getByText(TriggerText)); 246 | expect(await screen.findByText(TooltipText)).toBeInTheDocument(); 247 | expect(onVisibleChange).toHaveBeenLastCalledWith(true); 248 | 249 | // Now visible, change visible to false when unhover 250 | userEvent.unhover(screen.getByText(TriggerText)); 251 | await waitFor(() => { 252 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 253 | }); 254 | expect(onVisibleChange).toHaveBeenLastCalledWith(false); 255 | expect(onVisibleChange).toHaveBeenCalledTimes(2); 256 | }); 257 | 258 | describe('visible option controls the state and', () => { 259 | test('with false value renders nothing', async () => { 260 | render(); 261 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 262 | 263 | // The state is controlled, click doesn't change it 264 | userEvent.click(screen.getByText(TriggerText)); 265 | act(() => { 266 | jest.runAllTimers(); 267 | }); 268 | expect(screen.queryByText(TooltipText)).not.toBeInTheDocument(); 269 | }); 270 | 271 | test('with true value renders tooltip', async () => { 272 | render(); 273 | expect(await screen.findByText(TooltipText)).toBeInTheDocument(); 274 | 275 | // The state is controlled, click doesn't change it 276 | userEvent.click(screen.getByText(TriggerText)); 277 | act(() => { 278 | jest.runAllTimers(); 279 | }); 280 | expect(await screen.findByText(TooltipText)).toBeInTheDocument(); 281 | }); 282 | }); 283 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": [ 7 | "dom", 8 | "esnext" 9 | ], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "declarationDir": "dist", 18 | "outDir": "compiled", /* Redirect output structure to the directory. */ 19 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 20 | // "composite": true, /* Enable project compilation */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | "forceConsistentCasingInFileNames": true, 27 | "resolveJsonModule": true, 28 | 29 | /* Strict Type-Checking Options */ 30 | "strict": true, /* Enable all strict type-checking options. */ 31 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 32 | // "strictNullChecks": true, /* Enable strict null checks. */ 33 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 34 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 35 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 36 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 37 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 38 | 39 | /* Additional Checks */ 40 | "noUnusedLocals": true, /* Report errors on unused locals. */ 41 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 42 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | 45 | /* Module Resolution Options */ 46 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | }, 66 | "include": [ 67 | "src", 68 | ] 69 | } 70 | --------------------------------------------------------------------------------