@gfazioli/mantine-onboarding-tour
Version:
A Mantine 9 onboarding tour component with focus-reveal overlays, cutout highlights, step-by-step popover navigation, and compound components for guided user experiences.
225 lines (222 loc) • 7.28 kB
JavaScript
'use client';
import { useProps, useStyles, Popover, Transition, Overlay, useMatches } from '@mantine/core';
import React, { useId, useState, useRef, useEffect, useMemo, cloneElement } from 'react';
import { useUncontrolled, useInViewport, useMergedRef, useDidUpdate } from '@mantine/hooks';
import { useScrollIntoView } from '../hooks/use-scroll-into-view/use-scroll-into-view.mjs';
import { OnboardingTourFocusRevealGroup } from '../OnboardingTourFocusRevealGroup/OnboardingTourFocusRevealGroup.mjs';
import { useOnboardingTourFocusRevealGroupContext } from '../OnboardingTourFocusRevealGroup/OnboardingTourFocusRevealGroup.context.mjs';
import classes from './OnboardingTourFocusReveal.module.css.mjs';
function isResponsiveObject(value) {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return false;
}
const keys = Object.keys(value);
const breakpoints = ["base", "xs", "sm", "md", "lg", "xl"];
return keys.length > 0 && keys.every((k) => breakpoints.includes(k));
}
function useResponsiveProp(value, defaultValue) {
const matchesInput = isResponsiveObject(value) ? value : { base: value ?? defaultValue };
return useMatches(matchesInput);
}
const defaultProps = {
focusedMode: "none",
withReveal: true,
withOverlay: true,
focusedZIndex: 201,
overlayProps: {
blur: 2,
backgroundOpacity: 0.5,
color: "black"
},
transitionProps: { transition: "fade", duration: 150 },
popoverProps: {
position: { base: "bottom", sm: "left" },
withArrow: true,
arrowSize: 16,
arrowRadius: 4,
offset: -4,
radius: "md",
shadow: "xl",
middlewares: { shift: { padding: 20 }, flip: true }
}
};
function OnboardingTourFocusReveal(_props) {
const ctx = useOnboardingTourFocusRevealGroupContext();
const props = useProps(
"OnboardingTourFocusReveal",
{
...defaultProps,
...ctx ? ctx : {}
},
_props
);
props.overlayProps = { ...defaultProps.overlayProps, ..._props.overlayProps };
props.transitionProps = { ...defaultProps.transitionProps, ..._props.transitionProps };
props.popoverProps = { ...defaultProps.popoverProps, ..._props.popoverProps };
props.defaultFocused = ctx && _props.defaultFocused === void 0 ? true : _props.defaultFocused;
const {
focused,
defaultFocused,
disableTargetInteraction,
withOverlay,
overlayProps,
withReveal,
revealProps,
transitionProps,
focusedMode,
popoverContent,
popoverProps,
focusedZIndex,
scrollableRef,
onChange,
onFocus,
onBlur,
onRevealFinish,
classNames,
style,
styles,
unstyled,
vars,
children,
className
} = props;
const getStyles = useStyles({
name: "OnboardingTourFocusReveal",
props,
classes,
className,
style,
classNames,
styles,
unstyled,
vars
});
const uuid = useId();
const [_focused, setFocused, controlled] = useUncontrolled({
value: focused,
defaultValue: defaultFocused,
finalValue: false,
onChange
});
const blur = () => {
setFocused(false);
_focused && onBlur?.();
};
const focus = () => {
setFocused(true);
!_focused && onFocus?.();
};
const [realFocused, setRealFocused] = useState(false);
const fallbackRef = useRef(null);
const { scrollIntoView, targetRef } = useScrollIntoView({
...revealProps,
onScrollFinish: onRevealFinish,
scrollableRef: scrollableRef || fallbackRef
});
const { ref: inViewportRef, inViewport } = useInViewport();
const mergedRef = useMergedRef(inViewportRef, targetRef);
const resolvedPosition = useResponsiveProp(popoverProps?.position, "left");
const resolvedOffset = useResponsiveProp(popoverProps?.offset, -4);
const resolvedWidth = useResponsiveProp(popoverProps?.width, "max-content");
const resolvedArrowSize = useResponsiveProp(popoverProps?.arrowSize, 16);
useEffect(() => {
ctx?.setMeInViewport?.(uuid, inViewport);
if (targetRef.current) {
if (_focused || defaultFocused) {
if (withReveal) {
scrollIntoView({ alignment: "center" });
}
}
if (_focused && inViewport && !realFocused) {
setRealFocused(true);
setFocused(_focused && inViewport);
} else if (!_focused && realFocused) {
setRealFocused(false);
}
}
}, [_focused, withReveal, inViewport]);
useDidUpdate(() => {
ctx?.setMeInViewport?.(uuid, inViewport);
if (_focused && !inViewport) {
blur();
setRealFocused(false);
ctx?.setRequestOverlay?.(false);
}
if (inViewport && !controlled && defaultFocused === true) {
focus();
setRealFocused(true);
ctx?.setRequestOverlay?.(true);
}
}, [inViewport, defaultFocused]);
const clonedChildren = useMemo(() => {
const child = children;
const newProps = {
...realFocused && { "data-onboarding-tour-focus-reveal-focused": true },
"data-onboarding-tour-focus-reveal-mode": focusedMode,
ref: mergedRef,
className: [realFocused ? classes.focused : "", className, child.props.className].filter(Boolean).join(" "),
style: {
position: "relative",
...child.props.style,
zIndex: realFocused ? focusedZIndex ?? 201 : 0,
pointerEvents: realFocused && disableTargetInteraction ? "none" : void 0
}
};
newProps["data-popover-dropdown"] = !!popoverContent;
const {
position: _pos,
offset: _off,
width: _w,
arrowSize: _as,
...restPopoverProps
} = popoverProps || {};
const finalPopoverProps = {
...restPopoverProps,
position: resolvedPosition,
offset: resolvedOffset,
width: resolvedWidth,
arrowSize: resolvedArrowSize
};
return /* @__PURE__ */ React.createElement(Popover, { opened: realFocused && !!popoverContent, ...finalPopoverProps }, /* @__PURE__ */ React.createElement(Popover.Target, null, cloneElement(child, newProps)), /* @__PURE__ */ React.createElement(Popover.Dropdown, null, popoverContent));
}, [
children,
realFocused,
_focused,
inViewport,
defaultFocused,
resolvedPosition,
resolvedOffset,
resolvedWidth,
resolvedArrowSize,
focusedZIndex,
popoverProps
]);
if (ctx) {
return clonedChildren;
}
return /* @__PURE__ */ React.createElement(React.Fragment, null, withOverlay && /* @__PURE__ */ React.createElement(
Transition,
{
transition: transitionProps?.transition,
mounted: _focused && inViewport,
duration: transitionProps?.duration,
exitDuration: transitionProps?.exitDuration
},
(transitionStyles) => /* @__PURE__ */ React.createElement(
Overlay,
{
"data-onboarding-tour-focus-reveal-overlay": true,
...overlayProps,
...getStyles("overlay", {
className: overlayProps?.className,
style: [transitionStyles, overlayProps?.style]
})
}
)
), clonedChildren);
}
OnboardingTourFocusReveal.classes = classes;
OnboardingTourFocusReveal.displayName = "OnboardingTourFocusReveal";
OnboardingTourFocusReveal.Group = OnboardingTourFocusRevealGroup;
export { OnboardingTourFocusReveal, defaultProps };
//# sourceMappingURL=OnboardingTourFocusReveal.mjs.map