Skip to content

Commit e72c6b2

Browse files
committed
Add guided tour and intent survey to checklist
1 parent c837f84 commit e72c6b2

File tree

11 files changed

+341
-56
lines changed

11 files changed

+341
-56
lines changed

code/.storybook/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ const config = defineMain({
8181
directory: '../addons/onboarding/src',
8282
titlePrefix: 'addons/onboarding',
8383
},
84+
{
85+
directory: '../addons/onboarding/example-stories',
86+
},
8487
{
8588
directory: '../addons/pseudo-states/src',
8689
titlePrefix: 'addons/pseudo-states',
@@ -100,6 +103,7 @@ const config = defineMain({
100103
},
101104
],
102105
addons: [
106+
'@storybook/addon-onboarding',
103107
'@storybook/addon-themes',
104108
'@storybook/addon-docs',
105109
'@storybook/addon-designs',
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite';
2+
3+
import { fn } from 'storybook/test';
4+
5+
import { Button } from './Button';
6+
7+
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
8+
const meta = {
9+
title: 'Example/Button',
10+
component: Button,
11+
parameters: {
12+
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
13+
layout: 'centered',
14+
},
15+
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
16+
tags: ['autodocs'],
17+
// More on argTypes: https://storybook.js.org/docs/api/argtypes
18+
argTypes: {
19+
backgroundColor: { control: 'color' },
20+
},
21+
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#story-args
22+
args: { onClick: fn() },
23+
} satisfies Meta<typeof Button>;
24+
25+
export default meta;
26+
type Story = StoryObj<typeof meta>;
27+
28+
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
29+
export const Primary: Story = {
30+
args: {
31+
primary: true,
32+
label: 'Button',
33+
},
34+
};
35+
36+
export const Secondary: Story = {
37+
args: {
38+
label: 'Button',
39+
},
40+
};
41+
42+
export const Large: Story = {
43+
args: {
44+
size: 'large',
45+
label: 'Button',
46+
},
47+
};
48+
49+
export const Small: Story = {
50+
args: {
51+
size: 'small',
52+
label: 'Button',
53+
},
54+
};
55+
56+
export const Ad: Story = {
57+
args: {
58+
primary: false,
59+
label: 'Button',
60+
},
61+
};
62+
63+
export const Df: Story = {
64+
args: {
65+
primary: false,
66+
label: 'Button',
67+
},
68+
};
69+
70+
export const Gdf: Story = {
71+
args: {
72+
primary: false,
73+
label: 'Button',
74+
},
75+
};
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react';
2+
3+
import './button.css';
4+
5+
export interface ButtonProps {
6+
/** Is this the principal call to action on the page? */
7+
primary?: boolean;
8+
/** What background color to use */
9+
backgroundColor?: string;
10+
/** How large should the button be? */
11+
size?: 'small' | 'medium' | 'large';
12+
/** Button contents */
13+
label: string;
14+
/** Optional click handler */
15+
onClick?: () => void;
16+
}
17+
18+
/** Primary UI component for user interaction */
19+
export const Button = ({
20+
primary = false,
21+
size = 'medium',
22+
backgroundColor,
23+
label,
24+
...props
25+
}: ButtonProps) => {
26+
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
27+
return (
28+
<button
29+
type="button"
30+
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
31+
style={{ backgroundColor }}
32+
{...props}
33+
>
34+
{label}
35+
</button>
36+
);
37+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
.storybook-button {
2+
display: inline-block;
3+
cursor: pointer;
4+
border: 0;
5+
border-radius: 3em;
6+
font-weight: 700;
7+
line-height: 1;
8+
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
9+
}
10+
.storybook-button--primary {
11+
background-color: #555ab9;
12+
color: white;
13+
}
14+
.storybook-button--secondary {
15+
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
16+
background-color: transparent;
17+
color: #333;
18+
}
19+
.storybook-button--small {
20+
padding: 10px 16px;
21+
font-size: 12px;
22+
}
23+
.storybook-button--medium {
24+
padding: 11px 20px;
25+
font-size: 14px;
26+
}
27+
.storybook-button--large {
28+
padding: 12px 24px;
29+
font-size: 16px;
30+
}

code/addons/onboarding/src/Onboarding.tsx

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,26 @@ export default function Onboarding({ api }: { api: API }) {
9898
[api]
9999
);
100100

101-
const disableOnboarding = useCallback(() => {
102-
// remove onboarding query parameter from current url
103-
const url = new URL(window.location.href);
104-
// @ts-expect-error (not strict)
105-
const path = decodeURIComponent(url.searchParams.get('path'));
106-
url.search = `?path=${path}&onboarding=false`;
107-
history.replaceState({}, '', url.href);
108-
api.setQueryParams({ onboarding: 'false' });
109-
setEnabled(false);
110-
}, [api, setEnabled]);
101+
const disableOnboarding = useCallback(
102+
(dismissedStep?: StepKey) => {
103+
if (dismissedStep) {
104+
api.emit(STORYBOOK_ADDON_ONBOARDING_CHANNEL, {
105+
step: dismissedStep,
106+
type: 'dismiss',
107+
userAgent,
108+
});
109+
}
110+
// remove onboarding query parameter from current url
111+
const url = new URL(window.location.href);
112+
// @ts-expect-error (not strict)
113+
const path = decodeURIComponent(url.searchParams.get('path'));
114+
url.search = `?path=${path}&onboarding=false`;
115+
history.replaceState({}, '', url.href);
116+
api.setQueryParams({ onboarding: 'false' });
117+
setEnabled(false);
118+
},
119+
[api, setEnabled, userAgent]
120+
);
111121

112122
const completeOnboarding = useCallback(
113123
(answers: Record<string, unknown>) => {
@@ -288,12 +298,15 @@ export default function Onboarding({ api }: { api: API }) {
288298
{step === '1:Intro' ? (
289299
<SplashScreen onDismiss={() => setStep('2:Controls')} />
290300
) : step === '6:IntentSurvey' ? (
291-
<IntentSurvey onComplete={completeOnboarding} onDismiss={disableOnboarding} />
301+
<IntentSurvey
302+
onComplete={completeOnboarding}
303+
onDismiss={() => disableOnboarding('6:IntentSurvey')}
304+
/>
292305
) : (
293306
<GuidedTour
294307
step={step}
295308
steps={steps}
296-
onClose={disableOnboarding}
309+
onClose={() => disableOnboarding(step)}
297310
onComplete={() => setStep('6:IntentSurvey')}
298311
/>
299312
)}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, { useCallback } from 'react';
2+
3+
import { type API } from 'storybook/manager-api';
4+
import { ThemeProvider, convert } from 'storybook/theming';
5+
6+
import { STORYBOOK_ADDON_ONBOARDING_CHANNEL } from './constants';
7+
import { IntentSurvey } from './features/IntentSurvey/IntentSurvey';
8+
9+
const theme = convert();
10+
11+
export default function Survey({ api }: { api: API }) {
12+
// eslint-disable-next-line compat/compat
13+
const userAgent = globalThis?.navigator?.userAgent;
14+
15+
const disableOnboarding = useCallback(() => {
16+
// remove onboarding query parameter from current url
17+
const url = new URL(window.location.href);
18+
// @ts-expect-error (not strict)
19+
const path = decodeURIComponent(url.searchParams.get('path'));
20+
url.search = `?path=${path}&onboarding=false`;
21+
history.replaceState({}, '', url.href);
22+
api.setQueryParams({ onboarding: 'false' });
23+
}, [api]);
24+
25+
const complete = useCallback(
26+
(answers: Record<string, unknown>) => {
27+
api.emit(STORYBOOK_ADDON_ONBOARDING_CHANNEL, {
28+
answers,
29+
type: 'survey',
30+
userAgent,
31+
});
32+
disableOnboarding();
33+
},
34+
[api, disableOnboarding, userAgent]
35+
);
36+
37+
const dismiss = useCallback(() => {
38+
api.emit(STORYBOOK_ADDON_ONBOARDING_CHANNEL, {
39+
type: 'skipSurvey',
40+
});
41+
disableOnboarding();
42+
}, [api, disableOnboarding]);
43+
44+
return (
45+
<ThemeProvider theme={theme}>
46+
<IntentSurvey onComplete={complete} onDismiss={dismiss} />
47+
</ThemeProvider>
48+
);
49+
}

code/addons/onboarding/src/features/IntentSurvey/IntentSurvey.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export const IntentSurvey = ({
173173
<Modal defaultOpen width={420} onEscapeKeyDown={onDismiss}>
174174
<Form onSubmit={onSubmitForm} id="intent-survey-form">
175175
<Content>
176-
<Modal.Header>
176+
<Modal.Header onClose={onDismiss}>
177177
<Modal.Title>Help improve Storybook</Modal.Title>
178178
</Modal.Header>
179179

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { Suspense, lazy } from 'react';
2-
import ReactDOM from 'react-dom';
2+
import { createRoot } from 'react-dom/client';
33

44
import { STORY_SPECIFIED } from 'storybook/internal/core-events';
55

@@ -8,49 +8,50 @@ import { addons } from 'storybook/manager-api';
88
import { ADDON_CONTROLS_ID } from './constants';
99

1010
const Onboarding = lazy(() => import('./Onboarding'));
11+
const Survey = lazy(() => import('./Survey'));
12+
13+
const render = (node: React.ReactNode) => {
14+
let container = document.getElementById('storybook-addon-onboarding');
15+
if (!container) {
16+
container = document.createElement('div');
17+
container.id = 'storybook-addon-onboarding';
18+
document.body.appendChild(container);
19+
}
20+
createRoot(container).render(<Suspense fallback={<div />}>{node}</Suspense>);
21+
};
1122

1223
// The addon is enabled only when:
1324
// 1. The onboarding query parameter is present
1425
// 2. The example button stories are present
1526
addons.register('@storybook/addon-onboarding', async (api) => {
16-
const urlState = api.getUrlState();
17-
const isOnboarding =
18-
urlState.path === '/onboarding' || urlState.queryParams.onboarding === 'true';
19-
20-
api.once(STORY_SPECIFIED, () => {
21-
const hasButtonStories =
22-
!!api.getData('example-button--primary') ||
23-
!!document.getElementById('example-button--primary');
24-
25-
if (!hasButtonStories) {
26-
console.warn(
27-
`[@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/storybook/tree/next/code/addons/onboarding#uninstalling`
28-
);
29-
return;
30-
}
31-
32-
if (!isOnboarding || window.innerWidth < 730) {
33-
return;
34-
}
35-
36-
api.togglePanel(true);
37-
api.togglePanelPosition('bottom');
38-
api.setSelectedPanel(ADDON_CONTROLS_ID);
39-
40-
// Add a new DOM element to document.body, where we will bootstrap our React app
41-
const domNode = document.createElement('div');
42-
43-
domNode.id = 'storybook-addon-onboarding';
44-
// Append the new DOM element to document.body
45-
document.body.appendChild(domNode);
46-
47-
// Render the React app
48-
// eslint-disable-next-line react/no-deprecated
49-
ReactDOM.render(
50-
<Suspense fallback={<div />}>
51-
<Onboarding api={api} />
52-
</Suspense>,
53-
domNode
27+
const { path, queryParams } = api.getUrlState();
28+
const isOnboarding = path === '/onboarding' || queryParams.onboarding === 'true';
29+
const isSurvey = queryParams.onboarding === 'survey';
30+
31+
if (isSurvey) {
32+
return render(<Survey api={api} />);
33+
}
34+
35+
await new Promise((resolve) => api.once(STORY_SPECIFIED, resolve));
36+
37+
const hasButtonStories =
38+
!!api.getData('example-button--primary') ||
39+
!!document.getElementById('example-button--primary');
40+
41+
if (!hasButtonStories) {
42+
console.warn(
43+
`[@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/storybook/tree/next/code/addons/onboarding#uninstalling`
5444
);
55-
});
45+
return;
46+
}
47+
48+
if (!isOnboarding || window.innerWidth < 730) {
49+
return;
50+
}
51+
52+
api.togglePanel(true);
53+
api.togglePanelPosition('bottom');
54+
api.setSelectedPanel(ADDON_CONTROLS_ID);
55+
56+
return render(<Onboarding api={api} />);
5657
});

code/core/src/components/components/Modal/Modal.styled.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,13 @@ export const Col = styled.div({
8989
gap: 4,
9090
});
9191

92-
export const Header = (props: React.ComponentProps<typeof Col>) => (
92+
export const Header = ({
93+
onClose,
94+
...props
95+
}: React.ComponentProps<typeof Col> & { onClose?: () => void }) => (
9396
<Row>
9497
<Col {...props} />
95-
<CloseButton />
98+
<CloseButton onClick={onClose} />
9699
</Row>
97100
);
98101

code/core/src/manager/settings/Checklist/Checklist.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ export const Checklist = ({
424424
Skip
425425
</Button>
426426
)}
427-
{(isAccepted || isSkipped) && !isLocked && (
427+
{((isAccepted && !item.once) || isSkipped) && !isLocked && (
428428
<Button
429429
variant="ghost"
430430
padding="small"

0 commit comments

Comments
 (0)