Skip to content

Commit

Permalink
Merge pull request #46 from storybookjs/fix/improve-addon-bootstrapping
Browse files Browse the repository at this point in the history
Improve addon bootstrapping
  • Loading branch information
cdedreuille authored Jun 8, 2023
2 parents 5f27441 + c779e85 commit 569a919
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 82 deletions.
48 changes: 17 additions & 31 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 (
<ThemeProvider theme={theme}>
{showConfetti && (
{enabled && showConfetti && (
<Confetti
numberOfPieces={800}
recycle={false}
Expand All @@ -95,13 +77,17 @@ export default function App({ api }: { api: API }) {
isOpen={enabled && step === "1:Welcome"}
skipOnboarding={skipOnboarding}
/>
{(step === "2:StorybookTour" || step === "5:ConfigureYourProject") && (
{enabled && (step === "2:StorybookTour" || step === "5:ConfigureYourProject") && (
<GuidedTour
api={api}
isFinalStep={step === "5:ConfigureYourProject"}
onFirstTourDone={() => {
setStep("3:WriteYourStory");
}}
onLastTourDone={() => {
api.selectStory("configure-your-project--docs");
skipOnboarding();
}}
/>
)}
<WriteStoriesModal
Expand Down
16 changes: 10 additions & 6 deletions src/features/GuidedTour/GuidedTour.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>();

Expand All @@ -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();
},
},
]
: [
Expand Down
9 changes: 6 additions & 3 deletions src/features/GuidedTour/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -49,7 +49,10 @@ export const Tooltip = ({ step, primaryProps, tooltipProps }: TooltipProps) => {
</Wrapper>
{!step.hideNextButton && (
<TooltipFooter id="buttonNext">
<Button {...primaryProps}>Next</Button>
<Button {...{
...primaryProps,
...(step.onNextButtonClick ? { onClick: step.onNextButtonClick } : {})
}}>Next</Button>
</TooltipFooter>
)}
</TooltipBody>
Expand Down
22 changes: 15 additions & 7 deletions src/features/WriteStoriesModal/WriteStoriesModal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof WriteStoriesModal> = {
component: WriteStoriesModal,
args: {
api: {
getData: () => ({ some: "data" }),
getData,
} as any,
addonsStore: {
getChannel: () => {
Expand All @@ -31,15 +33,21 @@ const meta: Meta<typeof WriteStoriesModal> = {
storyIndexInvalidatedCb = cb;
}
},
off: () => {},
off: () => { },
}),
getData: () => ({ some: "data" }),
} as any,
},
decorators: [
(storyFn) => (
<div style={{ width: "1200px", height: "800px" }}>{storyFn()}</div>
),
(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 (
<div style={{ width: "1200px", height: "800px" }}>{storyFn()}</div>
)
},
],
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 32 additions & 32 deletions src/manager.tsx
Original file line number Diff line number Diff line change
@@ -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"));

Expand All @@ -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(
<Suspense fallback={<div>Loading...</div>}>
<App api={api} />
</Suspense>,
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(
<Suspense fallback={<div>Loading...</div>}>
<App api={api} />
</Suspense>,
domNode
);
})
});

0 comments on commit 569a919

Please sign in to comment.