├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── integration.yml ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __mocks__ └── intro.js.js ├── babel.config.js ├── index.d.ts ├── package.json ├── src ├── components │ ├── Hints │ │ ├── __snapshots__ │ │ │ └── index.test.js.snap │ │ ├── index.js │ │ └── index.test.js │ └── Steps │ │ ├── __snapshots__ │ │ └── index.test.js.snap │ │ ├── index.js │ │ └── index.test.js ├── helpers │ ├── defaultProps.js │ ├── proptypes.js │ └── server.js └── index.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.d.ts -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "airbnb", 5 | "prettier", 6 | "prettier/react", 7 | "plugin:jest/recommended" 8 | ], 9 | "plugins": [ 10 | "prettier", 11 | "jest" 12 | ], 13 | "rules": { 14 | "prettier/prettier": 2, 15 | "react/jsx-filename-extension": 0, 16 | "import/no-extraneous-dependencies": [ 17 | 2, 18 | { 19 | "devDependencies": true 20 | } 21 | ], 22 | "no-underscore-dangle": 0, 23 | "import/prefer-default-export": 0 24 | }, 25 | "env": { 26 | "browser": true, 27 | "es6": true, 28 | "jest/globals": true 29 | }, 30 | "parserOptions": { 31 | "sourceType": "module", 32 | "ecmaFeatures": { 33 | "impliedStrict": true, 34 | "jsx": true, 35 | "experimentalObjectRestSpread": true 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: integration 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | 12 | - name: Setup Node.js 13 | uses: actions/setup-node@v2 14 | with: 15 | node-version: '12' 16 | 17 | - name: Install dependencies 18 | run: yarn 19 | 20 | - name: Lint 21 | run: yarn lint 22 | 23 | - name: Test with coverage 24 | run: yarn test:ci 25 | 26 | - name: Coveralls 27 | uses: coverallsapp/github-action@master 28 | with: 29 | github-token: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | npm-debug.log.* 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | notifications: 5 | email: 6 | on_success: never 7 | script: 8 | - yarn run lint 9 | - yarn run test 10 | cache: 11 | directories: 12 | - node_modules 13 | after_success: 14 | - yarn run coveralls 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | ###    🚨 Breaking Changes 4 | 5 | * 💥 Add ESM/CJS package compatibility. 6 | 7 | This new major release [adds support for ESM](https://github.com/HiDeoo/intro.js-react/pull/104) to the published package while preserving CJS compatibility. The `main/module/exports` fields in `package.json` are now pointing to new artifacts: `dist/esm/index.mjs` which is an ESM file that most modern tooling should pick up, `dist/cjs/index.cjs` which is a CJS file and `dist/esm/index.js` which is a copy of the ESM file with a `.js` extension to support Webpack 4. Thanks to [@lewisl9029](https://github.com/lewisl9029) for the contribution. 8 | 9 | ## 0.7.1 10 | 11 | * Update TypeScript `onBeforeChange` return type to support promises. 12 | 13 | ## 0.7.0 14 | 15 | * Add missing TypeScript `title` attribute type to steps. 16 | * Fix various issues with Next.js and SSR. 17 | 18 | ## 0.6.0 19 | 20 | * The `onBeforeChange` callback now receives the `nextElement` as second parameter. 21 | * Add missing `updateStepElement()` to the TypeScript type declarations. 22 | * Fix Steps's `element` parameter type to also accept an `HTMLElement`. 23 | 24 | ## 0.5.0 25 | 26 | * Bump peer dependencies versions. 27 | 28 | ## 0.4.0 29 | 30 | * Add TypeScript type declarations. 31 | 32 | ## 0.3.0 33 | 34 | * Add support for React elements to Steps's `intro` parameter. 35 | 36 | ## 0.2.0 37 | 38 | * 💥 `onBeforeChange` is no longer called with the `nextElement` parameter. 39 | 40 | * Add React 16 support. 41 | * If at least intro.js 2.8.0 is used, `false` can be returned in `onBeforeChange` to prevent transitioning to the next / previous step. 42 | * Add the `onPreventChange` callback called when a transition is prevented by returning `false` in `onBeforeChange`. 43 | * Add the `onBeforeExit` callback to prevent exiting the intro. 44 | * Update intro.js `import` statements to avoid a deprecation warning. 45 | 46 | ## 0.1.5 47 | 48 | * Add the `updateStepElement()` API to update the element associated to a step based on its index. This is useful when the associated element is not present in the DOM on page load. 49 | 50 | ## 0.1.4 51 | 52 | * `element` CSS selector are no longer required for a Step so floating steps can be created more easily. 53 | 54 | ## 0.1.3 55 | 56 | * Improve consistency around callbacks in the Steps component. 57 | 58 | ## 0.1.2 59 | 60 | * Add onComplete callback for Steps. 61 | 62 | ## 0.1.1 63 | 64 | * Hide steps & hints when unmounting. 65 | 66 | ## 0.1.0 67 | 68 | * First release. 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 HiDeoo 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # intro.js-react 2 | 3 | [![integration](https://github.com/HiDeoo/intro.js-react/actions/workflows/integration.yml/badge.svg)](https://github.com/HiDeoo/intro.js-react/actions/workflows/integration.yml) [![Coverage Status](https://coveralls.io/repos/github/HiDeoo/intro.js-react/badge.svg?branch=master)](https://coveralls.io/github/HiDeoo/intro.js-react?branch=master) 4 | 5 | A small React wrapper around [Intro.js](http://introjs.com/). The wrapper provides support for both steps and hints. 6 | 7 | ## Quicklinks 8 | 9 | - [Example](#example) 10 | - [Installation](#installation) 11 | - [Usage](#usage) 12 | - [Steps](#steps) 13 | - [Hints](#hints) 14 | - [Intro.js API](#introjs-api) 15 | - [Intro.js options](#introjs-options) 16 | 17 | ## Example 18 | 19 | You can find a small example [here on codesandbox.io](https://codesandbox.io/embed/o2A4gwXE3?hidenavigation=1). 20 | 21 | ## Installation 22 | 23 | Install using Npm *(don't forget to add the `--save` option if you're still using npm < 5)*: 24 | 25 | ```sh 26 | npm install intro.js-react 27 | ``` 28 | 29 | Or Yarn: 30 | 31 | 32 | ```sh 33 | yarn add intro.js-react 34 | ``` 35 | 36 | Make sure to have [React](https://facebook.github.io/react/) & [Intro.js](http://introjs.com/) installed (they're peer dependencies) and the Intro.js CSS definitions [properly loaded](http://introjs.com/docs/getting-started/install) into your project. 37 | 38 | This would usually looks like: 39 | 40 | ```js 41 | import 'intro.js/introjs.css'; 42 | ``` 43 | 44 | ## Usage 45 | 46 | Two component are available for both steps and hints: 47 | 48 | ```js 49 | import { Steps, Hints } from 'intro.js-react'; 50 | ``` 51 | 52 | ## Steps 53 | 54 | **Note: Steps indexes always starts from 0 in this wrapper instead of 1 like in Intro.js.** 55 | 56 | ### Basic example: 57 | 58 | ```js 59 | 65 | ``` 66 | 67 | ### Props 68 | 69 | | Name | Description | Type | Required | 70 | | --- | --- | :---: | :---: | 71 | | `enabled` | Defines if the steps are visible or not.
*Default: false.* | Boolean | | 72 | | `initialStep` | Step index to start with when showing the steps. | Number | ✅ | 73 | | `steps` | All the steps. | [Step[]](#step) | ✅ | 74 | | `onExit` | Callback called when the steps are disabled
*Required to force keeping track of the state when the steps are dismissed with an Intro.js event and not the `enabled` prop.* | Function
*(stepIndex)* | ✅ | 75 | | `onBeforeExit` | Callback called before exiting the intro.
*If you want to prevent exiting the intro, you can return `false` in this callback (available since intro.js 0.2.7).* | Function
*(stepIndex)* | | 76 | | `onStart` | Callback called when the steps are enabled. | Function
*(stepIndex)* | | 77 | | `onChange` | Callback called when the current step is changed. | Function
*(nextStepIndex, nextElement)* | | 78 | | `onBeforeChange` | Callback called before changing the current step.
*If you want to prevent the transition to the next / previous step, you can return `false` in this callback (available since intro.js 2.8.0).* | Function
*(nextStepIndex, nextElement)* | | 79 | | `onAfterChange` | Callback called after changing the current step. | Function
*(newStepIndex, newElement)* | | 80 | | `onPreventChange` | Callback called if you prevented transitioning to a new step by returning `false` in `onBeforeChange`. | Function
*(stepIndex)* | | 81 | | `onComplete` | Callback called when all the steps are completed. | Function
*()* | | 82 | | `options` | Intro.js options. | [Object](#introjs-options) | | | 83 | 84 | ### Step 85 | 86 | ```js 87 | const steps = [ 88 | { 89 | element: '.selector1', 90 | intro: 'test 1', 91 | position: 'right', 92 | tooltipClass: 'myTooltipClass', 93 | highlightClass: 'myHighlightClass', 94 | }, 95 | { 96 | element: '.selector2', 97 | intro: 'test 2', 98 | }, 99 | { 100 | element: '.selector3', 101 | intro: 'test 3', 102 | }, 103 | ]; 104 | ``` 105 | 106 | | Name | Description | Type | Required | 107 | | --- | --- | :---: | :---: | 108 | | `element` | CSS selector to use for the step. | String | | 109 | | `intro` | The tooltip content. | String \| React element | ✅ | 110 | | `position` | Position of the tooltip. | String | | 111 | | `tooltipClass` | CSS class of the tooltip. | String | | 112 | | `highlightClass` | CSS class of the helperLayer. | String | | | 113 | 114 | ### Dynamic elements 115 | 116 | If you want to use Intro.js Steps with dynamically created elements, you have to update the element associated to the step when it's available. 117 | 118 | To do that, you can use the `updateStepElement()` API and pass to it the index of the step to update: 119 | 120 | ```js 121 | (this.steps = steps)} 125 | /> 126 | ``` 127 | 128 | ```js 129 | onBeforeChange = nextStepIndex => { 130 | if (nextStepIndex === 4) { 131 | this.steps.updateStepElement(nextStepIndex); 132 | } 133 | } 134 | ``` 135 | 136 | ## Hints 137 | 138 | ### Basic example: 139 | 140 | ```js 141 | 145 | ``` 146 | 147 | ### Props 148 | 149 | | Name | Description | Type | Required | 150 | | --- | --- | :---: | :---: | 151 | | `enabled` | Defines if the hints are visible or not.
*Default: false.* | Boolean | | 152 | | `hints` | All the hints. | [Hint[]](#hint) | ✅ | 153 | | `onClick` | Callback called when a hint is clicked. | Function
*( )* | | 154 | | `onClose` | Callback called when a hint is closed. | Function
*( )* | | 155 | | `options` | Intro.js options. | [Object](#introjs-options) | | | 156 | 157 | ### Hint 158 | 159 | ```js 160 | const hints = [ 161 | { 162 | element: '.selector1', 163 | hint: 'test 1', 164 | hintPosition: 'middle-middle', 165 | }, 166 | { 167 | element: '.selector2', 168 | hint: 'test 2', 169 | }, 170 | ]; 171 | ``` 172 | 173 | | Name | Description | Type | Required | 174 | | --- | --- | :---: | :---: | 175 | | `element` | CSS selector to use for the hint. | String | ✅ | 176 | | `hint` | The tooltip text. | String | ✅ | 177 | | `hintPosition` | Position of the tooltip. | String | | | 178 | 179 | ## Intro.js API 180 | 181 | If for some reasons you need to use the [Intro.js API](http://introjs.com/docs/), you can still get the Intro.js instance by using a ref on either the `` or `` components and using `this.refName.introJs`. 182 | 183 | ```js 184 | (this.hints = hints)} 188 | /> 189 | ``` 190 | 191 | ## Intro.js options 192 | 193 | You can find more details regarding Intro.js options and their default values in [the documentation](http://introjs.com/docs/) or directly in [their code](https://github.com/usablica/intro.js/blob/31f7def834664efb26b964f0a2a03444ef29a32c/src/index.js#L34). 194 | 195 | The wrapper overrides some Intro.js default options in the `helpers/defaultProps.js` file. 196 | 197 | | Name | Description | Type | 198 | | --- | --- | :---: | 199 | | `nextLabel` | Next button label. | String | 200 | | `prevLabel` | Previous button label. | String | 201 | | `skipLabel` | Skip button label. | String | 202 | | `doneLabel` | Done button label. | String | 203 | | `hidePrev` | Hides the Previous button in the first step. | Boolean | 204 | | `hideNext` | Hide the Next button in the last step. | Boolean | 205 | | `tooltipPosition` | Position of the tooltips. | String | 206 | | `tooltipClass` | CSS class of the tooltips. | String | 207 | | `highlightClass` | CSS class of the helperLayer. | String | 208 | | `exitOnEsc` | Exit by pressing Escape. | Boolean | 209 | | `exitOnOverlayClick` | Exit by clicking on the overlay layer. | Boolean | 210 | | `showStepNumbers` | Show steps number in a red circle. | Boolean | 211 | | `keyboardNavigation` | Allows navigation between steps using the keyboard. | Boolean | 212 | | `showButtons` | Show navigation buttons. | Boolean | 213 | | `showBullets` | Show bullets. | Boolean | 214 | | `showProgress` | Show progress indicator. | Boolean | 215 | | `scrollToElement` | Enables scrolling to hidden elements. | Boolean | 216 | | `overlayOpacity` | Opacity of the overlay. | Number | 217 | | `scrollPadding` | Padding when automatically scrolling to an element. | Number | 218 | | `positionPrecedence` | Precedence of positions. | String[] | 219 | | `disableInteraction` | Disables interaction inside elements. | Boolean | 220 | | `hintPosition` | Position of the hints. | String | 221 | | `hintButtonLabel` | Hint button label. | String | 222 | | `hintAnimation` | Enables hint animations. | Boolean | 223 | 224 | ## License 225 | 226 | Licensed under the MIT License, Copyright © HiDeoo. 227 | 228 | See [LICENSE](./LICENSE) for more information. 229 | -------------------------------------------------------------------------------- /__mocks__/intro.js.js: -------------------------------------------------------------------------------- 1 | class IntroJsMock { 2 | constructor() { 3 | this._currentStep = 0; 4 | } 5 | 6 | onexit(fn) { 7 | if (fn) { 8 | this.exitFn = fn; 9 | } 10 | } 11 | 12 | onbeforeexit(fn) { 13 | if (fn) { 14 | this.onBeforeExitFn = fn; 15 | } 16 | } 17 | 18 | exit() { 19 | if (this.onBeforeExitFn) { 20 | this.onBeforeExitFn(); 21 | } 22 | 23 | if (this.exitFn) { 24 | this.exitFn(); 25 | } 26 | } 27 | 28 | onchange(fn) { 29 | if (fn) { 30 | this.onChangeFn = fn; 31 | } 32 | } 33 | 34 | goToStepNumber(step) { 35 | this._currentStep = step; 36 | 37 | if (this.onBeforeChangeFn) { 38 | this.onBeforeChangeFn(null); 39 | } 40 | 41 | if (this.onChangeFn) { 42 | this.onChangeFn(null); 43 | } 44 | 45 | if (this.onAfterChangeFn) { 46 | this.onAfterChangeFn(null); 47 | } 48 | } 49 | 50 | onafterchange(fn) { 51 | if (fn) { 52 | this.onAfterChangeFn = fn; 53 | } 54 | } 55 | 56 | onbeforechange(fn) { 57 | if (fn) { 58 | this.onBeforeChangeFn = fn; 59 | } 60 | } 61 | 62 | start() { 63 | if (this.onBeforeChangeFn) { 64 | this.onBeforeChangeFn(null); 65 | } 66 | 67 | if (this.onChangeFn) { 68 | this.onChangeFn(null); 69 | } 70 | 71 | if (this.onAfterChangeFn) { 72 | this.onAfterChangeFn(null); 73 | } 74 | } 75 | 76 | onhintclick(fn) { 77 | if (fn) { 78 | this.onHintClick = fn; 79 | } 80 | } 81 | 82 | onhintclose(fn) { 83 | if (fn) { 84 | this.onHintClose = fn; 85 | } 86 | } 87 | 88 | setOptions(options) { 89 | this._options = options; 90 | this._introItems = options.steps; 91 | } 92 | 93 | /* eslint-disable class-methods-use-this */ 94 | removeHints() {} 95 | showHints() {} 96 | hideHints() {} 97 | oncomplete() {} 98 | /* eslint-enable class-methods-use-this */ 99 | } 100 | 101 | const intro = () => new IntroJsMock(); 102 | 103 | export const introJs = intro; 104 | 105 | export default intro; 106 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | const isTest = api.env('test'); 3 | 4 | api.cache(true); 5 | 6 | const config = { 7 | presets: [ 8 | [ 9 | '@babel/preset-env', 10 | { 11 | targets: '> 0.5%, last 2 versions, Firefox ESR, not dead', 12 | modules: process.env.CJS || isTest ? 'commonjs' : false, 13 | }, 14 | ], 15 | '@babel/preset-react', 16 | ], 17 | }; 18 | 19 | if (!isTest) { 20 | config.plugins = [['babel-plugin-add-import-extension', { extension: process.env.CJS ? 'cjs' : 'mjs' }]]; 21 | } 22 | 23 | return config; 24 | }; 25 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IntroJs, Options } from 'intro.js'; 3 | 4 | interface Step { 5 | /** 6 | * CSS selector or element to use for the step. 7 | */ 8 | element?: string | HTMLElement | Element; 9 | /** 10 | * The tooltip content. 11 | */ 12 | intro: string | React.ReactNode; 13 | /** 14 | * Position of the tooltip. 15 | */ 16 | position?: string; 17 | /** 18 | * The tooltip title. 19 | */ 20 | title?: string; 21 | /** 22 | * CSS class of the tooltip. 23 | */ 24 | tooltipClass?: string; 25 | /** 26 | * CSS class of the helperLayer. 27 | */ 28 | highlightClass?: string; 29 | } 30 | 31 | interface Hint { 32 | /** 33 | * CSS selector to use for the hint. 34 | */ 35 | element: string; 36 | /** 37 | * The tooltip text. 38 | */ 39 | hint: string; 40 | /** 41 | * Position of the tooltip. 42 | */ 43 | hintPosition?: string; 44 | } 45 | 46 | interface StepsProps { 47 | /** 48 | * Defines if the steps are visible or not. 49 | * @default false 50 | */ 51 | enabled?: boolean; 52 | /** 53 | * Step index to start with when showing the steps. 54 | */ 55 | initialStep: number; 56 | /** 57 | * All the steps. 58 | */ 59 | steps: Step[]; 60 | /** 61 | * Callback called when the steps are disabled. 62 | * Required to force keeping track of the state when the steps are dismissed with an Intro.js event and not the 63 | * enabled prop. 64 | */ 65 | onExit(stepIndex: number): void; 66 | /** 67 | * Callback called before exiting the intro. 68 | * If you want to prevent exiting the intro, you can return false in this callback (available since intro.js 0.2.7). 69 | */ 70 | onBeforeExit?(stepIndex: number): void | false; 71 | /** 72 | * Callback called when the steps are enabled. 73 | */ 74 | onStart?(stepIndex: number): void; 75 | /** 76 | * Callback called when the current step is changed. 77 | */ 78 | onChange?(nextStepIndex: number, nextElement: Element): void; 79 | /** 80 | * Callback called before changing the current step. 81 | * If you want to prevent the transition to the next / previous step, you can return false in this callback 82 | * (available since intro.js 2.8.0). 83 | */ 84 | onBeforeChange?(nextStepIndex: number, nextElement: Element): void | false | Promise; 85 | /** 86 | * Callback called after changing the current step. 87 | */ 88 | onAfterChange?(newStepIndex: number, newElement: Element): void; 89 | /** 90 | * Callback called if you prevented transitioning to a new step by returning false in onBeforeChange. 91 | */ 92 | onPreventChange?(stepIndex: number): void; 93 | /** 94 | * Callback called when all the steps are completed. 95 | */ 96 | onComplete?(): void; 97 | /** 98 | * Intro.js options. 99 | */ 100 | options?: Options; 101 | } 102 | 103 | interface HintsProps { 104 | /** 105 | * Defines if the hints are visible or not. 106 | * @default false 107 | */ 108 | enabled?: boolean; 109 | /** 110 | * All the hints. 111 | */ 112 | hints: Hint[]; 113 | /** 114 | * Callback called when a hint is clicked. 115 | */ 116 | onClick?(): void; 117 | /** 118 | * Callback called when a hint is closed. 119 | */ 120 | onClose?(): void; 121 | /** 122 | * Intro.js options. 123 | */ 124 | options?: Options; 125 | } 126 | 127 | export class Steps extends React.Component { 128 | public introJs: IntroJs; 129 | public updateStepElement(stepIndex: number): void; 130 | } 131 | 132 | export class Hints extends React.Component {} 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "intro.js-react", 3 | "version": "1.0.0", 4 | "description": "Intro.js React Wrapper", 5 | "main": "dist/cjs/index.js", 6 | "module": "dist/esm/index.js", 7 | "types": "index.d.ts", 8 | "exports": { 9 | "./package.json": "./package.json", 10 | ".": { 11 | "import": { 12 | "types": "./dist/esm/index.d.mts", 13 | "default": "./dist/esm/index.mjs" 14 | }, 15 | "default": { 16 | "types": "./dist/cjs/index.d.cts", 17 | "default": "./dist/cjs/index.cjs" 18 | } 19 | } 20 | }, 21 | "scripts": { 22 | "prebuild": "rimraf dist", 23 | "build": "npm run build:cjs && npm run build:esm", 24 | "build:cjs": "CJS=true babel --ignore '**/*.test.js' --out-dir dist/cjs --out-file-extension .cjs src && cp dist/cjs/index.cjs dist/cjs/index.js && cp index.d.ts dist/cjs/index.d.cts", 25 | "build:esm": "babel --ignore '**/*.test.js' --out-dir dist/esm --out-file-extension .mjs src && cp dist/esm/index.mjs dist/esm/index.js && cp index.d.ts dist/esm/index.d.mts", 26 | "build:watch": "npm run build:esm -- --watch", 27 | "lint": "eslint src", 28 | "test": "jest", 29 | "test:watch": "jest --watch", 30 | "test:coverage": "jest --coverage && open coverage/index.html", 31 | "test:ci": "jest --coverage", 32 | "precommit": "lint-staged", 33 | "prepublish": "npm run build" 34 | }, 35 | "devDependencies": { 36 | "@babel/cli": "7.21.0", 37 | "@babel/core": "^7.21.4", 38 | "@babel/preset-env": "7.21.4", 39 | "@babel/preset-react": "7.18.6", 40 | "@types/intro.js": "3.0.1", 41 | "@types/react": "17.0.4", 42 | "babel-eslint": "^7.2.3", 43 | "babel-jest": "26.0.1", 44 | "babel-plugin-add-import-extension": "^1.6.0", 45 | "coveralls": "^3.0.1", 46 | "enzyme": "^2.8.2", 47 | "eslint": "7.0.0", 48 | "eslint-config-airbnb": "^15.0.1", 49 | "eslint-config-prettier": "^2.1.1", 50 | "eslint-plugin-import": "^2.3.0", 51 | "eslint-plugin-jest": "^20.0.3", 52 | "eslint-plugin-jsx-a11y": "^5.0.3", 53 | "eslint-plugin-prettier": "^2.4.0", 54 | "eslint-plugin-react": "^7.0.1", 55 | "husky": "^0.13.4", 56 | "intro.js": "^2.9.3", 57 | "jest": "26.0.1", 58 | "lint-staged": "^3.6.0", 59 | "prettier": "^1.7.0", 60 | "prop-types": "^15.5.10", 61 | "react": "^15.5.4", 62 | "react-dom": "^15.5.4", 63 | "react-test-renderer": "^15.5.4", 64 | "rimraf": "^2.6.1" 65 | }, 66 | "peerDependencies": { 67 | "intro.js": ">=2.5.0", 68 | "react": ">=0.14.0" 69 | }, 70 | "repository": { 71 | "type": "git", 72 | "url": "git+https://github.com/HiDeoo/intro.js-react.git" 73 | }, 74 | "keywords": [ 75 | "intro.js", 76 | "react", 77 | "wrapper" 78 | ], 79 | "author": "HiDeoo", 80 | "license": "MIT", 81 | "bugs": { 82 | "url": "https://github.com/HiDeoo/intro.js-react/issues" 83 | }, 84 | "homepage": "https://github.com/HiDeoo/intro.js-react#readme", 85 | "lint-staged": { 86 | "*.js": [ 87 | "prettier --write", 88 | "git add" 89 | ] 90 | }, 91 | "jest": { 92 | "coverageReporters": [ 93 | "html", 94 | "lcov" 95 | ] 96 | }, 97 | "files": [ 98 | "dist", 99 | "index.d.ts" 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /src/components/Hints/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Hints should render nothing 1`] = `null`; 4 | -------------------------------------------------------------------------------- /src/components/Hints/index.js: -------------------------------------------------------------------------------- 1 | import introJs from 'intro.js'; 2 | import PropTypes from 'prop-types'; 3 | import { Component } from 'react'; 4 | 5 | import * as introJsPropTypes from '../../helpers/proptypes'; 6 | import * as introJsDefaultProps from '../../helpers/defaultProps'; 7 | import { isServer } from '../../helpers/server'; 8 | 9 | /** 10 | * Intro.js Hints Component. 11 | */ 12 | export default class Hints extends Component { 13 | /** 14 | * React Props 15 | * @type {Object} 16 | */ 17 | static propTypes = { 18 | enabled: PropTypes.bool, 19 | hints: PropTypes.arrayOf( 20 | PropTypes.shape({ 21 | element: PropTypes.string.isRequired, 22 | hint: PropTypes.string.isRequired, 23 | hintPosition: introJsPropTypes.hintPosition, 24 | }) 25 | ).isRequired, 26 | onClick: PropTypes.func, 27 | onClose: PropTypes.func, 28 | options: introJsPropTypes.options, 29 | }; 30 | 31 | /** 32 | * React Default Props 33 | * @type {Object} 34 | */ 35 | static defaultProps = { 36 | enabled: false, 37 | onClick: null, 38 | onClose: null, 39 | options: introJsDefaultProps.options, 40 | }; 41 | 42 | /** 43 | * Creates a new instance of the component. 44 | * @class 45 | * @param {Object} props - The props of the component. 46 | */ 47 | constructor(props) { 48 | super(props); 49 | 50 | this.introJs = null; 51 | this.isConfigured = false; 52 | 53 | this.installIntroJs(); 54 | } 55 | 56 | /** 57 | * Lifecycle: componentDidMount. 58 | * We use this event to enable Intro.js hints at mount time if enabled right from the start. 59 | */ 60 | componentDidMount() { 61 | if (this.props.enabled) { 62 | this.configureIntroJs(); 63 | this.renderHints(); 64 | } 65 | } 66 | 67 | /** 68 | * Lifecycle: componentDidUpdate. 69 | * @param {Object} prevProps - The previous props. 70 | */ 71 | componentDidUpdate(prevProps) { 72 | const { enabled, hints, options } = this.props; 73 | 74 | if (!this.isConfigured || prevProps.hints !== hints || prevProps.options !== options) { 75 | this.configureIntroJs(); 76 | this.renderHints(); 77 | } 78 | 79 | if (prevProps.enabled !== enabled) { 80 | this.renderHints(); 81 | } 82 | } 83 | 84 | /** 85 | * Lifecycle: componentWillUnmount. 86 | * We use this even to hide the hints when the component is unmounted. 87 | */ 88 | componentWillUnmount() { 89 | this.introJs.hideHints(); 90 | } 91 | 92 | /** 93 | * Installs Intro.js. 94 | */ 95 | installIntroJs() { 96 | if (isServer()) { 97 | return; 98 | } 99 | 100 | this.introJs = introJs(); 101 | 102 | const { onClick, onClose } = this.props; 103 | 104 | if (onClick) { 105 | this.introJs.onhintclick(onClick); 106 | } 107 | 108 | if (onClose) { 109 | this.introJs.onhintclose(onClose); 110 | } 111 | } 112 | 113 | /** 114 | * Configures Intro.js if not already configured. 115 | */ 116 | configureIntroJs() { 117 | const { options, hints } = this.props; 118 | 119 | // We need to remove all hints otherwise new hints won't be added. 120 | this.introJs.removeHints(); 121 | 122 | this.introJs.setOptions({ ...options, hints }); 123 | 124 | this.isConfigured = true; 125 | } 126 | 127 | /** 128 | * Renders the Intro.js hints. 129 | */ 130 | renderHints() { 131 | const { enabled, hints } = this.props; 132 | 133 | if (enabled && hints.length > 0) { 134 | this.introJs.showHints(); 135 | } else if (!enabled) { 136 | this.introJs.hideHints(); 137 | } 138 | } 139 | 140 | /** 141 | * Renders the component. 142 | * @return {null} We do not want to render anything. 143 | */ 144 | render() { 145 | return null; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/components/Hints/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import Hints from './index'; 6 | import * as server from '../../helpers/server'; 7 | 8 | /** 9 | * Hints. 10 | * @type {Hint[]} 11 | */ 12 | const hints = [ 13 | { 14 | element: '.test', 15 | hint: 'test', 16 | }, 17 | ]; 18 | 19 | describe('Hints', () => { 20 | test('should render nothing', () => { 21 | const tree = renderer.create().toJSON(); 22 | 23 | expect(tree).toMatchSnapshot(); 24 | }); 25 | 26 | test('should not configure Intro.js at mount time if not enabled', () => { 27 | const wrapper = shallow(, { 28 | lifecycleExperimental: true, 29 | }); 30 | 31 | expect(wrapper.instance().isConfigured).toBe(false); 32 | }); 33 | 34 | test('should configure Intro.js at mount time if enabled at least once', () => { 35 | const wrapper = shallow(, { 36 | lifecycleExperimental: true, 37 | }); 38 | 39 | wrapper.setProps({ enabled: false }); 40 | 41 | expect(wrapper.instance().isConfigured).toBe(true); 42 | }); 43 | 44 | test('should configure Intro.js when enabled if not already configured', () => { 45 | const wrapper = shallow(, { 46 | lifecycleExperimental: true, 47 | }); 48 | 49 | wrapper.instance().configureIntroJs = jest.fn(); 50 | wrapper.update(); 51 | wrapper.setProps({ enabled: true }); 52 | 53 | expect(wrapper.instance().configureIntroJs).toHaveBeenCalled(); 54 | }); 55 | 56 | test('should call renderHints when enabled while being disabled', () => { 57 | const wrapper = shallow(, { 58 | lifecycleExperimental: true, 59 | }); 60 | 61 | wrapper.instance().renderHints = jest.fn(); 62 | wrapper.update(); 63 | wrapper.setProps({ enabled: true }); 64 | 65 | expect(wrapper.instance().renderHints).toHaveBeenCalled(); 66 | }); 67 | 68 | test('should call renderHints when enabled while being already enabled', () => { 69 | const wrapper = shallow(, { 70 | lifecycleExperimental: true, 71 | }); 72 | 73 | wrapper.instance().renderHints = jest.fn(); 74 | wrapper.update(); 75 | wrapper.setProps({ enabled: false }); 76 | 77 | expect(wrapper.instance().renderHints).toHaveBeenCalled(); 78 | }); 79 | 80 | test('should call renderHints everytime the enabled prop is modified', () => { 81 | const wrapper = shallow(, { 82 | lifecycleExperimental: true, 83 | }); 84 | 85 | wrapper.instance().renderHints = jest.fn(); 86 | wrapper.update(); 87 | wrapper.setProps({ enabled: true }); 88 | wrapper.setProps({ enabled: false }); 89 | wrapper.setProps({ enabled: true }); 90 | 91 | expect(wrapper.instance().renderHints).toHaveBeenCalledTimes(4); 92 | 93 | wrapper.unmount(); 94 | }); 95 | 96 | test('should not call renderHints when the enabled, hints or options are not modified', () => { 97 | const wrapper = shallow(, { 98 | lifecycleExperimental: true, 99 | }); 100 | 101 | wrapper.instance().renderHints = jest.fn(); 102 | wrapper.update(); 103 | wrapper.setProps({ enabled: true, hints }); 104 | 105 | expect(wrapper.instance().renderHints).not.toHaveBeenCalled(); 106 | }); 107 | 108 | test('should register the onClick callback', () => { 109 | const onClick = jest.fn(); 110 | 111 | const wrapper = shallow(); 112 | 113 | expect(wrapper.instance().introJs.onHintClick).toBe(onClick); 114 | }); 115 | 116 | test('should register the onClose callback', () => { 117 | const onClose = jest.fn(); 118 | 119 | const wrapper = shallow(); 120 | 121 | expect(wrapper.instance().introJs.onHintClose).toBe(onClose); 122 | }); 123 | 124 | test('should not install intro.js during SSR', () => { 125 | jest.spyOn(server, 'isServer').mockReturnValueOnce(true); 126 | 127 | const wrapper = shallow(); 128 | 129 | expect(wrapper.instance().introJs).toBe(null); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /src/components/Steps/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Steps should render nothing 1`] = `null`; 4 | -------------------------------------------------------------------------------- /src/components/Steps/index.js: -------------------------------------------------------------------------------- 1 | import introJs from 'intro.js'; 2 | import PropTypes from 'prop-types'; 3 | import { Component, isValidElement } from 'react'; 4 | import { renderToStaticMarkup } from 'react-dom/server'; 5 | 6 | import * as introJsPropTypes from '../../helpers/proptypes'; 7 | import * as introJsDefaultProps from '../../helpers/defaultProps'; 8 | import { isServer } from '../../helpers/server'; 9 | 10 | /** 11 | * Intro.js Steps Component. 12 | */ 13 | export default class Steps extends Component { 14 | /** 15 | * React Props 16 | * @type {Object} 17 | */ 18 | static propTypes = { 19 | enabled: PropTypes.bool, 20 | initialStep: PropTypes.number.isRequired, 21 | steps: PropTypes.arrayOf( 22 | PropTypes.shape({ 23 | element: PropTypes.oneOfType([ 24 | PropTypes.string, 25 | /* istanbul ignore next */ 26 | typeof Element === 'undefined' ? PropTypes.any : PropTypes.instanceOf(Element), 27 | ]), 28 | intro: PropTypes.node.isRequired, 29 | position: introJsPropTypes.tooltipPosition, 30 | tooltipClass: PropTypes.string, 31 | highlightClass: PropTypes.string, 32 | }) 33 | ).isRequired, 34 | onStart: PropTypes.func, 35 | onExit: PropTypes.func.isRequired, 36 | onBeforeExit: PropTypes.func, 37 | onBeforeChange: PropTypes.func, 38 | onAfterChange: PropTypes.func, 39 | onChange: PropTypes.func, 40 | onPreventChange: PropTypes.func, 41 | onComplete: PropTypes.func, 42 | options: introJsPropTypes.options, 43 | }; 44 | 45 | /** 46 | * React Default Props 47 | * @type {Object} 48 | */ 49 | static defaultProps = { 50 | enabled: false, 51 | onStart: null, 52 | onBeforeExit: null, 53 | onBeforeChange: null, 54 | onAfterChange: null, 55 | onChange: null, 56 | onPreventChange: null, 57 | onComplete: null, 58 | options: introJsDefaultProps.options, 59 | }; 60 | 61 | /** 62 | * Creates a new instance of the component. 63 | * @class 64 | * @param {Object} props - The props of the component. 65 | */ 66 | constructor(props) { 67 | super(props); 68 | 69 | this.introJs = null; 70 | this.isConfigured = false; 71 | // We need to manually keep track of the visibility state to avoid a callback hell. 72 | this.isVisible = false; 73 | 74 | this.installIntroJs(); 75 | } 76 | 77 | /** 78 | * Lifecycle: componentDidMount. 79 | * We use this event to enable Intro.js steps at mount time if enabled right from the start. 80 | */ 81 | componentDidMount() { 82 | if (this.props.enabled) { 83 | this.configureIntroJs(); 84 | this.renderSteps(); 85 | } 86 | } 87 | 88 | /** 89 | * Lifecycle: componentDidUpdate. 90 | * @param {Object} prevProps - The previous props. 91 | */ 92 | componentDidUpdate(prevProps) { 93 | const { enabled, steps, options } = this.props; 94 | 95 | if (!this.isConfigured || prevProps.steps !== steps || prevProps.options !== options) { 96 | this.configureIntroJs(); 97 | this.renderSteps(); 98 | } 99 | 100 | if (prevProps.enabled !== enabled) { 101 | this.renderSteps(); 102 | } 103 | } 104 | 105 | /** 106 | * Lifecycle: componentWillUnmount. 107 | * We use this even to hide the steps when the component is unmounted. 108 | */ 109 | componentWillUnmount() { 110 | this.introJs.exit(); 111 | } 112 | 113 | /** 114 | * Triggered when Intro.js steps are exited. 115 | */ 116 | onExit = () => { 117 | const { onExit } = this.props; 118 | 119 | this.isVisible = false; 120 | 121 | onExit(this.introJs._currentStep); 122 | }; 123 | 124 | /** 125 | * Triggered before exiting the intro. 126 | * @return {Boolean} Returning `false` will prevent exiting the intro. 127 | */ 128 | onBeforeExit = () => { 129 | const { onBeforeExit } = this.props; 130 | 131 | if (onBeforeExit) { 132 | return onBeforeExit(this.introJs._currentStep); 133 | } 134 | 135 | return true; 136 | }; 137 | 138 | /** 139 | * Triggered before changing step. 140 | * @return {Boolean} Returning `false` will prevent the step transition. 141 | */ 142 | onBeforeChange = nextElement => { 143 | if (!this.isVisible) { 144 | return true; 145 | } 146 | 147 | const { onBeforeChange, onPreventChange } = this.props; 148 | 149 | if (onBeforeChange) { 150 | const continueStep = onBeforeChange(this.introJs._currentStep, nextElement); 151 | 152 | if (continueStep === false && onPreventChange) { 153 | setTimeout(() => { 154 | onPreventChange(this.introJs._currentStep); 155 | }, 0); 156 | } 157 | 158 | return continueStep; 159 | } 160 | 161 | return true; 162 | }; 163 | 164 | /** 165 | * Triggered after changing step. 166 | * @param {HTMLElement} element - The element associated to the new step. 167 | */ 168 | onAfterChange = element => { 169 | if (!this.isVisible) { 170 | return; 171 | } 172 | 173 | const { onAfterChange } = this.props; 174 | 175 | if (onAfterChange) { 176 | onAfterChange(this.introJs._currentStep, element); 177 | } 178 | }; 179 | 180 | /** 181 | * Triggered when changing step. 182 | * @param {HTMLElement} element - The element associated to the next step. 183 | */ 184 | onChange = element => { 185 | if (!this.isVisible) { 186 | return; 187 | } 188 | 189 | const { onChange } = this.props; 190 | 191 | if (onChange) { 192 | onChange(this.introJs._currentStep, element); 193 | } 194 | }; 195 | 196 | /** 197 | * Triggered when completing all the steps. 198 | */ 199 | onComplete = () => { 200 | const { onComplete } = this.props; 201 | 202 | if (onComplete) { 203 | onComplete(); 204 | } 205 | }; 206 | 207 | /** 208 | * Updates the element associated to a step based on its index. 209 | * This is useful when the associated element is not present in the DOM on page load. 210 | * @param {number} stepIndex - The index of the step to update. 211 | */ 212 | updateStepElement = stepIndex => { 213 | const element = document.querySelector(this.introJs._options.steps[stepIndex].element); 214 | 215 | if (element) { 216 | this.introJs._introItems[stepIndex].element = element; 217 | this.introJs._introItems[stepIndex].position = this.introJs._options.steps[stepIndex].position || 'auto'; 218 | } 219 | }; 220 | 221 | /** 222 | * Installs Intro.js. 223 | */ 224 | installIntroJs() { 225 | if (isServer()) { 226 | return; 227 | } 228 | 229 | this.introJs = introJs(); 230 | 231 | this.introJs.onexit(this.onExit); 232 | this.introJs.onbeforeexit(this.onBeforeExit); 233 | this.introJs.onbeforechange(this.onBeforeChange); 234 | this.introJs.onafterchange(this.onAfterChange); 235 | this.introJs.onchange(this.onChange); 236 | this.introJs.oncomplete(this.onComplete); 237 | } 238 | 239 | /** 240 | * Configures Intro.js if not already configured. 241 | */ 242 | configureIntroJs() { 243 | const { options, steps } = this.props; 244 | 245 | const sanitizedSteps = steps.map(step => { 246 | if (isValidElement(step.intro)) { 247 | return { 248 | ...step, 249 | intro: renderToStaticMarkup(step.intro), 250 | }; 251 | } 252 | 253 | return step; 254 | }); 255 | 256 | this.introJs.setOptions({ ...options, steps: sanitizedSteps }); 257 | 258 | this.isConfigured = true; 259 | } 260 | 261 | /** 262 | * Renders the Intro.js steps. 263 | */ 264 | renderSteps() { 265 | const { enabled, initialStep, steps, onStart } = this.props; 266 | 267 | if (enabled && steps.length > 0 && !this.isVisible) { 268 | this.introJs.start(); 269 | 270 | this.isVisible = true; 271 | 272 | this.introJs.goToStepNumber(initialStep + 1); 273 | 274 | if (onStart) { 275 | onStart(this.introJs._currentStep); 276 | } 277 | } else if (!enabled && this.isVisible) { 278 | this.isVisible = false; 279 | 280 | this.introJs.exit(); 281 | } 282 | } 283 | 284 | /** 285 | * Renders the component. 286 | * @return {null} We do not want to render anything. 287 | */ 288 | render() { 289 | return null; 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/components/Steps/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import renderer from 'react-test-renderer'; 4 | import { shallow } from 'enzyme'; 5 | 6 | import Steps from './index'; 7 | import * as server from '../../helpers/server'; 8 | 9 | jest.useFakeTimers(); 10 | 11 | /** 12 | * Steps. 13 | * @type {Step[]} 14 | */ 15 | const steps = [ 16 | { 17 | element: '.test', 18 | intro: 'test', 19 | }, 20 | ]; 21 | 22 | describe('Steps', () => { 23 | test('should render nothing', () => { 24 | const tree = renderer.create( {}} />).toJSON(); 25 | 26 | expect(tree).toMatchSnapshot(); 27 | }); 28 | 29 | test('should render a React element using renderToStaticMarkup', () => { 30 | const Element = () =>
react test
; 31 | 32 | const spy = jest.spyOn(ReactDOMServer, 'renderToStaticMarkup'); 33 | 34 | const wrapper = shallow( 35 | }]} onExit={() => {}} />, 36 | { 37 | lifecycleExperimental: true, 38 | } 39 | ); 40 | wrapper.setProps({ enabled: true }); 41 | 42 | expect(spy).toHaveBeenCalledTimes(1); 43 | }); 44 | 45 | test('should not call the onStart callback at mount time if not enabled', () => { 46 | const onStart = jest.fn(); 47 | 48 | renderer.create( {}} onStart={onStart} />); 49 | 50 | expect(onStart).not.toHaveBeenCalled(); 51 | }); 52 | 53 | test('should not call the onStart callback at mount time if enabled without steps', () => { 54 | const onStart = jest.fn(); 55 | 56 | renderer.create( {}} onStart={onStart} enabled />); 57 | 58 | expect(onStart).not.toHaveBeenCalled(); 59 | }); 60 | 61 | test('should call the onStart callback at mount time if enabled with steps', () => { 62 | const onStart = jest.fn(); 63 | 64 | renderer.create( {}} onStart={onStart} enabled />); 65 | 66 | expect(onStart).toHaveBeenCalledTimes(1); 67 | }); 68 | 69 | test('should call the onStart callback when enabled with steps', () => { 70 | const onStart = jest.fn(); 71 | 72 | const wrapper = shallow( {}} onStart={onStart} />, { 73 | lifecycleExperimental: true, 74 | }); 75 | wrapper.setProps({ enabled: true }); 76 | 77 | expect(onStart).toHaveBeenCalledTimes(1); 78 | }); 79 | 80 | test('should call the onComplete callback when the tutorial is done', () => { 81 | const onComplete = jest.fn(); 82 | 83 | const wrapper = shallow( {}} onComplete={onComplete} />, { 84 | lifecycleExperimental: true, 85 | }); 86 | wrapper.instance().onComplete(); 87 | 88 | expect(onComplete).toHaveBeenCalledTimes(1); 89 | }); 90 | 91 | test('should call the onStart callback with the step number', () => { 92 | const onStart = jest.fn(); 93 | 94 | const wrapper = shallow( {}} onStart={onStart} />, { 95 | lifecycleExperimental: true, 96 | }); 97 | wrapper.setProps({ enabled: true }); 98 | 99 | expect(onStart).toHaveBeenCalledWith(1); 100 | 101 | wrapper.setProps({ enabled: false, initialStep: 10 }); 102 | wrapper.setProps({ enabled: true }); 103 | 104 | expect(onStart).toHaveBeenCalledWith(11); 105 | }); 106 | 107 | test('should call the onExit callback when disabled while being enabled', () => { 108 | const onExit = jest.fn(); 109 | 110 | const wrapper = shallow(, { 111 | lifecycleExperimental: true, 112 | }); 113 | wrapper.setProps({ enabled: false }); 114 | wrapper.instance().onComplete(); 115 | 116 | expect(onExit).toHaveBeenCalledTimes(1); 117 | }); 118 | 119 | test('should call the onExit callback when unmounting if enabled', () => { 120 | const onExit = jest.fn(); 121 | 122 | const wrapper = shallow(, { 123 | lifecycleExperimental: true, 124 | }); 125 | wrapper.unmount(); 126 | 127 | expect(onExit).toHaveBeenCalledTimes(1); 128 | }); 129 | 130 | test('should not call the onExit callback when unmounting if not enabled', () => { 131 | const onExit = jest.fn(); 132 | 133 | const wrapper = shallow(, { 134 | lifecycleExperimental: true, 135 | }); 136 | wrapper.unmount(); 137 | 138 | expect(onExit).toHaveBeenCalledTimes(1); 139 | }); 140 | 141 | test('should call the onExit callback with the step number', () => { 142 | const onExit = jest.fn(); 143 | 144 | const wrapper = shallow(, { 145 | lifecycleExperimental: true, 146 | }); 147 | wrapper.setProps({ enabled: false }); 148 | 149 | expect(onExit).toHaveBeenCalledWith(1); 150 | 151 | wrapper.setProps({ enabled: true, initialStep: 10 }); 152 | wrapper.setProps({ enabled: false }); 153 | 154 | expect(onExit).toHaveBeenCalledWith(11); 155 | }); 156 | 157 | test('should not call the onExit callback when disabled while being already disabled', () => { 158 | const onExit = jest.fn(); 159 | 160 | const wrapper = shallow(, { 161 | lifecycleExperimental: true, 162 | }); 163 | wrapper.setProps({ enabled: false }); 164 | 165 | expect(onExit).not.toHaveBeenCalled(); 166 | }); 167 | 168 | test('should not call the onChange callback when disabled', () => { 169 | const onChange = jest.fn(); 170 | 171 | renderer.create( {}} onChange={onChange} />); 172 | 173 | expect(onChange).not.toHaveBeenCalled(); 174 | }); 175 | 176 | test('should call the onChange callback when enabled', () => { 177 | const onChange = jest.fn(); 178 | 179 | const wrapper = shallow( {}} onChange={onChange} />, { 180 | lifecycleExperimental: true, 181 | }); 182 | wrapper.setProps({ enabled: true }); 183 | 184 | expect(onChange).toHaveBeenCalledTimes(1); 185 | }); 186 | 187 | test('should call the onChange callback with the step number and the new element', () => { 188 | const onChange = jest.fn(); 189 | 190 | const wrapper = shallow( {}} onChange={onChange} />, { 191 | lifecycleExperimental: true, 192 | }); 193 | wrapper.setProps({ enabled: true }); 194 | 195 | expect(onChange).toHaveBeenCalledWith(1, null); 196 | }); 197 | 198 | test('should not call the onBeforeExit callback when disabled', () => { 199 | const onBeforeExit = jest.fn(); 200 | 201 | renderer.create( {}} onBeforeExit={onBeforeExit} />); 202 | 203 | expect(onBeforeExit).not.toHaveBeenCalled(); 204 | }); 205 | 206 | test('should call the onBeforeExit callback when disabling', () => { 207 | const onBeforeExit = jest.fn(); 208 | 209 | const wrapper = shallow( 210 | {}} onBeforeExit={onBeforeExit} />, 211 | { 212 | lifecycleExperimental: true, 213 | } 214 | ); 215 | wrapper.setProps({ enabled: false }); 216 | 217 | expect(onBeforeExit).toHaveBeenCalledTimes(1); 218 | }); 219 | 220 | test('should call the onBeforeExit callback with the step number', () => { 221 | const onBeforeExit = jest.fn(); 222 | 223 | const wrapper = shallow( 224 | {}} onBeforeExit={onBeforeExit} />, 225 | { 226 | lifecycleExperimental: true, 227 | } 228 | ); 229 | wrapper.setProps({ enabled: false }); 230 | 231 | expect(onBeforeExit).toHaveBeenCalledWith(1); 232 | expect(onBeforeExit).toHaveBeenCalledTimes(1); 233 | 234 | wrapper.setProps({ enabled: true, initialStep: 10 }); 235 | wrapper.setProps({ enabled: false }); 236 | 237 | expect(onBeforeExit).toHaveBeenCalledWith(11); 238 | expect(onBeforeExit).toHaveBeenCalledTimes(2); 239 | }); 240 | 241 | test('should not call the onBeforeChange callback when disabled', () => { 242 | const onBeforeChange = jest.fn(); 243 | 244 | renderer.create( {}} onBeforeChange={onBeforeChange} />); 245 | 246 | expect(onBeforeChange).not.toHaveBeenCalled(); 247 | }); 248 | 249 | test('should call the onBeforeChange callback when enabled', () => { 250 | const onBeforeChange = jest.fn(); 251 | 252 | const wrapper = shallow( {}} onBeforeChange={onBeforeChange} />, { 253 | lifecycleExperimental: true, 254 | }); 255 | wrapper.setProps({ enabled: true }); 256 | 257 | expect(onBeforeChange).toHaveBeenCalledTimes(1); 258 | }); 259 | 260 | test('should call the onBeforeChange callback with the step number and the new element', () => { 261 | const onBeforeChange = jest.fn(); 262 | 263 | const wrapper = shallow( {}} onBeforeChange={onBeforeChange} />, { 264 | lifecycleExperimental: true, 265 | }); 266 | wrapper.setProps({ enabled: true }); 267 | 268 | expect(onBeforeChange).toHaveBeenCalledWith(1, null); 269 | }); 270 | 271 | test('should call the onPreventChange callback if onBeforeChange returned false to prevent transition', () => { 272 | const onPreventChange = jest.fn(); 273 | 274 | const wrapper = shallow( 275 | {}} 279 | onBeforeChange={() => false} 280 | onPreventChange={onPreventChange} 281 | />, 282 | { 283 | lifecycleExperimental: true, 284 | } 285 | ); 286 | wrapper.setProps({ enabled: true }); 287 | 288 | jest.runAllTimers(); 289 | 290 | expect(onPreventChange).toHaveBeenCalled(); 291 | }); 292 | 293 | test('should not call the onAfterChange callback when disabled', () => { 294 | const onAfterChange = jest.fn(); 295 | 296 | renderer.create( {}} onAfterChange={onAfterChange} />); 297 | 298 | expect(onAfterChange).not.toHaveBeenCalled(); 299 | }); 300 | 301 | test('should call the onAfterChange callback when enabled', () => { 302 | const onAfterChange = jest.fn(); 303 | 304 | const wrapper = shallow( {}} onAfterChange={onAfterChange} />, { 305 | lifecycleExperimental: true, 306 | }); 307 | wrapper.setProps({ enabled: true }); 308 | 309 | expect(onAfterChange).toHaveBeenCalledTimes(1); 310 | }); 311 | 312 | test('should call the onAfterChange callback with the step number and the new element', () => { 313 | const onAfterChange = jest.fn(); 314 | 315 | const wrapper = shallow( {}} onAfterChange={onAfterChange} />, { 316 | lifecycleExperimental: true, 317 | }); 318 | wrapper.setProps({ enabled: true }); 319 | 320 | expect(onAfterChange).toHaveBeenCalledWith(1, null); 321 | }); 322 | 323 | test('should not update the element if it does not exist when calling updateStepElement()', () => { 324 | const wrapper = shallow( {}} />, { 325 | lifecycleExperimental: true, 326 | }); 327 | wrapper.setProps({ enabled: true }); 328 | 329 | wrapper.instance().updateStepElement(0); 330 | 331 | expect(wrapper.instance().introJs._introItems[0].element).not.toEqual(expect.any(HTMLDivElement)); 332 | }); 333 | 334 | test('should update the element if it does exist when calling updateStepElement()', () => { 335 | const wrapper = shallow( {}} />, { 336 | lifecycleExperimental: true, 337 | }); 338 | wrapper.setProps({ enabled: true }); 339 | 340 | const div = document.createElement('div'); 341 | div.className = 'test'; 342 | document.body.appendChild(div); 343 | 344 | wrapper.instance().updateStepElement(0); 345 | 346 | expect(wrapper.instance().introJs._introItems[0].element).toEqual(expect.any(HTMLDivElement)); 347 | }); 348 | 349 | test('should not install intro.js during SSR', () => { 350 | jest.spyOn(server, 'isServer').mockReturnValueOnce(true); 351 | 352 | const wrapper = shallow( {}} />); 353 | 354 | expect(wrapper.instance().introJs).toBe(null); 355 | }); 356 | }); 357 | -------------------------------------------------------------------------------- /src/helpers/defaultProps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Intro.js options default proptypes. 3 | * @type {Object} 4 | */ 5 | export const options = { 6 | hidePrev: true, 7 | hideNext: true, 8 | }; 9 | -------------------------------------------------------------------------------- /src/helpers/proptypes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | /** 4 | * Intro.js tooltip position proptype. 5 | * @type {Function} 6 | */ 7 | export const tooltipPosition = PropTypes.oneOf([ 8 | 'top', 9 | 'right', 10 | 'bottom', 11 | 'left', 12 | 'bottom-left-aligned', 13 | 'bottom-middle-aligned', 14 | 'bottom-right-aligned', 15 | 'top-left-aligned', 16 | 'top-middle-aligned', 17 | 'top-right-aligned', 18 | 'auto', 19 | ]); 20 | 21 | /** 22 | * Intro.js hint position proptype. 23 | * @type {Function} 24 | */ 25 | export const hintPosition = PropTypes.oneOf([ 26 | 'top-middle', 27 | 'top-left', 28 | 'top-right', 29 | 'bottom-left', 30 | 'bottom-right', 31 | 'bottom-middle', 32 | 'middle-left', 33 | 'middle-right', 34 | 'middle-middle', 35 | ]); 36 | 37 | export const options = PropTypes.shape({ 38 | nextLabel: PropTypes.string, 39 | prevLabel: PropTypes.string, 40 | skipLabel: PropTypes.string, 41 | doneLabel: PropTypes.string, 42 | hidePrev: PropTypes.bool, 43 | hideNext: PropTypes.bool, 44 | tooltipPosition, 45 | tooltipClass: PropTypes.string, 46 | highlightClass: PropTypes.string, 47 | exitOnEsc: PropTypes.bool, 48 | exitOnOverlayClick: PropTypes.bool, 49 | showStepNumbers: PropTypes.bool, 50 | keyboardNavigation: PropTypes.bool, 51 | showButtons: PropTypes.bool, 52 | showBullets: PropTypes.bool, 53 | showProgress: PropTypes.bool, 54 | scrollToElement: PropTypes.bool, 55 | overlayOpacity: PropTypes.number, 56 | scrollPadding: PropTypes.number, 57 | positionPrecedence: PropTypes.arrayOf(PropTypes.string), 58 | disableInteraction: PropTypes.bool, 59 | hintPosition, 60 | hintButtonLabel: PropTypes.string, 61 | hintAnimation: PropTypes.bool, 62 | }); 63 | -------------------------------------------------------------------------------- /src/helpers/server.js: -------------------------------------------------------------------------------- 1 | export function isServer() { 2 | return typeof window === 'undefined'; 3 | } 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as Steps } from './components/Steps'; 2 | export { default as Hints } from './components/Hints'; 3 | --------------------------------------------------------------------------------