Skip to content

Commit 6a29db2

Browse files
Add initial visual tour component
1 parent 6f97793 commit 6a29db2

5 files changed

Lines changed: 183 additions & 0 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React from "react";
2+
import { Meta, Story } from "@storybook/react";
3+
4+
import {
5+
Button,
6+
Icon,
7+
OverviewItem,
8+
OverviewItemDepiction,
9+
OverviewItemDescription,
10+
OverviewItemLine,
11+
Toolbar,
12+
ToolbarSection,
13+
VisualTourProps,
14+
} from "../../../index";
15+
16+
import VisualTour from "./VisualTour";
17+
18+
export default {
19+
title: "Components/VisualTour",
20+
component: VisualTour,
21+
argTypes: {},
22+
} as Meta<typeof VisualTour>;
23+
24+
const Template: Story<typeof VisualTour> = (args: VisualTourProps) => {
25+
const [closed, setClosed] = React.useState<boolean>(true);
26+
27+
return (
28+
<div style={{ minHeight: "600px", minWidth: "800px" }}>
29+
<Toolbar id={"tourContainer"}>
30+
<ToolbarSection id={"textSection"} canGrow={true}>
31+
Some text
32+
</ToolbarSection>
33+
<ToolbarSection id={"buttonSection"}>
34+
<Button id={"actionA"}>Action A</Button>
35+
<Button id={"actionB"}>Action B</Button>
36+
<Button id={"startTour"} intent={"primary"} onClick={() => setClosed(false)}>
37+
Start tour!
38+
</Button>
39+
</ToolbarSection>
40+
</Toolbar>
41+
{!closed ? <VisualTour {...args} onClose={() => setClosed(true)} /> : null}
42+
</div>
43+
);
44+
};
45+
46+
export const Default = Template.bind({});
47+
const defaultArgs: VisualTourProps = {
48+
containerElementQuery: "#tourContainer",
49+
steps: [
50+
{
51+
title: "First step",
52+
content: "This is a demonstration of a visual tour. A step can be simple text.",
53+
},
54+
{
55+
title: "Custom content",
56+
content: () => (
57+
<OverviewItem>
58+
<OverviewItemDepiction>
59+
<Icon name={"item-info"} />
60+
</OverviewItemDepiction>
61+
<OverviewItemDescription>
62+
<OverviewItemLine>
63+
Or a step can be arbitrary content that is displayed in a modal by default.
64+
</OverviewItemLine>
65+
<OverviewItemLine>The developer can choose what's appropriate.</OverviewItemLine>
66+
</OverviewItemDescription>
67+
</OverviewItem>
68+
),
69+
},
70+
{
71+
title: "Highlight elements",
72+
content:
73+
"It's possible to highlight specific elements on a page. The step content is then displayed in a kind of tooltip instead of a modal.",
74+
highlightElementQuery: "#actionA",
75+
},
76+
],
77+
onClose: () => {},
78+
};
79+
Default.args = defaultArgs;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import React from "react";
2+
3+
import { CLASSPREFIX as eccgui } from "../../configuration/constants";
4+
import Button from "../Button/Button";
5+
import { SimpleDialog } from "../Dialog";
6+
7+
export interface VisualTourProps {
8+
/** The CSS query of the container element that contains the feature that will be given a tour of.
9+
* If element highlighting is enabled, this will grey out every other element in the container element.
10+
* Default: body*/
11+
containerElementQuery?: string;
12+
/** The steps of the tour. */
13+
steps: VisualTourStep[];
14+
/** Called when the tour is cancelled or closed at then end. This should usually remove the component from the outside. */
15+
onClose: () => void;
16+
/** Label of the button to close the tour. */
17+
closeLabel?: string;
18+
/** The label for the next button. */
19+
nextLabel?: string;
20+
}
21+
22+
interface VisualTourStep {
23+
/** (Short) Title of the current step. */
24+
title: string;
25+
/** The description or more elaborate content element that is shown in the modal/overlay. */
26+
content: string | (() => React.JSX.Element);
27+
/** Optional element that should be highlighted, every other element in the container element is greyed out. */
28+
highlightElementQuery?: string;
29+
}
30+
31+
const containerHighlightClass = `${eccgui}-visual-tour__container`;
32+
const highlightElementClass = `${eccgui}-visual-tour__highlighted-element`;
33+
34+
/** A visual tour multi-step tour of the current view. */
35+
export const VisualTour = ({
36+
containerElementQuery = "body",
37+
steps,
38+
onClose,
39+
closeLabel = "Close",
40+
nextLabel = "Next",
41+
}: VisualTourProps) => {
42+
const [currentStepIndex, setCurrentStepIndex] = React.useState<number>(0);
43+
const [currentStepComponent, setCurrentStepComponent] = React.useState<React.JSX.Element | null>(null);
44+
45+
React.useEffect(() => {
46+
const step = steps[currentStepIndex];
47+
if (!step) {
48+
// This should not happen
49+
setCurrentStepComponent(null);
50+
onClose();
51+
return;
52+
}
53+
const hasNextStep = currentStepIndex + 1 < steps.length;
54+
// Configure optional highlighting
55+
const element = document.querySelector(containerElementQuery);
56+
if (element) {
57+
// Remove previous element highlight
58+
element.classList.remove(containerHighlightClass);
59+
document.querySelector(`.${highlightElementClass}`)?.classList.remove(highlightElementClass);
60+
if (step.highlightElementQuery) {
61+
const elementToHighlight = document.querySelector(step.highlightElementQuery);
62+
if (elementToHighlight) {
63+
element.classList.add(containerHighlightClass);
64+
elementToHighlight.classList.add(highlightElementClass);
65+
}
66+
}
67+
}
68+
setCurrentStepComponent(
69+
<SimpleDialog
70+
title={step.title + ` ${currentStepIndex + 1} / ${steps.length}`}
71+
isOpen={true}
72+
preventSimpleClosing={true}
73+
onClose={onClose}
74+
actions={[
75+
<Button onClick={onClose}>{closeLabel}</Button>,
76+
hasNextStep ? (
77+
<Button
78+
onClick={() => {
79+
setCurrentStepIndex(currentStepIndex + 1);
80+
}}
81+
>
82+
{nextLabel}: {steps[currentStepIndex + 1].title}
83+
</Button>
84+
) : null,
85+
]}
86+
>
87+
{typeof step.content === "string" ? step.content : step.content()}
88+
</SimpleDialog>
89+
);
90+
}, [currentStepIndex]);
91+
92+
return currentStepComponent;
93+
};
94+
95+
export default VisualTour;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.eccapp-visual-tour__container {
2+
:not(.eccapp-visual-tour__highlighted-element) {
3+
z-index: 0;
4+
opacity: $eccgui-opacity-disabled;
5+
filter: grayscale(1);
6+
}
7+
}

src/components/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
@import "./TagInput/taginput";
3535
@import "./Toolbar/toolbar";
3636
@import "./Tooltip/tooltip";
37+
@import "./VisualTour/visualTour";
3738
@import "./Tree/tree";
3839
@import "./Typography/typography";
3940
@import "./Workspace/workspace";

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,6 @@ export * from "./Tree/Tree";
4949
export * from "./Typography";
5050
export * from "./Workspace";
5151
export * from "./ContentGroup/ContentGroup";
52+
export * from "./VisualTour/VisualTour";
5253

5354
export * from "./interfaces";

0 commit comments

Comments
 (0)