Greenlens/context/CoachMarksContext.tsx

78 lines
2.0 KiB
TypeScript

import React, { createContext, useContext, useState, useCallback, useRef } from 'react';
import { LayoutRectangle } from 'react-native';
export interface CoachStep {
elementKey: string;
title: string;
description: string;
tooltipSide: 'above' | 'below' | 'left' | 'right';
}
interface CoachMarksState {
isActive: boolean;
currentStep: number;
steps: CoachStep[];
layouts: Record<string, LayoutRectangle>;
registerLayout: (key: string, layout: LayoutRectangle) => void;
startTour: (steps: CoachStep[]) => void;
next: () => void;
skip: () => void;
}
const CoachMarksContext = createContext<CoachMarksState | null>(null);
export const useCoachMarks = () => {
const ctx = useContext(CoachMarksContext);
if (!ctx) throw new Error('useCoachMarks must be within CoachMarksProvider');
return ctx;
};
export const CoachMarksProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isActive, setIsActive] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [steps, setSteps] = useState<CoachStep[]>([]);
const layouts = useRef<Record<string, LayoutRectangle>>({});
const [, forceRender] = useState(0);
const registerLayout = useCallback((key: string, layout: LayoutRectangle) => {
layouts.current[key] = layout;
forceRender(n => n + 1);
}, []);
const startTour = useCallback((newSteps: CoachStep[]) => {
setSteps(newSteps);
setCurrentStep(0);
setIsActive(true);
}, []);
const next = useCallback(() => {
setCurrentStep(prev => {
if (prev + 1 >= steps.length) {
setIsActive(false);
return 0;
}
return prev + 1;
});
}, [steps.length]);
const skip = useCallback(() => {
setIsActive(false);
setCurrentStep(0);
}, []);
return (
<CoachMarksContext.Provider value={{
isActive,
currentStep,
steps,
layouts: layouts.current,
registerLayout,
startTour,
next,
skip,
}}>
{children}
</CoachMarksContext.Provider>
);
};