From 4029105dd6f30f0468f1d082365fbc3b3f75d2f8 Mon Sep 17 00:00:00 2001 From: Daniel Huang Date: Tue, 16 Oct 2018 10:36:47 -0700 Subject: [PATCH 01/11] add initial support for scroll view --- src/hocs/copilot.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/hocs/copilot.js b/src/hocs/copilot.js index 5627b7d3..32d454c1 100644 --- a/src/hocs/copilot.js +++ b/src/hocs/copilot.js @@ -1,7 +1,9 @@ // @flow import React, { Component } from 'react'; +import ReactNative from "react-native"; import PropTypes from 'prop-types'; + import { View } from 'react-native'; import mitt from 'mitt'; @@ -25,7 +27,7 @@ type State = { currentStep: ?Step, visible: boolean, androidStatusBarVisible: boolean, - backdropColor: string + scrollView?: React.RefObject }; const copilot = ({ @@ -34,7 +36,6 @@ const copilot = ({ stepNumberComponent, animated, androidStatusBarVisible, - backdropColor, } = {}) => (WrappedComponent) => { class Copilot extends Component { @@ -42,6 +43,7 @@ const copilot = ({ steps: {}, currentStep: null, visible: false, + scrollView: null }; getChildContext(): { _copilot: CopilotContext } { @@ -78,6 +80,14 @@ const copilot = ({ setCurrentStep = async (step: Step, move?: boolean = true): void => { await this.setState({ currentStep: step }); this.eventEmitter.emit('stepChange', step); + const scrollView = this.state.scrollView.current; + + if (this.state.scrollView) { + const relativeSize = await this.state.currentStep.wrapper.measureLayout(ReactNative.findNodeHandle(scrollView), (x, y, w, h) => { + const yOffsett = y > 0 ? y - (h / 2) : 0; + scrollView.scrollTo({ y: yOffsett, animated: false }); + }); + } if (move) { this.moveToCurrentStep(); @@ -126,9 +136,12 @@ const copilot = ({ await this.setCurrentStep(this.getPrevStep()); } - start = async (fromStep?: string): void => { + start = async (fromStep?: string, scrollView?: React.RefObject): void => { + const { steps } = this.state; + this.state.scrollView ? null : this.setState({ scrollView }) + const currentStep = fromStep ? steps[fromStep] : this.getFirstStep(); @@ -143,7 +156,7 @@ const copilot = ({ requestAnimationFrame(() => this.start(fromStep)); } else { this.eventEmitter.emit('start'); - await this.setCurrentStep(currentStep); + await this.setCurrentStep(currentStep, true); await this.moveToCurrentStep(); await this.setVisibility(true); this.startTries = 0; @@ -155,7 +168,7 @@ const copilot = ({ this.eventEmitter.emit('stop'); } - async moveToCurrentStep(): void { + async moveToCurrentStep(): void { const size = await this.state.currentStep.target.measure(); await this.modal.animateMove({ @@ -190,7 +203,6 @@ const copilot = ({ overlay={overlay} animated={animated} androidStatusBarVisible={androidStatusBarVisible} - backdropColor={backdropColor} ref={(modal) => { this.modal = modal; }} /> From 21f532163e577f3dc6ee3bd0833176c0bafe059c Mon Sep 17 00:00:00 2001 From: Daniel Huang Date: Tue, 16 Oct 2018 10:48:04 -0700 Subject: [PATCH 02/11] move variable after check and set timeout if scroll view to allow scrolling to finish before measuring --- src/hocs/copilot.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/hocs/copilot.js b/src/hocs/copilot.js index 32d454c1..2057f443 100644 --- a/src/hocs/copilot.js +++ b/src/hocs/copilot.js @@ -80,18 +80,20 @@ const copilot = ({ setCurrentStep = async (step: Step, move?: boolean = true): void => { await this.setState({ currentStep: step }); this.eventEmitter.emit('stepChange', step); - const scrollView = this.state.scrollView.current; if (this.state.scrollView) { + const scrollView = this.state.scrollView.current; const relativeSize = await this.state.currentStep.wrapper.measureLayout(ReactNative.findNodeHandle(scrollView), (x, y, w, h) => { const yOffsett = y > 0 ? y - (h / 2) : 0; scrollView.scrollTo({ y: yOffsett, animated: false }); }); } - - if (move) { - this.moveToCurrentStep(); - } + + setTimeout(() => { + if (move) { + this.moveToCurrentStep(); + } + }, this.state.scrollView ? 100 : 0); } setVisibility = (visible: boolean): void => new Promise((resolve) => { From abfd25d8203d3ef2830c1a6736f02bbd614b0fb9 Mon Sep 17 00:00:00 2001 From: Daniel Huang Date: Tue, 16 Oct 2018 10:57:16 -0700 Subject: [PATCH 03/11] cleanup --- src/hocs/copilot.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/hocs/copilot.js b/src/hocs/copilot.js index 2057f443..3ffc5af8 100644 --- a/src/hocs/copilot.js +++ b/src/hocs/copilot.js @@ -27,6 +27,7 @@ type State = { currentStep: ?Step, visible: boolean, androidStatusBarVisible: boolean, + backdropColor: string, scrollView?: React.RefObject }; @@ -36,6 +37,7 @@ const copilot = ({ stepNumberComponent, animated, androidStatusBarVisible, + backdropColor, } = {}) => (WrappedComponent) => { class Copilot extends Component { @@ -88,12 +90,12 @@ const copilot = ({ scrollView.scrollTo({ y: yOffsett, animated: false }); }); } - setTimeout(() => { if (move) { this.moveToCurrentStep(); } - }, this.state.scrollView ? 100 : 0); + }, this.state.scrollView ? 100 : 0) + } setVisibility = (visible: boolean): void => new Promise((resolve) => { @@ -158,7 +160,7 @@ const copilot = ({ requestAnimationFrame(() => this.start(fromStep)); } else { this.eventEmitter.emit('start'); - await this.setCurrentStep(currentStep, true); + await this.setCurrentStep(currentStep); await this.moveToCurrentStep(); await this.setVisibility(true); this.startTries = 0; @@ -170,7 +172,7 @@ const copilot = ({ this.eventEmitter.emit('stop'); } - async moveToCurrentStep(): void { + async moveToCurrentStep(): void { const size = await this.state.currentStep.target.measure(); await this.modal.animateMove({ @@ -205,6 +207,7 @@ const copilot = ({ overlay={overlay} animated={animated} androidStatusBarVisible={androidStatusBarVisible} + backdropColor={backdropColor} ref={(modal) => { this.modal = modal; }} /> From 8b4383b5acaa61ef4dd7d1c85d0a479f56f67a42 Mon Sep 17 00:00:00 2001 From: Daniel Huang Date: Tue, 16 Oct 2018 10:59:25 -0700 Subject: [PATCH 04/11] remove added empty line --- src/hocs/copilot.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hocs/copilot.js b/src/hocs/copilot.js index 3ffc5af8..bf9d4fb7 100644 --- a/src/hocs/copilot.js +++ b/src/hocs/copilot.js @@ -3,7 +3,6 @@ import React, { Component } from 'react'; import ReactNative from "react-native"; import PropTypes from 'prop-types'; - import { View } from 'react-native'; import mitt from 'mitt'; From 559bf57bc489857cd93ba417d63c9bf19f69407d Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Sat, 30 Mar 2019 21:24:47 +0500 Subject: [PATCH 05/11] Fixed the double conversion error. --- src/hocs/copilot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hocs/copilot.js b/src/hocs/copilot.js index bf9d4fb7..6242ab31 100644 --- a/src/hocs/copilot.js +++ b/src/hocs/copilot.js @@ -83,7 +83,7 @@ const copilot = ({ this.eventEmitter.emit('stepChange', step); if (this.state.scrollView) { - const scrollView = this.state.scrollView.current; + const scrollView = this.state.scrollView; const relativeSize = await this.state.currentStep.wrapper.measureLayout(ReactNative.findNodeHandle(scrollView), (x, y, w, h) => { const yOffsett = y > 0 ? y - (h / 2) : 0; scrollView.scrollTo({ y: yOffsett, animated: false }); From 40b941cc7cce6b33baa9810a13ecae9d9e724805 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Sat, 30 Mar 2019 21:32:38 +0500 Subject: [PATCH 06/11] Added documentation for the scrollview feature. --- README.md | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fa81c242..ff60eab6 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,34 @@ class HomeScreen { ### Triggering the tutorial Use `this.props.start()` in the root component in order to trigger the tutorial. You can either invoke it with a touch event or in `componentDidMount`. Note that the component and all its descendants must be mounted before starting the tutorial since the `CopilotStep`s need to be registered first. +### Usage inside a ScrollView +Pass the ScrollView reference as the second argument to the `this.props.start()` function. +eg `this.props.start(false, ScrollViewRef)` + +```js +import { ScrollView } from 'react-native'; +import { copilot } from '@okgrow/react-native-copilot'; + +class HomeScreen { + componentDidMount() { + // Starting the tutorial and passing the scrollview reference. + this.props.start(false, this.scrollView); + } + + componentWillUnmount() { + // Don't forget to disable event handlers to prevent errors + this.props.copilotEvents.off('stop'); + } + + render() { + this.scrollView = ref}> + // ... + + } +} +export default copilot()(HomeScreen); +``` + ### Listening to the events Along with `this.props.start()`, `copilot` HOC passes `copilotEvents` function to the component to help you with tracking of tutorial progress. It utilizes [mitt](https://github.com/developit/mitt) under the hood, you can see how full API there. @@ -200,12 +228,15 @@ List of available events is: **Example:** ```js +import { ScrollView } from 'react-native'; import { copilot, CopilotStep } from '@okgrow/react-native-copilot'; const CustomComponent = ({ copilot }) => Hello world!; class HomeScreen { componentDidMount() { + // Starting the tutorial. + this.props.start(false, this.scrollView); this.props.copilotEvents.on('stop', () => { // Copilot tutorial finished! }); @@ -217,7 +248,9 @@ class HomeScreen { } render() { - // ... + this.scrollView = ref}> + // ... + } } ``` From aecc25b7843f772d77e57140d1e0ad1dc10b9a73 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Mon, 1 Apr 2019 01:45:34 +0500 Subject: [PATCH 07/11] Added feature to add custom component over the walkthrough overlay --- src/components/ConnectedCopilotStep.js | 42 +++++-- src/components/CopilotModal.js | 160 ++++++++++++++----------- src/components/CopilotStep.js | 30 +++-- src/components/SvgMask.js | 120 ++++++++++++------- src/components/Tooltip.js | 60 +++++----- src/components/style.js | 62 +++++----- src/hocs/walkthroughable.js | 11 +- src/types.js | 16 ++- 8 files changed, 289 insertions(+), 212 deletions(-) diff --git a/src/components/ConnectedCopilotStep.js b/src/components/ConnectedCopilotStep.js index 4408a9b4..aa631b2d 100644 --- a/src/components/ConnectedCopilotStep.js +++ b/src/components/ConnectedCopilotStep.js @@ -1,12 +1,13 @@ // @flow -import React, { Component } from 'react'; +import React, { Component } from "react"; -import type { CopilotContext } from '../types'; +import type { CopilotContext, OverLayElement } from "../types"; type Props = { name: string, text: string, order: number, + overlayElement?: OverLayElement, active?: boolean, _copilot: CopilotContext, children: React$Element @@ -14,7 +15,7 @@ type Props = { class ConnectedCopilotStep extends Component { static defaultProps = { - active: true, + active: true }; componentDidMount() { @@ -48,6 +49,9 @@ class ConnectedCopilotStep extends Component { order: this.props.order, target: this, wrapper: this.wrapper, + ...(this.props.overlayElement && { + overlayElement: this.props.overlayElement + }) }); } @@ -56,10 +60,16 @@ class ConnectedCopilotStep extends Component { } measure() { - if (typeof __TEST__ !== 'undefined' && __TEST__) { // eslint-disable-line no-undef - return new Promise(resolve => resolve({ - x: 0, y: 0, width: 0, height: 0, - })); + if (typeof __TEST__ !== "undefined" && __TEST__) { + // eslint-disable-line no-undef + return new Promise(resolve => + resolve({ + x: 0, + y: 0, + width: 0, + height: 0 + }) + ); } return new Promise((resolve, reject) => { @@ -67,10 +77,14 @@ class ConnectedCopilotStep extends Component { // Wait until the wrapper element appears if (this.wrapper.measure) { this.wrapper.measure( - (ox, oy, width, height, x, y) => resolve({ - x, y, width, height, - }), - reject, + (ox, oy, width, height, x, y) => + resolve({ + x, + y, + width, + height + }), + reject ); } else { requestAnimationFrame(measure); @@ -83,8 +97,10 @@ class ConnectedCopilotStep extends Component { render() { const copilot = { - ref: (wrapper) => { this.wrapper = wrapper; }, - onLayout: () => { }, // Android hack + ref: wrapper => { + this.wrapper = wrapper; + }, + onLayout: () => {} // Android hack }; return React.cloneElement(this.props.children, { copilot }); diff --git a/src/components/CopilotModal.js b/src/components/CopilotModal.js index abff9c0c..b53e6c81 100644 --- a/src/components/CopilotModal.js +++ b/src/components/CopilotModal.js @@ -1,9 +1,22 @@ // @flow -import React, { Component } from 'react'; -import { Animated, Easing, View, NativeModules, Modal, StatusBar, Platform } from 'react-native'; -import Tooltip from './Tooltip'; -import StepNumber from './StepNumber'; -import styles, { MARGIN, ARROW_SIZE, STEP_NUMBER_DIAMETER, STEP_NUMBER_RADIUS } from './style'; +import React, { Component } from "react"; +import { + Animated, + Easing, + View, + NativeModules, + Modal, + StatusBar, + Platform +} from "react-native"; +import Tooltip from "./Tooltip"; +import StepNumber from "./StepNumber"; +import styles, { + MARGIN, + ARROW_SIZE, + STEP_NUMBER_DIAMETER, + STEP_NUMBER_RADIUS +} from "./style"; type Props = { stop: () => void, @@ -18,7 +31,7 @@ type Props = { animationDuration: ?number, tooltipComponent: ?React$Component, stepNumberComponent: ?React$Component, - overlay: 'svg' | 'view', + overlay: "svg" | "view", animated: boolean, androidStatusBarVisible: boolean, backdropColor: string @@ -31,8 +44,8 @@ type State = { notAnimated: boolean, layout: ?{ width: number, - height: number, - }, + height: number + } }; const noop = () => {}; @@ -44,11 +57,12 @@ class CopilotModal extends Component { tooltipComponent: Tooltip, stepNumberComponent: StepNumber, // If react-native-svg native module was avaialble, use svg as the default overlay component - overlay: typeof NativeModules.RNSVGSvgViewManager !== 'undefined' ? 'svg' : 'view', + overlay: + typeof NativeModules.RNSVGSvgViewManager !== "undefined" ? "svg" : "view", // If animated was not specified, rely on the default overlay type - animated: typeof NativeModules.RNSVGSvgViewManager !== 'undefined', + animated: typeof NativeModules.RNSVGSvgViewManager !== "undefined", androidStatusBarVisible: false, - backdropColor: 'rgba(0, 0, 0, 0.4)', + backdropColor: "rgba(0, 0, 0, 0.4)" }; state = { @@ -56,10 +70,10 @@ class CopilotModal extends Component { arrow: {}, animatedValues: { top: new Animated.Value(0), - stepNumberLeft: new Animated.Value(0), + stepNumberLeft: new Animated.Value(0) }, animated: false, - containerVisible: false, + containerVisible: false }; componentWillReceiveProps(nextProps: Props) { @@ -70,22 +84,27 @@ class CopilotModal extends Component { layout = { width: 0, - height: 0, - } + height: 0 + }; handleLayoutChange = ({ nativeEvent: { layout } }) => { this.layout = layout; - } + }; measure(): Promise { - if (typeof __TEST__ !== 'undefined' && __TEST__) { // eslint-disable-line no-undef - return new Promise(resolve => resolve({ - x: 0, y: 0, width: 0, height: 0, - })); + if (typeof __TEST__ !== "undefined" && __TEST__) { + // eslint-disable-line no-undef + return new Promise(resolve => + resolve({ + x: 0, + y: 0, + width: 0, + height: 0 + }) + ); } - - return new Promise((resolve) => { + return new Promise(resolve => { const setLayout = () => { if (this.layout.width !== 0) { resolve(this.layout); @@ -99,22 +118,22 @@ class CopilotModal extends Component { async _animateMove(obj = {}): void { const layout = await this.measure(); - if (!this.props.androidStatusBarVisible && Platform.OS === 'android') { + if (!this.props.androidStatusBarVisible && Platform.OS === "android") { obj.top -= StatusBar.currentHeight; // eslint-disable-line no-param-reassign } let stepNumberLeft = obj.left - STEP_NUMBER_RADIUS; if (stepNumberLeft < 0) { - stepNumberLeft = (obj.left + obj.width) - STEP_NUMBER_RADIUS; + stepNumberLeft = obj.left + obj.width - STEP_NUMBER_RADIUS; if (stepNumberLeft > layout.width - STEP_NUMBER_DIAMETER) { stepNumberLeft = layout.width - STEP_NUMBER_DIAMETER; } } const center = { - x: obj.left + (obj.width / 2), - y: obj.top + (obj.height / 2), + x: obj.left + obj.width / 2, + y: obj.top + obj.height / 2 }; const relativeToLeft = center.x; @@ -122,25 +141,28 @@ class CopilotModal extends Component { const relativeToBottom = Math.abs(center.y - layout.height); const relativeToRight = Math.abs(center.x - layout.width); - const verticalPosition = relativeToBottom > relativeToTop ? 'bottom' : 'top'; - const horizontalPosition = relativeToLeft > relativeToRight ? 'left' : 'right'; + const verticalPosition = + relativeToBottom > relativeToTop ? "bottom" : "top"; + const horizontalPosition = + relativeToLeft > relativeToRight ? "left" : "right"; const tooltip = {}; const arrow = {}; - if (verticalPosition === 'bottom') { + if (verticalPosition === "bottom") { tooltip.top = obj.top + obj.height + MARGIN; - arrow.borderBottomColor = '#fff'; - arrow.top = tooltip.top - (ARROW_SIZE * 2); + arrow.borderBottomColor = "#fff"; + arrow.top = tooltip.top - ARROW_SIZE * 2; } else { tooltip.bottom = layout.height - (obj.top - MARGIN); - arrow.borderTopColor = '#fff'; - arrow.bottom = tooltip.bottom - (ARROW_SIZE * 2); + arrow.borderTopColor = "#fff"; + arrow.bottom = tooltip.bottom - ARROW_SIZE * 2; } - if (horizontalPosition === 'left') { + if (horizontalPosition === "left") { tooltip.right = Math.max(layout.width - (obj.left + obj.width), 0); - tooltip.right = tooltip.right === 0 ? tooltip.right + MARGIN : tooltip.right; + tooltip.right = + tooltip.right === 0 ? tooltip.right + MARGIN : tooltip.right; tooltip.maxWidth = layout.width - tooltip.right - MARGIN; arrow.right = tooltip.right + MARGIN; } else { @@ -152,20 +174,21 @@ class CopilotModal extends Component { const animate = { top: obj.top, - stepNumberLeft, + stepNumberLeft }; if (this.state.animated) { - Animated - .parallel(Object.keys(animate) - .map(key => Animated.timing(this.state.animatedValues[key], { + Animated.parallel( + Object.keys(animate).map(key => + Animated.timing(this.state.animatedValues[key], { toValue: animate[key], duration: this.props.animationDuration, - easing: this.props.easing, - }))) - .start(); + easing: this.props.easing + }) + ) + ).start(); } else { - Object.keys(animate).forEach((key) => { + Object.keys(animate).forEach(key => { this.state.animatedValues[key].setValue(animate[key]); }); } @@ -177,23 +200,22 @@ class CopilotModal extends Component { animated: this.props.animated, size: { x: obj.width, - y: obj.height, + y: obj.height }, position: { x: Math.floor(Math.max(obj.left, 0)), - y: Math.floor(Math.max(obj.top, 0)), - }, + y: Math.floor(Math.max(obj.top, 0)) + } }); } animateMove(obj = {}): void { - return new Promise((resolve) => { - this.setState( - { containerVisible: true }, - () => requestAnimationFrame(async () => { + return new Promise(resolve => { + this.setState({ containerVisible: true }, () => + requestAnimationFrame(async () => { await this._animateMove(obj); resolve(); - }), + }) ); }); } @@ -202,28 +224,29 @@ class CopilotModal extends Component { this.setState({ animated: false, containerVisible: false, - layout: undefined, + layout: undefined }); } handleNext = () => { this.props.next(); - } + }; handlePrev = () => { this.props.prev(); - } + }; handleStop = () => { this.reset(); this.props.stop(); - } + }; renderMask() { /* eslint-disable global-require */ - const MaskComponent = this.props.overlay === 'svg' - ? require('./SvgMask').default - : require('./ViewMask').default; + const MaskComponent = + this.props.overlay === "svg" + ? require("./SvgMask").default + : require("./ViewMask").default; /* eslint-enable */ return ( { easing={this.props.easing} animationDuration={this.props.animationDuration} backdropColor={this.props.backdropColor} + step={this.props.currentStep} + handleNext={this.handleNext} + handlePrevious={this.handlePrev} /> ); } @@ -242,7 +268,7 @@ class CopilotModal extends Component { renderTooltip() { const { tooltipComponent: TooltipComponent, - stepNumberComponent: StepNumberComponent, + stepNumberComponent: StepNumberComponent } = this.props; return [ @@ -252,8 +278,11 @@ class CopilotModal extends Component { styles.stepNumberContainer, { left: this.state.animatedValues.stepNumberLeft, - top: Animated.add(this.state.animatedValues.top, -STEP_NUMBER_RADIUS), - }, + top: Animated.add( + this.state.animatedValues.top, + -STEP_NUMBER_RADIUS + ) + } ]} > { handlePrev={this.handlePrev} handleStop={this.handleStop} /> - , + ]; } @@ -287,12 +316,9 @@ class CopilotModal extends Component { visible={containerVisible} onRequestClose={noop} transparent - supportedOrientations={['portrait', 'landscape']} + supportedOrientations={["portrait", "landscape"]} > - + {contentVisible && this.renderMask()} {contentVisible && this.renderTooltip()} diff --git a/src/components/CopilotStep.js b/src/components/CopilotStep.js index 17cd9168..be5db307 100644 --- a/src/components/CopilotStep.js +++ b/src/components/CopilotStep.js @@ -1,37 +1,35 @@ // @flow -import { Component, createElement } from 'react'; -import PropTypes from 'prop-types'; +import { Component, createElement } from "react"; +import PropTypes from "prop-types"; -import ConnectedCopilotStep from './ConnectedCopilotStep'; +import ConnectedCopilotStep from "./ConnectedCopilotStep"; -import type { CopilotContext } from '../types'; +import type { CopilotContext, OverLayElement } from "../types"; type Props = { name: string, order: number, // eslint-disable-line react/no-unused-prop-types text: string, // eslint-disable-line react/no-unused-prop-types + overlayElement?: OverLayElement }; class CopilotStep extends Component { static contextTypes = { - _copilot: PropTypes.object, - } + _copilot: PropTypes.object + }; context: { - _copilot: CopilotContext, - } + _copilot: CopilotContext + }; render() { const currentStep = this.context._copilot.getCurrentStep(); - return createElement( - ConnectedCopilotStep, - { - ...this.props, - _copilot: this.context._copilot, - visible: currentStep && currentStep.name === this.props.name, - }, - ); + return createElement(ConnectedCopilotStep, { + ...this.props, + _copilot: this.context._copilot, + visible: currentStep && currentStep.name === this.props.name + }); } } diff --git a/src/components/SvgMask.js b/src/components/SvgMask.js index f97713c9..0a7219e8 100644 --- a/src/components/SvgMask.js +++ b/src/components/SvgMask.js @@ -1,19 +1,19 @@ // @flow -import React, { Component } from 'react'; -import { - View, - Animated, - Easing, - Dimensions, -} from 'react-native'; +import React, { Component } from "react"; +import { View, Animated, Easing, Dimensions, Text } from "react-native"; // import { Svg } from 'expo'; -import Svg from 'react-native-svg'; -import AnimatedSvgPath from './AnimatedPath'; +import Svg from "react-native-svg"; +import AnimatedSvgPath from "./AnimatedPath"; -import type { valueXY } from '../types'; +import type { valueXY, Step } from "../types"; -const windowDimensions = Dimensions.get('window'); -const path = (size, position, canvasSize): string => `M0,0H${canvasSize.x}V${canvasSize.y}H0V0ZM${position.x._value},${position.y._value}H${position.x._value + size.x._value}V${position.y._value + size.y._value}H${position.x._value}V${position.y._value}Z`; +const windowDimensions = Dimensions.get("window"); +const path = (size, position, canvasSize): string => + `M0,0H${canvasSize.x}V${canvasSize.y}H0V0ZM${position.x._value},${ + position.y._value + }H${position.x._value + size.x._value}V${position.y._value + size.y._value}H${ + position.x._value + }V${position.y._value}Z`; type Props = { size: valueXY, @@ -23,18 +23,21 @@ type Props = { animationDuration: number, animated: boolean, backdropColor: string, + step: Step, + handleNext: Function, + handlePrevious: Function }; type State = { size: Animated.ValueXY, position: Animated.ValueXY, - canvasSize: ?valueXY, + canvasSize: ?valueXY }; class SvgMask extends Component { static defaultProps = { animationDuration: 300, - easing: Easing.linear, + easing: Easing.linear }; constructor(props) { @@ -43,75 +46,106 @@ class SvgMask extends Component { this.state = { canvasSize: { x: windowDimensions.width, - y: windowDimensions.height, + y: windowDimensions.height }, size: new Animated.ValueXY(props.size), - position: new Animated.ValueXY(props.position), + position: new Animated.ValueXY(props.position) }; this.state.position.addListener(this.animationListener); } componentWillReceiveProps(nextProps) { - if (this.props.position !== nextProps.position || this.props.size !== nextProps.size) { + if ( + this.props.position !== nextProps.position || + this.props.size !== nextProps.size + ) { this.animate(nextProps.size, nextProps.position); } } animationListener = (): void => { - const d: string = path(this.state.size, this.state.position, this.state.canvasSize); + const d: string = path( + this.state.size, + this.state.position, + this.state.canvasSize + ); if (this.mask) { this.mask.setNativeProps({ d }); } }; - animate = (size: valueXY = this.props.size, position: valueXY = this.props.position): void => { + animate = ( + size: valueXY = this.props.size, + position: valueXY = this.props.position + ): void => { if (this.props.animated) { Animated.parallel([ Animated.timing(this.state.size, { toValue: size, duration: this.props.animationDuration, - easing: this.props.easing, + easing: this.props.easing }), Animated.timing(this.state.position, { toValue: position, duration: this.props.animationDuration, - easing: this.props.easing, - }), + easing: this.props.easing + }) ]).start(); } else { this.state.size.setValue(size); this.state.position.setValue(position); } - } + }; - handleLayout = ({ nativeEvent: { layout: { width, height } } }) => { + handleLayout = ({ + nativeEvent: { + layout: { width, height } + } + }) => { this.setState({ canvasSize: { x: width, - y: height, - }, + y: height + } }); - } + }; render() { return ( - - { - this.state.canvasSize - ? ( - - { this.mask = ref; }} - fill={this.props.backdropColor} - fillRule="evenodd" - strokeWidth={1} - d={path(this.state.size, this.state.position, this.state.canvasSize)} - /> - - ) - : null - } + + {this.state.canvasSize ? ( + + { + this.mask = ref; + }} + fill={this.props.backdropColor} + fillRule="evenodd" + strokeWidth={1} + d={path( + this.state.size, + this.state.position, + this.state.canvasSize + )} + /> + {this.props.step.overlayElement && + this.props.step.overlayElement( + this.props.position, + this.props.size, + this.props.handleNext, + this.props.handlePrevious + )} + + ) : null} ); } diff --git a/src/components/Tooltip.js b/src/components/Tooltip.js index 94c2a7fa..b8e4a63f 100644 --- a/src/components/Tooltip.js +++ b/src/components/Tooltip.js @@ -1,12 +1,12 @@ // @flow -import React from 'react'; -import { View, Text, TouchableOpacity } from 'react-native'; +import React from "react"; +import { View, Text, TouchableOpacity } from "react-native"; -import Button from './Button'; +import Button from "./Button"; -import styles from './style'; +import styles from "./style"; -import type { Step } from '../types'; +import type { Step } from "../types"; type Props = { isFirstStep: boolean, @@ -14,7 +14,7 @@ type Props = { handleNext: func, handlePrev: func, handleStop: func, - currentStep: Step, + currentStep: Step }; const Tooltip = ({ @@ -23,36 +23,34 @@ const Tooltip = ({ handleNext, handlePrev, handleStop, - currentStep, + currentStep }: Props) => ( - {currentStep.text} + + {currentStep.text} + - { - !isLastStep ? - - - - : null - } - { - !isFirstStep ? - - - - : null - } - { - !isLastStep ? - - - : - - - - } + {!isLastStep ? ( + + + + ) : null} + {!isFirstStep ? ( + + + + ) : null} + {!isLastStep ? ( + + + + ) : ( + + + + )} ); diff --git a/src/components/style.js b/src/components/style.js index ce9fdfcd..7ee18176 100644 --- a/src/components/style.js +++ b/src/components/style.js @@ -1,5 +1,5 @@ // @flow -import { StyleSheet } from 'react-native'; +import { StyleSheet } from "react-native"; export const STEP_NUMBER_RADIUS: number = 14; export const STEP_NUMBER_DIAMETER: number = STEP_NUMBER_RADIUS * 2; @@ -10,77 +10,75 @@ export const ARROW_SIZE: number = 6; export default StyleSheet.create({ container: { - position: 'absolute', + position: "absolute", left: 0, top: 0, right: 0, bottom: 0, - zIndex: ZINDEX, + zIndex: ZINDEX }, arrow: { - position: 'absolute', - borderColor: 'transparent', - borderWidth: ARROW_SIZE, + position: "absolute", + borderColor: "transparent", + borderWidth: ARROW_SIZE }, tooltip: { - position: 'absolute', + position: "absolute", paddingTop: 15, paddingHorizontal: 15, - backgroundColor: '#fff', + backgroundColor: "#fff", borderRadius: 3, - overflow: 'hidden', - }, - tooltipText: { - + overflow: "hidden" }, + tooltipText: {}, tooltipContainer: { - flex: 1, + flex: 1 }, stepNumberContainer: { - position: 'absolute', + position: "absolute", width: STEP_NUMBER_DIAMETER, height: STEP_NUMBER_DIAMETER, - overflow: 'hidden', - zIndex: ZINDEX + 1, + overflow: "hidden", + zIndex: ZINDEX + 1 }, stepNumber: { flex: 1, - alignItems: 'center', - justifyContent: 'center', + alignItems: "center", + justifyContent: "center", borderWidth: 2, borderRadius: STEP_NUMBER_RADIUS, - borderColor: '#FFFFFF', - backgroundColor: '#27ae60', + borderColor: "#FFFFFF", + backgroundColor: "#4285F4" }, stepNumberText: { fontSize: 10, - backgroundColor: 'transparent', - color: '#FFFFFF', + backgroundColor: "transparent", + color: "#FFFFFF" }, button: { - padding: 10, + padding: 10 }, buttonText: { - color: '#27ae60', + color: "#4285F4" }, bottomBar: { marginTop: 10, - flexDirection: 'row', - justifyContent: 'flex-end', + flexDirection: "row", + justifyContent: "flex-end" }, overlayRectangle: { - position: 'absolute', - backgroundColor: 'rgba(0,0,0,0.2)', + position: "absolute", + backgroundColor: "rgba(0,0,0,0.2)", left: 0, top: 0, bottom: 0, - right: 0, + right: 0 }, overlayContainer: { - position: 'absolute', + position: "absolute", left: 0, top: 0, bottom: 0, - right: 0, - }, + right: 0 + } }); diff --git a/src/hocs/walkthroughable.js b/src/hocs/walkthroughable.js index bd7a9f74..ffbf1ff6 100644 --- a/src/hocs/walkthroughable.js +++ b/src/hocs/walkthroughable.js @@ -1,13 +1,12 @@ // @flow -import React from 'react'; +import React from "react"; type Props = { - copilot: Object, + copilot: Object }; -const walkthroughable = - WrappedComponent => - ({ copilot, ...props }: Props) => - ; +const walkthroughable = WrappedComponent => ({ copilot, ...props }: Props) => ( + +); export default walkthroughable; diff --git a/src/types.js b/src/types.js index 48cd72e1..e83e1be8 100644 --- a/src/types.js +++ b/src/types.js @@ -5,15 +5,23 @@ export type Step = { visible: boolean, target: React$Element, wrapper: React$Element, + overlayElement?: OverLayElement }; +export type OverLayElement = ( + position: valueXY, + size: valueXY, + handleNext: Function, + handlePrevious: Function +) => React$Element; + export type CopilotContext = { - registerStep: (Step) => void, + registerStep: Step => void, unregisterStep: (name: string) => void, - getCurrentStep: () => Step, -} + getCurrentStep: () => Step +}; export type valueXY = { x: number, - y: number, + y: number }; From c0678c77a02f591f838b6fa9112e289fd8221be0 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Mon, 1 Apr 2019 02:10:59 +0500 Subject: [PATCH 08/11] Updated readme.md --- README.md | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ff60eab6..bc766387 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@

