onboarder
Version:
Un package React simple et puissant pour créer des expériences d'onboarding interactives
358 lines (355 loc) • 10.8 kB
JavaScript
// 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