diff --git a/src/App.tsx b/src/App.tsx
index 7d6df0c2ba54..636ebcc53627 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from "react";
import { ThemeProvider, ensure, themes } from "@storybook/theming";
-import { STORY_CHANGED, CURRENT_STORY_WAS_SET } from "@storybook/core-events";
+import { STORY_CHANGED } from "@storybook/core-events";
import { addons, type API } from "@storybook/manager-api";
import { GuidedTour } from "./features/GuidedTour/GuidedTour";
@@ -47,39 +47,21 @@ export default function App({ api }: { api: API }) {
}, [step]);
useEffect(() => {
- api.once(CURRENT_STORY_WAS_SET, ({ storyId }) => {
- api.setQueryParams({ onboarding: "true" });
- // 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") {
- skipOnboarding();
- }
- };
-
- api.on(STORY_CHANGED, onStoryChanged);
-
- return () => {
- api.off(STORY_CHANGED, onStoryChanged);
- };
+ const storyId = api.getCurrentStoryData()?.id;
+ api.setQueryParams({ onboarding: "true" });
+ // 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,
+ });
+ }
}, []);
return (
- {showConfetti && (
+ {enabled && showConfetti && (
)}
- {enabled && step === "1:Welcome" && (
- setStep("2:StorybookTour")}
- skipOnboarding={skipOnboarding}
- />
- )}
- {(step === "2:StorybookTour" || step === "5:ConfigureYourProject") && (
+ setStep("2:StorybookTour")}
+ isOpen={enabled && step === "1:Welcome"}
+ skipOnboarding={skipOnboarding}
+ />
+ {enabled && (step === "2:StorybookTour" || step === "5:ConfigureYourProject") && (
{
setStep("3:WriteYourStory");
}}
- />
- )}
- {enabled && step === "3:WriteYourStory" && (
- {
- api.selectStory("example-button--warning");
- setStep("4:VisitNewStory");
+ onLastTourDone={() => {
+ api.selectStory("configure-your-project--docs");
+ skipOnboarding();
}}
- skipOnboarding={skipOnboarding}
/>
)}
+ {
+ api.selectStory("example-button--warning");
+ setStep("4:VisitNewStory");
+ }}
+ isOpen={enabled && step === "3:WriteYourStory"}
+ skipOnboarding={skipOnboarding}
+ />
);
}
diff --git a/src/features/GuidedTour/GuidedTour.tsx b/src/features/GuidedTour/GuidedTour.tsx
index 5e5a88486ee9..171c2b71f4e9 100644
--- a/src/features/GuidedTour/GuidedTour.tsx
+++ b/src/features/GuidedTour/GuidedTour.tsx
@@ -5,18 +5,20 @@ import { PulsatingEffect } from "../../components/PulsatingEffect/PulsatingEffec
import { Confetti } from "../../components/Confetti/Confetti";
import { API } from "@storybook/manager-api";
import { UPDATE_STORY_ARGS } from "@storybook/core-events";
-import { Tooltip } from "./Tooltip";
+import { Tooltip, TooltipProps } from "./Tooltip";
-type GuidedTourStep = Step & { hideNextButton?: boolean };
+type GuidedTourStep = TooltipProps['step'];
export function GuidedTour({
api,
isFinalStep,
onFirstTourDone,
+ onLastTourDone,
}: {
api: API;
isFinalStep?: boolean;
onFirstTourDone: () => void;
+ onLastTourDone: () => void;
}) {
const [stepIndex, setStepIndex] = useState();
@@ -29,17 +31,19 @@ export function GuidedTour({
const steps: GuidedTourStep[] = isFinalStep
? [
{
- target: "#configure-your-project--docs",
- title: "Continue setting up your project",
+ target: "#example-button--warning",
+ title: "Congratulations!",
content:
- "You nailed the basics. Now get started writing stories for your own components.",
+ "You just created your first story. You nailed the basics. Continue setting up your project and start writing stories for your components.",
placement: "right",
disableOverlay: true,
disableBeacon: true,
floaterProps: {
disableAnimation: true,
},
- hideNextButton: true,
+ onNextButtonClick() {
+ onLastTourDone();
+ },
},
]
: [
diff --git a/src/features/GuidedTour/Tooltip.tsx b/src/features/GuidedTour/Tooltip.tsx
index 26af5e75cdbd..6b80a96f872d 100644
--- a/src/features/GuidedTour/Tooltip.tsx
+++ b/src/features/GuidedTour/Tooltip.tsx
@@ -36,8 +36,8 @@ const TooltipFooter = styled.div`
margin-top: 15px;
`;
-type TooltipProps = TooltipRenderProps & {
- step: TooltipRenderProps["step"] & { hideNextButton?: boolean };
+export type TooltipProps = TooltipRenderProps & {
+ step: TooltipRenderProps["step"] & { hideNextButton?: boolean; onNextButtonClick?: () => void };
};
export const Tooltip = ({ step, primaryProps, tooltipProps }: TooltipProps) => {
@@ -49,7 +49,10 @@ export const Tooltip = ({ step, primaryProps, tooltipProps }: TooltipProps) => {
{!step.hideNextButton && (
-
+
)}
diff --git a/src/features/WriteStoriesModal/WriteStoriesModal.stories.tsx b/src/features/WriteStoriesModal/WriteStoriesModal.stories.tsx
index 7e13a739194e..1aa9896f5805 100644
--- a/src/features/WriteStoriesModal/WriteStoriesModal.stories.tsx
+++ b/src/features/WriteStoriesModal/WriteStoriesModal.stories.tsx
@@ -3,17 +3,19 @@ import { Meta, StoryObj } from "@storybook/react";
import { WriteStoriesModal } from "./WriteStoriesModal";
import { waitFor, within } from "@storybook/testing-library";
-import { expect } from "@storybook/jest";
+import { expect, jest } from "@storybook/jest";
import {
STORY_INDEX_INVALIDATED,
STORY_RENDERED,
} from "@storybook/core-events";
+const getData = jest.fn()
+
const meta: Meta = {
component: WriteStoriesModal,
args: {
api: {
- getData: () => ({ some: "data" }),
+ getData,
} as any,
addonsStore: {
getChannel: () => {
@@ -31,15 +33,21 @@ const meta: Meta = {
storyIndexInvalidatedCb = cb;
}
},
- off: () => {},
+ off: () => { },
}),
- getData: () => ({ some: "data" }),
} as any,
},
decorators: [
- (storyFn) => (
-
{storyFn()}
- ),
+ (storyFn, context) => {
+ (context.args.api.getData as typeof getData)
+ // do not respond to the first call, this would only return the data correctly if the story already exists
+ // which is not the case in this story, it only makes sense in the real scenario
+ .mockReturnValueOnce(null)
+ .mockReturnValueOnce({ some: "data" })
+ return (
+
{storyFn()}
+ )
+ },
],
};
diff --git a/src/features/WriteStoriesModal/hooks/useGetWarningButtonStatus.tsx b/src/features/WriteStoriesModal/hooks/useGetWarningButtonStatus.tsx
index eedfbc8326c5..bed79c034ec9 100644
--- a/src/features/WriteStoriesModal/hooks/useGetWarningButtonStatus.tsx
+++ b/src/features/WriteStoriesModal/hooks/useGetWarningButtonStatus.tsx
@@ -25,9 +25,14 @@ export const useGetWarningButtonStatus = (
});
};
- addonsStore
- .getServerChannel()
- .on(STORY_INDEX_INVALIDATED, getWarningButtonStatus);
+ // If the story already exists, we don't need to listen to any events
+ if(api.getData("example-button--warning")) {
+ setStatus({ data: true, error: null });
+ } else {
+ addonsStore
+ .getServerChannel()
+ .on(STORY_INDEX_INVALIDATED, getWarningButtonStatus);
+ }
return () => {
addonsStore
diff --git a/src/manager.tsx b/src/manager.tsx
index fb839fac225a..c8b2bfce74db 100644
--- a/src/manager.tsx
+++ b/src/manager.tsx
@@ -1,6 +1,7 @@
import ReactDOM from "react-dom";
import React, { lazy, Suspense } from "react";
import { addons } from "@storybook/manager-api";
+import { STORY_SPECIFIED } from "@storybook/core-events";
const App = lazy(() => import("./App"));
@@ -11,36 +12,35 @@ addons.register("@storybook/addon-onboarding", async (api) => {
const urlState = api.getUrlState();
const isOnboarding = urlState.path === '/onboarding' || urlState.queryParams.onboarding === 'true';
- let hasButtonStories = false;
- try {
- const response = await fetch("./index.json");
- const index = await response.json();
- hasButtonStories = !!index.entries["example-button--primary"];
- } catch (e) {
- hasButtonStories = !!document.getElementById('example-button--primary')
- }
-
- if (!hasButtonStories) {
- console.warn(`[@storybook/addon-onboarding] It seems like you have finished the onboarding experience in Storybook! Therefore this addon is not necessary anymore and will not be loaded. You are free to remove it from your project. More info: https://github.com/storybookjs/addon-onboarding#uninstalling`);
- return;
- }
-
- if (!isOnboarding || window.innerWidth < 730) {
- return;
- }
-
- // Add a new DOM element to document.body, where we will bootstrap our React app
- const domNode = document.createElement("div");
-
- domNode.id = "addon-onboarding";
- // Append the new DOM element to document.body
- document.body.appendChild(domNode);
-
- // Render the React app
- ReactDOM.render(
- Loading...}>
-
- ,
- domNode
- );
+ api.once(STORY_SPECIFIED, () => {
+ let hasButtonStories = !!api.getData("example-button--primary") || !!document.getElementById('example-button--primary')
+
+ if (!hasButtonStories) {
+ console.warn(`[@storybook/addon-onboarding] It seems like you have finished the onboarding experience in Storybook! Therefore this addon is not necessary anymore and will not be loaded. You are free to remove it from your project. More info: https://github.com/storybookjs/addon-onboarding#uninstalling`);
+ return;
+ }
+
+ if (!isOnboarding || window.innerWidth < 730) {
+ return;
+ }
+
+ api.togglePanel(true);
+ api.togglePanelPosition("bottom");
+ api.setSelectedPanel("addon-controls");
+
+ // Add a new DOM element to document.body, where we will bootstrap our React app
+ const domNode = document.createElement("div");
+
+ domNode.id = "storybook-addon-onboarding";
+ // Append the new DOM element to document.body
+ document.body.appendChild(domNode);
+
+ // Render the React app
+ ReactDOM.render(
+ Loading...}>
+
+ ,
+ domNode
+ );
+ })
});