- React Native Copilot + React Native Copilot

@@ -115,6 +115,39 @@ copilot({ animated: true, // or false })(RootComponent); ``` +### Custom component over the overlay +This comes in handy when you want to show some kind of icon (like swipe gesture) on top of the overlay. The CopilotStep component accepts a prop called "overlayElement" that must return a react component wrapped inside a function. The position, size, nextHandle and previousHandle are automatically injected into this function. + +```js +import { React, { Component } } from "react"; +import { View } from "react-native"; + +class Example extends Component { + renderOverlayElement = (position, size, handleNext, handlePrevious) => { + //Displays this in the center of the overlay + return ( + + //... + + ); + } + render(){ + + //... + + } +} ### Custom tooltip component You can customize the tooltip by passing a component to the `copilot` HOC maker. If you are looking for an example tooltip component, take a look at [the default tooltip implementation](https://github.com/okgrow/react-native-copilot/blob/master/src/components/Tooltip.js). From 76c9211bb4ac4e3181eb28581c714f0792bed1f1 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Mon, 1 Apr 2019 02:12:21 +0500 Subject: [PATCH 09/11] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bc766387..f85a463d 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ class Example extends Component { } } - +``` ### Custom tooltip component You can customize the tooltip by passing a component to the `copilot` HOC maker. If you are looking for an example tooltip component, take a look at [the default tooltip implementation](https://github.com/okgrow/react-native-copilot/blob/master/src/components/Tooltip.js). From 80d8f069ca1945657fed2cd54d83bc726fee9c98 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Mon, 1 Apr 2019 02:26:33 +0500 Subject: [PATCH 10/11] Update README.md --- README.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/README.md b/README.md index f85a463d..bc9ba0b7 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,5 @@

React Native Copilot

- -

Step-by-step walkthrough for your react native app!

From 04cfb9bc2a6659e0d0e69f2242c8c0c03dd55ed6 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 3 Apr 2019 16:00:54 +0500 Subject: [PATCH 11/11] Changed tooltip button text --- src/components/Tooltip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Tooltip.js b/src/components/Tooltip.js index b8e4a63f..50fd7b5b 100644 --- a/src/components/Tooltip.js +++ b/src/components/Tooltip.js @@ -34,7 +34,7 @@ const Tooltip = ({ {!isLastStep ? ( - + ) : null} {!isFirstStep ? (