├── .dumi ├── app.tsx └── tsconfig.json ├── .dumirc.ts ├── .editorconfig ├── .eslintrc.js ├── .fatherrc.js ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── react-component-ci.yml │ └── site-deploy.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── boostrap │ └── anim.less ├── bootstrap.less └── bootstrap_white.less ├── docs ├── changelog.md ├── demo │ ├── arrowContent.md │ ├── formError.md │ ├── onVisibleChange.md │ ├── placement.md │ ├── point.md │ ├── showArrow.md │ └── simple.md ├── examples │ ├── arrowContent.tsx │ ├── formError.tsx │ ├── onVisibleChange.tsx │ ├── placement.tsx │ ├── point.tsx │ ├── showArrow.tsx │ └── simple.tsx └── index.md ├── package.json ├── src ├── Popup.tsx ├── Tooltip.tsx ├── index.tsx └── placements.tsx ├── tests ├── __mocks__ │ └── @rc-component │ │ └── trigger.js ├── index.test.tsx ├── popup.test.tsx └── setup.js ├── tsconfig.json ├── type.d.ts └── vercel.json /.dumi/app.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate } from 'dumi'; 2 | import * as React from 'react'; 3 | 4 | export function patchClientRoutes({ routes }) { 5 | routes[0].children.unshift({ 6 | id: 'demo-redirect', 7 | path: '/demo', 8 | element: , 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /.dumi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /.dumirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi'; 2 | import path from 'path'; 3 | 4 | const isProdSite = 5 | // 不是预览模式 同时是生产环境 6 | process.env.PREVIEW !== 'true' && process.env.NODE_ENV === 'production'; 7 | 8 | const name = 'tooltip'; 9 | 10 | export default defineConfig({ 11 | alias: { 12 | 'rc-tooltip$': path.resolve('src'), 13 | 'rc-tooltip/es': path.resolve('src'), 14 | }, 15 | mfsu: false, 16 | favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'], 17 | themeConfig: { 18 | name: 'Tooltip', 19 | logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', 20 | }, 21 | base: isProdSite ? `/${name}/` : '/', 22 | publicPath: isProdSite ? `/${name}/` : '/', 23 | }); 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const base = require('@umijs/fabric/dist/eslint'); 2 | 3 | module.exports = { 4 | ...base, 5 | rules: { 6 | ...base.rules, 7 | 'no-template-curly-in-string': 0, 8 | 'prefer-promise-reject-errors': 0, 9 | 'react/no-array-index-key': 0, 10 | 'react/sort-comp': 0, 11 | '@typescript-eslint/no-explicit-any': 0, 12 | 'jsx-a11y/label-has-associated-control': 0, 13 | 'jsx-a11y/label-has-for': 0, 14 | 'import/no-extraneous-dependencies': 0, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /.fatherrc.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | plugins: ['@rc-component/father-plugin'], 5 | }); -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ant-design # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: ant-design # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "21:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: "@types/react" 11 | versions: 12 | - 17.0.0 13 | - 17.0.1 14 | - 17.0.2 15 | - 17.0.3 16 | - dependency-name: "@types/react-dom" 17 | versions: 18 | - 17.0.0 19 | - 17.0.1 20 | - 17.0.2 21 | - dependency-name: less 22 | versions: 23 | - 4.1.0 24 | -------------------------------------------------------------------------------- /.github/workflows/react-component-ci.yml: -------------------------------------------------------------------------------- 1 | name: ✅ test 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | uses: react-component/rc-test/.github/workflows/test.yml@main 6 | secrets: inherit -------------------------------------------------------------------------------- /.github/workflows/site-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy website 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build-and-deploy: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: setup node 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 14 22 | 23 | - name: create package-lock.json 24 | run: npm i --package-lock-only --ignore-scripts 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - name: build Docs 30 | run: npm run build 31 | 32 | - name: Deploy to GitHub Pages 33 | uses: peaceiris/actions-gh-pages@v3 34 | with: 35 | github_token: ${{ secrets.GITHUB_TOKEN }} 36 | publish_dir: ./dist 37 | force_orphan: true 38 | user_name: 'github-actions[bot]' 39 | user_email: 'github-actions[bot]@users.noreply.github.com' 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .storybook 2 | *.iml 3 | *.log 4 | *.log.* 5 | .idea/ 6 | .ipr 7 | .iws 8 | *~ 9 | ~* 10 | *.diff 11 | *.patch 12 | *.bak 13 | .DS_Store 14 | Thumbs.db 15 | .project 16 | .*proj 17 | .svn/ 18 | *.swp 19 | *.swo 20 | *.pyc 21 | *.pyo 22 | .build 23 | node_modules 24 | .cache 25 | dist 26 | assets/**/*.css 27 | build 28 | lib 29 | es 30 | coverage 31 | yarn.lock 32 | package-lock.json 33 | pnpm-lock.yaml 34 | 35 | # umi 36 | .umi 37 | .umi-production 38 | .umi-test 39 | .env.local 40 | 41 | # dumi 42 | .dumi/tmp 43 | .dumi/tmp-production 44 | 45 | bun.lockb 46 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "printWidth": 100 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.6.0 4 | 5 | - Support React 16. 6 | 7 | ## 3.5.0 8 | 9 | - Add id for ARIA. 10 | 11 | ## 3.4.4 12 | 13 | - Fix babel-runtime module not found 14 | 15 | ## 3.4.3 16 | 17 | - Fix `createClass` and `PropTypes` warning. 18 | 19 | ## 3.4.0 20 | 21 | - allow overlay prop as function type 22 | 23 | ## 3.3.0 24 | 25 | - support arrowContent prop 26 | 27 | ## 3.2.0 28 | 29 | - support destroyTooltipOnHide prop 30 | 31 | ## 3.0.0 32 | 33 | - only support react 0.14 34 | - add align prop to allow set offset and targetOffset when placement's type is String 35 | 36 | ## 2.10.0 37 | 38 | - auto adjust align if current tooltip is not visible 39 | 40 | ## 2.9.0 41 | 42 | - support 'topLeft', 'topRight', 'bottomLeft', 'bottomRight' for placement 43 | 44 | ## 2.8.0 45 | 46 | - add getTooltipContainer prop 47 | 48 | ## 2.7.0 49 | 50 | - add overlayClassName prop #16 51 | - split delay into mouseEnterDelay and mouseLeaveDelay #15 52 | 53 | ## 2.6.0 54 | 55 | remove renderOverlayToBody prop. defaults to true 56 | 57 | ## 2.5.0 / 2015-07-28 58 | 59 | use rc-animate & rc-align 60 | 61 | ## 2.4.0 / 2015-07-08 62 | 63 | revert to document click and fix focus/click conflict [#13](https://github.com/react-component/tooltip/issues/6) 64 | 65 | ## 2.3.0 / 2015-07-07 66 | 67 | `new` [#7](https://github.com/react-component/tooltip/issues/7) support delay prop 68 | 69 | ## 2.2.0 / 2015-06-30 70 | 71 | - use mask instead of document click 72 | 73 | ## 2.1.0 / 2015-06-15 74 | 75 | - support overlayStyle props 76 | - support wrapStyle props 77 | 78 | ## 2.0.0 / 2015-06-08 79 | 80 | - support click document to hide 81 | - support animation props 82 | - support renderPopupToBody props 83 | 84 | ## 1.1.1 / 2015-05-14 85 | 86 | add defaultVisible and onVisibleChange 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Alipay.com, https://www.alipay.com/ 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 16 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 19 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rc-tooltip 2 | 3 | React Tooltip 4 | 5 | [![NPM version][npm-image]][npm-url] 6 | [![npm download][download-image]][download-url] 7 | [![build status][github-actions-image]][github-actions-url] 8 | [![Codecov][codecov-image]][codecov-url] 9 | [![bundle size][bundlephobia-image]][bundlephobia-url] 10 | [![dumi][dumi-image]][dumi-url] 11 | 12 | [npm-image]: http://img.shields.io/npm/v/rc-tooltip.svg?style=flat-square 13 | [npm-url]: http://npmjs.org/package/rc-tooltip 14 | [travis-image]: https://img.shields.io/travis/react-component/tooltip/master?style=flat-square 15 | [travis-url]: https://travis-ci.com/react-component/tooltip 16 | [github-actions-image]: https://github.com/react-component/tooltip/workflows/CI/badge.svg 17 | [github-actions-url]: https://github.com/react-component/tooltip/actions 18 | [codecov-image]: https://img.shields.io/codecov/c/github/react-component/tooltip/master.svg?style=flat-square 19 | [codecov-url]: https://app.codecov.io/gh/react-component/tooltip 20 | [david-url]: https://david-dm.org/react-component/tooltip 21 | [david-image]: https://david-dm.org/react-component/tooltip/status.svg?style=flat-square 22 | [david-dev-url]: https://david-dm.org/react-component/tooltip?type=dev 23 | [david-dev-image]: https://david-dm.org/react-component/tooltip/dev-status.svg?style=flat-square 24 | [download-image]: https://img.shields.io/npm/dm/rc-tooltip.svg?style=flat-square 25 | [download-url]: https://npmjs.org/package/rc-tooltip 26 | [bundlephobia-url]: https://bundlephobia.com/package/rc-tooltip 27 | [bundlephobia-image]: https://badgen.net/bundlephobia/minzip/rc-tooltip 28 | [dumi-url]: https://github.com/umijs/dumi 29 | [dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square 30 | 31 | ## Screenshot 32 | 33 | 34 | 35 | ## Browsers support 36 | 37 | | [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | 38 | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 39 | | IE 8 + ✔ | Firefox 31.0+ ✔ | Chrome 31.0+ ✔ | Safari 7.0+ ✔ | Opera 30.0+ ✔ | 40 | 41 | ## Install 42 | 43 | [![rc-tooltip](https://nodei.co/npm/rc-tooltip.png)](https://npmjs.org/package/rc-tooltip) 44 | 45 | ## Usage 46 | 47 | ```js 48 | var Tooltip = require('rc-tooltip'); 49 | var React = require('react'); 50 | var ReactDOM = require('react-dom'); 51 | 52 | // By default, the tooltip has no style. 53 | // Consider importing the stylesheet it comes with: 54 | // 'rc-tooltip/assets/bootstrap_white.css' 55 | 56 | ReactDOM.render( 57 | tooltip}> 58 | hover 59 | , 60 | container, 61 | ); 62 | ``` 63 | 64 | ## Examples 65 | 66 | `npm start` and then go to 67 | 68 | 69 | Online demo: 70 | 71 | ## API 72 | 73 | ### Props 74 | 75 | | name | type | default | description | 76 | | ------------------- | ------------------------------------------------------------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | 77 | | trigger | string \| string\[] | 'hover' | which actions cause tooltip shown. enum of 'hover','click','focus' | 78 | | visible | boolean | false | whether tooltip is visible | 79 | | defaultVisible | boolean | false | whether tooltip is visible by default | 80 | | placement | string | 'right' | tooltip placement. enum of 'top','left','right','bottom', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight', 'leftTop', 'leftBottom', 'rightTop', 'rightBottom' | 81 | | motion | object | | Config popup motion. Please ref demo for example | 82 | | onVisibleChange | (visible: boolean) => void; | | Callback when visible change | 83 | | afterVisibleChange | (visible: boolean) => void; | | Callback after visible change | 84 | | overlay | ReactNode \| () => ReactNode | | tooltip overlay content | 85 | | overlayStyle | object | | deprecated, Please use `styles={{ root: {} }}` | 86 | | overlayClassName | string | | deprecated, Please use `classNames={{ root: {} }}` | 87 | | prefixCls | string | 'rc-tooltip' | prefix class name of tooltip | 88 | | mouseEnterDelay | number | 0 | delay time (in second) before tooltip shows when mouse enter | 89 | | mouseLeaveDelay | number | 0.1 | delay time (in second) before tooltip hides when mouse leave | 90 | | getTooltipContainer | (triggerNode: HTMLElement) => HTMLElement | () => document.body | get container of tooltip, default to body | 91 | | destroyOnHidden | boolean | false | destroy tooltip when it is hidden | 92 | | align | object | | align config of tooltip. Please ref demo for usage example | 93 | | showArrow | boolean \| object | false | whether to show arrow of tooltip | 94 | | zIndex | number | | config popup tooltip zIndex | 95 | | classNames | classNames?: { root?: string; body?: string;}; | | Semantic DOM class | 96 | | styles | styles?: {root?: React.CSSProperties;body?: React.CSSProperties;}; | | Semantic DOM styles | 97 | 98 | ## Important Note 99 | 100 | `Tooltip` requires a child node that accepts an `onMouseEnter`, `onMouseLeave`, `onFocus`, `onClick` event. This means the child node must be a built-in component like `div` or `span`, or a custom component that passes its props to its built-in component child. 101 | 102 | ## Accessibility 103 | 104 | For accessibility purpose you can use the `id` prop to link your tooltip with another element. For example attaching it to an input element: 105 | 106 | ```js 107 | 110 | 113 | 114 | ``` 115 | 116 | If you do it like this, a screenreader would read the content of your tooltip if you focus the input element. 117 | 118 | **NOTE:** `role="tooltip"` is also added to expose the purpose of the tooltip element to a screenreader. 119 | 120 | ## Development 121 | 122 | ```bash 123 | npm install 124 | npm start 125 | ``` 126 | 127 | ## Test Case 128 | 129 | ```bash 130 | npm test 131 | npm run chrome-test 132 | ``` 133 | 134 | ## Coverage 135 | 136 | ```bash 137 | npm run coverage 138 | ``` 139 | 140 | ## License 141 | 142 | `rc-tooltip` is released under the MIT license. 143 | -------------------------------------------------------------------------------- /assets/boostrap/anim.less: -------------------------------------------------------------------------------- 1 | .@{tooltip-prefix-cls} { 2 | .effect() { 3 | animation-duration: 0.3s; 4 | animation-fill-mode: both; 5 | } 6 | 7 | &&-zoom-appear, 8 | &&-zoom-enter { 9 | opacity: 0; 10 | } 11 | 12 | &&-zoom-enter, &&-zoom-leave { 13 | display: block; 14 | } 15 | 16 | &-zoom-enter, &-zoom-appear { 17 | opacity: 0; 18 | .effect(); 19 | animation-timing-function: cubic-bezier(0.18, 0.89, 0.32, 1.28); 20 | animation-play-state: paused; 21 | } 22 | 23 | &-zoom-leave { 24 | .effect(); 25 | animation-timing-function: cubic-bezier(0.6, -0.3, 0.74, 0.05); 26 | animation-play-state: paused; 27 | } 28 | 29 | &-zoom-enter&-zoom-enter-active, &-zoom-appear&-zoom-appear-active { 30 | animation-name: rcToolTipZoomIn; 31 | animation-play-state: running; 32 | } 33 | 34 | &-zoom-leave&-zoom-leave-active { 35 | animation-name: rcToolTipZoomOut; 36 | animation-play-state: running; 37 | } 38 | 39 | @keyframes rcToolTipZoomIn { 40 | 0% { 41 | opacity: 0; 42 | transform-origin: 50% 50%; 43 | transform: scale(0, 0); 44 | } 45 | 100% { 46 | opacity: 1; 47 | transform-origin: 50% 50%; 48 | transform: scale(1, 1); 49 | } 50 | } 51 | @keyframes rcToolTipZoomOut { 52 | 0% { 53 | opacity: 1; 54 | transform-origin: 50% 50%; 55 | transform: scale(1, 1); 56 | } 57 | 100% { 58 | opacity: 0; 59 | transform-origin: 50% 50%; 60 | transform: scale(0, 0); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /assets/bootstrap.less: -------------------------------------------------------------------------------- 1 | @import "./boostrap/anim.less"; 2 | 3 | @tooltip-prefix-cls: rc-tooltip; 4 | 5 | // 6 | // Tooltips 7 | // -------------------------------------------------- 8 | @font-size-base : 12px; 9 | @line-height-base : 1.5; 10 | @border-radius-base : 6px; 11 | @overlay-shadow : 0 0 4px rgba(0, 0, 0, 0.17); 12 | //** Tooltip text color 13 | @tooltip-color: #fff; 14 | //** Tooltip background color 15 | @tooltip-bg: #373737; 16 | @tooltip-opacity: 0.9; 17 | 18 | //** Tooltip arrow width 19 | @tooltip-arrow-width: 5px; 20 | //** Tooltip distance with trigger 21 | @tooltip-distance: @tooltip-arrow-width + 4; 22 | //** Tooltip arrow color 23 | @tooltip-arrow-color: @tooltip-bg; 24 | 25 | // Base class 26 | .@{tooltip-prefix-cls} { 27 | position: absolute; 28 | z-index: 1070; 29 | display: block; 30 | visibility: visible; 31 | // remove left/top by yiminghe 32 | // left: -9999px; 33 | // top: -9999px; 34 | font-size: @font-size-base; 35 | line-height: @line-height-base; 36 | opacity: @tooltip-opacity; 37 | 38 | &-hidden { 39 | display: none; 40 | } 41 | 42 | &-placement-top, &-placement-topLeft, &-placement-topRight { 43 | padding: @tooltip-arrow-width 0 @tooltip-distance 0; 44 | } 45 | &-placement-right, &-placement-rightTop, &-placement-rightBottom { 46 | padding: 0 @tooltip-arrow-width 0 @tooltip-distance; 47 | } 48 | &-placement-bottom, &-placement-bottomLeft, &-placement-bottomRight { 49 | padding: @tooltip-distance 0 @tooltip-arrow-width 0; 50 | } 51 | &-placement-left, &-placement-leftTop, &-placement-leftBottom { 52 | padding: 0 @tooltip-distance 0 @tooltip-arrow-width; 53 | } 54 | } 55 | 56 | // Wrapper for the tooltip content 57 | .@{tooltip-prefix-cls}-inner { 58 | padding: 8px 10px; 59 | color: @tooltip-color; 60 | text-align: left; 61 | text-decoration: none; 62 | background-color: @tooltip-bg; 63 | border-radius: @border-radius-base; 64 | box-shadow: @overlay-shadow; 65 | min-height: 34px; 66 | } 67 | 68 | // Arrows 69 | .@{tooltip-prefix-cls}-arrow { 70 | position: absolute; 71 | width: 0; 72 | height: 0; 73 | border-color: transparent; 74 | border-style: solid; 75 | } 76 | 77 | .@{tooltip-prefix-cls} { 78 | &-placement-top &-arrow, 79 | &-placement-topLeft &-arrow, 80 | &-placement-topRight &-arrow { 81 | bottom: @tooltip-distance - @tooltip-arrow-width; 82 | margin-left: -@tooltip-arrow-width; 83 | border-width: @tooltip-arrow-width @tooltip-arrow-width 0; 84 | border-top-color: @tooltip-arrow-color; 85 | } 86 | 87 | &-placement-top &-arrow { 88 | left: 50%; 89 | } 90 | 91 | &-placement-topLeft &-arrow { 92 | left: 15%; 93 | } 94 | 95 | &-placement-topRight &-arrow { 96 | right: 15%; 97 | } 98 | 99 | &-placement-right &-arrow, 100 | &-placement-rightTop &-arrow, 101 | &-placement-rightBottom &-arrow { 102 | left: @tooltip-distance - @tooltip-arrow-width; 103 | margin-top: -@tooltip-arrow-width; 104 | border-width: @tooltip-arrow-width @tooltip-arrow-width @tooltip-arrow-width 0; 105 | border-right-color: @tooltip-arrow-color; 106 | } 107 | 108 | &-placement-right &-arrow { 109 | top: 50%; 110 | } 111 | 112 | &-placement-rightTop &-arrow { 113 | top: 15%; 114 | margin-top: 0; 115 | } 116 | 117 | &-placement-rightBottom &-arrow { 118 | bottom: 15%; 119 | } 120 | 121 | &-placement-left &-arrow, 122 | &-placement-leftTop &-arrow, 123 | &-placement-leftBottom &-arrow { 124 | right: @tooltip-distance - @tooltip-arrow-width; 125 | margin-top: -@tooltip-arrow-width; 126 | border-width: @tooltip-arrow-width 0 @tooltip-arrow-width @tooltip-arrow-width; 127 | border-left-color: @tooltip-arrow-color; 128 | } 129 | 130 | &-placement-left &-arrow { 131 | top: 50%; 132 | } 133 | 134 | &-placement-leftTop &-arrow { 135 | top: 15%; 136 | margin-top: 0; 137 | } 138 | 139 | &-placement-leftBottom &-arrow { 140 | bottom: 15%; 141 | } 142 | 143 | &-placement-bottom &-arrow, 144 | &-placement-bottomLeft &-arrow, 145 | &-placement-bottomRight &-arrow { 146 | top: @tooltip-distance - @tooltip-arrow-width; 147 | margin-left: -@tooltip-arrow-width; 148 | border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; 149 | border-bottom-color: @tooltip-arrow-color; 150 | } 151 | 152 | &-placement-bottom &-arrow { 153 | left: 50%; 154 | } 155 | 156 | &-placement-bottomLeft &-arrow { 157 | left: 15%; 158 | } 159 | 160 | &-placement-bottomRight &-arrow { 161 | right: 15%; 162 | } 163 | } 164 | 165 | -------------------------------------------------------------------------------- /assets/bootstrap_white.less: -------------------------------------------------------------------------------- 1 | @import "./boostrap/anim.less"; 2 | 3 | @tooltip-prefix-cls: rc-tooltip; 4 | 5 | // 6 | // Tooltips 7 | // -------------------------------------------------- 8 | @font-size-base : 12px; 9 | @line-height-base : 1.5; 10 | @border-radius-base : 3px; 11 | @overlay-shadow : 0 0 4px rgba(0, 0, 0, 0.17); 12 | //** Tooltip text color 13 | @tooltip-color: #333333; 14 | //** Tooltip background color 15 | @tooltip-bg: #ffffff; 16 | @tooltip-opacity: 0.9; 17 | 18 | @tooltip-border-width: 1px; 19 | @tooltip-border-color: #b1b1b1; 20 | @tooltip-shadow-width: 1px; 21 | 22 | //** Tooltip arrow width 23 | @tooltip-arrow-width: 6px; 24 | //** Tooltip distance with trigger 25 | //** Tooltip arrow color 26 | @tooltip-arrow-color: @tooltip-border-color; 27 | @tooltip-arrow-inner-color: @tooltip-bg; 28 | 29 | // Base class 30 | .@{tooltip-prefix-cls} { 31 | position: absolute; 32 | z-index: 1070; 33 | display: block; 34 | visibility: visible; 35 | line-height: @line-height-base; 36 | font-size: @font-size-base; 37 | background-color:rgba(0, 0, 0, 0.05); 38 | padding: @tooltip-shadow-width; 39 | opacity: @tooltip-opacity; 40 | 41 | &-hidden { 42 | display: none; 43 | } 44 | } 45 | 46 | // Wrapper for the tooltip content 47 | .@{tooltip-prefix-cls}-inner { 48 | padding: 8px 10px; 49 | color: @tooltip-color; 50 | text-align: left; 51 | text-decoration: none; 52 | background-color: @tooltip-bg; 53 | border-radius: @border-radius-base; 54 | min-height: 34px; 55 | border:@tooltip-border-width solid @tooltip-border-color; 56 | } 57 | 58 | // Arrows 59 | .@{tooltip-prefix-cls}-arrow, 60 | .@{tooltip-prefix-cls}-arrow-inner{ 61 | position: absolute; 62 | width: 0; 63 | height: 0; 64 | border-color: transparent; 65 | border-style: solid; 66 | } 67 | 68 | .@{tooltip-prefix-cls} { 69 | &-placement-top &-arrow, 70 | &-placement-topLeft &-arrow, 71 | &-placement-topRight &-arrow{ 72 | // bottom: -@tooltip-arrow-width + @tooltip-shadow-width; 73 | transform: translate(-50%, @tooltip-arrow-width - @tooltip-shadow-width); 74 | margin-left: -@tooltip-arrow-width; 75 | border-width: @tooltip-arrow-width @tooltip-arrow-width 0; 76 | border-top-color: @tooltip-arrow-color; 77 | } 78 | 79 | &-placement-top &-arrow-inner, 80 | &-placement-topLeft &-arrow-inner, 81 | &-placement-topRight &-arrow-inner{ 82 | bottom: @tooltip-border-width; 83 | margin-left: -@tooltip-arrow-width; 84 | border-width: @tooltip-arrow-width @tooltip-arrow-width 0; 85 | border-top-color: @tooltip-arrow-inner-color; 86 | } 87 | 88 | &-placement-top &-arrow { 89 | left: 50%; 90 | } 91 | 92 | &-placement-topLeft &-arrow { 93 | left: 15%; 94 | } 95 | 96 | &-placement-topRight &-arrow { 97 | right: 15%; 98 | } 99 | 100 | &-placement-right &-arrow, 101 | &-placement-rightTop &-arrow, 102 | &-placement-rightBottom &-arrow { 103 | left: -@tooltip-arrow-width + @tooltip-shadow-width; 104 | margin-top: -@tooltip-arrow-width; 105 | border-width: @tooltip-arrow-width @tooltip-arrow-width @tooltip-arrow-width 0; 106 | border-right-color: @tooltip-arrow-color; 107 | transform: translateX(calc(-100% + @tooltip-shadow-width)); 108 | } 109 | 110 | &-placement-right &-arrow-inner, 111 | &-placement-rightTop &-arrow-inner, 112 | &-placement-rightBottom &-arrow-inner { 113 | left: @tooltip-border-width; 114 | margin-top: -@tooltip-arrow-width; 115 | border-width: @tooltip-arrow-width @tooltip-arrow-width @tooltip-arrow-width 0; 116 | border-right-color: @tooltip-arrow-inner-color; 117 | } 118 | 119 | &-placement-right &-arrow { 120 | top: 50%; 121 | } 122 | 123 | &-placement-rightTop &-arrow { 124 | top: 15%; 125 | margin-top: 0; 126 | } 127 | 128 | &-placement-rightBottom &-arrow { 129 | bottom: 15%; 130 | } 131 | 132 | &-placement-left &-arrow, 133 | &-placement-leftTop &-arrow, 134 | &-placement-leftBottom &-arrow { 135 | right: -@tooltip-arrow-width + @tooltip-shadow-width; 136 | margin-top: -@tooltip-arrow-width; 137 | border-width: @tooltip-arrow-width 0 @tooltip-arrow-width @tooltip-arrow-width; 138 | border-left-color: @tooltip-arrow-color; 139 | transform: translateX(calc(100% - @tooltip-shadow-width)); 140 | } 141 | 142 | &-placement-left &-arrow-inner, 143 | &-placement-leftTop &-arrow-inner, 144 | &-placement-leftBottom &-arrow-inner { 145 | right: @tooltip-border-width; 146 | margin-top: -@tooltip-arrow-width; 147 | border-width: @tooltip-arrow-width 0 @tooltip-arrow-width @tooltip-arrow-width; 148 | border-left-color: @tooltip-arrow-inner-color; 149 | } 150 | 151 | &-placement-left &-arrow { 152 | top: 50%; 153 | } 154 | 155 | &-placement-leftTop &-arrow { 156 | top: 15%; 157 | margin-top: 0; 158 | } 159 | 160 | &-placement-leftBottom &-arrow { 161 | bottom: 15%; 162 | } 163 | 164 | &-placement-bottom &-arrow, 165 | &-placement-bottomLeft &-arrow, 166 | &-placement-bottomRight &-arrow { 167 | // top: -@tooltip-arrow-width + @tooltip-shadow-width;; 168 | transform: translate(-50%, -@tooltip-arrow-width + @tooltip-shadow-width); 169 | margin-left: -@tooltip-arrow-width; 170 | border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; 171 | border-bottom-color: @tooltip-arrow-color; 172 | } 173 | 174 | &-placement-bottom &-arrow-inner, 175 | &-placement-bottomLeft &-arrow-inner, 176 | &-placement-bottomRight &-arrow-inner { 177 | top: @tooltip-border-width; 178 | margin-left: -@tooltip-arrow-width; 179 | border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; 180 | border-bottom-color: @tooltip-arrow-inner-color; 181 | } 182 | 183 | &-placement-bottom &-arrow { 184 | left: 50%; 185 | } 186 | 187 | &-placement-bottomLeft &-arrow { 188 | left: 15%; 189 | } 190 | 191 | &-placement-bottomRight &-arrow { 192 | right: 15%; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/demo/arrowContent.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: arrowContent 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | -------------------------------------------------------------------------------- /docs/demo/formError.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: formError 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | -------------------------------------------------------------------------------- /docs/demo/onVisibleChange.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: onVisibleChange 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | -------------------------------------------------------------------------------- /docs/demo/placement.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: placement 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | -------------------------------------------------------------------------------- /docs/demo/point.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Point 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | -------------------------------------------------------------------------------- /docs/demo/showArrow.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: showArrow 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/simple.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: simple 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | -------------------------------------------------------------------------------- /docs/examples/arrowContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import Tooltip from 'rc-tooltip'; 3 | import '../../assets/bootstrap_white.less'; 4 | 5 | const text = Tooltip Text; 6 | 7 | const styles: React.CSSProperties = { 8 | display: 'table-cell', 9 | height: '60px', 10 | width: '80px', 11 | textAlign: 'center', 12 | background: '#f6f6f6', 13 | verticalAlign: 'middle', 14 | border: '5px solid white', 15 | }; 16 | 17 | const rowStyle: React.CSSProperties = { 18 | display: 'table-row', 19 | }; 20 | 21 | const Test: React.FC = () => ( 22 |
23 |
24 |
} 28 | > 29 | 30 | Left 31 | 32 | 33 |
} 37 | > 38 | 39 | Top 40 | 41 | 42 | } 46 | > 47 | 48 | Bottom 49 | 50 | 51 | } 55 | > 56 | 57 | Right 58 | 59 | 60 | 61 |
62 |
} 66 | > 67 | 68 | Left Top 69 | 70 | 71 | } 75 | > 76 | 77 | Left Bottom 78 | 79 | 80 | } 84 | > 85 | 86 | Right Top 87 | 88 | 89 | } 93 | > 94 | 95 | Right Bottom 96 | 97 | 98 | 99 |
100 |
} 104 | > 105 | 106 | Top Left 107 | 108 | 109 | } 113 | > 114 | 115 | Top Right 116 | 117 | 118 | } 122 | > 123 | 124 | Bottom Left 125 | 126 | 127 | } 131 | > 132 | 133 | Bottom Right 134 | 135 | 136 | 137 | 138 | ); 139 | 140 | export default Test; 141 | -------------------------------------------------------------------------------- /docs/examples/formError.tsx: -------------------------------------------------------------------------------- 1 | import Tooltip from 'rc-tooltip'; 2 | import React, { Component } from 'react'; 3 | 4 | import '../../assets/bootstrap.less'; 5 | 6 | interface TestState { 7 | visible: boolean; 8 | destroy?: boolean; 9 | } 10 | 11 | class Test extends Component { 12 | state = { 13 | visible: false, 14 | } as TestState; 15 | 16 | handleDestroy = () => { 17 | this.setState({ 18 | destroy: true, 19 | }); 20 | }; 21 | 22 | handleChange = (e: React.ChangeEvent) => { 23 | this.setState({ visible: !e.target.value }); 24 | }; 25 | 26 | render() { 27 | if (this.state.destroy) { 28 | return null; 29 | } 30 | return ( 31 |
32 |
33 | required!} 39 | > 40 | 41 | 42 |
43 | 46 |
47 | ); 48 | } 49 | } 50 | 51 | export default Test; 52 | -------------------------------------------------------------------------------- /docs/examples/onVisibleChange.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Tooltip from 'rc-tooltip'; 3 | import '../../assets/bootstrap.less'; 4 | 5 | function preventDefault(e: React.MouseEvent) { 6 | e.preventDefault(); 7 | } 8 | 9 | interface TestState { 10 | visible: boolean; 11 | destroy?: boolean; 12 | } 13 | 14 | class Test extends Component { 15 | state = { 16 | visible: false, 17 | } as TestState; 18 | 19 | onVisibleChange = (visible: boolean) => { 20 | this.setState({ visible }); 21 | }; 22 | 23 | onDestroy = () => { 24 | this.setState({ 25 | destroy: true, 26 | }); 27 | }; 28 | 29 | render() { 30 | if (this.state.destroy) { 31 | return null; 32 | } 33 | return ( 34 |
35 |
36 | I am a tooltip} 42 | > 43 | 44 | trigger 45 | 46 | 47 |
48 | 51 |
52 | ); 53 | } 54 | } 55 | 56 | export default Test; 57 | -------------------------------------------------------------------------------- /docs/examples/placement.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tooltip from 'rc-tooltip'; 3 | import '../../assets/bootstrap.less'; 4 | import Popup from '../../src/Popup'; 5 | 6 | const text = Tooltip Text; 7 | 8 | const styles: React.CSSProperties = { 9 | display: 'table-cell', 10 | height: '60px', 11 | width: '80px', 12 | textAlign: 'center', 13 | background: '#f6f6f6', 14 | verticalAlign: 'middle', 15 | border: '5px solid white', 16 | }; 17 | 18 | const rowStyle: React.CSSProperties = { 19 | display: 'table-row', 20 | }; 21 | 22 | const Test: React.FC = () => ( 23 | <> 24 |
25 |
26 | 27 | 28 | Left 29 | 30 | 31 | 32 | 33 | Top 34 | 35 | 36 | 37 | 38 | Bottom 39 | 40 | 41 | 42 | 43 | Right 44 | 45 | 46 |
47 |
48 | 49 | 50 | Left Top 51 | 52 | 53 | 54 | 55 | Left Bottom 56 | 57 | 58 | 59 | 60 | Right Top 61 | 62 | 63 | 64 | 65 | Right Bottom 66 | 67 | 68 |
69 |
70 | 71 | 72 | Top Left 73 | 74 | 75 | 76 | 77 | Top Right 78 | 79 | 80 | 81 | 82 | Bottom Left 83 | 84 | 85 | 86 | 87 | Bottom Right 88 | 89 | 90 |
91 |
92 |
93 |
94 |
Debug Usage
95 | 100 | Test 101 | 102 |
103 | 104 | ); 105 | 106 | export default Test; 107 | -------------------------------------------------------------------------------- /docs/examples/point.tsx: -------------------------------------------------------------------------------- 1 | import Tooltip from 'rc-tooltip'; 2 | import React from 'react'; 3 | import '../../assets/bootstrap_white.less'; 4 | 5 | const text = Tooltip Text; 6 | 7 | const Test: React.FC = () => { 8 | const scrollRef = React.useRef(null); 9 | 10 | return ( 11 |
12 |
22 |
30 | } 36 | > 37 |
47 | Hover Me 48 |
49 |
50 |
51 |
52 |
53 | ); 54 | }; 55 | 56 | export default Test; 57 | -------------------------------------------------------------------------------- /docs/examples/showArrow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tooltip from 'rc-tooltip'; 3 | import '../../assets/bootstrap_white.less'; 4 | 5 | const text = Tooltip Text; 6 | 7 | const styles: React.CSSProperties = { 8 | display: 'table-cell', 9 | height: '60px', 10 | width: '80px', 11 | textAlign: 'center', 12 | background: '#f6f6f6', 13 | verticalAlign: 'middle', 14 | border: '5px solid white', 15 | }; 16 | 17 | const rowStyle: React.CSSProperties = { 18 | display: 'table-row', 19 | }; 20 | 21 | const Test: React.FC = () => ( 22 |
23 |
24 | 25 | 26 | Left 27 | 28 | 29 | 30 | 31 | Top 32 | 33 | 34 | 35 | 36 | Bottom 37 | 38 | 39 | 40 | 41 | Right 42 | 43 | 44 |
45 |
46 | 47 | 48 | Left Top 49 | 50 | 51 | 52 | 53 | Left Bottom 54 | 55 | 56 | 57 | 58 | Right Top 59 | 60 | 61 | 62 | 63 | Right Bottom 64 | 65 | 66 |
67 |
68 | 69 | 70 | Top Left 71 | 72 | 73 | 74 | 75 | Top Right 76 | 77 | 78 | 79 | 80 | Bottom Left 81 | 82 | 83 | 84 | 85 | Bottom Right 86 | 87 | 88 |
89 |
90 | ); 91 | 92 | export default Test; 93 | -------------------------------------------------------------------------------- /docs/examples/simple.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionType } from '@rc-component/trigger'; 2 | import type { OffsetType } from '@rc-component/trigger/lib/interface'; 3 | import Tooltip from 'rc-tooltip'; 4 | import type { CSSProperties } from 'react'; 5 | import React, { Component } from 'react'; 6 | import '../../assets/bootstrap.less'; 7 | import { placements } from '../../src/placements'; 8 | 9 | interface TestState { 10 | destroyOnHidden: boolean; 11 | destroyTooltipOptions: { name: string; value: number }[]; 12 | placement: string; 13 | transitionName: string; 14 | trigger: Record; 15 | offsetX?: OffsetType; 16 | offsetY?: OffsetType; 17 | overlayInnerStyle?: CSSProperties; 18 | } 19 | 20 | class Test extends Component { 21 | state = { 22 | destroyOnHidden: false, 23 | destroyTooltipOptions: [ 24 | { 25 | name: "don't destroy", 26 | value: 0, 27 | }, 28 | { 29 | name: 'destroy parent', 30 | value: 1, 31 | }, 32 | { 33 | name: 'keep parent', 34 | value: 2, 35 | }, 36 | ], 37 | placement: 'right', 38 | transitionName: 'rc-tooltip-zoom', 39 | trigger: { 40 | hover: 1, 41 | click: 0, 42 | focus: 0, 43 | } as Record, 44 | offsetX: placements.right.offset[0], 45 | offsetY: placements.right.offset[1], 46 | overlayInnerStyle: undefined, 47 | }; 48 | 49 | onPlacementChange = (e) => { 50 | const placement = e.target.value; 51 | const { offset } = placements[placement]; 52 | this.setState({ 53 | placement: e.target.value, 54 | offsetX: offset[0], 55 | offsetY: offset[1], 56 | }); 57 | }; 58 | 59 | onTransitionChange = (e) => { 60 | this.setState({ 61 | transitionName: e.target.checked ? e.target.value : '', 62 | }); 63 | }; 64 | 65 | onTriggerChange = (e) => { 66 | const { trigger } = this.state; 67 | if (e.target.checked) { 68 | trigger[e.target.value] = 1; 69 | } else { 70 | delete trigger[e.target.value]; 71 | } 72 | this.setState({ 73 | trigger, 74 | }); 75 | }; 76 | 77 | onOffsetXChange = (e) => { 78 | const targetValue = e.target.value; 79 | this.setState({ 80 | offsetX: targetValue || undefined, 81 | }); 82 | }; 83 | 84 | onOffsetYChange = (e) => { 85 | const targetValue = e.target.value; 86 | this.setState({ 87 | offsetY: targetValue || undefined, 88 | }); 89 | }; 90 | 91 | onVisibleChange = (visible) => { 92 | console.log('tooltip', visible); // eslint-disable-line no-console 93 | }; 94 | 95 | onDestroyChange = (e) => { 96 | const { value } = e.target; 97 | this.setState({ 98 | destroyOnHidden: [false, { keepParent: false }, { keepParent: true }][value] as boolean, 99 | }); 100 | }; 101 | 102 | onOverlayInnerStyleChange = () => { 103 | this.setState((prevState) => ({ 104 | overlayInnerStyle: prevState.overlayInnerStyle ? undefined : { background: 'red' }, 105 | })); 106 | }; 107 | 108 | preventDefault = (e) => { 109 | e.preventDefault(); 110 | }; 111 | 112 | render() { 113 | const { state } = this; 114 | const { trigger } = state; 115 | return ( 116 |
117 |
118 | 128 |      129 | 138 |      139 | 149 |      trigger: 150 | 159 | 168 | 177 |
178 | 187 |      188 | 197 | 206 |
207 |
208 | i am a tooltip
} 216 | align={{ 217 | offset: [this.state.offsetX, this.state.offsetY], 218 | }} 219 | motion={{ motionName: this.state.transitionName }} 220 | overlayInnerStyle={state.overlayInnerStyle} 221 | > 222 |
trigger
223 | 224 |
225 | 226 | ); 227 | } 228 | } 229 | 230 | export default Test; 231 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | title: rc-tooltip 4 | description: React Tooltip 5 | --- 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rc-component/tooltip", 3 | "version": "1.2.0", 4 | "description": "React Tooltip", 5 | "keywords": [ 6 | "react", 7 | "react-component", 8 | "react-tooltip", 9 | "tooltip" 10 | ], 11 | "homepage": "http://github.com/react-component/tooltip", 12 | "bugs": { 13 | "url": "http://github.com/react-component/tooltip/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git@github.com:react-component/tooltip.git" 18 | }, 19 | "license": "MIT", 20 | "maintainers": [ 21 | "yiminghe@gmail.com" 22 | ], 23 | "main": "lib/index", 24 | "module": "es/index", 25 | "files": [ 26 | "lib", 27 | "es", 28 | "assets/*.css", 29 | "assets/*.less" 30 | ], 31 | "scripts": { 32 | "compile": "father build && lessc assets/bootstrap.less assets/bootstrap.css && lessc assets/bootstrap_white.less assets/bootstrap_white.css", 33 | "docs:build": "dumi build", 34 | "docs:deploy": "npm run docs:build && gh-pages -d dist", 35 | "lint": "eslint src/ --ext .tsx,.ts,.jsx,.js", 36 | "now-build": "npm run docs:build", 37 | "prepare": "dumi setup", 38 | "prepublishOnly": "npm run compile && rc-np", 39 | "postpublish": "npm run docs:build && npm run docs:deploy", 40 | "start": "dumi dev", 41 | "test": "rc-test" 42 | }, 43 | "dependencies": { 44 | "@rc-component/father-plugin": "^2.0.1", 45 | "@rc-component/trigger": "^3.0.0", 46 | "@rc-component/util": "^1.0.1", 47 | "classnames": "^2.3.1" 48 | }, 49 | "devDependencies": { 50 | "@rc-component/np": "^1.0.3", 51 | "@testing-library/react": "^16.3.0", 52 | "@types/jest": "^29.4.0", 53 | "@types/node": "^22.15.18", 54 | "@types/react": "^19.1.4", 55 | "@types/react-dom": "^19.1.5", 56 | "@types/warning": "^3.0.0", 57 | "cross-env": "^7.0.0", 58 | "dumi": "^2.2.13", 59 | "eslint": "^8.56.0", 60 | "eslint-plugin-unicorn": "^55.0.0", 61 | "father": "^4.0.0", 62 | "gh-pages": "^3.1.0", 63 | "less": "^4.1.1", 64 | "rc-test": "^7.0.9", 65 | "react": "^19.1.0", 66 | "react-dom": "^19.1.0", 67 | "typescript": "^4.0.5" 68 | }, 69 | "peerDependencies": { 70 | "react": ">=18.0.0", 71 | "react-dom": ">=18.0.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Popup.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import * as React from 'react'; 3 | 4 | export interface ContentProps { 5 | prefixCls?: string; 6 | children: (() => React.ReactNode) | React.ReactNode; 7 | id?: string; 8 | overlayInnerStyle?: React.CSSProperties; 9 | className?: string; 10 | style?: React.CSSProperties; 11 | bodyClassName?: string; 12 | } 13 | 14 | const Popup: React.FC = (props) => { 15 | const { 16 | children, 17 | prefixCls, 18 | id, 19 | overlayInnerStyle: innerStyle, 20 | bodyClassName, 21 | className, 22 | style, 23 | } = props; 24 | 25 | return ( 26 |
27 | 35 |
36 | ); 37 | }; 38 | 39 | export default Popup; 40 | -------------------------------------------------------------------------------- /src/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import type { ArrowType, TriggerProps, TriggerRef } from '@rc-component/trigger'; 2 | import Trigger from '@rc-component/trigger'; 3 | import type { ActionType, AlignType } from '@rc-component/trigger/lib/interface'; 4 | import useId from '@rc-component/util/lib/hooks/useId'; 5 | import classNames from 'classnames'; 6 | import * as React from 'react'; 7 | import { useImperativeHandle, useRef } from 'react'; 8 | import { placements } from './placements'; 9 | import Popup from './Popup'; 10 | 11 | export interface TooltipProps 12 | extends Pick< 13 | TriggerProps, 14 | | 'onPopupAlign' 15 | | 'builtinPlacements' 16 | | 'fresh' 17 | | 'children' 18 | | 'mouseLeaveDelay' 19 | | 'mouseEnterDelay' 20 | | 'prefixCls' 21 | | 'forceRender' 22 | | 'popupVisible' 23 | > { 24 | trigger?: ActionType | ActionType[]; 25 | defaultVisible?: boolean; 26 | visible?: boolean; 27 | placement?: string; 28 | /** Config popup motion */ 29 | motion?: TriggerProps['popupMotion']; 30 | onVisibleChange?: (visible: boolean) => void; 31 | afterVisibleChange?: (visible: boolean) => void; 32 | overlay: (() => React.ReactNode) | React.ReactNode; 33 | /** @deprecated Please use `styles={{ root: {} }}` */ 34 | overlayStyle?: React.CSSProperties; 35 | /** @deprecated Please use `classNames={{ root: {} }}` */ 36 | overlayClassName?: string; 37 | getTooltipContainer?: (node: HTMLElement) => HTMLElement; 38 | destroyOnHidden?: boolean; 39 | align?: AlignType; 40 | showArrow?: boolean | ArrowType; 41 | arrowContent?: React.ReactNode; 42 | id?: string; 43 | /** @deprecated Please use `styles={{ body: {} }}` */ 44 | overlayInnerStyle?: React.CSSProperties; 45 | zIndex?: number; 46 | styles?: TooltipStyles; 47 | classNames?: TooltipClassNames; 48 | } 49 | 50 | export interface TooltipStyles { 51 | root?: React.CSSProperties; 52 | body?: React.CSSProperties; 53 | } 54 | 55 | export interface TooltipClassNames { 56 | root?: string; 57 | body?: string; 58 | } 59 | 60 | export interface TooltipRef extends TriggerRef {} 61 | 62 | const Tooltip = React.forwardRef((props, ref) => { 63 | const { 64 | overlayClassName, 65 | trigger = ['hover'], 66 | mouseEnterDelay = 0, 67 | mouseLeaveDelay = 0.1, 68 | overlayStyle, 69 | prefixCls = 'rc-tooltip', 70 | children, 71 | onVisibleChange, 72 | afterVisibleChange, 73 | motion, 74 | placement = 'right', 75 | align = {}, 76 | destroyOnHidden = false, 77 | defaultVisible, 78 | getTooltipContainer, 79 | overlayInnerStyle, 80 | arrowContent, 81 | overlay, 82 | id, 83 | showArrow = true, 84 | classNames: tooltipClassNames, 85 | styles: tooltipStyles, 86 | ...restProps 87 | } = props; 88 | 89 | const mergedId = useId(id); 90 | const triggerRef = useRef(null); 91 | 92 | useImperativeHandle(ref, () => triggerRef.current); 93 | 94 | const extraProps: Partial = { ...restProps }; 95 | 96 | if ('visible' in props) { 97 | extraProps.popupVisible = props.visible; 98 | } 99 | 100 | const getPopupElement = () => ( 101 | 108 | {overlay} 109 | 110 | ); 111 | 112 | const getChildren = () => { 113 | const child = React.Children.only(children); 114 | const originalProps = child?.props || {}; 115 | const childProps = { 116 | ...originalProps, 117 | 'aria-describedby': overlay ? mergedId : null, 118 | }; 119 | return React.cloneElement(children, childProps) as any; 120 | }; 121 | 122 | return ( 123 | 144 | {getChildren()} 145 | 146 | ); 147 | }); 148 | 149 | export default Tooltip; 150 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import Popup from './Popup'; 2 | import Tooltip from './Tooltip'; 3 | 4 | export type { TooltipRef } from './Tooltip'; 5 | export { Popup }; 6 | 7 | export default Tooltip; 8 | -------------------------------------------------------------------------------- /src/placements.tsx: -------------------------------------------------------------------------------- 1 | import type { BuildInPlacements } from '@rc-component/trigger'; 2 | 3 | const autoAdjustOverflowTopBottom = { 4 | shiftX: 64, 5 | adjustY: 1, 6 | }; 7 | 8 | const autoAdjustOverflowLeftRight = { adjustX: 1, shiftY: true }; 9 | 10 | const targetOffset = [0, 0]; 11 | 12 | export const placements: BuildInPlacements = { 13 | left: { 14 | points: ['cr', 'cl'], 15 | overflow: autoAdjustOverflowLeftRight, 16 | offset: [-4, 0], 17 | targetOffset, 18 | }, 19 | right: { 20 | points: ['cl', 'cr'], 21 | overflow: autoAdjustOverflowLeftRight, 22 | offset: [4, 0], 23 | targetOffset, 24 | }, 25 | top: { 26 | points: ['bc', 'tc'], 27 | overflow: autoAdjustOverflowTopBottom, 28 | offset: [0, -4], 29 | targetOffset, 30 | }, 31 | bottom: { 32 | points: ['tc', 'bc'], 33 | overflow: autoAdjustOverflowTopBottom, 34 | offset: [0, 4], 35 | targetOffset, 36 | }, 37 | topLeft: { 38 | points: ['bl', 'tl'], 39 | overflow: autoAdjustOverflowTopBottom, 40 | offset: [0, -4], 41 | targetOffset, 42 | }, 43 | leftTop: { 44 | points: ['tr', 'tl'], 45 | overflow: autoAdjustOverflowLeftRight, 46 | offset: [-4, 0], 47 | targetOffset, 48 | }, 49 | topRight: { 50 | points: ['br', 'tr'], 51 | overflow: autoAdjustOverflowTopBottom, 52 | offset: [0, -4], 53 | targetOffset, 54 | }, 55 | rightTop: { 56 | points: ['tl', 'tr'], 57 | overflow: autoAdjustOverflowLeftRight, 58 | offset: [4, 0], 59 | targetOffset, 60 | }, 61 | bottomRight: { 62 | points: ['tr', 'br'], 63 | overflow: autoAdjustOverflowTopBottom, 64 | offset: [0, 4], 65 | targetOffset, 66 | }, 67 | rightBottom: { 68 | points: ['bl', 'br'], 69 | overflow: autoAdjustOverflowLeftRight, 70 | offset: [4, 0], 71 | targetOffset, 72 | }, 73 | bottomLeft: { 74 | points: ['tl', 'bl'], 75 | overflow: autoAdjustOverflowTopBottom, 76 | offset: [0, 4], 77 | targetOffset, 78 | }, 79 | leftBottom: { 80 | points: ['br', 'bl'], 81 | overflow: autoAdjustOverflowLeftRight, 82 | offset: [-4, 0], 83 | targetOffset, 84 | }, 85 | }; 86 | 87 | export default placements; 88 | -------------------------------------------------------------------------------- /tests/__mocks__/@rc-component/trigger.js: -------------------------------------------------------------------------------- 1 | import Trigger from '@rc-component/trigger/lib/mock'; 2 | 3 | export default Trigger; 4 | -------------------------------------------------------------------------------- /tests/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { act, fireEvent, render } from '@testing-library/react'; 2 | import React from 'react'; 3 | import Tooltip, { TooltipRef } from '../src'; 4 | 5 | const verifyContent = (wrapper: HTMLElement, content: string) => { 6 | expect(wrapper.querySelector('.x-content').textContent).toBe(content); 7 | fireEvent.click(wrapper.querySelector('.target')); 8 | expect(wrapper.querySelector('.rc-tooltip').classList.contains('rc-tooltip-hidden')).toBe(true); 9 | }; 10 | 11 | describe('rc-tooltip', () => { 12 | window.requestAnimationFrame = window.setTimeout; 13 | window.cancelAnimationFrame = window.clearTimeout; 14 | beforeEach(() => { 15 | jest.useFakeTimers(); 16 | }); 17 | 18 | afterEach(() => { 19 | jest.useRealTimers(); 20 | }); 21 | 22 | async function waitFakeTimers() { 23 | for (let i = 0; i < 100; i += 1) { 24 | await act(async () => { 25 | jest.advanceTimersByTime(100); 26 | await Promise.resolve(); 27 | }); 28 | } 29 | } 30 | 31 | describe('shows and hides itself on click', () => { 32 | it('using an element overlay', () => { 33 | const { container } = render( 34 | Tooltip content} 38 | > 39 |
Click this
40 |
, 41 | ); 42 | 43 | fireEvent.click(container.querySelector('.target')); 44 | 45 | verifyContent(container, 'Tooltip content'); 46 | }); 47 | 48 | it('using a function overlay', () => { 49 | const { container } = render( 50 | Tooltip content} 54 | > 55 |
Click this
56 |
, 57 | ); 58 | fireEvent.click(container.querySelector('.target')); 59 | verifyContent(container, 'Tooltip content'); 60 | }); 61 | 62 | // https://github.com/ant-design/ant-design/pull/23155 63 | it('using style inner style', () => { 64 | const { container } = render( 65 | Tooltip content} 69 | overlayInnerStyle={{ background: 'red' }} 70 | > 71 |
Click this
72 |
, 73 | ); 74 | fireEvent.click(container.querySelector('.target')); 75 | expect( 76 | (container.querySelector('.rc-tooltip-inner') as HTMLElement).style.background, 77 | ).toEqual('red'); 78 | }); 79 | 80 | it('access of ref', () => { 81 | const domRef = React.createRef(); 82 | render( 83 | Tooltip content} 87 | ref={domRef} 88 | > 89 |
Click this
90 |
, 91 | ); 92 | expect(domRef.current).toBeTruthy(); 93 | }); 94 | }); 95 | 96 | describe('destroyOnHidden', () => { 97 | const destroyVerifyContent = async (wrapper: HTMLElement, content: string) => { 98 | fireEvent.click(wrapper.querySelector('.target')); 99 | await waitFakeTimers(); 100 | 101 | expect(wrapper.querySelector('.x-content').textContent).toBe(content); 102 | 103 | fireEvent.click(wrapper.querySelector('.target')); 104 | await waitFakeTimers(); 105 | }; 106 | it('default value', () => { 107 | const { container } = render( 108 | Tooltip content} 112 | > 113 |
Click this
114 |
, 115 | ); 116 | fireEvent.click(container.querySelector('.target')); 117 | verifyContent(container, 'Tooltip content'); 118 | }); 119 | 120 | it('should only remove tooltip when value is true', async () => { 121 | const { container } = render( 122 | Tooltip content} 127 | > 128 |
Click this
129 |
, 130 | ); 131 | await destroyVerifyContent(container, 'Tooltip content'); 132 | expect(document.querySelector('.x-content')).toBeFalsy(); 133 | }); 134 | }); 135 | 136 | it('zIndex', () => { 137 | jest.useFakeTimers(); 138 | 139 | const { container } = render( 140 | 141 |
Light
142 |
, 143 | ); 144 | fireEvent.click(container.querySelector('.target')); 145 | 146 | jest.runAllTimers(); 147 | 148 | expect((container.querySelector('div.rc-tooltip') as HTMLElement).style.zIndex).toBe('903'); 149 | 150 | jest.useRealTimers(); 151 | }); 152 | 153 | describe('showArrow', () => { 154 | it('should show tooltip arrow default', () => { 155 | const { container } = render( 156 | Tooltip content} 161 | > 162 |
Click this
163 |
, 164 | ); 165 | fireEvent.click(container.querySelector('.target')); 166 | expect(container.querySelector('.rc-tooltip-arrow')).toBeTruthy(); 167 | }); 168 | it('should show tooltip arrow when showArrow is true', () => { 169 | const { container } = render( 170 | Tooltip content} 175 | showArrow 176 | > 177 |
Click this
178 |
, 179 | ); 180 | fireEvent.click(container.querySelector('.target')); 181 | expect(container.querySelector('.rc-tooltip-arrow')).toBeTruthy(); 182 | }); 183 | it('should show tooltip arrow when showArrow is object', () => { 184 | const { container } = render( 185 | Tooltip content} 190 | showArrow={{ 191 | className: 'abc', 192 | }} 193 | > 194 |
Click this
195 |
, 196 | ); 197 | fireEvent.click(container.querySelector('.target')); 198 | expect(container.querySelector('.rc-tooltip-arrow')).toBeTruthy(); 199 | expect(container.querySelector('.rc-tooltip-arrow').classList.contains('abc')).toBeTruthy(); 200 | }); 201 | it('should hide tooltip arrow when showArrow is false', () => { 202 | const { container } = render( 203 | Tooltip content} 208 | showArrow={false} 209 | > 210 |
Click this
211 |
, 212 | ); 213 | fireEvent.click(container.querySelector('.target')); 214 | expect(container.querySelector('.rc-tooltip').classList).not.toContain( 215 | 'rc-tooltip-show-arrow', 216 | ); 217 | expect(container.querySelector('.rc-tooltip-arrow')).toBeFalsy(); 218 | }); 219 | }); 220 | 221 | it('visible', () => { 222 | const App = () => { 223 | const [open, setOpen] = React.useState(false); 224 | return ( 225 | Tooltip content} visible={open}> 226 |
{ 229 | setOpen(true); 230 | }} 231 | /> 232 | 233 | ); 234 | }; 235 | const { container } = render(); 236 | 237 | expect(container.querySelector('.x-content')).toBeFalsy(); 238 | 239 | fireEvent.click(container.querySelector('.target')); 240 | expect(container.querySelector('.x-content')).toBeTruthy(); 241 | }); 242 | 243 | it('ref support nativeElement', () => { 244 | const nodeRef = React.createRef(); 245 | 246 | const { container } = render( 247 | }> 248 | 288 | , 289 | ); 290 | 291 | expect(container.querySelector('button')).toHaveAttribute('aria-describedby', 'test-id'); 292 | }); 293 | 294 | it('should not pass aria-describedby when overlay is empty', () => { 295 | const { container } = render( 296 | 297 | 298 | , 299 | ); 300 | 301 | expect(container.querySelector('button')).not.toHaveAttribute('aria-describedby'); 302 | }); 303 | 304 | it('should preserve original props of children', () => { 305 | const onMouseEnter = jest.fn(); 306 | 307 | const { container } = render( 308 | 309 | 312 | , 313 | ); 314 | 315 | const btn = container.querySelector('button'); 316 | expect(btn).toHaveClass('custom-btn'); 317 | 318 | // 触发原始事件处理器 319 | fireEvent.mouseEnter(btn); 320 | expect(onMouseEnter).toHaveBeenCalled(); 321 | }); 322 | 323 | it('should throw error when multiple children provided', () => { 324 | const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); 325 | 326 | expect(() => { 327 | render( 328 | // @ts-expect-error 329 | 330 | 331 | 332 | , 333 | ); 334 | }).toThrow(); 335 | 336 | errorSpy.mockRestore(); 337 | }); 338 | }); 339 | }); 340 | -------------------------------------------------------------------------------- /tests/popup.test.tsx: -------------------------------------------------------------------------------- 1 | import { Popup } from '../src'; 2 | 3 | describe('Popup', () => { 4 | // Used in antd for C2D2C 5 | it('should export', () => { 6 | expect(Popup).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const util = require('util'); 3 | 4 | // eslint-disable-next-line no-console 5 | console.log('Current React Version:', React.version); 6 | 7 | /* eslint-disable global-require */ 8 | if (typeof window !== 'undefined') { 9 | global.window.resizeTo = (width, height) => { 10 | global.window.innerWidth = width || global.window.innerWidth; 11 | global.window.innerHeight = height || global.window.innerHeight; 12 | global.window.dispatchEvent(new Event('resize')); 13 | }; 14 | global.window.scrollTo = () => {}; 15 | // ref: https://github.com/ant-design/ant-design/issues/18774 16 | if (!window.matchMedia) { 17 | Object.defineProperty(global.window, 'matchMedia', { 18 | writable: true, 19 | configurable: true, 20 | value: jest.fn((query) => ({ 21 | matches: query.includes('max-width'), 22 | addListener: jest.fn(), 23 | removeListener: jest.fn(), 24 | })), 25 | }); 26 | } 27 | 28 | // Fix css-animation or rc-motion deps on these 29 | // https://github.com/react-component/motion/blob/9c04ef1a210a4f3246c9becba6e33ea945e00669/src/util/motion.ts#L27-L35 30 | // https://github.com/yiminghe/css-animation/blob/a5986d73fd7dfce75665337f39b91483d63a4c8c/src/Event.js#L44 31 | window.AnimationEvent = window.AnimationEvent || window.Event; 32 | window.TransitionEvent = window.TransitionEvent || window.Event; 33 | 34 | // ref: https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom 35 | // ref: https://github.com/jsdom/jsdom/issues/2524 36 | Object.defineProperty(window, 'TextEncoder', { writable: true, value: util.TextEncoder }); 37 | Object.defineProperty(window, 'TextDecoder', { writable: true, value: util.TextDecoder }); 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "baseUrl": "./", 6 | "jsx": "react", 7 | "declaration": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "paths": { 11 | "@/*": [ 12 | "src/*" 13 | ], 14 | "@@/*": [ 15 | ".dumi/tmp/*" 16 | ], 17 | "rc-tooltip": [ 18 | "src/index.tsx" 19 | ] 20 | } 21 | }, 22 | "include": [ 23 | ".dumirc.ts", 24 | "./src/**/*.ts", 25 | "./src/**/*.tsx", 26 | "./docs/**/*.tsx", 27 | "./tests/**/*.tsx" 28 | ] 29 | } -------------------------------------------------------------------------------- /type.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.less'; -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "umijs" 3 | } 4 | --------------------------------------------------------------------------------