Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Add guided tour example #5

Merged
merged 17 commits into from
Jun 1, 2023
8 changes: 7 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
{}
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false
}
8 changes: 5 additions & 3 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<script>
window.global = window;
</script>
<style>
body {
font-family: "Nunito Sans",-apple-system,".SFNSText-Regular","San Francisco",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Helvetica,Arial,sans-serif;
}
</style>
5 changes: 5 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import React from "react";

const preview: Preview = {
parameters: {
options: {
storySort: {
order: ["Example", ["Introduction"]],
},
},
backgrounds: {
default: "light",
},
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ Storybook Addon Onboarding - Introduces a new onboarding experience

### Development scripts

- `yarn start` runs babel in watch mode and starts Storybook
- `yarn start` runs tsup in watch mode and starts Storybook
- `yarn build` build and package your addon code
- `yarn storybook:watch` runs nodemon in watch mode so it reruns Storybook on changes. This is useful when testing the actual addon (as we cannot have HMR for addon changes) rather than just stories in Storybook

## Release Management

Expand Down
25 changes: 22 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,45 @@
"start": "run-p build:watch 'storybook --quiet'",
"release": "yarn build && auto shipit",
"storybook": "storybook dev -p 6006",
"storybook:watch": "nodemon",
"build-storybook": "storybook build",
"chromatic": "npx chromatic"
},
"nodemonConfig": {
"ignore": [
"src/stories",
"src/**/*.stories.*"
],
"watch": [
"src",
".storybook/main.ts",
"vite.config.ts"
],
"ext": "js,jsx,ts,tsx",
"exec": "yarn storybook"
},
"devDependencies": {
"@storybook/addon-essentials": "^7.0.0",
"@storybook/addon-interactions": "^7.0.0",
"@storybook/addon-links": "^7.0.0",
"@storybook/blocks": "^7.0.0",
"@storybook/components": "^7.0.0",
"@storybook/core-events": "^7.0.0",
"@storybook/manager-api": "^7.0.0",
"@storybook/jest": "^0.1.0",
"@storybook/react": "^7.0.0",
"@storybook/react-vite": "^7.0.0",
"@storybook/testing-library": "^0.0.14-next.1",
"@storybook/theming": "^7.0.17",
"@types/node": "^18.15.0",
"@types/react": "^18.0.34",
"@types/react": "^18.2.7",
"@types/react-dom": "^18.2.4",
"@vitejs/plugin-react": "^3.1.0",
"auto": "^10.3.0",
"boxen": "^5.0.1",
"chromatic": "^6.17.4",
"dedent": "^0.7.0",
"nodemon": "^2.0.22",
"npm-run-all": "^4.1.5",
"prettier": "^2.3.1",
"prompts": "^2.4.2",
Expand All @@ -87,7 +106,6 @@
"@storybook/components": "^7.0.0",
"@storybook/core-events": "^7.0.0",
"@storybook/manager-api": "^7.0.0",
"@storybook/preview-api": "^7.0.0",
"@storybook/theming": "^7.0.0",
"@storybook/types": "^7.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
Expand All @@ -111,6 +129,7 @@
"homepage": "https://github.com/storybookjs/addon-onboarding#readme",
"dependencies": {
"@radix-ui/react-dialog": "^1.0.4",
"react-confetti": "^6.1.0"
"react-confetti": "^6.1.0",
"react-joyride": "^2.5.4"
}
}
118 changes: 115 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,124 @@
import React, { useCallback, useEffect, useState } from "react";
import { ThemeProvider, ensure, themes } from "@storybook/theming";
import React from "react";
import { STORY_CHANGED, CURRENT_STORY_WAS_SET } from "@storybook/core-events";
import { type API } from "@storybook/manager-api";

import { GuidedTour } from "./features/GuidedTour/GuidedTour";
import { WelcomeModal } from "./features/WelcomeModal/WelcomeModal";
import { WriteStoriesModal } from "./features/WriteStoriesModal/WriteStoriesModal";
import { Confetti } from "./components/Confetti/Confetti";

type Step =
| "1:Welcome"
| "2:StorybookTour"
| "3:WriteYourStory"
| "4:VisitNewStory"
| "5:ConfigureYourProject";

const theme = ensure(themes.light);

export function App() {
export default function App({ api }: { api: API }) {
const [enabled, setEnabled] = useState(true);
const [showConfetti, setShowConfetti] = useState(false);
const [step, setStep] = useState<Step>("1:Welcome");

const skipTour = useCallback(() => {
// remove onboarding query parameter from current url
const url = new URL(window.location.href);
url.searchParams.delete("onboarding");
const path = decodeURIComponent(url.searchParams.get("path"));
url.search = `?path=${path}`;
history.replaceState({}, "", url.href);
setEnabled(false);
}, [setEnabled]);

useEffect(() => {
let stepTimeout: number;
if (step === "4:VisitNewStory") {
stepTimeout = window.setTimeout(() => {
setShowConfetti(true);
setStep("5:ConfigureYourProject");
}, 2000);
}

return () => {
clearTimeout(stepTimeout);
};
}, [step]);

useEffect(() => {
api.once(CURRENT_STORY_WAS_SET, ({ storyId }) => {
// make sure the initial state is set correctly:
// 1. Selected story is primary button
// 2. The addon panel is opened, in the bottom and the controls tab is selected
if (storyId !== "example-button--primary") {
api.selectStory("example-button--primary", undefined, {
ref: undefined,
});
}
api.togglePanel(true);
api.togglePanelPosition("bottom");
api.setSelectedPanel("addon-controls");
});
}, []);

useEffect(() => {
const onStoryChanged = (storyId: string) => {
if (storyId === "configure-your-project--docs") {
skipTour();
}
};

api.on(STORY_CHANGED, onStoryChanged);

return () => {
api.off(STORY_CHANGED, onStoryChanged);
};
}, []);

if (!enabled) {
return null;
}

return (
<ThemeProvider theme={theme}>
<div>Hello World</div>
{showConfetti && (
<Confetti
numberOfPieces={1000}
initialVelocityY={3}
recycle={false}
onConfettiComplete={(confetti) => {
confetti.reset();
setShowConfetti(false);
}}
/>
)}
{step === "1:Welcome" && (
<WelcomeModal
onProceed={() => {
setStep("2:StorybookTour");
}}
onSkip={skipTour}
/>
)}
{(step === "2:StorybookTour" || step === "5:ConfigureYourProject") && (
<GuidedTour
api={api}
isFinalStep={step === "5:ConfigureYourProject"}
onFirstTourDone={() => {
setStep("3:WriteYourStory");
}}
/>
)}
{step === "3:WriteYourStory" && (
<WriteStoriesModal
onFinish={() => {
// TODO: enable this
// api.selectStory("example-button--warning");
setStep("4:VisitNewStory");
}}
/>
)}
</ThemeProvider>
);
}
22 changes: 22 additions & 0 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from "react";

export const buttonStyles: React.ComponentProps<"button">["style"] = {
border: 0,
cursor: "pointer",
fontSize: 13,
lineHeight: 1,
padding: "9px 12px",
backgroundColor: "#029CFD",
borderRadius: 4,
color: "#fff",
fontWeight: 700,
};

export function Button(props: React.ComponentProps<"button">) {
const style = {
...buttonStyles,
...(props.style || {}),
};

return <button type="button" {...props} style={style} />;
}
12 changes: 6 additions & 6 deletions src/components/Confetti/Confetti.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import ReactConfetti from "react-confetti";
import React, { useEffect, useRef } from "react";
import React, { useEffect } from "react";
import { styled } from "@storybook/theming";
import { createPortal } from "react-dom";
import { useState } from "react";

interface ConfettiProps
extends Omit<React.ComponentProps<typeof ReactConfetti>, "drawShape"> {
top: number;
left: number;
width: number;
height: number;
top?: number;
left?: number;
width?: number;
height?: number;
}

const Wrapper = styled.div<{
Expand All @@ -32,7 +32,7 @@ export function Confetti({
width = window.innerWidth,
height = window.innerHeight,
...confettiProps
}: ConfettiProps) {
}: ConfettiProps): React.ReactPortal {
const [confettiContainer] = useState(() => {
const container = document.createElement("div");
container.setAttribute("id", "confetti-container");
Expand Down
31 changes: 31 additions & 0 deletions src/components/Icons/StorybookLogo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react";

export function StorybookLogo() {
return (
<svg
width="32px"
height="40px"
viewBox="0 0 256 319"
preserveAspectRatio="xMidYMid"
>
<defs>
<path
d="M9.87245893,293.324145 L0.0114611411,30.5732167 C-0.314208957,21.8955842 6.33948896,14.5413918 15.0063196,13.9997149 L238.494389,0.0317105427 C247.316188,-0.519651867 254.914637,6.18486163 255.466,15.0066607 C255.486773,15.339032 255.497167,15.6719708 255.497167,16.0049907 L255.497167,302.318596 C255.497167,311.157608 248.331732,318.323043 239.492719,318.323043 C239.253266,318.323043 239.013844,318.317669 238.774632,318.306926 L25.1475605,308.712253 C16.8276309,308.338578 10.1847994,301.646603 9.87245893,293.324145 L9.87245893,293.324145 Z"
id="path-1"
></path>
</defs>
<g>
<mask id="mask-2" fill="white">
<use xlinkHref="#path-1"></use>
</mask>
<use fill="#FF4785" fillRule="nonzero" xlinkHref="#path-1"></use>
<path
d="M188.665358,39.126973 L190.191903,2.41148534 L220.883535,0 L222.205755,37.8634126 C222.251771,39.1811466 221.22084,40.2866846 219.903106,40.3327009 C219.338869,40.3524045 218.785907,40.1715096 218.342409,39.8221376 L206.506729,30.4984116 L192.493574,41.1282444 C191.443077,41.9251106 189.945493,41.7195021 189.148627,40.6690048 C188.813185,40.2267976 188.6423,39.6815326 188.665358,39.126973 Z M149.413703,119.980309 C149.413703,126.206975 191.355678,123.222696 196.986019,118.848893 C196.986019,76.4467826 174.234041,54.1651411 132.57133,54.1651411 C90.9086182,54.1651411 67.5656805,76.7934542 67.5656805,110.735941 C67.5656805,169.85244 147.345341,170.983856 147.345341,203.229219 C147.345341,212.280549 142.913138,217.654777 133.162291,217.654777 C120.456641,217.654777 115.433477,211.165914 116.024438,189.103298 C116.024438,184.317101 67.5656805,182.824962 66.0882793,189.103298 C62.3262146,242.56887 95.6363019,257.990394 133.753251,257.990394 C170.688279,257.990394 199.645341,238.303123 199.645341,202.663511 C199.645341,139.304202 118.683759,141.001326 118.683759,109.604526 C118.683759,96.8760922 128.139127,95.178968 133.753251,95.178968 C139.662855,95.178968 150.300143,96.2205679 149.413703,119.980309 Z"
fill="#FFFFFF"
fillRule="nonzero"
mask="url(#mask-2)"
></path>
</g>
</svg>
);
}
5 changes: 3 additions & 2 deletions src/components/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Meta, StoryObj } from "@storybook/react";
import { Modal } from "./Modal";
import React, { useState } from "react";
import { Meta, StoryObj } from "@storybook/react";
import { userEvent, within } from "@storybook/testing-library";
import { expect } from "@storybook/jest";

import { Modal } from "./Modal";

const meta: Meta<typeof Modal> = {
component: Modal,
decorators: [
Expand Down
15 changes: 13 additions & 2 deletions src/components/Modal/Modal.styled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ export const ContentWrapper = React.forwardRef<
</Content>
));

export const StyledTitle = styled(Title)``;
export const StyledDescription = styled(Description)``;
export const StyledTitle = styled(Title)`
color: #000;
font-weight: 700;
font-size: 20px;
line-height: 20px;
`;
export const StyledDescription = styled(Description)`
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: #454e54;
`;

export const StyledClose = styled(Close)``;
44 changes: 44 additions & 0 deletions src/components/PulsatingEffect/PulsatingEffect.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Meta, StoryObj } from "@storybook/react";
import { PulsatingEffect } from "./PulsatingEffect";
import React from "react";
import { within } from "@storybook/testing-library";
import { expect } from "@storybook/jest";

const meta: Meta<typeof PulsatingEffect> = {
component: PulsatingEffect,
parameters: {
layout: "centered",
chromatic: {
disableSnapshot: true,
},
},
};

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
render: () => (
<>
<PulsatingEffect targetSelector="#the-button" />
<button
id="the-button"
style={{
borderRadius: 20,
border: "1px solid #c9c9ff",
padding: 6,
}}
>
I should be pulsating
</button>
</>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.parentElement);
const button = canvas.getByRole("button");
await expect(button).toHaveStyle(
"animation: 3s ease-in-out 0s infinite normal none running pulsate"
);
},
};
Loading