PromucFlow_constructor/app/client/src/components/featureWalkthrough/walkthroughRenderer.tsx

259 lines
7.4 KiB
TypeScript
Raw Normal View History

import { Icon, Text } from "design-system";
import { showIndicator } from "pages/Editor/GuidedTour/utils";
import React, { useContext, useEffect, useState } from "react";
import styled from "styled-components";
import { PADDING_HIGHLIGHT, getPosition } from "./utils";
import type {
FeatureDetails,
FeatureParams,
OffsetType,
} from "./walkthroughContext";
import WalkthroughContext from "./walkthroughContext";
import AnalyticsUtil from "utils/AnalyticsUtil";
const CLIPID = "clip__feature";
const Z_INDEX = 1000;
const WalkthroughWrapper = styled.div`
left: 0px;
top: 0px;
position: fixed;
width: 100%;
height: 100%;
color: rgb(0, 0, 0, 0.7);
z-index: ${Z_INDEX};
// This allows the user to click on the target element rather than the overlay div
pointer-events: none;
`;
const SvgWrapper = styled.svg`
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
`;
const InstructionsWrapper = styled.div`
padding: var(--ads-v2-spaces-4);
position: absolute;
background: white;
display: flex;
flex-direction: column;
width: 296px;
pointer-events: auto;
border-radius: var(--ads-radius-1);
`;
const ImageWrapper = styled.div`
border-radius: var(--ads-radius-1);
background: #f1f5f9;
display: flex;
align-items: center;
justify-content: center;
margin-top: 8px;
padding: var(--ads-v2-spaces-7);
img {
max-height: 220px;
}
`;
const InstructionsHeaderWrapper = styled.div`
display: flex;
p {
flex-grow: 1;
}
span {
align-self: flex-start;
margin-top: 5px;
cursor: pointer;
}
`;
type RefRectParams = {
// body params
bh: number;
bw: number;
// target params
th: number;
tw: number;
tx: number;
ty: number;
};
/*
* Clip Path Polygon :
* 1) 0 0 ----> (body start) (body start)
* 2) 0 ${boundingRect.bh} ----> (body start) (body end)
* 3) ${boundingRect.tx} ${boundingRect.bh} ----> (target start) (body end)
* 4) ${boundingRect.tx} ${boundingRect.ty} ----> (target start) (target start)
* 5) ${boundingRect.tx + boundingRect.tw} ${boundingRect.ty} ----> (target end) (target start)
* 6) ${boundingRect.tx + boundingRect.tw} ${boundingRect.ty + boundingRect.th} ----> (target end) (target end)
* 7) ${boundingRect.tx} ${boundingRect.ty + boundingRect.th} ----> (target start) (target end)
* 8) ${boundingRect.tx} ${boundingRect.bh} ----> (target start) (body end)
* 9) ${boundingRect.bw} ${boundingRect.bh} ----> (body end) (body end)
* 10) ${boundingRect.bw} 0 ----> (body end) (body start)
*
*
* 1 10
*
* Body
*
*
* 4 5
* / / / / / / /
* / / /Target/ /
* / / / / / / /
* 7 6
*
*
* 2 3,8 9
*/
/**
* Creates a Highlighting Clipping mask around a target container
* @param targetId Id for the target container to show highlighting around it
*/
const WalkthroughRenderer = ({
details,
offset,
onDismiss,
targetId,
eventParams = {},
}: FeatureParams) => {
const [boundingRect, setBoundingRect] = useState<RefRectParams | null>(null);
const { popFeature } = useContext(WalkthroughContext) || {};
const updateBoundingRect = () => {
const highlightArea = document.querySelector(`#${targetId}`);
if (highlightArea) {
const boundingRect = highlightArea.getBoundingClientRect();
const bodyRect = document.body.getBoundingClientRect();
const offsetHighlightPad =
typeof offset?.highlightPad === "number"
? offset?.highlightPad
: PADDING_HIGHLIGHT;
setBoundingRect({
bw: bodyRect.width,
bh: bodyRect.height,
tw: boundingRect.width + 2 * offsetHighlightPad,
th: boundingRect.height + 2 * offsetHighlightPad,
tx: boundingRect.x - offsetHighlightPad,
ty: boundingRect.y - offsetHighlightPad,
});
showIndicator(`#${targetId}`, offset?.position, {
top: offset?.indicatorTop || 0,
left: offset?.indicatorLeft || 0,
zIndex: Z_INDEX + 1,
});
}
};
useEffect(() => {
updateBoundingRect();
const highlightArea = document.querySelector(`#${targetId}`);
AnalyticsUtil.logEvent("WALKTHROUGH_SHOWN", eventParams);
window.addEventListener("resize", updateBoundingRect);
const resizeObserver = new ResizeObserver(updateBoundingRect);
if (highlightArea) {
resizeObserver.observe(highlightArea);
}
return () => {
window.removeEventListener("resize", updateBoundingRect);
if (highlightArea) resizeObserver.unobserve(highlightArea);
};
}, [targetId]);
const onDismissWalkthrough = () => {
onDismiss && onDismiss();
popFeature && popFeature();
};
if (!boundingRect) return null;
return (
<WalkthroughWrapper className="t--walkthrough-overlay">
<SvgWrapper
height={boundingRect.bh}
width={boundingRect.bw}
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<clipPath id={CLIPID}>
<polygon
// See the comments above the component declaration to understand the below points assignment.
points={`
0 0,
0 ${boundingRect.bh},
${boundingRect.tx} ${boundingRect.bh},
${boundingRect.tx} ${boundingRect.ty},
${boundingRect.tx + boundingRect.tw} ${boundingRect.ty},
${boundingRect.tx + boundingRect.tw} ${
boundingRect.ty + boundingRect.th
},
${boundingRect.tx} ${boundingRect.ty + boundingRect.th},
${boundingRect.tx} ${boundingRect.bh},
${boundingRect.bw} ${boundingRect.bh},
${boundingRect.bw} 0
`}
/>
</clipPath>
</defs>
<rect
style={{
clipPath: 'url("#' + CLIPID + '")',
fill: "currentcolor",
height: boundingRect.bh,
pointerEvents: "auto",
width: boundingRect.bw,
}}
/>
</SvgWrapper>
<InstructionsComponent
details={details}
offset={offset}
onClose={onDismissWalkthrough}
targetId={targetId}
/>
</WalkthroughWrapper>
);
};
const InstructionsComponent = ({
details,
offset,
onClose,
targetId,
}: {
details?: FeatureDetails;
offset?: OffsetType;
targetId: string;
onClose: () => void;
}) => {
if (!details) return null;
const positionAttr = getPosition({
targetId,
offset,
});
return (
<InstructionsWrapper style={{ ...positionAttr }}>
<InstructionsHeaderWrapper>
<Text kind="heading-s" renderAs="p">
{details.title}
</Text>
<Icon name="close" onClick={onClose} size="md" />
</InstructionsHeaderWrapper>
<Text>{details.description}</Text>
{details.imageURL && (
<ImageWrapper>
<img src={details.imageURL} />
</ImageWrapper>
)}
</InstructionsWrapper>
);
};
export default WalkthroughRenderer;