├── .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 | [](https://github.com/HiDeoo/intro.js-react/actions/workflows/integration.yml) [](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 |
--------------------------------------------------------------------------------