├── .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 | [](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 |
--------------------------------------------------------------------------------