├── .editorconfig ├── .eslintrc ├── .gitignore ├── .lintstagedrc ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENCE.md ├── README.md ├── demo └── src │ ├── demo.md │ ├── index.js │ └── routes.js ├── jest.config.json ├── jest.transform.js ├── nwb.config.js ├── package.json ├── setupTest.js ├── src ├── __snapshots__ │ └── index.test.js.snap ├── components │ ├── Arrow │ │ ├── index.js │ │ └── index.test.js │ ├── Bubble │ │ ├── index.js │ │ └── index.test.js │ └── Tooltip │ │ ├── __snapshots__ │ │ └── index.test.js.snap │ │ ├── index.js │ │ └── index.test.js ├── index.d.ts ├── index.js ├── index.test.js └── utils │ ├── propTypes.js │ └── propTypes.test.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 2 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [package.json] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "amd": true, 5 | "es6": true, 6 | "jest/globals": true 7 | }, 8 | "plugins": ["jest", "prettier", "react"], 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:jest/recommended", 12 | "plugin:react/recommended", 13 | "prettier" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | /node_modules 6 | /umd 7 | npm-debug.log* 8 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.js": [ 3 | "prettier --no-bracket-spacing --no-semi --trailing-comma=es5 --write", 4 | "git add" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "semi": false, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 14.9 3 | 4 | before_install: 5 | - yarn add codecov 6 | 7 | after_success: 8 | - cat ./coverage/lcov.info | ./node_modules/.bin/codecov 9 | 10 | branches: 11 | only: 12 | - master 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.6.1 — 2019-07-25 2 | 3 | - Fixed: ios touched devices by @vnoitkumar 4 | 5 | # 2.6.0 — 2019-05-23 6 | 7 | - Updated: Replacing styled-components with emotion/core 8 | 9 | # 2.5.0 — 2019-03-08 10 | 11 | - Added: `customCss` prop 12 | 13 | # 2.4.0 — 2019-03-08 14 | 15 | - Updated: dependencies 16 | - Updated: tests 17 | 18 | # 2.3.3 — 2018-11-08 19 | 20 | - Fixed: z-index prop 21 | - Updated: Dependencies 22 | 23 | # 2.3.2 — 2018-06-13 24 | 25 | - Removes: index.css in package.json 26 | 27 | # 2.3.1 — 2018-03-12 28 | 29 | - Fixed: Removed warning when onMouseEnter and Leave are booleans instead of undefined by @vincentdesmares 30 | 31 | # 2.3.0 - 2018-01-04 32 | 33 | - Updated: FadeDuration to be in ms, 34 | - Added: Offset prop to allow spacing between arrow and trigger 35 | 36 | # 2.2.0 - 2018-01-03 37 | 38 | - Added: Fade animation props (by @BenLorantfy) 39 | 40 | # 2.1.0 - 2017-12-08 41 | 42 | - Added: Standalone version 43 | 44 | # 2.0.0 - 2017-10-18 45 | 46 | - Added: Nwb build 47 | - Updated: Nwb build 48 | - Added: Tests 49 | - Added: styled-components dependency 50 | - Removed: classnames dependency 51 | 52 | # 1.0.6 - 2017-08-24 53 | 54 | - Updated: Import PropTypes from `prop-types` package 55 | - Added: Yarn 56 | 57 | # 1.0.5 - 2015-07-09 58 | 59 | - Updated: webpack build 60 | 61 | # 1.0.4 - 2015-07-08 62 | 63 | - Added: webpack build 64 | 65 | # 1.0.3 - 2015-07-03 66 | 67 | - Added: fixed tooltip example 68 | 69 | # 1.0.2 - 2015-07-02 70 | 71 | - Updated: tooltips example screenshot 72 | 73 | # 1.0.1 - 2015-07-02 74 | 75 | - Updated: repository url in package.json 76 | - Added: Npm Badge in README 77 | 78 | # 1.0.0 - 2015-07-01 79 | 80 | - Initial release 81 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Prerequisites 4 | 5 | [Node.js](http://nodejs.org/) >= v4 must be installed. 6 | 7 | ## Installation 8 | 9 | - Running `yarn` or `npm install` in the components's root directory will install everything you need for development. 10 | 11 | ## Demo Development Server 12 | 13 | - `npm run demo:start` will run a development server with the component's demo app at [http://localhost:1190](http://localhost:1190) with hot module reloading. 14 | 15 | ## Linting 16 | 17 | - `npm run lint` will lint the `src` and `demo/src` folders 18 | 19 | ## Running Tests 20 | 21 | - `npm test` will run the tests once and produce a coverage report in `coverage/`. 22 | 23 | - `npm run test:watch` will run the tests on every change. 24 | 25 | ## Building 26 | 27 | - `npm run build` will build the component for publishing to npm and also bundle the demo app. 28 | 29 | - `npm run clean` will delete built resources. 30 | 31 | > **Builds:** 32 | > * CommonJS build => `/lib`, 33 | > * ES6 modules build => `/es` 34 | > * UMD build => `/umd` 35 | > * Demo build => `/demo/dist` 36 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Cédric Delpoux 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-simple-tooltip 2 | 3 | [![npm package][npm-badge]][npm] 4 | [![Travis][build-badge]][build] 5 | [![Codecov][codecov-badge]][codecov] 6 | ![Module formats][module-formats] 7 | 8 | A lightweight and simple tooltip component for React 9 | 10 | ## Getting started 11 | 12 | [![react-simple-tooltip](https://nodei.co/npm/react-simple-tooltip.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/react-simple-tooltip/) 13 | 14 | You can download `react-simple-tooltip` from the NPM registry via the `npm` or `yarn` commands 15 | 16 | ```shell 17 | yarn add react-simple-tooltip 18 | npm install react-simple-tooltip --save 19 | ``` 20 | 21 | If you don't use package manager and you want to include `react-simple-tooltip` directly in your html, you could get it from the UNPKG CDN 22 | 23 | ```html 24 | https://unpkg.com/react-simple-tooltip/dist/react-simple-tooltip.min.js. 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### Attached to a Component 30 | 31 | ```javascript 32 | import React from "react" 33 | import Tooltip from "react-simple-tooltip" 34 | 35 | const App = () => ( 36 | 37 | 38 | 39 | ) 40 | ``` 41 | 42 | ### Standalone 43 | 44 | ```javascript 45 | import React from "react" 46 | import Tooltip from "react-simple-tooltip" 47 | 48 | const App = () => ( 49 |
50 | 54 |
55 | ) 56 | ``` 57 | 58 | ### Custom css 59 | 60 | ```javascript 61 | import React from "react" 62 | import Tooltip from "react-simple-tooltip" 63 | import {css} from "styled-components" 64 | 65 | const App = () => ( 66 | 72 | 73 | 74 | ) 75 | ``` 76 | 77 | ## Demo 78 | 79 | See [Demo page][github-page] 80 | 81 | ## Props 82 | 83 | | Name | PropType | Description | Default | 84 | | ------------ | --------------------------------------------------- | ---------------------------------- | --------- | 85 | | arrow | PropTypes.number | Arrow size, in pixels | 8 | 86 | | background | PropTypes.string | Tooltip background color | "#000" | 87 | | border | PropTypes.string | Tooltip border color | "#000" | 88 | | color | PropTypes.string | Tooltip text color | "#fff" | 89 | | content | PropTypes.any.isRequired | Tooltip content | - | 90 | | customCss | PropTypes.any | Custom css | - | 91 | | fadeDuration | PropTypes.number | Fade duration, in milliseconds | 0 | 92 | | fadeEasing | PropTypes.string | Fade easing | "linear" | 93 | | fixed | PropTypes.bool | Tooltip behavior, hover by default | false | 94 | | fontFamily | PropTypes.bool | Tooltip text font-family | "inherit" | 95 | | fontSize | PropTypes.bool | Tooltip text font-size | "inherit" | 96 | | padding | PropTypes.number | Tooltip padding, in pixels | 16 | 97 | | placement | PropTypes.oneOf(["left", "top", "right", "bottom"]) | Tooltip placement | "top" | 98 | | radius | PropTypes.number | Tooltip border radius | 0 | 99 | | zIndex | PropTypes.number | Tooltip z-index | 1 | 100 | 101 | ## Contributing 102 | 103 | - ⇄ Pull/Merge requests and ★ Stars are always welcome. 104 | - For bugs and feature requests, please [create an issue][github-issue]. 105 | - Pull requests must be accompanied by passing automated tests (`npm test`). 106 | 107 | See [CONTRIBUTING.md](./CONTRIBUTING.md) guidelines 108 | 109 | ## Changelog 110 | 111 | See [CHANGELOG.md](./CHANGELOG.md) 112 | 113 | ## License 114 | 115 | This project is licensed under the MIT License - see the [LICENCE.md](./LICENCE.md) file for details 116 | 117 | [npm-badge]: https://img.shields.io/npm/v/react-simple-tooltip.svg?style=flat-square 118 | [npm]: https://www.npmjs.org/package/react-simple-tooltip 119 | [build-badge]: https://img.shields.io/travis/cedricdelpoux/react-simple-tooltip/master.svg?style=flat-square 120 | [build]: https://travis-ci.org/cedricdelpoux/react-simple-tooltip 121 | [codecov-badge]: https://img.shields.io/codecov/c/github/cedricdelpoux/react-simple-tooltip.svg?style=flat-square 122 | [codecov]: https://codecov.io/gh/cedricdelpoux/react-simple-tooltip 123 | [module-formats]: https://img.shields.io/badge/module%20formats-umd%2C%20cjs%2C%20esm-green.svg?style=flat-square 124 | [github-page]: https://cedricdelpoux.github.io/react-simple-tooltip 125 | [github-issue]: https://github.com/cedricdelpoux/react-simple-tooltip/issues/new 126 | -------------------------------------------------------------------------------- /demo/src/demo.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | `react-simple-tooltip` is a lightweight and simple tooltip component for React. 4 | 5 | You can use it standalone or attach it to any component. 6 | 7 | ## Demo 8 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {render} from "react-dom" 3 | 4 | import ReactDemoPage from "react-demo-page" 5 | import routes from "./routes" 6 | import pkg from "../../package.json" 7 | 8 | const header = { 9 | title: pkg.name, 10 | buttons: [ 11 | {label: "Github", url: pkg.homepage}, 12 | {label: "Npm", url: `https://www.npmjs.com/package/${pkg.name}`}, 13 | {label: "Download", url: `${pkg.homepage}/archive/master.zip`}, 14 | ], 15 | } 16 | 17 | const footer = { 18 | author: pkg.author, 19 | } 20 | 21 | const Demo = () => ( 22 | 29 | ) 30 | 31 | // eslint-disable-next-line 32 | render(, document.querySelector("#demo")) 33 | -------------------------------------------------------------------------------- /demo/src/routes.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import ReactSimpleTooltip from "../../src" 3 | import {css, jsx} from "@emotion/core" 4 | 5 | import demoHtml from "./demo.md" 6 | import readmeHtml from "../../README.md" 7 | 8 | // eslint-disable-next-line react/prop-types 9 | const Zone = ({children}) => ( 10 |
17 | {children} 18 |
19 | ) 20 | 21 | const data = [] 22 | 23 | for (let x = 1; x <= 30; x++) { 24 | data.push({x: x, y: Math.floor(Math.random() * 100)}) 25 | } 26 | 27 | const routes = [ 28 | { 29 | path: "/", 30 | exact: true, 31 | demo: { 32 | component: ( 33 | 39 | Hover me ! 40 | 41 | ), 42 | displayName: "ReactSimpleTooltip", 43 | hiddenProps: ["children"], 44 | html: demoHtml, 45 | }, 46 | label: "Demo", 47 | }, 48 | { 49 | path: "/standalone", 50 | demo: { 51 | component: ( 52 |
60 | 65 |
66 | ), 67 | displayName: "ReactSimpleTooltip", 68 | hiddenProps: ["children"], 69 | html: demoHtml, 70 | }, 71 | label: "Standalone", 72 | }, 73 | { 74 | path: "/readme", 75 | html: readmeHtml, 76 | label: "Read me", 77 | }, 78 | ] 79 | 80 | export default routes 81 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverageDirectory": "./coverage/", 3 | "collectCoverage": true, 4 | "transform": { 5 | "^.+\\.js$": "/jest.transform.js" 6 | }, 7 | "moduleNameMapper": { 8 | "\\.(css)$": "/node_modules/jest-css-modules" 9 | }, 10 | "setupTestFrameworkScriptFile": "/setupTest", 11 | "snapshotSerializers": ["enzyme-to-json/serializer"] 12 | } 13 | -------------------------------------------------------------------------------- /jest.transform.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | module.exports = require("babel-jest").createTransformer({ 3 | presets: ["es2015", "react"], 4 | plugins: ["transform-object-rest-spread"], 5 | }) 6 | -------------------------------------------------------------------------------- /nwb.config.js: -------------------------------------------------------------------------------- 1 | var extraWebpackConfig = { 2 | module: { 3 | rules: [ 4 | { 5 | test: /\.js$/, 6 | enforce: "pre", 7 | loader: "eslint-loader", 8 | include: /src/, 9 | }, 10 | { 11 | test: /\.md$/, 12 | loader: "html-loader!markdown-loader", 13 | exclude: /node_modules/, 14 | }, 15 | ], 16 | }, 17 | } 18 | 19 | // eslint-disable-next-line 20 | module.exports = { 21 | type: "react-component", 22 | polyfill: false, 23 | babel: { 24 | plugins: ["babel-plugin-transform-object-rest-spread"], 25 | }, 26 | npm: { 27 | cjs: true, 28 | esModules: true, 29 | umd: { 30 | global: "ReactDemoPage", 31 | externals: { 32 | react: "React", 33 | "prop-types": "PropTypes", 34 | }, 35 | }, 36 | }, 37 | uglify: false, 38 | webpack: { 39 | extra: extraWebpackConfig, 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-simple-tooltip", 3 | "version": "2.6.3", 4 | "author": { 5 | "name": "Cédric Delpoux", 6 | "email": "cedric.delpoux@gmail.com" 7 | }, 8 | "description": "A lightweight and simple tooltip component for React", 9 | "files": [ 10 | "css", 11 | "es", 12 | "lib", 13 | "umd" 14 | ], 15 | "homepage": "https://github.com/cedricdelpoux/react-simple-tooltip#readme", 16 | "keywords": [ 17 | "react", 18 | "tooltip", 19 | "bubble" 20 | ], 21 | "license": "MIT", 22 | "main": "lib/index.js", 23 | "module": "es/index.js", 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/cedricdelpoux/react-simple-tooltip.git" 27 | }, 28 | "scripts": { 29 | "build": "nwb build-react-component", 30 | "clean": "nwb clean-module && nwb clean-demo", 31 | "deploy": "gh-pages -d demo/dist", 32 | "lint": "eslint src demo/src", 33 | "precommit": "lint-staged", 34 | "prepublishOnly": "yarn clean && yarn build", 35 | "start": "nwb serve-react-demo --port 1190", 36 | "test": "jest --config jest.config.json --colors --no-cache", 37 | "test:snapshot:update": "jest --config jest.config.json --updateSnapshot", 38 | "test:watch": "yarn run test -- --watch" 39 | }, 40 | "dependencies": { 41 | "@emotion/core": "^10.0.10" 42 | }, 43 | "devDependencies": { 44 | "@emotion/babel-preset-css-prop": "^10.0.9", 45 | "babel-eslint": "^7.2.3", 46 | "babel-jest": "^20.0.3", 47 | "babel-plugin-emotion": "^10.0.9", 48 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 49 | "babel-preset-es2015": "^6.24.1", 50 | "babel-preset-react": "^6.24.1", 51 | "emotion": "^10.0.9", 52 | "enzyme": "^3.9.0", 53 | "enzyme-adapter-react-16": "^1.10.0", 54 | "enzyme-to-json": "^3.3.5", 55 | "eslint": "^4.5.0", 56 | "eslint-config-prettier": "^2.3.0", 57 | "eslint-loader": "^1.9.0", 58 | "eslint-plugin-jest": "^20.0.3", 59 | "eslint-plugin-prettier": "^2.1.2", 60 | "eslint-plugin-react": "^7.3.0", 61 | "gh-pages": "^1.0.0", 62 | "html-loader": "^0.5.1", 63 | "husky": "^0.14.3", 64 | "jest": "^20.0.4", 65 | "jest-css-modules": "^1.1.0", 66 | "jest-emotion": "^10.0.11", 67 | "lint-staged": "^4.0.2", 68 | "markdown-loader": "^2.0.1", 69 | "nwb": "^0.18.0", 70 | "prettier": "^1.5.3", 71 | "prop-types": "^15.6.0", 72 | "react": "^16.8.4", 73 | "react-demo-page": "^0.2.2", 74 | "react-dom": "^16.8.4", 75 | "react-test-renderer": "^15.6.1", 76 | "sinon": "^4.2.2" 77 | }, 78 | "peerDependencies": { 79 | "react": "^15.0.0 || ^16.0.0 || ^17.0.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /setupTest.js: -------------------------------------------------------------------------------- 1 | import {configure} from "enzyme" 2 | import Adapter from "enzyme-adapter-react-16" 3 | 4 | configure({adapter: new Adapter()}) 5 | -------------------------------------------------------------------------------- /src/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Tooltip Should render without children 1`] = ` 4 | 21 | 39 |
42 | 50 | 56 | 79 |
82 | 91 | 111 |
114 | 121 | 126 | 151 |
154 | 155 | 156 | 157 | my content 158 |
159 |
160 | 161 |
162 |
163 | 164 | 165 |
166 |
167 | 168 | `; 169 | 170 | exports[`Tooltip should render 1`] = ` 171 | 188 | 209 |
215 | 218 | 226 |
227 |
228 |
229 | `; 230 | -------------------------------------------------------------------------------- /src/components/Arrow/index.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import PropTypes from "prop-types" 3 | import {css, jsx} from "@emotion/core" 4 | 5 | const Base = props => css` 6 | position: absolute; 7 | width: ${props.width}px; 8 | height: ${props.width}px; 9 | background: ${props.background}; 10 | ` 11 | 12 | const Up = props => css` 13 | ${Base(props)}; 14 | transform: translateX(-50%) translateY(50%) rotateZ(45deg); 15 | bottom: 100%; 16 | left: 50%; 17 | border-left: 1px solid ${props.border}; 18 | border-top: 1px solid ${props.border}; 19 | ` 20 | const Down = props => css` 21 | ${Base(props)}; 22 | transform: translateX(-50%) translateY(-50%) rotateZ(45deg); 23 | top: 100%; 24 | left: 50%; 25 | border-right: 1px solid ${props.border}; 26 | border-bottom: 1px solid ${props.border}; 27 | ` 28 | const Left = props => css` 29 | ${Base(props)}; 30 | transform: translateX(50%) translateY(-50%) rotateZ(45deg); 31 | right: 100%; 32 | top: 50%; 33 | border-left: 1px solid ${props.border}; 34 | border-bottom: 1px solid ${props.border}; 35 | ` 36 | 37 | const Right = props => css` 38 | ${Base(props)}; 39 | transform: translateX(-50%) translateY(-50%) rotateZ(45deg); 40 | left: 100%; 41 | top: 50%; 42 | border-right: 1px solid ${props.border}; 43 | border-top: 1px solid ${props.border}; 44 | ` 45 | 46 | const BaseArrow = ({fn, ...props}) =>
47 | 48 | BaseArrow.propTypes = { 49 | fn: PropTypes.func.isRequired, 50 | background: PropTypes.string.isRequired, 51 | border: PropTypes.string.isRequired, 52 | width: PropTypes.number.isRequired, 53 | } 54 | 55 | const arrows = { 56 | left: props => BaseArrow({fn: Right, ...props}), 57 | top: props => BaseArrow({fn: Down, ...props}), 58 | right: props => BaseArrow({fn: Left, ...props}), 59 | bottom: props => BaseArrow({fn: Up, ...props}), 60 | } 61 | 62 | const Arrow = ({background, border, placement, width}) => { 63 | const Component = arrows[placement] || arrows.top 64 | return ( 65 | width > 0 && ( 66 | 67 | ) 68 | ) 69 | } 70 | 71 | Arrow.propTypes = { 72 | background: PropTypes.string.isRequired, 73 | border: PropTypes.string.isRequired, 74 | placement: PropTypes.string.isRequired, 75 | width: PropTypes.number.isRequired, 76 | } 77 | 78 | export default Arrow 79 | -------------------------------------------------------------------------------- /src/components/Arrow/index.test.js: -------------------------------------------------------------------------------- 1 | import {mount} from "enzyme" 2 | import React from "react" 3 | import Arrow from "./index" 4 | 5 | const arrowProps = { 6 | background: "#000", 7 | border: "#0f0", 8 | color: "#fff", 9 | width: 8, 10 | } 11 | const ArrowUpFixture = 12 | const ArrowBottomFixture = 13 | const ArrowLeftFixture = 14 | const ArrowRightFixture = 15 | const NoArrowFixture = 16 | const ArrowWrongPlacementPropsFixture = ( 17 | 18 | ) 19 | 20 | describe("Arrow", () => { 21 | it("renders", () => { 22 | mount(ArrowUpFixture) 23 | mount(ArrowBottomFixture) 24 | mount(ArrowLeftFixture) 25 | mount(ArrowRightFixture) 26 | mount(ArrowWrongPlacementPropsFixture) 27 | }) 28 | 29 | it("do not render", () => { 30 | mount(NoArrowFixture) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/components/Bubble/index.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import PropTypes from "prop-types"; 3 | import { css, jsx } from "@emotion/core"; 4 | 5 | const Bubble = (props) => ( 6 |
17 | {props.children} 18 |
19 | ); 20 | 21 | Bubble.propTypes = { 22 | color: PropTypes.string, 23 | background: PropTypes.string, 24 | border: PropTypes.string, 25 | padding: PropTypes.number, 26 | radius: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 27 | fontSize: PropTypes.string, 28 | fontFamily: PropTypes.string, 29 | children: PropTypes.array, 30 | }; 31 | 32 | export default Bubble; 33 | -------------------------------------------------------------------------------- /src/components/Bubble/index.test.js: -------------------------------------------------------------------------------- 1 | import {mount} from "enzyme" 2 | import React from "react" 3 | import Bubble from "./index" 4 | 5 | const bubbleProps = { 6 | background: "#000", 7 | color: "#fff", 8 | padding: 10, 9 | radius: 2, 10 | } 11 | const BubbleFixture = 12 | const BubbleNoPropsFixture = 13 | 14 | describe("Bubble", () => { 15 | it("renders", () => { 16 | mount(BubbleFixture) 17 | mount(BubbleNoPropsFixture) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/components/Tooltip/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Tooltip renders 1`] = ` 4 | .emotion-0 { 5 | position: absolute; 6 | z-index: 8; 7 | top: 100%; 8 | left: 50%; 9 | -webkit-transform: translateX(-50%); 10 | -ms-transform: translateX(-50%); 11 | transform: translateX(-50%); 12 | margin-top: 8px; 13 | } 14 | 15 | .emotion-0 { 16 | position: absolute; 17 | z-index: 8; 18 | top: 100%; 19 | left: 50%; 20 | -webkit-transform: translateX(-50%); 21 | -ms-transform: translateX(-50%); 22 | transform: translateX(-50%); 23 | margin-top: 8px; 24 | } 25 | 26 | 32 | 36 | 59 |
62 | 😎 63 |
64 |
65 |
66 |
67 | `; 68 | 69 | exports[`Tooltip renders 2`] = ` 70 | .emotion-0 { 71 | position: absolute; 72 | z-index: 8; 73 | bottom: 100%; 74 | left: 50%; 75 | -webkit-transform: translateX(-50%); 76 | -ms-transform: translateX(-50%); 77 | transform: translateX(-50%); 78 | margin-bottom: 8px; 79 | } 80 | 81 | .emotion-0 { 82 | position: absolute; 83 | z-index: 8; 84 | bottom: 100%; 85 | left: 50%; 86 | -webkit-transform: translateX(-50%); 87 | -ms-transform: translateX(-50%); 88 | transform: translateX(-50%); 89 | margin-bottom: 8px; 90 | } 91 | 92 | 98 | 102 | 125 |
128 | 😎 129 |
130 |
131 |
132 |
133 | `; 134 | 135 | exports[`Tooltip renders 3`] = ` 136 | .emotion-0 { 137 | position: absolute; 138 | z-index: 8; 139 | left: 100%; 140 | top: 50%; 141 | -webkit-transform: translateY(-50%); 142 | -ms-transform: translateY(-50%); 143 | transform: translateY(-50%); 144 | margin-left: 8px; 145 | } 146 | 147 | .emotion-0 { 148 | position: absolute; 149 | z-index: 8; 150 | left: 100%; 151 | top: 50%; 152 | -webkit-transform: translateY(-50%); 153 | -ms-transform: translateY(-50%); 154 | transform: translateY(-50%); 155 | margin-left: 8px; 156 | } 157 | 158 | 164 | 168 | 191 |
194 | 😎 195 |
196 |
197 |
198 |
199 | `; 200 | 201 | exports[`Tooltip renders 4`] = ` 202 | .emotion-0 { 203 | position: absolute; 204 | z-index: 8; 205 | right: 100%; 206 | top: 50%; 207 | -webkit-transform: translateY(-50%); 208 | -ms-transform: translateY(-50%); 209 | transform: translateY(-50%); 210 | margin-right: 8px; 211 | } 212 | 213 | .emotion-0 { 214 | position: absolute; 215 | z-index: 8; 216 | right: 100%; 217 | top: 50%; 218 | -webkit-transform: translateY(-50%); 219 | -ms-transform: translateY(-50%); 220 | transform: translateY(-50%); 221 | margin-right: 8px; 222 | } 223 | 224 | 230 | 234 | 257 |
260 | 😎 261 |
262 |
263 |
264 |
265 | `; 266 | 267 | exports[`Tooltip should create a Tooltip with an animation 1`] = ` 268 | @keyframes animation-0 { 269 | 0% { 270 | opacity: 0; 271 | } 272 | 273 | 100% { 274 | opacity: 1; 275 | } 276 | } 277 | 278 | .emotion-0 { 279 | position: absolute; 280 | -webkit-animation: 100ms ease-out 0s 1 animation-0; 281 | animation: 100ms ease-out 0s 1 animation-0; 282 | z-index: 8; 283 | bottom: 100%; 284 | left: 50%; 285 | -webkit-transform: translateX(-50%); 286 | -ms-transform: translateX(-50%); 287 | transform: translateX(-50%); 288 | margin-bottom: 8px; 289 | } 290 | 291 | .emotion-0 { 292 | position: absolute; 293 | -webkit-animation: 100ms ease-out 0s 1 animation-0; 294 | animation: 100ms ease-out 0s 1 animation-0; 295 | z-index: 8; 296 | bottom: 100%; 297 | left: 50%; 298 | -webkit-transform: translateX(-50%); 299 | -ms-transform: translateX(-50%); 300 | transform: translateX(-50%); 301 | margin-bottom: 8px; 302 | } 303 | 304 | 311 | 317 | 353 |
356 | 😎 357 |
358 |
359 |
360 |
361 | `; 362 | -------------------------------------------------------------------------------- /src/components/Tooltip/index.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import PropTypes from "prop-types" 3 | import {css, keyframes, jsx} from "@emotion/core" 4 | import {easingPropType} from "../../utils/propTypes" 5 | 6 | const fadeAnimation = keyframes` 7 | 0% { 8 | opacity: 0; 9 | } 10 | 100% { 11 | opacity: 1; 12 | } 13 | ` 14 | 15 | const animation = props => css` 16 | animation: ${props.fadeDuration}ms ${props.fadeEasing} 0s 1 ${fadeAnimation}; 17 | ` 18 | 19 | // prettier-ignore 20 | const Base = (props) => css` 21 | position: absolute; 22 | ${props.fadeDuration && props.fadeDuration > 0 && animation(props)}; 23 | ${props.zIndex && `z-index: ${props.zIndex};`}; 24 | ` 25 | 26 | const Top = props => css` 27 | ${Base(props)}; 28 | bottom: 100%; 29 | left: 50%; 30 | transform: translateX(-50%); 31 | margin-bottom: ${props.offset}px; 32 | ` 33 | 34 | const Bottom = props => css` 35 | ${Base(props)}; 36 | top: 100%; 37 | left: 50%; 38 | transform: translateX(-50%); 39 | margin-top: ${props.offset}px; 40 | ` 41 | 42 | const Left = props => css` 43 | ${Base(props)}; 44 | right: 100%; 45 | top: 50%; 46 | transform: translateY(-50%); 47 | margin-right: ${props.offset}px; 48 | ` 49 | 50 | const Right = props => css` 51 | ${Base(props)}; 52 | left: 100%; 53 | top: 50%; 54 | transform: translateY(-50%); 55 | margin-left: ${props.offset}px; 56 | ` 57 | 58 | const BaseToolTop = ({fn, children, ...props}) => ( 59 |
{children}
60 | ) 61 | 62 | BaseToolTop.propTypes = { 63 | fn: PropTypes.func.isRequired, 64 | children: PropTypes.any.isRequired, 65 | offset: PropTypes.number, 66 | open: PropTypes.bool, 67 | zIndex: PropTypes.number, 68 | fadeEasing: easingPropType, 69 | fadeDuration: PropTypes.number, 70 | } 71 | 72 | const tooltips = { 73 | left: ({children, ...props}) => BaseToolTop({fn: Left, children, ...props}), 74 | top: ({children, ...props}) => BaseToolTop({fn: Top, children, ...props}), 75 | right: ({children, ...props}) => BaseToolTop({fn: Right, children, ...props}), 76 | bottom: ({children, ...props}) => 77 | BaseToolTop({fn: Bottom, children, ...props}), 78 | } 79 | 80 | const Tooltip = ({ 81 | children, 82 | offset, 83 | open, 84 | placement, 85 | zIndex, 86 | fadeDuration, 87 | fadeEasing, 88 | }) => { 89 | const Component = tooltips[placement] || tooltips.top 90 | return ( 91 | open && ( 92 | 98 | {children} 99 | 100 | ) 101 | ) 102 | } 103 | 104 | Tooltip.propTypes = { 105 | children: PropTypes.any.isRequired, 106 | offset: PropTypes.number, 107 | open: PropTypes.bool, 108 | placement: PropTypes.string, 109 | zIndex: PropTypes.number, 110 | fadeEasing: easingPropType, 111 | fadeDuration: PropTypes.number, 112 | } 113 | 114 | export default Tooltip 115 | -------------------------------------------------------------------------------- /src/components/Tooltip/index.test.js: -------------------------------------------------------------------------------- 1 | import {mount} from "enzyme" 2 | import React from "react" 3 | import Tooltip from "./index" 4 | import * as emotion from "emotion" 5 | import {matchers, createSerializer} from "jest-emotion" 6 | 7 | expect.extend(matchers) 8 | expect.addSnapshotSerializer(createSerializer(emotion)) 9 | 10 | const tooltipProps = { 11 | offset: 8, 12 | zIndex: 8, 13 | open: true, 14 | children: "😎", 15 | } 16 | const TooltipUpFixture = 17 | const TooltipBottomFixture = 18 | const TooltipLeftFixture = 19 | const TooltipRightFixture = 20 | const NoTooltipFixture = 21 | 22 | describe("Tooltip", () => { 23 | it("renders", () => { 24 | const wrappers = [ 25 | mount(TooltipUpFixture), 26 | mount(TooltipBottomFixture), 27 | mount(TooltipLeftFixture), 28 | mount(TooltipRightFixture), 29 | ] 30 | 31 | wrappers.forEach(wrapper => { 32 | expect(wrapper).not.toHaveStyleRule("animation") 33 | expect(wrapper).toMatchSnapshot() 34 | }) 35 | }) 36 | 37 | it("do not render", () => { 38 | expect( 39 | mount(NoTooltipFixture) 40 | .children() 41 | .get(0) 42 | ).toBeFalsy() 43 | }) 44 | 45 | it("should create a Tooltip with an animation", () => { 46 | const wrapper = mount( 47 | 48 | ) 49 | expect(wrapper).toHaveStyleRule("animation", /100ms ease-out 0s 1 \w+/) 50 | expect(wrapper).toMatchSnapshot() 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-simple-tooltip"; 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import React from "react" 3 | import PropTypes from "prop-types" 4 | import {css, jsx} from "@emotion/core" 5 | 6 | import Arrow from "./components/Arrow" 7 | import Tooltip from "./components/Tooltip" 8 | import Bubble from "./components/Bubble" 9 | import {easingPropType} from "./utils/propTypes" 10 | 11 | const ContainerCss = css` 12 | position: relative; 13 | display: inline-block; 14 | ` 15 | 16 | class Wrapper extends React.Component { 17 | constructor() { 18 | super() 19 | 20 | this.state = { 21 | open: false, 22 | } 23 | 24 | this.handleMouseEnter = this.handleMouseEnter.bind(this) 25 | this.handleMouseLeave = this.handleMouseLeave.bind(this) 26 | this.handleTouch = this.handleTouch.bind(this) 27 | } 28 | 29 | handleMouseEnter() { 30 | this.setState({open: true}) 31 | } 32 | 33 | handleMouseLeave() { 34 | this.setState({open: false}) 35 | } 36 | 37 | handleTouch() { 38 | const isOpen = this.state.open 39 | this.setState({open: !isOpen}) 40 | } 41 | 42 | render() { 43 | const {open} = this.state 44 | const { 45 | arrow, 46 | background, 47 | border, 48 | children, 49 | color, 50 | content, 51 | customCss, 52 | fadeDuration, 53 | fadeEasing, 54 | fixed, 55 | fontFamily, 56 | fontSize, 57 | offset, 58 | padding, 59 | placement, 60 | radius, 61 | zIndex, 62 | ...props 63 | } = this.props 64 | const hasTrigger = children !== undefined && children !== null 65 | const tooltipElement = ( 66 | 74 | 83 | 90 | {content} 91 | 92 | 93 | ) 94 | return hasTrigger ? ( 95 |
105 | {children} 106 | {tooltipElement} 107 |
108 | ) : ( 109 |
116 | {tooltipElement} 117 |
118 | ) 119 | } 120 | } 121 | 122 | Wrapper.propTypes = { 123 | arrow: PropTypes.number, 124 | background: PropTypes.string, 125 | border: PropTypes.string, 126 | children: PropTypes.any, 127 | color: PropTypes.string, 128 | content: PropTypes.any.isRequired, 129 | customCss: PropTypes.any, 130 | fadeDuration: PropTypes.number, 131 | fadeEasing: easingPropType, 132 | fixed: PropTypes.bool, 133 | fontFamily: PropTypes.string, 134 | fontSize: PropTypes.string, 135 | offset: PropTypes.number, 136 | padding: PropTypes.number, 137 | placement: PropTypes.oneOf(["left", "top", "right", "bottom"]), 138 | radius: PropTypes.number, 139 | zIndex: PropTypes.number, 140 | } 141 | 142 | Wrapper.defaultProps = { 143 | arrow: 8, 144 | background: "#000", 145 | border: "#000", 146 | children: null, 147 | color: "#fff", 148 | fadeDuration: 0, 149 | fadeEasing: "linear", 150 | fixed: false, 151 | fontFamily: "inherit", 152 | fontSize: "inherit", 153 | offset: 0, 154 | padding: 16, 155 | placement: "top", 156 | radius: 0, 157 | zIndex: 1, 158 | } 159 | 160 | Wrapper.displayName = "Tooltip.Wrapper" 161 | Tooltip.displayName = "Tooltip" 162 | Bubble.displayName = "Tooltip.Bubble" 163 | Arrow.displayName = "Tooltip.Arrow" 164 | 165 | export default Wrapper 166 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import {mount} from "enzyme" 2 | import React from "react" 3 | import Tooltip from "./index" 4 | import TooltipElement from "./components/Tooltip" 5 | 6 | describe("Tooltip", () => { 7 | const render = props => 8 | mount( 9 | 10 | 11 | 12 | ) 13 | 14 | const renderWithoutChildren = props => 15 | mount() 16 | 17 | it("should render", () => { 18 | const wrapper = render() 19 | expect(wrapper).toMatchSnapshot() 20 | }) 21 | 22 | it("Should render without children", () => { 23 | const wrapper = renderWithoutChildren() 24 | expect(wrapper).toMatchSnapshot() 25 | }) 26 | 27 | it("Should render when there is an offset", () => { 28 | const arrowSize = 8 29 | const arrowOffset = 8 30 | const wrapper = render({arrow: arrowSize, offset: arrowOffset}) 31 | const toolTip = wrapper.find(TooltipElement) 32 | expect(toolTip.prop("offset")).toEqual(arrowSize + arrowOffset) 33 | }) 34 | 35 | it("should open when user hovers and close when the mouse leaves", () => { 36 | const wrapper = render() 37 | const container = wrapper 38 | .findWhere(n => typeof n.prop("onMouseEnter") === "function") 39 | .first() 40 | const toolTip = wrapper.find(TooltipElement) 41 | 42 | // Expect tooltip to be closed by default 43 | expect(wrapper.state("open")).toEqual(false) 44 | expect(toolTip.prop("open")).toEqual(false) 45 | 46 | // Simulate a mouse enter event 47 | container.simulate("mouseEnter") 48 | wrapper.update() 49 | 50 | // Expect the tooltip to be open 51 | expect(wrapper.state("open")).toEqual(true) 52 | // TODO: Make this work 53 | // expect(toolTip.prop("open")).toEqual(true) 54 | 55 | // Simulate a mouse leave event 56 | container.simulate("mouseLeave") 57 | wrapper.update() 58 | 59 | // Expect the tooltip to be closed 60 | expect(wrapper.state("open")).toEqual(false) 61 | expect(toolTip.prop("open")).toEqual(false) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /src/utils/propTypes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types" 2 | 3 | const normalEasingPropType = PropTypes.oneOf([ 4 | "linear", 5 | "ease", 6 | "ease-in", 7 | "ease-out", 8 | "ease-in-out", 9 | ]) 10 | 11 | // A regex to test if a string matches the CSS cubic-beizer format 12 | // cubic-bezier(n,n,n,n) 13 | // See: https://regex101.com/r/n2fAzV for details 14 | const cubicEasingRegex = /^cubic-bezier\((-?((\d*\.\d+)|\d+),){3}(-?(\d*\.\d+)|\d+)\)$/ 15 | 16 | const cubicEasingPropType = (props, propName, componentName) => { 17 | if (!cubicEasingRegex.test(props[propName])) { 18 | return new Error( 19 | "Invalid prop `" + 20 | propName + 21 | "` supplied to" + 22 | " `" + 23 | componentName + 24 | "`. Validation failed." 25 | ) 26 | } 27 | } 28 | 29 | const easingPropType = PropTypes.oneOfType([ 30 | normalEasingPropType, 31 | cubicEasingPropType, 32 | ]) 33 | 34 | export {easingPropType} 35 | -------------------------------------------------------------------------------- /src/utils/propTypes.test.js: -------------------------------------------------------------------------------- 1 | import {checkPropTypes} from "prop-types" 2 | import {easingPropType} from "./propTypes" 3 | import sinon from "sinon" 4 | 5 | describe("easingPropType", () => { 6 | let consoleSpy = null 7 | let consoleStub = null 8 | beforeEach(() => { 9 | consoleSpy = jest.fn() 10 | consoleStub = sinon.stub(global.console, "error").callsFake(consoleSpy) // eslint-disable-line no-undef 11 | }) 12 | 13 | afterEach(() => { 14 | consoleSpy.mockClear() 15 | consoleStub.restore() 16 | }) 17 | 18 | it("should not error for normal easing types", () => { 19 | const normalEasingProps = [ 20 | "linear", 21 | "ease", 22 | "ease-in", 23 | "ease-out", 24 | "ease-in-out", 25 | ] 26 | 27 | normalEasingProps.forEach(easing => { 28 | checkPropTypes( 29 | {fadeEasing: easingPropType}, 30 | {fadeEasing: easing}, 31 | "fadeEasing", 32 | "DummyComponent" 33 | ) 34 | }) 35 | 36 | expect(consoleSpy).not.toHaveBeenCalled() 37 | }) 38 | 39 | it("should not error for valid cubic-bezier easings", () => { 40 | const validCubicBezierProps = [ 41 | // Normal 42 | "cubic-bezier(2.1,0.1,0.1,0.1)", 43 | 44 | // Some without 0s 45 | "cubic-bezier(.1,0.1,0.1,.1)", 46 | 47 | // Some with negative 48 | "cubic-bezier(-.1,0.1,.1,-0.1)", 49 | 50 | // Some without decimals 51 | "cubic-bezier(20,0.1,.1,30)", 52 | ] 53 | 54 | validCubicBezierProps.forEach(easing => { 55 | checkPropTypes( 56 | {fadeEasing: easingPropType}, 57 | {fadeEasing: easing}, 58 | "fadeEasing", 59 | "DummyComponent" 60 | ) 61 | }) 62 | 63 | expect(consoleSpy).not.toHaveBeenCalled() 64 | }) 65 | 66 | it("should error for invalid cubic-bezier easings", () => { 67 | const invalidCubicBezierProps = [ 68 | // Just numbers 69 | "(0.1,0.1,0.1,0.1)", 70 | "0.1,0.1,0.1,0.1", 71 | 72 | // Mis-spelled 73 | "cubic-beizer(0.1,0.1,0.1,0.1)", 74 | 75 | // Extra number 76 | "cubic-bezier(0.1,0.1,0.1,0.1,0.1)", 77 | 78 | // Not enough numbers 79 | "cubic-bezier(0.1,0.1,0.1)", 80 | 81 | // Missing brackets 82 | "cubic-bezier(0.1,0.1,0.1,0.1", 83 | "cubic-bezier0.1,0.1,0.1,0.1)", 84 | ] 85 | 86 | invalidCubicBezierProps.forEach((easing, idx) => { 87 | // checkPropTypes only warns once for the same prop, 88 | // so we need to change the prop name for every call 89 | const propName = `fadeEasing_${idx}` 90 | checkPropTypes( 91 | {[propName]: easingPropType}, 92 | {[propName]: easing}, 93 | propName, 94 | "DummyComponent" 95 | ) 96 | }) 97 | 98 | expect(consoleSpy).toHaveBeenCalledTimes(invalidCubicBezierProps.length) 99 | }) 100 | }) 101 | --------------------------------------------------------------------------------