├── book.json
├── src
├── index.js
├── constants
│ ├── lifecycle.js
│ ├── status.js
│ ├── index.js
│ ├── actions.js
│ └── events.js
├── components
│ ├── Spotlight.js
│ ├── Tooltip
│ │ ├── CloseBtn.js
│ │ ├── Container.js
│ │ └── index.js
│ ├── Portal.js
│ ├── Beacon.js
│ ├── Overlay.js
│ ├── Step.js
│ └── index.js
├── config
│ ├── defaults.js
│ └── types.js
├── modules
│ ├── scope.js
│ ├── step.js
│ ├── helpers.js
│ ├── dom.js
│ └── store.js
└── styles.js
├── .gitignore
├── test
├── __setup__
│ ├── styleMock.js
│ ├── setupTests.js
│ ├── fileMock.js
│ ├── shim.js
│ └── index.js
├── .eslintrc
├── index.spec.js
├── modules
│ ├── helpers.spec.js
│ └── store.spec.js
└── __fixtures__
│ ├── steps.js
│ └── Tour.js
├── docs
├── SUMMARY.md
├── accessibility.md
├── constants.md
├── migration.md
├── README.md
├── styling.md
├── callback.md
├── step.md
└── props.md
├── .editorconfig
├── issue_template.md
├── .flowconfig
├── rollup.config.js
├── .babelrc
├── .codeclimate.yml
├── LICENSE
├── jest.config.js
├── CONTRIBUTING.md
├── .travis.yml
├── tools
└── index.js
├── README.md
├── package.json
└── .eslintrc
/book.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "./docs"
3 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Joyride from './components';
2 |
3 | export default Joyride;
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .tmp
2 | _book/
3 | coverage
4 | dist
5 | es
6 | lib
7 | node_modules
8 | reports
9 |
--------------------------------------------------------------------------------
/test/__setup__/styleMock.js:
--------------------------------------------------------------------------------
1 | // Return an object to emulate css modules (if you are using them)
2 | module.exports = {};
3 |
--------------------------------------------------------------------------------
/test/__setup__/setupTests.js:
--------------------------------------------------------------------------------
1 | require('jest-enzyme/lib/index.js');
2 | require('jest-extended');
3 | require('jest-chain');
4 |
--------------------------------------------------------------------------------
/test/__setup__/fileMock.js:
--------------------------------------------------------------------------------
1 | // Return an empty string or other mock path to emulate the url that
2 | // webpack provides via the file-loader
3 | module.exports = '';
4 |
--------------------------------------------------------------------------------
/src/constants/lifecycle.js:
--------------------------------------------------------------------------------
1 | export default {
2 | INIT: 'init',
3 | READY: 'ready',
4 | BEACON: 'beacon',
5 | TOOLTIP: 'tooltip',
6 | COMPLETE: 'complete',
7 | ERROR: 'error',
8 | };
9 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "prefer-destructuring": "off",
4 | "react/destructuring-assignment": "off",
5 | "react/jsx-filename-extension": "off",
6 | "react/prop-types": "off"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/test/__setup__/shim.js:
--------------------------------------------------------------------------------
1 | global.requestAnimationFrame = (callback) => {
2 | setTimeout(callback, 0);
3 | };
4 |
5 | global.matchMedia = () => ({
6 | matches: false,
7 | addListener: () => {},
8 | removeListener: () => {},
9 | });
10 |
--------------------------------------------------------------------------------
/src/constants/status.js:
--------------------------------------------------------------------------------
1 | export default {
2 | IDLE: 'idle',
3 | READY: 'ready',
4 | WAITING: 'waiting',
5 | RUNNING: 'running',
6 | PAUSED: 'paused',
7 | SKIPPED: 'skipped',
8 | FINISHED: 'finished',
9 | ERROR: 'error',
10 | };
11 |
--------------------------------------------------------------------------------
/docs/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Summary
2 |
3 | * [Overview](README.md)
4 | * [Props](props.md)
5 | * [Step](step.md)
6 | * [Styling](styling.md)
7 | * [Callback](callback.md)
8 | * [Accessibility](accessibility.md)
9 | * [Constants](constants.md)
10 | * [Migration](migration.md)
11 |
12 |
--------------------------------------------------------------------------------
/src/constants/index.js:
--------------------------------------------------------------------------------
1 | import ACTIONS from './actions';
2 | import EVENTS from './events';
3 | import LIFECYCLE from './lifecycle';
4 | import STATUS from './status';
5 |
6 | export { ACTIONS, EVENTS, LIFECYCLE, STATUS };
7 | export default { ACTIONS, EVENTS, LIFECYCLE, STATUS };
8 |
--------------------------------------------------------------------------------
/src/constants/actions.js:
--------------------------------------------------------------------------------
1 | export default {
2 | INIT: 'init',
3 | START: 'start',
4 | STOP: 'stop',
5 | RESET: 'reset',
6 | RESTART: 'restart',
7 | PREV: 'prev',
8 | NEXT: 'next',
9 | GO: 'go',
10 | INDEX: 'index',
11 | CLOSE: 'close',
12 | SKIP: 'skip',
13 | UPDATE: 'update',
14 | };
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | indent_style = space
11 | indent_size = 2
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/src/constants/events.js:
--------------------------------------------------------------------------------
1 | export default {
2 | TOUR_START: 'tour:start',
3 | STEP_BEFORE: 'step:before',
4 | BEACON: 'beacon',
5 | TOOLTIP: 'tooltip',
6 | TOOLTIP_CLOSE: 'close',
7 | STEP_AFTER: 'step:after',
8 | TOUR_END: 'tour:end',
9 | TOUR_STATUS: 'tour:status',
10 | TARGET_NOT_FOUND: 'error:target_not_found',
11 | ERROR: 'error',
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/Spotlight.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const JoyrideSpotlight = ({ styles }) => (
5 |
10 | );
11 |
12 | JoyrideSpotlight.propTypes = {
13 | styles: PropTypes.object.isRequired,
14 | };
15 |
16 | export default JoyrideSpotlight;
17 |
--------------------------------------------------------------------------------
/issue_template.md:
--------------------------------------------------------------------------------
1 | >Expected behavior
2 |
3 |
4 |
5 | >Actual behavior
6 |
7 |
8 |
9 | >Steps to reproduce the problem
10 |
11 |
12 |
13 | >React version
14 |
15 |
16 |
17 | >React-Joyride version
18 |
19 |
20 |
21 | >Browser name and version
22 |
23 |
24 |
25 | >Error stack (if available)
26 |
27 |
28 |
29 | *If you want to get this issue fixed quickly, make sure to send a public URL or codesandbox example.*
30 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 | .*/node_modules/config-chain/test/broken.json
3 | .*/node_modules/npmconf/test/.*
4 | .*/node_modules/react-side-effect/.*
5 |
6 | [include]
7 |
8 | [libs]
9 | ./defs
10 |
11 | [options]
12 | module.name_mapper='^scripts\/?\(.*\)?$' -> '/src/scripts/\1'
13 | module.name_mapper='^styles\/\(.*\)$' -> '/src/styles/\1'
14 |
15 | munge_underscores=true
16 |
17 | suppress_type=$FlowIssue
18 | suppress_type=$FlowFixMe
19 | suppress_type=$FixMe
20 |
--------------------------------------------------------------------------------
/src/config/defaults.js:
--------------------------------------------------------------------------------
1 | export default {
2 | floaterProps: {
3 | options: {
4 | preventOverflow: {
5 | boundariesElement: 'scrollParent',
6 | },
7 | },
8 | wrapperOptions: {
9 | offset: -18,
10 | position: true,
11 | },
12 | },
13 | locale: {
14 | back: 'Back',
15 | close: 'Close',
16 | last: 'Last',
17 | next: 'Next',
18 | open: 'Open',
19 | skip: 'Skip',
20 | },
21 | step: {
22 | event: 'click',
23 | placement: 'bottom',
24 | offset: 10,
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/docs/accessibility.md:
--------------------------------------------------------------------------------
1 | # Accessibility
2 |
3 | react-joyride aims to be fully accessible, using the [WAI-ARIA](https://www.w3.org/WAI/intro/aria) guidelines to support users of assistive technologies.
4 |
5 | ## Keyboard navigation
6 |
7 | When the tooltip is opened, the TAB key will be hijacked to only focus on form elements \(input\|select\|textarea\|button\|object\) within its contents. Elements outside the tooltip won't receive focus.
8 |
9 | When the tooltip is closed the focus returns to the default.
10 |
11 | ## ARIA attributes
12 |
13 | Soon
14 |
15 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import commonjs from 'rollup-plugin-commonjs';
3 | import packageJSON from './package.json';
4 |
5 | export default {
6 | input: 'src/index.js',
7 | output: {
8 | file: 'es/index.js',
9 | format: 'es',
10 | exports: 'named',
11 | },
12 | external: [
13 | ...Object.keys(packageJSON.peerDependencies),
14 | ...Object.keys(packageJSON.dependencies),
15 | ],
16 | plugins: [
17 | babel({
18 | exclude: 'node_modules/**',
19 | runtimeHelpers: true,
20 | }),
21 | commonjs(),
22 | ],
23 | };
24 |
--------------------------------------------------------------------------------
/test/__setup__/index.js:
--------------------------------------------------------------------------------
1 | import Enzyme from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 |
4 | Enzyme.configure({ adapter: new Adapter() });
5 |
6 | Object.defineProperty(Element.prototype, 'clientHeight', {
7 | writable: true,
8 | value: '',
9 | });
10 |
11 | Object.defineProperty(Element.prototype, 'clientWidth', {
12 | writable: true,
13 | value: '',
14 | });
15 |
16 | const react = document.createElement('div');
17 | react.id = 'react';
18 | react.style.height = '100vh';
19 | document.body.appendChild(react);
20 |
21 | window.matchMedia = () => ({
22 | matches: false,
23 | addListener: () => {
24 | },
25 | removeListener: () => {
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/test/index.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 |
4 | import Tour from './__fixtures__/Tour';
5 |
6 | const props = {
7 | content: 'Hello! This is my content!',
8 | };
9 |
10 | function setup(ownProps = props) {
11 | return mount(
12 | ,
13 | { attachTo: document.getElementById('react') }
14 | );
15 | }
16 |
17 | describe('ReactJoyride', () => {
18 | let joyride;
19 |
20 | describe('basic usage', () => {
21 | beforeAll(() => {
22 | joyride = setup();
23 | });
24 |
25 | it('should render properly', () => {
26 | expect(joyride.find('Joyride')).toExist();
27 | });
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "env", {
5 | "useBuiltIns": true,
6 | "modules": false,
7 | "targets": {
8 | "browsers": ["last 2 versions", "safari >= 7"]
9 | }
10 | }
11 | ],
12 | "react",
13 | "stage-1"
14 | ],
15 | "plugins": [],
16 | "env": {
17 | "production": {
18 | "plugins": [
19 | "array-includes",
20 | "external-helpers",
21 | "transform-runtime",
22 | "transform-flow-strip-types",
23 | "transform-node-env-inline"
24 | ]
25 | },
26 | "test": {
27 | "plugins": [
28 | "transform-es2015-modules-commonjs"
29 | ],
30 | "sourceMaps": "both"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | ---
2 | engines:
3 | eslint:
4 | enabled: true
5 | channel: "eslint-4"
6 | config:
7 | extensions:
8 | - .js
9 | - .jsx
10 | checks:
11 | import/no-duplicates:
12 | enabled: false
13 | no-template-curly-in-string:
14 | enabled: false
15 | import/no-unresolved:
16 | enabled: false
17 | import/no-extraneous-dependencies:
18 | enabled: false
19 | import/no-named-as-default-member:
20 | enabled: false
21 | stylelint:
22 | enabled: true
23 | ratings:
24 | paths:
25 | - "**.js"
26 | - "**.jsx"
27 | - "**.scss"
28 | exclude_paths:
29 | - coverage/**/*
30 | - cypress/**/*
31 | - es/**/*
32 | - lib/**/*
33 | - node_modules/**/*
34 | - test/**/*
35 |
--------------------------------------------------------------------------------
/docs/constants.md:
--------------------------------------------------------------------------------
1 | # Constants
2 |
3 | Joyride uses a few constants to keep its state and lifecycle.
4 | You can use these in your component for the callback events.
5 |
6 | ```javascript
7 | import { ACTIONS, EVENTS, LIFECYCLE, STATUS } from 'react-joyride/es/constants';
8 | ```
9 |
10 | [Actions](https://github.com/gilbarbara/react-joyride/tree/3e08384415a831b20ce21c8423b6c271ad419fbf/src/constants/actions.js) - The action that updated the state.
11 |
12 | [Events](https://github.com/gilbarbara/react-joyride/tree/3e08384415a831b20ce21c8423b6c271ad419fbf/src/constants/events.js) - The type of the event.
13 |
14 | [Lifecycle](https://github.com/gilbarbara/react-joyride/tree/3e08384415a831b20ce21c8423b6c271ad419fbf/src/constants/lifecycle.js) - The step lifecycle.
15 |
16 | [Status](https://github.com/gilbarbara/react-joyride/tree/3e08384415a831b20ce21c8423b6c271ad419fbf/src/constants/status.js) - The tour's status.
17 |
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2015, Gil Barbara
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/test/modules/helpers.spec.js:
--------------------------------------------------------------------------------
1 | import {
2 | hexToRGB,
3 | log,
4 | } from '../../src/modules/helpers';
5 |
6 | const mockLog = jest.fn();
7 | const mockWarn = jest.fn();
8 | const mockError = jest.fn();
9 |
10 | console.log = mockLog; //eslint-disable-line no-console
11 | console.warn = mockWarn; //eslint-disable-line no-console
12 | console.error = mockError; //eslint-disable-line no-console
13 |
14 | describe('utils', () => {
15 | it('should be able to call `hexToRGB`', () => {
16 | expect(hexToRGB('#ff0044')).toEqual([255, 0, 68]);
17 | expect(hexToRGB('#0f4')).toEqual([0, 255, 68]);
18 | });
19 |
20 | xit('should be able to call `log`', () => {
21 | log({
22 | title: 'hello',
23 | debug: true,
24 | });
25 |
26 | expect(mockLog.mock.calls[0][0]).toBe('%cjoyride');
27 | expect(mockLog.mock.calls[0][1]).toBe('color: #760bc5; font-weight: bold; font-size: 12px;');
28 |
29 | log({
30 | title: 'noou',
31 | msg: ['bye', 'bye'],
32 | warn: true,
33 | debug: true,
34 | });
35 |
36 | expect(mockWarn.mock.calls[0][0]).toBe('bye');
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transform: {
3 | '^.+\\.jsx?$': 'babel-jest',
4 | },
5 | moduleFileExtensions: [
6 | 'js',
7 | 'jsx',
8 | 'json',
9 | ],
10 | moduleDirectories: [
11 | 'node_modules',
12 | 'src',
13 | './',
14 | ],
15 | moduleNameMapper: {
16 | '^.+\\.(css|scss)$': '/test/__setup__/styleMock.js',
17 | '^.+\\.(jpe?g|png|gif|ttf|eot|svg|md)$': '/test/__setup__/fileMock.js',
18 | },
19 | setupFiles: [
20 | '/test/__setup__/shim.js',
21 | '/test/__setup__/index.js',
22 | ],
23 | setupTestFrameworkScriptFile: '/test/__setup__/setupTests.js',
24 | testEnvironment: 'jest-environment-jsdom-global',
25 | testEnvironmentOptions: {
26 | resources: 'usable',
27 | },
28 | testRegex: '/test/.*?\\.(test|spec)\\.js$',
29 | testURL: 'http://localhost:3000',
30 | watchPlugins: [
31 | 'jest-watch-typeahead/filename',
32 | 'jest-watch-typeahead/testname',
33 | ],
34 | collectCoverage: false,
35 | collectCoverageFrom: [
36 | 'src/**/*.{js,jsx}',
37 | ],
38 | coverageThreshold: {
39 | global: {
40 | branches: 15,
41 | functions: 15,
42 | lines: 15,
43 | statements: 15,
44 | },
45 | },
46 | verbose: true,
47 | };
48 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to react-joyride
2 |
3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
4 |
5 | **Reporting Bugs**
6 | Before creating bug reports, please check this [list](https://github.com/gilbarbara/react-joyride/issues) as you might find out that you don't need to create one. When you are creating a bug report, please include as many details as possible.
7 |
8 | **Implementation**
9 | Make sure you browse all the examples in the [demo](https://codesandbox.io/s/2zpjporp4p) and check the source code before opening an issue. We don't usually help with those directly but maybe some of the users might have some advice.
10 |
11 | **Pull Requests**
12 | Before submitting a new pull request, open a new issue to discuss it. It may already been implemented but not published or we might have found the same situation before and decide against it.
13 |
14 | In any case:
15 | - Format files using these rules [EditorConfig](https://github.com/gilbarbara/react-joyride/blob/master/.editorconfig)
16 | - Follow the [Javascript](https://github.com/gilbarbara/react-joyride/blob/master/.eslintrc) (ESLint) and [CSS](https://github.com/gilbarbara/react-joyride/blob/master/.scss-lint.yml) (scss-lint) styleguides.
17 | - Document new code with jsdoc.
18 |
19 | Thank you!
20 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '8'
4 | cache:
5 | directories:
6 | - "node_modules"
7 | before_script:
8 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
9 | - chmod +x ./cc-test-reporter
10 | - ./cc-test-reporter before-build
11 | script:
12 | - npm test
13 | after_script:
14 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
15 | deploy:
16 | provider: npm
17 | email: gilbarbara@gmail.com
18 | api_key:
19 | secure: udTSoiJXNkpRBrbwNtzflguIc/QvRbW/xdajwJqYUxitA409jBO+VhsxwlDJBl0HHHKW7yOwTUXvKAxr92y/6FByVM9yLuduQLOvG9J++l12Lc7fehKXbuNBZZDwjUF3MwftF9mCj4gJTUPrajZjXxUx78hI7dTCjJuJkpXHfZSr00Xu8In59rzr9OhDGBITlMSynE0yEIVBEfboxf9q5uSuJtp6qaK5ohbZqQnz2KIVoI2TUZ9KXpU8765mAd5xxvF4kgpmLYFN9TSd49oipZYPZs4UjlfVrPmKq2nmzepew9HuaXEvg0DLpcI9YjzBkR4AGlRIal2t4gNmy9m2XbaeyTS5q5Aoa+UchR9GS+LLsBIfl+1bI9yqijURZp68xMdQRC2iUrPcJeTKY9WpXEYFjxPmJXqfQl2BAiOlaxCzvNnD2i8th6PpeI+2aU79pWRSwUSQqV4PLjDN+M86cEpVRxZePLte6RIVqtRBes9HY8sQph0MAdzLUTbmMehnB1XzSRbuR77687NrbW8epRsxqRSK2Uqman8ZWSJan9apwP4niVciDJbfVL9M1J7pUZX8HKvfC3l5NJWO69xzQFouSVhtkZZWU4bxGtgS5gM+H/AHKv9/qktoA/K8hGPEFR1S7OoWSP310wetGHMHuXniKQh7glDh/IkpFp8QF14=
20 | on:
21 | tags: true
22 | branch: master
23 | repo: gilbarbara/react-joyride
24 | addons:
25 | code_climate: true
26 |
--------------------------------------------------------------------------------
/docs/migration.md:
--------------------------------------------------------------------------------
1 | # Migration
2 |
3 | Version 2 changed radically and you will need to update your component.
4 |
5 | ## Props
6 |
7 | ### Renamed \(breaking\)
8 |
9 | **allowClicksThruHole** ▶︎ **spotlightClicks**
10 |
11 | **disableOverlay** `false` ▶︎ **disableOverlayClicks** `false`
12 |
13 | **keyboardNavigation** `true` ▶︎ **disableCloseOnEsc** `false`
14 | the space, return and tab keys are now controlled with tabIndex
15 |
16 | **scrollToSteps** `true` ▶︎ disableScrolling `false`
17 |
18 | **showBackButton** `true` ▶︎ **hideBackButton** `false`
19 |
20 | **showOverlay** `true` ▶︎ **disableOverlay** `false`
21 |
22 | **showStepsProgress** `false` ▶︎ **showProgress** `false`
23 |
24 | **tooltipOffset** `30` ▶︎ Use the step **floaterProps.offset**
25 |
26 | **type** `'single'` ▶︎ **continuous** `false`
27 |
28 | ### Removed \(breaking\)
29 |
30 | **autoStart** \(use the `disableBeacon` prop on the first step\)
31 |
32 | **offsetParent**
33 |
34 | **resizeDebounce**
35 |
36 | **resizeDebounceDelay**
37 |
38 | ## Step
39 |
40 | There are a few changes to the step syntax too:
41 |
42 | **selector** ▶︎ **target
43 | **Now it supports HTMLElement and string.
44 |
45 | **position** ▶︎ **placement
46 | **The default is** `bottom` **now
47 |
48 | **text** ▶︎ **content**
49 |
50 | **type** ▶︎ **event**
51 |
52 | **allowClicksThruHole** ▶︎ **spotlightClicks**
53 |
54 | **style** ▶︎ **styles**
55 | The properties have changed. Be sure to update to the new [syntax](styling.md).
56 |
57 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # React Joyride
2 |
3 | A React component to create tours for your app!
4 |
5 | You can use it to showcase your app to new users or explain functionality of new features.
6 | It uses [react-floater](https://github.com/gilbarbara/react-floater) \(with [popper.js](https://github.com/FezVrasta/popper.js) for positioning and styling\).
7 | And you can use your own components if you want.
8 |
9 | ### View the demo [here](https://2zpjporp4p.codesandbox.io/)
10 |
11 | ## Setup
12 |
13 | ```bash
14 | npm i react-joyride
15 | ```
16 |
17 | ## Getting Started
18 |
19 | ```javascript
20 | import Joyride from 'react-joyride';
21 |
22 | export class App extends React.Component {
23 | state = {
24 | run: false,
25 | steps: [
26 | {
27 | target: '.my-first-step',
28 | content: 'This if my awesome feature!',
29 | placement: 'bottom',
30 | },
31 | {
32 | target: '.my-other-step',
33 | content: 'This if my awesome feature!',
34 | placement: 'bottom',
35 | },
36 | ...
37 | ]
38 | };
39 |
40 | componentDidMount() {
41 | this.setState({ run: true });
42 | }
43 |
44 | callback = (tour) => {
45 | const { action, index, type } = data;
46 | };
47 |
48 | render () {
49 | const { steps, run } = this.state;
50 |
51 | return (
52 |
53 |
60 | ...
61 |
62 | );
63 | }
64 | }
65 | ```
66 |
67 | ## Documentation
68 |
69 | [Props](props.md)
70 |
71 | [Step](step.md)
72 |
73 | [Styling](styling.md)
74 |
75 | [Callback](callback.md)
76 |
77 | [Constants](constants.md)
78 |
79 | [Migration from 1.x](migration.md)
80 |
81 |
--------------------------------------------------------------------------------
/src/components/Tooltip/CloseBtn.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const CloseBtn = ({ styles, ...props }) => {
5 | const { color, height, width, ...style } = styles;
6 |
7 | return (
8 |
29 | );
30 | };
31 |
32 | CloseBtn.propTypes = {
33 | styles: PropTypes.object.isRequired,
34 | };
35 |
36 | export default CloseBtn;
37 |
--------------------------------------------------------------------------------
/src/components/Portal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import PropTypes from 'prop-types';
4 | import { canUseDOM, isReact16 } from '../modules/helpers';
5 |
6 | export default class JoyridePortal extends React.Component {
7 | constructor(props) {
8 | super(props);
9 |
10 | if (!canUseDOM) return;
11 |
12 | this.node = document.createElement('div');
13 | if (props.id) {
14 | this.node.id = props.id;
15 | }
16 |
17 | document.body.appendChild(this.node);
18 | }
19 |
20 | static propTypes = {
21 | children: PropTypes.element,
22 | id: PropTypes.oneOfType([
23 | PropTypes.string,
24 | PropTypes.number,
25 | ]),
26 | };
27 |
28 | componentDidMount() {
29 | if (!canUseDOM) return;
30 |
31 | if (!isReact16) {
32 | this.renderReact15();
33 | }
34 | }
35 |
36 | componentDidUpdate() {
37 | if (!canUseDOM) return;
38 |
39 | if (!isReact16) {
40 | this.renderReact15();
41 | }
42 | }
43 |
44 | componentWillUnmount() {
45 | if (!canUseDOM || !this.node) return;
46 |
47 | if (!isReact16) {
48 | ReactDOM.unmountComponentAtNode(this.node);
49 | }
50 |
51 | document.body.removeChild(this.node);
52 | }
53 |
54 | renderReact15() {
55 | if (!canUseDOM) return null;
56 |
57 | const { children } = this.props;
58 |
59 | ReactDOM.unstable_renderSubtreeIntoContainer(
60 | this,
61 | children,
62 | this.node,
63 | );
64 |
65 | return null;
66 | }
67 |
68 | renderReact16() {
69 | if (!canUseDOM || !isReact16) return null;
70 |
71 | const { children } = this.props;
72 |
73 | return ReactDOM.createPortal(
74 | children,
75 | this.node,
76 | );
77 | }
78 |
79 | render() {
80 | if (!isReact16) {
81 | return null;
82 | }
83 |
84 | return this.renderReact16();
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/docs/styling.md:
--------------------------------------------------------------------------------
1 | # Styling
2 |
3 | Version 2 uses inline styles instead of the previous CSS/SCSS files but you can also use you own components for the beacon and tooltip and style them anyway you want.
4 |
5 | To update the default theme, just pass a `styles` prop to the Joyride component or directly in a [step](step.md).
6 | You can control the overall theme with the special `options` object.
7 |
8 | ```text
9 | const defaultOptions = {
10 | arrowColor: '#fff',
11 | backgroundColor: '#fff',
12 | beaconSize: 36,
13 | overlayColor: 'rgba(0, 0, 0, 0.5)',
14 | primaryColor: '#f04',
15 | spotlightShadow: '0 0 15px rgba(0, 0, 0, 0.5)',
16 | textColor: '#333',
17 | width: undefined,
18 | zIndex: 100,
19 | };
20 | ```
21 |
22 | ## Example
23 |
24 | ```javascript
25 | import Joyride from 'react-joyride';
26 | import { ACTIONS, EVENTS } from 'react-joyride/es/constants';
27 |
28 | export class App extends React.Component {
29 | state = {
30 | run: false,
31 | steps: [],
32 | };
33 |
34 | render () {
35 | const { run, stepIndex, steps } = this.state;
36 |
37 | return (
38 |
39 |
54 | ...
55 |
56 | );
57 | }
58 | }
59 | ```
60 |
61 | You can also customize any element independently. Check [styles.js](https://github.com/gilbarbara/react-joyride/tree/3e08384415a831b20ce21c8423b6c271ad419fbf/src/styles.js) for more information.
62 |
63 | If you want to customize the arrow, check [react-floater](https://github.com/gilbarbara/react-floater) documentation.
64 |
65 |
--------------------------------------------------------------------------------
/tools/index.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable no-var, vars-on-top, no-console */
2 | const { exec } = require('child_process');
3 | const chalk = require('chalk');
4 |
5 | const args = process.argv.slice(2);
6 |
7 | if (!args[0]) {
8 | console.log(`Valid arguments:
9 | • docs (rebuild documentation)
10 | • update (if package.json has changed run \`npm update\`)
11 | • commits (has new remote commits)`);
12 | }
13 |
14 | if (args[0] === 'update') {
15 | exec('git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD', (err, stdout) => {
16 | if (err) {
17 | throw new Error(err);
18 | }
19 |
20 | if (stdout.match('package.json')) {
21 | exec('npm update').stdout.pipe(process.stdout);
22 | }
23 | });
24 | }
25 |
26 | if (args[0] === 'commits') {
27 | exec('git remote -v update', errRemote => {
28 | if (errRemote) {
29 | throw new Error(errRemote);
30 | }
31 |
32 | const local = new Promise((resolve, reject) => {
33 | exec('git rev-parse @', (err, stdout) => {
34 | if (err) {
35 | return reject(err);
36 | }
37 |
38 | return resolve(stdout);
39 | });
40 | });
41 |
42 | const remote = new Promise((resolve, reject) => {
43 | exec('git rev-parse @{u}', (err, stdout) => {
44 | if (err) {
45 | return reject(err);
46 | }
47 |
48 | return resolve(stdout);
49 | });
50 | });
51 |
52 | const base = new Promise((resolve, reject) => {
53 | exec('git merge-base @ @{u}', (err, stdout) => {
54 | if (err) {
55 | return reject(err);
56 | }
57 | return resolve(stdout);
58 | });
59 | });
60 |
61 | Promise.all([local, remote, base])
62 | .then(values => {
63 | const [$local, $remote, $base] = values;
64 |
65 | if ($local === $remote) {
66 | console.log(chalk.green('✔ Repo is up-to-date!'));
67 | }
68 | else if ($local === $base) {
69 | console.error(chalk.red('⊘ Error: You need to pull, there are new commits.'));
70 | process.exit(1);
71 | }
72 | })
73 | .catch(err => {
74 | console.log(chalk.red('⊘ Error: Commits failed'), err);
75 | });
76 | });
77 | }
78 |
--------------------------------------------------------------------------------
/docs/callback.md:
--------------------------------------------------------------------------------
1 | # Callback
2 |
3 | You can get Joyride's state change using the `callback` prop.
4 |
5 | It will receive a plain object with the state changes.
6 |
7 | Example data:
8 |
9 | ```text
10 | {
11 | action: 'start',
12 | controlled: true,
13 | index: 0,
14 | lifecycle: 'init',
15 | size: 4,
16 | status: 'running',
17 | step: { the.current.step },
18 | type: 'tour:start',
19 | }
20 | ```
21 |
22 | ```text
23 | {
24 | action: 'update',
25 | controlled: true,
26 | index: 0,
27 | lifecycle: 'beacon',
28 | size: 4,
29 | status: 'running',
30 | step: { the.current.step },
31 | type: 'beacon',
32 | }
33 | ```
34 |
35 | ```text
36 | {
37 | action: 'next',
38 | controlled: true,
39 | index: 0,
40 | lifecycle: 'complete',
41 | size: 4,
42 | status: 'running',
43 | step: { the.current.step },
44 | type: 'step:after',
45 | }
46 | ```
47 |
48 | ```javascript
49 | import Joyride from 'react-joyride';
50 | import { ACTIONS, EVENTS } from 'react-joyride/es/constants';
51 |
52 | export class App extends React.Component {
53 | state = {
54 | run: false,
55 | steps: [],
56 | stepIndex: 0, // a controlled tour
57 | };
58 |
59 | callback = (tour) => {
60 | const { action, index, type } = tour;
61 |
62 | if (type === EVENTS.TOUR_END) {
63 | // Update user preferences with completed tour flag
64 | } else if (type === EVENTS.STEP_AFTER && index === 1) {
65 | // pause the tour, load a new route and start it again once is done.
66 | this.setState({ run: false });
67 | }
68 | else if ([EVENTS.STEP_AFTER, EVENTS.CLOSE, EVENTS.TARGET_NOT_FOUND].includes(type)) {
69 | // Sunce this is a controlled tour you'll need to update the state to advance the tour
70 | this.setState({ stepIndex: index + (action === ACTIONS.PREV ? -1 : 1) });
71 | }
72 | };
73 |
74 | render () {
75 | const { run, stepIndex, steps } = this.state;
76 |
77 | return (
78 |
79 |
86 | ...
87 |
88 | );
89 | }
90 | }
91 | ```
92 |
93 | You can read more about the constants [here](constants.md)
94 |
95 |
--------------------------------------------------------------------------------
/test/__fixtures__/steps.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default [
4 | {
5 | title: 'Title only steps — As they say: Make the font bigger!',
6 | textAlign: 'center',
7 | selector: '.projects .list',
8 | position: 'top',
9 | },
10 | {
11 | title: 'Our Mission',
12 | text: 'Can be advanced by clicking an element through the overlay hole.',
13 | selector: '.mission button',
14 | position: 'bottom',
15 | allowClicksThruHole: true,
16 | style: {
17 | beacon: {
18 | offsetY: 20,
19 | },
20 | button: {
21 | display: 'none',
22 | },
23 | },
24 | },
25 | {
26 | title: 'Unmounted target',
27 | text: 'This step tests what happens when a target is missing',
28 | selector: '.not-mounted',
29 | },
30 | {
31 | text: (
32 |
33 |
We are the people
34 |
49 |
50 | ),
51 | selector: '.about h2 span',
52 | position: 'left',
53 | style: {
54 | beacon: {
55 | inner: '#27e200',
56 | offsetX: 20,
57 | outer: '#27e200',
58 | },
59 | arrow: {
60 | display: 'none',
61 | },
62 | },
63 | },
64 | {
65 | text: 'Text only steps — Because sometimes you don\'t really need a proper heading',
66 | selector: '.demo__footer a',
67 | position: 'top',
68 | isFixed: true,
69 | style: {
70 | beacon: '#000',
71 | },
72 | },
73 | ];
74 |
--------------------------------------------------------------------------------
/docs/step.md:
--------------------------------------------------------------------------------
1 | # Step
2 |
3 | The step is an plain object that only requires two properties to be valid: `target` and `content` \(or `tooltipComponent`\).
4 |
5 | ```text
6 | {
7 | target: '.my-selector',
8 | content: 'This is my super awesome feature!'
9 | }
10 | ```
11 |
12 | It will inherit some properties from the Joyride's own [props](props.md) that can be overridden per step:
13 |
14 | * beaconComponent
15 | * disableCloseOnEsc
16 | * disableOverlay
17 | * disableOverlayClose
18 | * disableScrolling
19 | * floaterProps (check the [getMergedStep](../src/modules/step.js) function for more information)
20 | * hideBackButton
21 | * locale
22 | * showProgress
23 | * showSkipButton
24 | * spotlightClicks
25 | * spotlightPadding
26 | * styles
27 | * tooltipComponent
28 |
29 | ## And you can use these
30 |
31 | **content** {React.Node\|string}
32 | The tooltip's body.
33 |
34 | **disableBeacon** {boolean} ▶︎ `false`
35 | Don't show the Beacon before the tooltip.
36 |
37 | **event** {string} ▶︎ `click`
38 | The event to trigger the beacon. It can be _**click**_ or _**hover**_
39 |
40 | **isFixed** {boolean} ▶︎ `false`
41 | Force the step to be fixed.
42 |
43 | **offset** {React.Node\|string} ▶︎ `10`
44 | The distance from the target to the tooltip.
45 |
46 | **placement** {string} ▶︎ `bottom`
47 | The placement of the beacon and tooltip. It will re-position itself if there's no space available.
48 | It can be:
49 |
50 | * top \(top-start, top-end\)
51 | * bottom \(bottom-start, bottom-end\)
52 | * left \(left-start, left-end\)
53 | * right \(right-start, right-end
54 | * auto
55 | * center
56 |
57 | Check [react-floater](https://github.com/gilbarbara/react-floater) for more information.
58 |
59 | **placementBeacon** {string} ▶︎ placement
60 | The placement of the beacon. It will use the placement if nothing is passed and it can be: `top, bottom, left, right`.
61 |
62 | **styles** {Object}
63 | Override the [styling](styling.md) of the step's Tooltip
64 |
65 | **target** {Element\|string} - **required**
66 | The target for the step. It can be a [CSS selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) or an HtmlElement directly \(but using refs created in the same render would required an additional render afterwards\).
67 |
68 | **title** {React.Node\|string}
69 | The tooltip's title.
70 |
71 |
--------------------------------------------------------------------------------
/docs/props.md:
--------------------------------------------------------------------------------
1 | # Props
2 |
3 | **beaconComponent** {React.Node}
4 | A React component or function to be used instead the default Beacon.
5 |
6 | **callback** {function}
7 | It will be called when Joyride's state changes. it returns a single parameter with the state.
8 |
9 | **continuous** {boolean} ▶︎ `false`
10 | The tour is played sequentially with the **Next** button.
11 |
12 | **debug** {boolean} ▶︎ `false`
13 | Log Joyride's actions to the console.
14 |
15 | **disableCloseOnEsc** {boolean} ▶︎ `false`
16 | Disable closing the tooltip on ESC.
17 |
18 | **disableOverlay** {boolean} ▶︎ `false`
19 | Don't show the overlay.
20 |
21 | **disableOverlayClose** {boolean} ▶︎ `false`
22 | Don't close the tooltip when clicking the overlay.
23 |
24 | **disableScrolling** {boolean} ▶︎ `false`
25 | Disable auto scrolling between steps.
26 |
27 | **floaterProps** {Object}
28 | Options to be passed to [react-floater](https://github.com/gilbarbara/react-floater).
29 |
30 | **hideBackButton** {boolean} ▶︎ `false`
31 | Hide the "back" button.
32 |
33 | **locale** {Object} ▶︎ `{ back: 'Back', close: 'Close', last: 'Last', next: 'Next', skip: 'Skip' }`
34 | The strings used in the tooltip.
35 |
36 | **run** {boolean} ▶︎ `true`
37 | Run/stop the tour.
38 |
39 | **scrollOffset** {number} ▶︎ `20`
40 | The scroll distance from the element scrollTop value.
41 |
42 | **scrollToFirstStep** {boolean} ▶︎ `false`
43 | Scroll the page for the first step.
44 |
45 | **showProgress** {boolean} ▶︎ `false`
46 | Display the tour progress in the next button \_e.g. 2/5 \_in `continuous` tours.
47 |
48 | **showSkipButton** {boolean} ▶︎ `false`
49 | Display a button to skip the tour.
50 |
51 | **spotlightClicks** {boolean} ▶︎ `false`
52 | Allow mouse and touch events thru the spotlight. You can click links in your app.
53 |
54 | **spotlightPadding** {boolean} ▶︎ `10`
55 | The padding of the spotlight.
56 |
57 | **stepIndex** {number}
58 | Setting a number here will turn Joyride into `controlled` mode.
59 | You will receive the state events in the `callback` and you'll have to update this prop by yourself.
60 |
61 | **steps** {Array<StepProps>} - **required**
62 | The tour's steps.
63 |
64 | **styles** {Object}
65 | Override the [styling](styling.md) of the Tooltip globally
66 |
67 | **tooltipComponent** {React.Node}
68 | A React component or function to be used instead the default Tooltip excluding the arrow.
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/modules/scope.js:
--------------------------------------------------------------------------------
1 | const validTabNodes = /input|select|textarea|button|object/;
2 | const TAB_KEY = 9;
3 | let modalElement = null;
4 |
5 | function isHidden(element) {
6 | const noSize = element.offsetWidth <= 0 && element.offsetHeight <= 0;
7 |
8 | if (noSize && !element.innerHTML) return true;
9 |
10 | const style = window.getComputedStyle(element);
11 | return noSize ? style.getPropertyValue('overflow') !== 'visible' : style.getPropertyValue('display') === 'none';
12 | }
13 |
14 | function isVisible(element) {
15 | let parentElement = element;
16 | while (parentElement) {
17 | if (parentElement === document.body) break;
18 | if (isHidden(parentElement)) return false;
19 | parentElement = parentElement.parentNode;
20 | }
21 | return true;
22 | }
23 |
24 | function canHaveFocus(element, isTabIndexNotNaN) {
25 | const nodeName = element.nodeName.toLowerCase();
26 | const res = (validTabNodes.test(nodeName) && !element.disabled)
27 | || (nodeName === 'a' ? element.href || isTabIndexNotNaN : isTabIndexNotNaN);
28 | return res && isVisible(element);
29 | }
30 |
31 | function canBeTabbed(element) {
32 | let tabIndex = element.getAttribute('tabindex');
33 | if (tabIndex === null) tabIndex = undefined;
34 | const isTabIndexNaN = isNaN(tabIndex);
35 | return (isTabIndexNaN || tabIndex >= 0) && canHaveFocus(element, !isTabIndexNaN);
36 | }
37 |
38 | function findValidTabElements(element) {
39 | return [].slice.call(element.querySelectorAll('*'), 0).filter(canBeTabbed);
40 | }
41 |
42 | function interceptTab(node, event) {
43 | const elements = findValidTabElements(node);
44 | const { shiftKey } = event;
45 |
46 | if (!elements.length) {
47 | event.preventDefault();
48 | return;
49 | }
50 |
51 | let x = elements.indexOf(document.activeElement);
52 |
53 | if (x === -1 || (!shiftKey && x + 1 === elements.length)) {
54 | x = 0;
55 | }
56 | else {
57 | x += shiftKey ? -1 : 1;
58 | }
59 |
60 | event.preventDefault();
61 |
62 | elements[x].focus();
63 | }
64 |
65 | function handleKeyDown(e) {
66 | if (!modalElement) {
67 | return;
68 | }
69 |
70 | if (e.keyCode === TAB_KEY) {
71 | interceptTab(modalElement, e);
72 | }
73 | }
74 |
75 | export function setScope(element) {
76 | modalElement = element;
77 |
78 | window.addEventListener('keydown', handleKeyDown, false);
79 | }
80 |
81 | export function removeScope() {
82 | modalElement = null;
83 |
84 | window.removeEventListener('keydown', handleKeyDown);
85 | }
86 |
--------------------------------------------------------------------------------
/src/config/types.js:
--------------------------------------------------------------------------------
1 | import type { Node } from 'react';
2 |
3 | export type StateHelpers = {
4 | close: Function,
5 | go: Function,
6 | index: Function,
7 | info: Function,
8 | next: Function,
9 | prev: Function,
10 | reset: Function,
11 | restart: Function,
12 | skip: Function,
13 | start: Function,
14 | stop: Function,
15 | };
16 |
17 | export type StateInstance = {
18 | ...StateHelpers,
19 | update: Function,
20 | };
21 |
22 | export type StateObject = {
23 | action: string,
24 | controlled: boolean,
25 | index: number,
26 | lifecycle: string,
27 | size: number,
28 | status: string,
29 | };
30 |
31 | type placement = 'top' | 'top-start' | 'top-end' |
32 | 'bottom' | 'bottom-start' | 'bottom-end' |
33 | 'left' | 'left-start' | 'left-end' |
34 | 'right' | 'right-start' | 'right-end' |
35 | 'auto' | 'center';
36 |
37 | type placementBeacon = 'top' | 'bottom' | 'left' | 'right';
38 |
39 | export type StepProps = {
40 | beaconComponent: ?Node,
41 | content: Node | string,
42 | disableBeacon: boolean,
43 | disableCloseOnEsc: boolean,
44 | disableOverlay: boolean,
45 | disableOverlayClose: boolean,
46 | disableScrolling: boolean,
47 | event: string,
48 | floaterProps: ?Object,
49 | hideBackButton: ?boolean,
50 | isFixed: ?boolean,
51 | offset: number,
52 | placement: placement,
53 | placementBeacon: placementBeacon,
54 | showProgress: ?boolean,
55 | showSkipButton: ?boolean,
56 | spotlightPadding: number,
57 | spotlightClicks: boolean,
58 | styles: ?Object,
59 | target: string | HTMLElement,
60 | title: ?Node,
61 | tooltipComponent: ?Node,
62 | }
63 |
64 | export type JoyrideProps = {
65 | beaconComponent: ?Node,
66 | callback: ?Function,
67 | continuous: boolean,
68 | debug: boolean,
69 | disableCloseOnEsc: boolean,
70 | disableOverlay: boolean,
71 | disableOverlayClose: boolean,
72 | disableScrolling: boolean,
73 | floaterProps: ?Object,
74 | hideBackButton: boolean,
75 | locale: ?Object,
76 | run: boolean,
77 | scrollOffset: number,
78 | scrollToFirstStep: boolean,
79 | showProgress: boolean,
80 | showSkipButton: boolean,
81 | spotlightPadding: boolean,
82 | spotlightClicks: boolean,
83 | stepIndex: ?number,
84 | steps: Array,
85 | styles: ?Object,
86 | tooltipComponent: ?Node,
87 | }
88 |
89 | export type CallBackProps = {
90 | action: string,
91 | controlled: boolean,
92 | index: number,
93 | lifecycle: string,
94 | size: number,
95 | status: string,
96 | step: StepProps,
97 | type: string,
98 | }
99 |
--------------------------------------------------------------------------------
/src/components/Beacon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export default class JoyrideBeacon extends React.Component {
5 | constructor(props) {
6 | super(props);
7 |
8 | if (!props.beaconComponent) {
9 | const head = document.head || document.getElementsByTagName('head')[0];
10 | const style = document.createElement('style');
11 | const css = `
12 | @keyframes joyride-beacon-inner {
13 | 20% {
14 | opacity: 0.9;
15 | }
16 |
17 | 90% {
18 | opacity: 0.7;
19 | }
20 | }
21 |
22 | @keyframes joyride-beacon-outer {
23 | 0% {
24 | transform: scale(1);
25 | }
26 |
27 | 45% {
28 | opacity: 0.7;
29 | transform: scale(0.75);
30 | }
31 |
32 | 100% {
33 | opacity: 0.9;
34 | transform: scale(1);
35 | }
36 | }
37 | `;
38 |
39 | style.type = 'text/css';
40 | style.id = 'joyride-beacon-animation';
41 | style.appendChild(document.createTextNode(css));
42 |
43 | head.appendChild(style);
44 | }
45 | }
46 |
47 | static propTypes = {
48 | beaconComponent: PropTypes.oneOfType([
49 | PropTypes.func,
50 | PropTypes.element,
51 | ]),
52 | locale: PropTypes.object.isRequired,
53 | onClickOrHover: PropTypes.func.isRequired,
54 | styles: PropTypes.object.isRequired,
55 | };
56 |
57 | componentWillUnmount() {
58 | const style = document.getElementById('joyride-beacon-animation');
59 |
60 | if (style) {
61 | style.parentNode.removeChild(style);
62 | }
63 | }
64 |
65 | render() {
66 | const { beaconComponent, locale, onClickOrHover, styles } = this.props;
67 | const props = {
68 | 'aria-label': locale.open,
69 | onClick: onClickOrHover,
70 | onMouseEnter: onClickOrHover,
71 | title: locale.open,
72 | };
73 | let component;
74 |
75 | if (beaconComponent) {
76 | if (React.isValidElement(beaconComponent)) {
77 | component = React.cloneElement(beaconComponent, props);
78 | }
79 | else {
80 | component = beaconComponent(props);
81 | }
82 | }
83 | else {
84 | component = (
85 |
95 | );
96 | }
97 |
98 | return component;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Joyride
2 |
3 | [](https://www.npmjs.com/package/react-joyride) [](https://travis-ci.org/gilbarbara/react-joyride) [](https://codeclimate.com/github/gilbarbara/react-joyride/maintainability) [](https://codeclimate.com/github/gilbarbara/react-joyride/test_coverage)
4 |
5 | [](http://gilbarbara.github.io/react-joyride/)
6 |
7 | Create a tour for your app!
8 | Use it to showcase your app for new users! Or explain functionality of complex features!
9 |
10 | #### View the demo [here](https://2zpjporp4p.codesandbox.io/)
11 |
12 | You can edit the demo [here](https://codesandbox.io/s/2zpjporp4p)
13 |
14 | > If you are looking for the documentation for the old 1.x version, go [here](https://github.com/gilbarbara/react-joyride/tree/v1.11.4)
15 |
16 | ## Setup
17 |
18 | ```bash
19 | npm i react-joyride@next
20 | ```
21 |
22 | ## Getting Started
23 |
24 | Just set a `steps` array to the Joyride component and you're good to go!
25 |
26 | You can use your own component for the tooltip body or beacon, if you want.
27 |
28 | ```js
29 | import Joyride from 'react-joyride';
30 |
31 | export class App extends React.Component {
32 | state = {
33 | run: false,
34 | steps: [
35 | {
36 | target: '.my-first-step',
37 | content: 'This if my awesome feature!',
38 | placement: 'bottom',
39 | },
40 | {
41 | target: '.my-other-step',
42 | content: 'This if my awesome feature!',
43 | placement: 'bottom',
44 | },
45 | ...
46 | ]
47 | };
48 |
49 | componentDidMount() {
50 | this.setState({ run: true });
51 | }
52 |
53 | callback = (data) => {
54 | const { action, index, type } = data;
55 | };
56 |
57 | render () {
58 | const { steps, run } = this.state;
59 |
60 | return (
61 |
62 |
68 | ...
69 |
70 | );
71 | }
72 | }
73 | ```
74 |
75 | ## Documentation
76 |
77 | [Props](docs/props.md)
78 |
79 | [Step](docs/step.md)
80 |
81 | [Styling](docs/styling.md)
82 |
83 | [Callback](docs/callback.md)
84 |
85 | [Constants](docs/constants.md)
86 |
87 | [Migration from 1.x](docs/migration.md)
88 |
89 | This library uses [react-floater](https://github.com/gilbarbara/react-floater) and [popper.js](https://github.com/FezVrasta/popper.js) for positioning and styling.
90 |
91 |
--------------------------------------------------------------------------------
/src/components/Tooltip/Container.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import CloseBtn from './CloseBtn';
5 |
6 | const JoyrideTooltipContainer = ({
7 | continuous,
8 | backProps,
9 | closeProps,
10 | primaryProps,
11 | skipProps,
12 | index,
13 | isLastStep,
14 | setTooltipRef,
15 | size,
16 | step,
17 | }) => {
18 | const { content, hideBackButton, locale, showProgress, showSkipButton, title, styles } = step;
19 | const { back, close, last, next, skip } = locale;
20 | const output = {
21 | primary: close,
22 | };
23 |
24 | if (continuous) {
25 | if (isLastStep) {
26 | output.primary = last;
27 | }
28 | else {
29 | output.primary = next;
30 | }
31 |
32 | if (showProgress) {
33 | output.primary += ` (${index + 1}/${size})`;
34 | }
35 | }
36 |
37 | if (showSkipButton && !isLastStep) {
38 | output.skip = (
39 |
46 | );
47 | }
48 |
49 | if (!hideBackButton && index > 0) {
50 | output.back = (
51 |
58 | );
59 | }
60 |
61 | output.close = ();
62 |
63 | return (
64 |
69 |
70 | {output.close}
71 | {title && (
{title}
)}
72 | {!!content && (
73 |
74 | {content}
75 |
76 | )}
77 |
78 |
79 | {output.skip}
80 | {output.back}
81 |
88 |
89 |
90 | );
91 | };
92 |
93 | JoyrideTooltipContainer.propTypes = {
94 | backProps: PropTypes.object.isRequired,
95 | closeProps: PropTypes.object.isRequired,
96 | continuous: PropTypes.bool.isRequired,
97 | index: PropTypes.number.isRequired,
98 | isLastStep: PropTypes.bool.isRequired,
99 | primaryProps: PropTypes.object.isRequired,
100 | setTooltipRef: PropTypes.func.isRequired,
101 | size: PropTypes.number.isRequired,
102 | skipProps: PropTypes.object.isRequired,
103 | step: PropTypes.object.isRequired,
104 | };
105 |
106 | export default JoyrideTooltipContainer;
107 |
--------------------------------------------------------------------------------
/src/components/Tooltip/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import Container from './Container';
5 |
6 | export default class JoyrideTooltip extends React.Component {
7 | static propTypes = {
8 | continuous: PropTypes.bool.isRequired,
9 | helpers: PropTypes.object.isRequired,
10 | index: PropTypes.number.isRequired,
11 | isLastStep: PropTypes.bool.isRequired,
12 | setTooltipRef: PropTypes.func.isRequired,
13 | size: PropTypes.number.isRequired,
14 | step: PropTypes.object.isRequired,
15 | };
16 |
17 | handleClickBack = (e) => {
18 | e.preventDefault();
19 | const { helpers } = this.props;
20 |
21 | helpers.prev();
22 | };
23 |
24 | handleClickClose = (e) => {
25 | e.preventDefault();
26 | const { helpers } = this.props;
27 |
28 | helpers.close();
29 | };
30 |
31 | handleClickPrimary = (e) => {
32 | e.preventDefault();
33 | const { continuous, helpers } = this.props;
34 |
35 | if (!continuous) {
36 | helpers.close();
37 | return;
38 | }
39 |
40 | helpers.next();
41 | };
42 |
43 | handleClickSkip = (e) => {
44 | e.preventDefault();
45 | const { helpers } = this.props;
46 |
47 | helpers.skip();
48 | };
49 |
50 | render() {
51 | const { continuous, index, isLastStep, setTooltipRef, size, step } = this.props;
52 | const { content, locale, title, tooltipComponent } = step;
53 | const { back, close, last, next, skip } = locale;
54 | let primaryText = continuous ? next : close;
55 |
56 | if (isLastStep) {
57 | primaryText = last;
58 | }
59 |
60 | let component;
61 | const buttonProps = {
62 | backProps: { 'aria-label': back, onClick: this.handleClickBack, role: 'button', title: back },
63 | closeProps: { 'aria-label': close, onClick: this.handleClickClose, role: 'button', title: close },
64 | primaryProps: { 'aria-label': primaryText, onClick: this.handleClickPrimary, role: 'button', title: primaryText },
65 | skipProps: { 'aria-label': skip, onClick: this.handleClickSkip, role: 'button', title: skip },
66 | };
67 |
68 | if (tooltipComponent) {
69 | const renderProps = {
70 | ...buttonProps,
71 | content,
72 | continuous,
73 | index,
74 | isLastStep,
75 | locale,
76 | setTooltipRef,
77 | size,
78 | title,
79 | };
80 |
81 | if (React.isValidElement(tooltipComponent)) {
82 | component = React.cloneElement(tooltipComponent, renderProps);
83 | }
84 | else {
85 | component = tooltipComponent(renderProps);
86 | }
87 | }
88 | else {
89 | component = (
90 |
99 | );
100 | }
101 |
102 | return component;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/modules/step.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import deepmerge from 'deepmerge';
3 | import is from 'is-lite';
4 |
5 | import { getElement, hasCustomScrollParent } from './dom';
6 | import { log } from './helpers';
7 | import getStyles from '../styles';
8 |
9 | import DEFAULTS from '../config/defaults';
10 |
11 | import type { StepProps, JoyrideProps } from '../config/types';
12 |
13 | /**
14 | * Validate if a step is valid
15 | *
16 | * @param {Object} step - A step object
17 | * @param {boolean} debug
18 | *
19 | * @returns {boolean} - True if the step is valid, false otherwise
20 | */
21 | export function validateStep(step: StepProps, debug: boolean = false): boolean {
22 | if (!is.plainObject(step)) {
23 | log({
24 | title: 'validateStep',
25 | data: 'step must be an object',
26 | warn: true,
27 | debug,
28 | });
29 | return false;
30 | }
31 |
32 | if (!step.target) {
33 | log({
34 | title: 'validateStep',
35 | data: 'target is missing from the step',
36 | warn: true,
37 | debug,
38 | });
39 | return false;
40 | }
41 |
42 | return true;
43 | }
44 |
45 | /**
46 | * Validate if steps is valid
47 | *
48 | * @param {Array} steps - A steps array
49 | * @param {boolean} debug
50 | *
51 | * @returns {boolean} - True if the steps are valid, false otherwise
52 | */
53 | export function validateSteps(steps: Array