Skip to content

Commit c081056

Browse files
authored
Merge pull request #2 from jasongaare/v0.1-refinements
Version 0.1
2 parents 5d32ead + b5cb65e commit c081056

File tree

7 files changed

+576
-493
lines changed

7 files changed

+576
-493
lines changed

README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ React Native Walkthrough is an app-wide walkthrough library, with a minimal foot
1212
yarn add react-native-walkthrough
1313
```
1414

15+
### Roadmap
16+
- [ ] Move library to Typescript (soon!)
17+
- [ ] add ability to pass external EventEmitter instance via props
18+
- [ ] add props for changing timeout durations
19+
- [ ] ???
20+
1521
### Basic Usage
1622

1723
React Native Walkthrough exports the following:
@@ -20,6 +26,7 @@ React Native Walkthrough exports the following:
2026
- [`WalkthroughElement component`](#walkthroughelement)
2127
- [`startWalkthrough` function](#startwalkthrough)
2228
- [`dispatchWalkthroughEvent` function](#dispatchwalkthroughevent)
29+
- [`goToWalkthroughElementWithId` function](#gotowalkthroughelementwithid)
2330

2431

2532
#### `WalkthroughProvider`
@@ -91,6 +98,22 @@ render (
9198
)
9299
```
93100

101+
#### `goToWalkthroughElementWithId`
102+
103+
Function that accepts a string element id. Finds the first element in the current walkthrough with a matching `id` and sets that element as the current element.
104+
105+
```js
106+
import { goToWalkthroughElementWithId } from 'react-native-walkthrough';
107+
108+
109+
<TouchableOpacity
110+
onPress={() => goToWalkthroughElementWithId('step-3')}
111+
>
112+
<Text>{"Skip to next step"}</Text>
113+
</TouchableOpacity>
114+
115+
```
116+
94117
### Creating a Walkthrough Guide
95118

