diff --git a/README.md b/README.md index b4530f5c..29edcee1 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,28 @@ -

React Native Copilot

+

@mr-mahabeer/react-native-copilot

+ +

+ This is a maintained fork of react-native-copilot with bug fixes and improvements. +

+ +## Why this fork? + +- **Fixes pending issues** — Addresses open problems from the upstream project where practical. +- **Active maintenance** — Regular updates, reviews, and releases for dependents who need a supported line. + +## Credits + +This project is based on [react-native-copilot](https://github.com/mohebifar/react-native-copilot) by Mohamad Mohebifar.

Build Status - - NPM Version + + NPM Version - - NPM Downloads + + NPM Downloads

@@ -31,11 +44,11 @@ ## Installation ``` -yarn add react-native-copilot +yarn add @mr-mahabeer/react-native-copilot # or with npm: -npm install --save react-native-copilot +npm install --save @mr-mahabeer/react-native-copilot ``` **Optional**: If you want to have the smooth SVG animation, you should install and link [`react-native-svg`](https://github.com/software-mansion/react-native-svg). @@ -45,7 +58,7 @@ npm install --save react-native-copilot Wrap the portion of your app that you want to use copilot with inside ``: ```js -import { CopilotProvider } from "react-native-copilot"; +import { CopilotProvider } from "@mr-mahabeer/react-native-copilot"; const AppWithCopilot = () => { return ( @@ -65,7 +78,7 @@ import { CopilotProvider, CopilotStep, walkthroughable, -} from "react-native-copilot"; +} from "@mr-mahabeer/react-native-copilot"; const CopilotText = walkthroughable(Text); @@ -212,6 +225,34 @@ You can customize the tooltip's arrow color: ``` +### Additional `CopilotProvider` options + +These optional props are available on ``: + +- **`stopOnOutsideClick`** (`boolean`, default `false`) — When `true`, tapping the dimmed backdrop (outside the highlighted step) ends the tour, same as skipping. + +- **`labelButtonStyle`** — `StyleProp` applied to the default tooltip’s action labels (Skip, Previous, Next, Finish). If you use a custom `tooltipComponent`, it receives `labelButtonStyle` (and `labels`) as props—see [`TooltipProps`](https://github.com/mahabeer-dev/react-native-copilot/blob/master/src/types.ts). + +- **`shouldShowStepNumber`** (`boolean`, default `true`) — Set to `false` to hide the circular step-number badge. + +- **`arrowSize`** (`number`, default `6`) — Size of the tooltip pointer (arrow). Use `0` to hide the arrow. + +- **`margin`** (`number`, default `13`) — Minimum spacing between the highlighted area and the tooltip when the library positions the tooltip on screen. + +Example combining several options: + +```js + + + +``` + ### Custom overlay color You can customize the mask color - default is `rgba(0, 0, 0, 0.4)`, by passing a color string to the `CopilotProvider` component. @@ -284,7 +325,7 @@ const customSvgPath = (args) => { The components wrapped inside `CopilotStep`, will receive a `copilot` prop with a mutable `ref` and `onLayou` which the outermost rendered element of the component or the element that you want the tooltip be shown around, must extend. ```js -import { CopilotStep } from "react-native-copilot"; +import { CopilotStep } from "@mr-mahabeer/react-native-copilot"; const CustomComponent = ({ copilot }) => ( @@ -368,7 +409,7 @@ List of available events is: **Example:** ```js -import { useCopilot } from "react-native-copilot"; +import { useCopilot } from "@mr-mahabeer/react-native-copilot"; const HomeScreen = () => { const { copilotEvents } = useCopilot(); diff --git a/package.json b/package.json index 46d32076..05b1e47d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "react-native-copilot", - "version": "3.3.3", - "description": "Make an interactive step by step tour guide for you react-native app", + "name": "@mr-mahabeer/react-native-copilot", + "version": "3.3.9", + "description": "Maintained fork of react-native-copilot with bug fixes and improvements", "main": "dist/index.js", "types": "dist/index.d.ts", "module": "dist/index.mjs", @@ -12,11 +12,12 @@ "lint": "eslint src/ --ext .ts,.tsx", "test": "jest", "changeset": "changeset", - "release": "changeset publish" + "release": "changeset publish", + "build:pack": "yarn build && npm pack" }, "repository": { "type": "git", - "url": "git+https://github.com/mohebifar/react-native-copilot.git" + "url": "git+https://github.com/mahabeer-dev/react-native-copilot.git" }, "keywords": [ "react-native", @@ -29,12 +30,12 @@ "files": [ "dist" ], - "author": "Mohamad Mohebifar ", + "author": "Mr Mahabeer", "license": "MIT", "bugs": { - "url": "https://github.com/mohebifar/react-native-copilot/issues" + "url": "https://github.com/mahabeer-dev/react-native-copilot/issues" }, - "homepage": "https://github.com/mohebifar/react-native-copilot#readme", + "homepage": "https://github.com/mahabeer-dev/react-native-copilot#readme", "devDependencies": { "@testing-library/jest-native": "^5.4.3", "@testing-library/react-native": "^12.4.3", diff --git a/src/components/CopilotModal.tsx b/src/components/CopilotModal.tsx index 772ab05c..e6b358fb 100644 --- a/src/components/CopilotModal.tsx +++ b/src/components/CopilotModal.tsx @@ -52,7 +52,9 @@ export const CopilotModal = forwardRef( animationDuration = 400, tooltipComponent: TooltipComponent = Tooltip, tooltipStyle = {}, + labelButtonStyle = {}, stepNumberComponent: StepNumberComponent = StepNumber, + shouldShowStepNumber = true, overlay = typeof NativeModules.RNSVGSvgViewManager !== "undefined" ? "svg" : "view", @@ -73,7 +75,7 @@ export const CopilotModal = forwardRef( }, ref, ) { - const { stop, currentStep, visible } = useCopilot(); + const { stop, currentStep, visible, steps } = useCopilot(); const [tooltipStyles, setTooltipStyles] = useState({}); const [arrowStyles, setArrowStyles] = useState({}); const [animatedValues] = useState({ @@ -153,7 +155,12 @@ export const CopilotModal = forwardRef( relativeToLeft > relativeToRight ? "left" : "right"; const tooltip: ViewStyle = {}; - const arrow: ViewStyle = {}; + const arrow: ViewStyle = { + width: 0, + height: 0, + backgroundColor: "transparent", + borderWidth: arrowSize, + }; arrow.position = "absolute"; @@ -330,6 +337,9 @@ export const CopilotModal = forwardRef( animationDuration={animationDuration} backdropColor={backdropColor} svgMaskPath={svgMaskPath} + borderRadius={ + currentStep ? steps[currentStep?.name]?.maskRadius ?? 0 : 0 + } onClick={handleMaskClick} currentStep={currentStep} /> @@ -342,18 +352,21 @@ export const CopilotModal = forwardRef( } return ( <> - - - + {shouldShowStepNumber && ( + + + + )} + {!!arrowSize && ( )} @@ -361,7 +374,10 @@ export const CopilotModal = forwardRef( key="tooltip" style={[styles.tooltip, tooltipStyles, tooltipStyle]} > - + ); diff --git a/src/components/CopilotStep.tsx b/src/components/CopilotStep.tsx index e0cdb588..6dfb4bbf 100644 --- a/src/components/CopilotStep.tsx +++ b/src/components/CopilotStep.tsx @@ -9,6 +9,8 @@ interface Props { text: string; children: React.ReactElement; active?: boolean; + deleted?: boolean; + maskRadius?: number; } export const CopilotStep = ({ @@ -17,7 +19,10 @@ export const CopilotStep = ({ text, children, active = true, + deleted = false, + maskRadius = 0, }: Props) => { + const shouldRegister = active && !deleted; const registeredName = useRef(null); const { registerStep, unregisterStep } = useCopilot(); const wrapperRef = React.useRef(null); @@ -50,7 +55,7 @@ export const CopilotStep = ({ }; useEffect(() => { - if (active) { + if (shouldRegister) { if (registeredName.current && registeredName.current !== name) { unregisterStep(registeredName.current); } @@ -61,27 +66,31 @@ export const CopilotStep = ({ measure, wrapperRef, visible: true, + maskRadius, }); registeredName.current = name; + } else if (registeredName.current) { + unregisterStep(registeredName.current); + registeredName.current = null; } - }, [name, order, text, registerStep, unregisterStep, active]); + }, [name, order, text, registerStep, unregisterStep, shouldRegister, maskRadius]); useEffect(() => { - if (active) { + if (shouldRegister) { return () => { if (registeredName.current) { unregisterStep(registeredName.current); } }; } - }, [name, unregisterStep, active]); + }, [name, unregisterStep, shouldRegister]); const copilotProps = useMemo( () => ({ ref: wrapperRef, onLayout: () => {}, // Android hack }), - [] + [], ); return React.cloneElement(children, { copilot: copilotProps }); diff --git a/src/components/SvgMask.tsx b/src/components/SvgMask.tsx index 7b9359e8..0d066bbf 100644 --- a/src/components/SvgMask.tsx +++ b/src/components/SvgMask.tsx @@ -17,15 +17,27 @@ const defaultSvgPath: SvgMaskPathFunction = ({ size, position, canvasSize, + borderRadius: br = 0, }): string => { - const positionX = (position.x as any)._value as number; - const positionY = (position.y as any)._value as number; - const sizeX = (size.x as any)._value as number; - const sizeY = (size.y as any)._value as number; + const x = (position.x as any)._value as number; + const y = (position.y as any)._value as number; + const w = (size.x as any)._value as number; + const h = (size.y as any)._value as number; - return `M0,0H${canvasSize.x}V${canvasSize.y}H0V0ZM${positionX},${positionY}H${ - positionX + sizeX - }V${positionY + sizeY}H${positionX}V${positionY}Z`; + const r = Math.max(0, Math.min(br, w / 2, h / 2)); + + if (r <= 0) { + return `M0,0H${canvasSize.x}V${canvasSize.y}H0V0ZM${x},${y}H${x + w}V${y + h}H${x}V${y}Z`; + } + + return [ + `M0,0H${canvasSize.x}V${canvasSize.y}H0V0Z`, + `M${x + r},${y}`, + `H${x + w - r}A${r},${r} 0 0 1 ${x + w},${y + r}`, + `V${y + h - r}A${r},${r} 0 0 1 ${x + w - r},${y + h}`, + `H${x + r}A${r},${r} 0 0 1 ${x},${y + h - r}`, + `V${y + r}A${r},${r} 0 0 1 ${x + r},${y}Z`, + ].join(""); }; export const SvgMask = ({ @@ -37,6 +49,7 @@ export const SvgMask = ({ animated, backdropColor, svgMaskPath = defaultSvgPath, + borderRadius, onClick, currentStep, }: MaskProps) => { @@ -58,12 +71,13 @@ export const SvgMask = ({ position: positionValue, canvasSize, step: currentStep, + borderRadius, }); if (maskRef.current) { maskRef.current.setNativeProps({ d }); } - }, [canvasSize, currentStep, svgMaskPath, positionValue, sizeValue]); + }, [canvasSize, currentStep, svgMaskPath, positionValue, sizeValue, borderRadius]); const animate = useCallback( (toSize: ValueXY = size, toPosition: ValueXY = position) => { @@ -140,6 +154,7 @@ export const SvgMask = ({ position: positionValue, canvasSize, step: currentStep, + borderRadius, })} /> diff --git a/src/components/default-ui/Tooltip.tsx b/src/components/default-ui/Tooltip.tsx index a624f712..85120a77 100644 --- a/src/components/default-ui/Tooltip.tsx +++ b/src/components/default-ui/Tooltip.tsx @@ -8,7 +8,7 @@ import { styles } from "../style"; import type { TooltipProps } from "../../types"; import { useCopilot } from "../../contexts/CopilotProvider"; -export const Tooltip = ({ labels }: TooltipProps) => { +export const Tooltip = ({ labels, labelButtonStyle = {} }: TooltipProps) => { const { goToNext, goToPrev, stop, currentStep, isFirstStep, isLastStep } = useCopilot(); @@ -33,21 +33,21 @@ export const Tooltip = ({ labels }: TooltipProps) => { {!isLastStep ? ( - + ) : null} {!isFirstStep ? ( - + ) : null} {!isLastStep ? ( - + ) : ( - + )} diff --git a/src/components/style.ts b/src/components/style.ts index 13ced248..20b11957 100644 --- a/src/components/style.ts +++ b/src/components/style.ts @@ -18,7 +18,9 @@ export const styles = StyleSheet.create({ }, arrow: { position: "absolute", - borderWidth: ARROW_SIZE, + width: 0, + height: 0, + backgroundColor: "transparent", }, tooltip: { position: "absolute", diff --git a/src/contexts/CopilotProvider.tsx b/src/contexts/CopilotProvider.tsx index 331c2735..57d2b691 100644 --- a/src/contexts/CopilotProvider.tsx +++ b/src/contexts/CopilotProvider.tsx @@ -16,7 +16,7 @@ import { import { OFFSET_WIDTH } from "../components/style"; import { useStateWithAwait } from "../hooks/useStateWithAwait"; import { useStepsMap } from "../hooks/useStepsMap"; -import { type CopilotOptions, type Step } from "../types"; +import type { StepsMap, CopilotOptions, Step } from "../types"; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions type Events = { @@ -31,7 +31,7 @@ interface CopilotContextType { currentStep: Step | undefined; start: ( fromStep?: string, - suppliedScrollView?: ScrollView | null + suppliedScrollView?: ScrollView | null, ) => Promise; stop: () => Promise; goToNext: () => Promise; @@ -43,6 +43,7 @@ interface CopilotContextType { isLastStep: boolean; currentStepNumber: number; totalStepsNumber: number; + steps: StepsMap; } /* @@ -96,7 +97,7 @@ export const CopilotProvider = ({ y: size.y - OFFSET_WIDTH / 2 + verticalOffset, }); }, - [verticalOffset] + [verticalOffset], ); const setCurrentStep = useCallback( @@ -112,7 +113,7 @@ export const CopilotProvider = ({ (_x, y, _w, h) => { const yOffset = y > 0 ? y - h / 2 : 0; scrollView.scrollTo({ y: yOffset, animated: false }); - } + }, ); } } @@ -123,10 +124,10 @@ export const CopilotProvider = ({ void moveModalToStep(step); } }, - scrollView != null ? 100 : 0 + scrollView != null ? 100 : 0, ); }, - [copilotEvents, moveModalToStep, scrollView, setCurrentStepState] + [copilotEvents, moveModalToStep, scrollView, setCurrentStepState], ); const start = useCallback( @@ -163,7 +164,7 @@ export const CopilotProvider = ({ setCurrentStep, setVisibility, steps, - ] + ], ); const stop = useCallback(async () => { @@ -179,7 +180,7 @@ export const CopilotProvider = ({ async (n: number) => { await setCurrentStep(getNthStep(n)); }, - [getNthStep, setCurrentStep] + [getNthStep, setCurrentStep], ); const prev = useCallback(async () => { @@ -202,6 +203,7 @@ export const CopilotProvider = ({ isLastStep, currentStepNumber, totalStepsNumber, + steps, }), [ registerStep, @@ -218,16 +220,14 @@ export const CopilotProvider = ({ isLastStep, currentStepNumber, totalStepsNumber, - ] + steps, + ], ); return ( <> - + {children} diff --git a/src/types.ts b/src/types.ts index 9ba8c31f..d8b33ea8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,8 @@ import type { Animated, LayoutRectangle, NativeMethods, + StyleProp, + TextStyle, ViewStyle, } from "react-native"; @@ -14,6 +16,7 @@ export interface Step { wrapperRef: React.RefObject; measure: () => Promise; text: string; + maskRadius?: number; } export interface CopilotContext { @@ -32,6 +35,7 @@ export type SvgMaskPathFunction = (args: { position: Animated.ValueXY; canvasSize: ValueXY; step: Step; + borderRadius?: number; }) => string; export type StepsMap = Record; @@ -44,6 +48,7 @@ export type Labels = Partial< export interface TooltipProps { labels: Labels; + labelButtonStyle?: StyleProp; } export interface MaskProps { @@ -55,6 +60,7 @@ export interface MaskProps { animated: boolean; backdropColor: string; svgMaskPath?: SvgMaskPathFunction; + borderRadius: number; layout: { width: number; height: number; @@ -69,15 +75,17 @@ export interface CopilotOptions { animationDuration?: number; tooltipComponent?: React.ComponentType; tooltipStyle?: ViewStyle; - stepNumberComponent?: React.ComponentType; + stepNumberComponent?: React.ComponentType; animated?: boolean; labels?: Labels; androidStatusBarVisible?: boolean; svgMaskPath?: SvgMaskPathFunction; verticalOffset?: number; arrowColor?: string; - arrowSize?: number - margin?: number + arrowSize?: number; + margin?: number; stopOnOutsideClick?: boolean; backdropColor?: string; + labelButtonStyle?: StyleProp; + shouldShowStepNumber?: boolean; } diff --git a/tsup.config.ts b/tsup.config.ts index 5933a4f7..58c5ff07 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -12,11 +12,11 @@ export default defineConfig({ async onSuccess() { if (process.env.NODE_ENV === "development") { const exampleOutputPath = path.resolve( - "./example/node_modules/react-native-copilot" + "./example/node_modules/react-native-copilot", ); const exampleOutputNodeModulesPath = path.resolve( exampleOutputPath, - "node_modules" + "node_modules", ); await Promise.all( @@ -24,7 +24,7 @@ export default defineConfig({ const outputPath = path.resolve(exampleOutputPath, file); console.log("Copying file: ", file, "to ->", outputPath); await fs.copyFile(file, outputPath); - }) + }), ); await fs.rm(exampleOutputNodeModulesPath, { recursive: true,