manuthecoder-react-native-spotlight-tour
Version:
React Native library to implement a highly customizable app tour feature with an awesome spotlight effect
143 lines • 7.34 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { arrow, flip, offset, shift, useFloating, } from "@floating-ui/react-native";
import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, } from "react";
import { Animated, Modal, Platform, View, } from "react-native";
import { Defs, Mask, Rect, Svg } from "react-native-svg";
import { vh, vw } from "../../../helpers/responsive";
import { SpotlightTourContext, } from "../../SpotlightTour.context";
import { Css, DEFAULT_ARROW, arrowCss } from "./TourOverlay.styles";
import { CircleShape } from "./shapes/CircleShape.component";
import { RectShape } from "./shapes/RectShape.component";
export const TourOverlay = forwardRef((props, ref) => {
const { backdropOpacity, color, current, motion, nativeDriver, onBackdropPress, shape, spot, tourStep, ...tooltipProps } = props;
const { goTo, next, pause, previous, resume, start, steps, stop } = useContext(SpotlightTourContext);
const arrowRef = useRef(null);
const floating = useMemo(() => ({
arrow: tourStep.arrow ?? tooltipProps.arrow,
flip: tourStep.flip ?? tooltipProps.flip,
offset: tourStep.offset ?? tooltipProps.offset,
placement: tourStep.placement ?? tooltipProps.placement,
shift: tourStep.shift ?? tooltipProps.shift,
}), [tooltipProps, tourStep.arrow, tourStep.flip, tourStep.offset, tourStep.placement, tourStep.shift]);
const floatingOptions = useMemo(() => {
return makeFloatingOptions(arrowRef, floating);
}, [floating]);
const { floatingStyles, middlewareData, placement, refs } = useFloating(floatingOptions);
const tooltipOpacity = useRef(new Animated.Value(0));
const stepMotion = useMemo(() => {
return tourStep.motion ?? motion;
}, [tourStep, motion]);
const shapeOptions = useMemo(() => {
const options = tourStep.shape ?? shape;
const padding = 16;
return typeof options !== "string"
? { padding, type: "circle", ...options }
: { padding, type: options };
}, [tourStep, shape]);
const useNativeDriver = useMemo(() => {
const driverConfig = typeof nativeDriver === "boolean"
? { android: nativeDriver, ios: nativeDriver, web: nativeDriver }
: nativeDriver;
return Platform.select({
android: driverConfig.android,
default: false,
ios: driverConfig.ios,
web: false,
});
}, [nativeDriver]);
const ShapeMask = useMemo(() => {
switch (shapeOptions.type) {
case "circle": return CircleShape;
case "rectangle": return RectShape;
}
}, [shapeOptions]);
const handleBackdropPress = useCallback(() => {
const handler = tourStep.onBackdropPress ?? onBackdropPress;
if (handler !== undefined && current !== undefined) {
switch (handler) {
case "continue":
return next();
case "stop":
return stop();
default:
return handler({ current, goTo, next, pause, previous, resume, start, status: "running", stop });
}
}
}, [tourStep, onBackdropPress, current, goTo, next, previous, start, stop, pause, resume]);
useEffect(() => {
const { height, width } = spot;
if ([height, width].every(value => value > 0)) {
Animated.timing(tooltipOpacity.current, {
delay: 400,
duration: 400,
toValue: 1,
useNativeDriver,
})
.start();
}
}, [spot, useNativeDriver]);
useImperativeHandle(ref, () => ({
hideTooltip: () => {
return new Promise(resolve => {
if (current !== undefined) {
Animated.timing(tooltipOpacity.current, {
duration: 400,
toValue: 0,
useNativeDriver,
})
.start(resolve);
}
else {
resolve({ finished: true });
}
});
},
}), [current, useNativeDriver]);
return (_jsx(Modal, { animationType: "fade", presentationStyle: "overFullScreen", transparent: true, visible: current !== undefined, children: _jsxs(View, { testID: "Overlay View", style: Css.overlayView, children: [_jsxs(Svg, { testID: "Spot Svg", height: "100%", width: "100%", viewBox: `0 0 ${vw(100)} ${vh(100)}`, onPress: handleBackdropPress, shouldRasterizeIOS: true, renderToHardwareTextureAndroid: true, children: [_jsx(Defs, { children: _jsxs(Mask, { id: "mask", x: 0, y: 0, height: "100%", width: "100%", children: [_jsx(Rect, { height: "100%", width: "100%", fill: "#fff" }), _jsx(ShapeMask, { spot: spot, setReference: refs.setReference, motion: stepMotion, padding: shapeOptions.padding, useNativeDriver: useNativeDriver })] }) }), _jsx(Rect, { height: "100%", width: "100%", fill: color, mask: "url(#mask)", opacity: backdropOpacity })] }), current !== undefined && (_jsxs(Animated.View, { ref: refs.setFloating, testID: "Tooltip View", style: { ...floatingStyles, opacity: tooltipOpacity.current }, children: [_jsx(tourStep.render, { current: current, isFirst: current === 0, isLast: current === steps.length - 1, next: next, previous: previous, stop: stop, pause: pause, resume: resume, goTo: goTo }), floating.arrow !== false && (_jsx(View, { style: [
Css.tooltipArrow,
arrowCss({
arrow: typeof floating.arrow !== "boolean"
? floating.arrow
: undefined,
data: middlewareData.arrow,
placement,
}),
], ref: arrowRef }))] }))] }) }));
});
function makeFloatingOptions(arrowRef, props) {
const arrowOption = typeof props?.arrow === "boolean"
? DEFAULT_ARROW
: props?.arrow;
const { size } = typeof arrowOption === "number"
? { ...DEFAULT_ARROW, size: arrowOption }
: { ...DEFAULT_ARROW, ...arrowOption };
const baseOffset = props?.offset || 4;
const offsetValue = props?.arrow !== false
? (Math.sqrt(2 * size ** 2) / 2) + baseOffset
: baseOffset;
const arrowMw = props?.arrow !== false
? arrow({ element: arrowRef })
: undefined;
const flipMw = flipMiddleware(props?.flip);
const offsetMw = props?.offset !== 0
? offset(offsetValue)
: undefined;
const shiftMw = shiftMiddleware(props?.shift);
return {
middleware: [flipMw, offsetMw, shiftMw, arrowMw].filter(Boolean),
placement: props?.placement,
};
}
function flipMiddleware(flipProps) {
if (flipProps !== false) {
return flip(flipProps === true ? undefined : flipProps);
}
return undefined;
}
function shiftMiddleware(shiftProps) {
if (shiftProps !== false) {
return shift(shiftProps === true ? undefined : shiftProps);
}
return undefined;
}
//# sourceMappingURL=TourOverlay.component.js.map