96119
A walkthrough guide is simply an array of objects, where each object correlates to a `WalkthroughElement`. Each element object in the guide must have an `id` and `content` to display in the tooltip bubble.
@@ -135,10 +158,12 @@ export default [
135158

136159
|Key|Type|Required|Description|
137160
|---|----|-----|----|
138-
|id|string|YES|id string that matches the corresponding WalkthroughElement|
139161
|content|function/Element| YES | This is the view displayed in the tooltip popover bubble |
162+
|id|string|YES|id string that matches the corresponding WalkthroughElement|
140163
|placement|string|NO | Determines placement of tooltip in relation to the element it is wrapping
141-
|triggerEvent|string|NO|string event id, this element will not appear until the triggerEvent is dispatched via `dispatchWalkthroughEvent`
164+
|possibleOutcomes|array|NO|An array of objects with keys (`event`, `action`) that creates event listeners for multiple events to provide the ability to have an outcome tree that responds to a user's actions (listens to events dispatched via `dispatchWalkthroughEvent`|
142165
|tooltipProps|object|NO|additional props to customize the tooltip functionality and style
166+
|triggerEvent|string|NO|string event id, this element will not appear until the triggerEvent is dispatched via `dispatchWalkthroughEvent`
167+
143168

144169
> To learn more about `placement` options and all the options for `tooltipProps` view the [`react-native-walkthrough-tooltip` README](https://github.com/jasongaare/react-native-walkthrough-tooltip#props)

index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import WalkthroughElement from './src/WalkthroughElement';
22
import WalkthroughProvider, {
33
dispatchWalkthroughEvent,
4+
exitWalkthrough,
5+
goToWalkthroughElementWithId,
46
startWalkthrough,
7+
startWalkthroughAtElement,
58
} from './src/WalkthroughProvider';
69

710
export {
811
dispatchWalkthroughEvent,
12+
exitWalkthrough,
13+
goToWalkthroughElementWithId,
914
startWalkthrough,
15+
startWalkthroughAtElement,
1016
WalkthroughElement,
1117
WalkthroughProvider,
1218
};

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-walkthrough",
3-
"version": "0.0.2",
3+
"version": "0.1.0",
44
"description": "A lightweight walkthrough library for React Native utilizing React's Context API",
55
"main": "index.js",
66
"scripts": {
@@ -18,7 +18,7 @@
1818
"homepage": "https://github.com/jasongaare/react-native-walkthrough#readme",
1919
"dependencies": {
2020
"events": "3.0.0",
21-
"react-native-walkthrough-tooltip": "1.0.2"
21+
"react-native-walkthrough-tooltip": "1.1.7"
2222
},
2323
"peerDependencies": {
2424
"prop-types": "^15.6.1",

src/ContextWrapper.js

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import React, {Component} from 'react';
1+
import React, { Component } from 'react';
22
import PropTypes from 'prop-types';
3-
import {InteractionManager} from 'react-native';
43

54
const WAIT_NO_MORE_TIMEOUT = 1000 * 60 * 10; // 10 minutes
6-
const HOT_SEC = 350;
5+
const HOT_SEC = 500;
76

87
const nullElement = {
98
id: null,
@@ -29,42 +28,103 @@ class ContextWrapper extends Component {
2928
constructor(props) {
3029
super(props);
3130

32-
this.state = {currentElement: nullElement, currentGuide: []};
31+
this.state = {
32+
currentElement: nullElement,
33+
currentGuide: [],
34+
currentPossibleOutcomes: [],
35+
outcomeListenerStartTimestamp: null,
36+
};
3337
}
3438

3539
getCurrentElementIndex = () =>
3640
this.state.currentGuide.findIndex(
3741
element => element.id === this.state.currentElement.id,
3842
);
3943

44+
clearGuide = () => this.setState(safeSetGuide([]));
45+
46+
clearCurrentPossibleOutcomes = () => {
47+
const { eventEmitter } = this.props;
48+
49+
this.state.currentPossibleOutcomes.forEach(({ event, action }) => {
50+
eventEmitter.removeListener(event, action);
51+
});
52+
53+
this.setState({
54+
currentPossibleOutcomes: [],
55+
outcomeListenerStartTimestamp: null,
56+
});
57+
};
58+
59+
addTimeoutCheckToOutcomeActions = ({ event, action: originalAction }) => ({
60+
event,
61+
action: () => {
62+
const { outcomeListenerStartTimestamp } = this.state;
63+
64+
if (Date.now() - outcomeListenerStartTimestamp >= WAIT_NO_MORE_TIMEOUT) {
65+
this.clearGuide();
66+
} else {
67+
originalAction();
68+
}
69+
70+
this.clearCurrentPossibleOutcomes();
71+
},
72+
});
73+
74+
listenForPossibleOutcomes = element => {
75+
const { eventEmitter } = this.props;
76+
const { possibleOutcomes } = element;
77+
78+
if (possibleOutcomes) {
79+
if (!Array.isArray(possibleOutcomes)) {
80+
console.warn(
81+
'[react-native-walkthrough] non-Array value provided to possibleOutcomes',
82+
);
83+
} else {
84+
this.setState(
85+
{
86+
currentPossibleOutcomes: possibleOutcomes.map(
87+
this.addTimeoutCheckToOutcomeActions,
88+
),
89+
outcomeListenerStartTimestamp: Date.now(),
90+
},
91+
() => {
92+
this.state.currentPossibleOutcomes.forEach(({ event, action }) =>
93+
eventEmitter.once(event, action),
94+
);
95+
},
96+
);
97+
}
98+
}
99+
};
100+
101+
setElementNull = () => this.setState(safeSetElement(nullElement));
102+
40103
setElement = element => {
41104
if (element.id !== this.state.currentElement.id) {
42105
// clear previous element
43-
this.setState(safeSetElement(nullElement));
44-
45-
// after interactions and a hot sec, set current element
46-
InteractionManager.runAfterInteractions(() => {
47-
setTimeout(() => {
48-
this.setState(safeSetElement(element));
49-
}, HOT_SEC);
50-
});
106+
this.setElementNull();
107+
this.clearCurrentPossibleOutcomes();
108+
109+
// after a hot sec, set current element
110+
setTimeout(() => {
111+
this.setState(safeSetElement(element));
112+
}, HOT_SEC);
51113
}
52114
};
53115

54-
setGuide = (guide, callback = () => {}) =>
116+
setGuide = (guide, callback = () => {}) => {
117+
this.setElementNull();
55118
this.setState(safeSetGuide(guide), callback);
56-
57-
setNull = () => this.setState(safeSetElement(nullElement));
58-
59-
clearGuide = () => this.setState(safeSetGuide([]));
119+
};
60120

61121
waitForTrigger = element => {
62-
const {eventEmitter} = this.props;
122+
const { eventEmitter } = this.props;
63123

64124
const waitStart = Date.now();
65125
const triggerGuide = JSON.stringify(this.state.currentGuide);
66126

67-
this.setNull();
127+
this.setElementNull();
68128

69129
eventEmitter.once(element.triggerEvent, () => {
70130
const waitEnd = Date.now();
@@ -87,13 +147,27 @@ class ContextWrapper extends Component {
87147
}
88148
};
89149

150+
goToElementWithId = id => {
151+
const elementWithId = this.state.currentGuide.find(
152+
element => element.id === id,
153+
);
154+
155+
if (elementWithId) {
156+
this.goToElement(elementWithId);
157+
}
158+
};
159+
90160
goToNext = () => {
161+
const { currentElement } = this.state;
91162
const nextIndex = this.getCurrentElementIndex() + 1;
92163

93-
if (nextIndex < this.state.currentGuide.length) {
164+
if (currentElement.possibleOutcomes) {
165+
this.listenForPossibleOutcomes(currentElement);
166+
this.setElementNull();
167+
} else if (nextIndex < this.state.currentGuide.length) {
94168
this.goToElement(this.state.currentGuide[nextIndex]);
95169
} else {
96-
this.setNull();
170+
this.setElementNull();
97171
this.clearGuide();
98172
}
99173
};
@@ -104,7 +178,8 @@ class ContextWrapper extends Component {
104178
value={{
105179
...this.state,
106180
goToNext: this.goToNext,
107-
}}>
181+
}}
182+
>
108183
{this.props.children}
109184
</WalkthroughContext.Provider>
110185
);

src/WalkthroughElement.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Tooltip, {
44
TooltipChildrenContext,
55
} from 'react-native-walkthrough-tooltip';
66

7-
import {WalkthroughContext} from './ContextWrapper';
7+
import { WalkthroughContext } from './ContextWrapper';
88

99
const WalkthroughElement = props => {
1010
const elementId = props.id;
@@ -14,13 +14,19 @@ const WalkthroughElement = props => {
1414

1515
return (
1616
<WalkthroughContext.Consumer>
17-
{({currentElement, goToNext}) => {
17+
{({ currentElement, goToNext }) => {
1818
const defaultTooltipProps = {
1919
useInteractionManager: true,
2020
isVisible: elementId === currentElement.id,
2121
content: props.content || currentElement.content,
2222
placement: currentElement.placement || defaultPlacement,
23-
onClose: goToNext,
23+
onClose: () => {
24+
goToNext();
25+
26+
if (typeof currentElement.onClose === 'function') {
27+
currentElement.onClose();
28+
}
29+
},
2430
};
2531

2632
const tooltipProps = {

src/WalkthroughProvider.js

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,33 @@ import ContextWrapper from './ContextWrapper';
77
const wrapperRef = React.createRef();
88
const ee = new EventEmitter();
99

10-
const WalkthroughProvider = ({children}) => (
10+
const WalkthroughProvider = ({ children }) => (
1111
<ContextWrapper ref={wrapperRef} eventEmitter={ee}>
1212
{children}
1313
</ContextWrapper>
1414
);
1515

16+
const goToWalkthroughElementWithId = id => {
17+
const { current: wrapper } = wrapperRef;
18+
19+
if (wrapper && typeof wrapper.goToElementWithId === 'function') {
20+
wrapper.goToElementWithId(id);
21+
}
22+
};
23+
1624
const goToWalkthroughElement = element => {
17-
const {current: wrapper} = wrapperRef;
25+
const { current: wrapper } = wrapperRef;
1826

1927
if (wrapper && typeof wrapper.goToElement === 'function') {
2028
wrapper.goToElement(element);
2129
}
2230
};
2331

24-
const setWalkthroughGuide = (guide, setGuide) => {
25-
const {current: wrapper} = wrapperRef;
32+
const setWalkthroughGuide = (guide, callback) => {
33+
const { current: wrapper } = wrapperRef;
2634

2735
if (wrapper && typeof wrapper.setElement === 'function') {
28-
wrapper.setGuide(guide, setGuide);
36+
wrapper.setGuide(guide, callback);
2937
}
3038
};
3139

@@ -41,11 +49,31 @@ const startWalkthrough = walkthrough => {
4149
}
4250
};
4351

52+
const startWalkthroughAtElement = (walkthrough, elementId) => {
53+
if (Array.isArray(walkthrough)) {
54+
setWalkthroughGuide(walkthrough, () => {
55+
goToWalkthroughElementWithId(elementId);
56+
});
57+
} else {
58+
console.warn(
59+
'[react-native-walkthrough] non-Array argument provided to startWalkthroughAtElement',
60+
);
61+
}
62+
};
63+
4464
const dispatchWalkthroughEvent = event => ee.emit(event);
4565

66+
const exitWalkthrough = () => startWalkthrough([{}]);
67+
4668
WalkthroughProvider.propTypes = {
4769
children: PropTypes.element,
4870
};
4971

50-
export {dispatchWalkthroughEvent, startWalkthrough};
72+
export {
73+
dispatchWalkthroughEvent,
74+
exitWalkthrough,
75+
goToWalkthroughElementWithId,
76+
startWalkthrough,
77+
startWalkthroughAtElement,
78+
};
5179
export default WalkthroughProvider;

0 commit comments

Comments
 (0)