UNPKG

onboarder

Version:

Un package React simple et puissant pour créer des expériences d'onboarding interactives

358 lines (355 loc) 10.8 kB
// src/components/OnBoarder.tsx import { Slot } from "@radix-ui/react-slot"; import React, { createContext, useCallback as useCallback2, useContext, useEffect as useEffect2, useState as useState2 } from "react"; // src/hooks/usePosition.ts import { useCallback, useEffect, useState } from "react"; var usePosition = (step) => { const [position, setPosition] = useState({ top: 0, left: 0, transform: "translate(0, 0)" }); const updatePosition = useCallback(() => { if (step?.target) { const target = document.querySelector(step.target); const onboarderElement = document.querySelector( "[data-onboarder-step]" ); if (target && onboarderElement) { const rect = target.getBoundingClientRect(); const onboarderRect = onboarderElement.getBoundingClientRect(); const placement = step.placement || "bottom"; const offset = step.offset || 10; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; let top = rect.bottom + offset; let left = rect.left + rect.width / 2; let transform = "translate(-50%, 0)"; switch (placement) { case "top": top = rect.top - offset; left = rect.left + rect.width / 2; transform = "translate(-50%, -100%)"; break; case "right": top = rect.top + rect.height / 2; left = rect.right + offset; transform = "translate(0, -50%)"; break; case "left": top = rect.top + rect.height / 2; left = rect.left - offset; transform = "translate(-100%, -50%)"; break; case "center": top = rect.top + rect.height / 2; left = rect.left + rect.width / 2; transform = "translate(-50%, -50%)"; break; } const onboarderWidth = onboarderRect.width; const onboarderHeight = onboarderRect.height; const overflowRight = left + onboarderWidth - viewportWidth; const overflowLeft = -left; const overflowBottom = top + onboarderHeight - viewportHeight; const overflowTop = -top; if (overflowRight > 0) { left -= overflowRight; } if (overflowLeft > 0) { left += overflowLeft; } if (overflowBottom > 0) { top -= overflowBottom; } if (overflowTop > 0) { top += overflowTop; } setPosition({ top, left, transform }); } } }, [step]); useEffect(() => { let timeoutId; let resizeObserver; const initializePosition = () => { timeoutId = setTimeout(() => { updatePosition(); const onboarderElement = document.querySelector( "[data-onboarder-step]" ); if (onboarderElement) { resizeObserver = new ResizeObserver(() => { updatePosition(); }); resizeObserver.observe(onboarderElement); } }, 100); }; initializePosition(); const handleResize = () => { requestAnimationFrame(updatePosition); }; window.addEventListener("resize", handleResize); window.addEventListener("scroll", handleResize); return () => { clearTimeout(timeoutId); if (resizeObserver) { resizeObserver.disconnect(); } window.removeEventListener("resize", handleResize); window.removeEventListener("scroll", handleResize); }; }, [updatePosition]); return position; }; // src/components/OnBoarder.tsx var OnBoarderContext = createContext(null); var useOnBoarder = () => { const context = useContext(OnBoarderContext); if (!context) { throw new Error("useOnBoarder must be used within an OnBoarder.Root"); } return context; }; var Root = ({ children, onStepChange, onComplete }) => { const [state, setState] = useState2({ currentStepIndex: 0, isOpen: false, steps: [] }); useEffect2(() => { const steps = React.Children.toArray(children).filter( (child) => React.isValidElement(child) && child.type === Step ).map((stepChild) => { const title = React.Children.toArray(stepChild.props.children).find( (child) => React.isValidElement(child) && child.type === Title )?.props.children; const content = React.Children.toArray(stepChild.props.children).find( (child) => React.isValidElement(child) && child.type === Content )?.props.children; return { target: stepChild.props.selector, title: typeof title === "string" ? title : "", content: content || "", placement: stepChild.props.placement || "bottom", offset: stepChild.props.offset, highlight: stepChild.props.highlight, highlightColor: stepChild.props.highlightColor, highlightBorderRadius: stepChild.props.highlightBorderRadius, isModal: stepChild.props.isModal, beforeEnter: stepChild.props.beforeEnter, afterExit: stepChild.props.afterExit }; }); setState((prev2) => ({ ...prev2, steps, isOpen: true })); }, [children]); const currentStep = state.steps[state.currentStepIndex]; const position = usePosition(currentStep); const next = useCallback2(() => { setState((prev2) => { const nextIndex = prev2.currentStepIndex + 1; if (nextIndex >= prev2.steps.length) { onComplete?.(); return { ...prev2, isOpen: false }; } onStepChange?.(nextIndex); return { ...prev2, currentStepIndex: nextIndex }; }); }, [onStepChange, onComplete]); const prev = useCallback2(() => { setState((prev2) => { const prevIndex = Math.max(prev2.currentStepIndex - 1, 0); onStepChange?.(prevIndex); return { ...prev2, currentStepIndex: prevIndex }; }); }, [onStepChange]); const stop = useCallback2(() => { setState((prev2) => ({ ...prev2, isOpen: false })); }, []); const value = { ...state, totalSteps: state.steps.length, next, prev, stop, isFirstStep: state.currentStepIndex === 0, isLastStep: state.currentStepIndex === state.steps.length - 1, onStepChange, onComplete, currentStep, position }; return /* @__PURE__ */ React.createElement(OnBoarderContext.Provider, { value }, children); }; var Step = ({ children, selector, asChild = false, style, ...props }) => { const { currentStep, position, isOpen } = useOnBoarder(); const Component = asChild ? Slot : "div"; const isActive = currentStep?.target === selector; if (!isOpen || !isActive) return null; return /* @__PURE__ */ React.createElement( "div", { style: { position: "fixed", top: 0, left: 0, width: "100%", height: "100%", pointerEvents: "none", zIndex: 9999 } }, /* @__PURE__ */ React.createElement( Component, { "data-onboarder-step": true, "data-selector": selector, style: { position: "absolute", top: position.top, left: position.left, transform: position.transform, pointerEvents: "auto", ...style }, ...props }, children ) ); }; var Title = ({ children, asChild = false, style, ...props }) => { const Component = asChild ? Slot : "h3"; return /* @__PURE__ */ React.createElement(Component, { "data-onboarder-title": true, style, ...props }, children); }; var Content = ({ children, asChild = false, style, ...props }) => { const Component = asChild ? Slot : "div"; return /* @__PURE__ */ React.createElement(Component, { "data-onboarder-content": true, style, ...props }, children); }; var Controls = ({ children, asChild = false, style, ...props }) => { const { isOpen } = useOnBoarder(); const Component = asChild ? Slot : "div"; if (!isOpen) return null; return /* @__PURE__ */ React.createElement(Component, { "data-onboarder-controls": true, style, ...props }, children); }; var Prev = ({ children, asChild = false, style, ...props }) => { const { prev, isFirstStep } = useOnBoarder(); const Component = asChild ? Slot : "button"; return /* @__PURE__ */ React.createElement( Component, { onClick: prev, disabled: isFirstStep, "data-onboarder-prev": true, style, ...props }, children || "Pr\xE9c\xE9dent" ); }; var Next = ({ children, asChild = false, style, ...props }) => { const { next, isLastStep } = useOnBoarder(); const Component = asChild ? Slot : "button"; if (isLastStep) return null; return /* @__PURE__ */ React.createElement(Component, { onClick: next, "data-onboarder-next": true, style, ...props }, children || "Suivant"); }; var Skip = ({ children, asChild = false, style, ...props }) => { const { stop } = useOnBoarder(); const Component = asChild ? Slot : "button"; return /* @__PURE__ */ React.createElement(Component, { onClick: stop, "data-onboarder-skip": true, style, ...props }, children || "Passer"); }; var Finish = ({ children, asChild = false, style, ...props }) => { const { next, isLastStep } = useOnBoarder(); const Component = asChild ? Slot : "button"; if (!isLastStep) return null; return /* @__PURE__ */ React.createElement(Component, { onClick: next, "data-onboarder-finish": true, style, ...props }, children || "Terminer"); }; var OnBoarder = { Root, Step, Title, Content, Controls, Prev, Next, Skip, Finish }; // src/components/OnBoarderProvider.tsx import React2, { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useState as useState3 } from "react"; var OnBoarderProviderContext = createContext2(null); var useOnBoarderProvider = () => { const context = useContext2(OnBoarderProviderContext); if (!context) { throw new Error( "useOnBoarderProvider must be used within an OnBoarderProvider" ); } return context; }; var OnBoarderProvider = ({ children, onStepChange, onComplete }) => { const [isOpen, setIsOpen] = useState3(false); const start = useCallback3(() => { setIsOpen(true); }, []); const stop = useCallback3(() => { setIsOpen(false); }, []); return /* @__PURE__ */ React2.createElement(OnBoarderProviderContext.Provider, { value: { start, stop, isOpen } }, /* @__PURE__ */ React2.createElement(OnBoarder.Root, { onStepChange, onComplete }, children)); }; export { OnBoarder, OnBoarderProvider, useOnBoarder, useOnBoarderProvider, usePosition }; //# sourceMappingURL=index.js.map