Skip to content

Commit

Permalink
[Storybook: Editor Page] Add a working preview for the editor page in…
Browse files Browse the repository at this point in the history
… storybook (#1372)

## Summary:
The preview iframe doesn't work in storybook, because in links to a local URL
to webapp when it's in prod. It has never worked in storybook.

We need to be able to see what the initial state of the questions on the editor
page would look like without making any changes to the actual preview (yet).

I've added a preview here that shows the question and the hints in the editor
page. This works for all widgets. It's sticky, so it follows as you scroll
down a long editor. You can also close it if it's taking too much space.

This is a prerequisite to viewing an interactive graph with initial coordinates.

Issue: https://khanacademy.atlassian.net/browse/LEMS-2051

## Test plan:
http://localhost:6006/?path=/story/perseuseditor-editorpage--demo

### Interactive graph widget

<img width="1568" alt="Screenshot 2024-06-21 at 2 58 45 PM" src="https://github.com/Khan/perseus/assets/13231763/69dd2c62-05af-4d9f-a279-1575c9adbc33">

### Expression widget

<img width="863" alt="Screenshot 2024-06-21 at 2 58 14 PM" src="https://github.com/Khan/perseus/assets/13231763/3609a064-b226-4a43-bc01-b833269e62b3">

### Demo video

https://github.com/Khan/perseus/assets/13231763/c39db9c4-380d-4e3f-a556-5542198046c8

Author: nishasy

Reviewers: benchristel

Required Reviewers:

Approved By: benchristel

Checks: ✅ codecov/project, ✅ codecov/patch, ✅ Upload Coverage (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Jest Coverage (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald

Pull Request URL: #1372
  • Loading branch information
nishasy authored Jun 21, 2024
1 parent e5a54d8 commit 8cbfeba
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 164 deletions.
5 changes: 5 additions & 0 deletions .changeset/sweet-zebras-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus-editor": patch
---

[Storybook: Editor Page] Add a storybook-only preview for the questions and hints in the EditorPage component
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import {
Renderer,
type APIOptions,
type DeviceType,
type Hint,
type PerseusAnswerArea,
type PerseusRenderer,
} from "@khanacademy/perseus";
import Button from "@khanacademy/wonder-blocks-button";
import {View} from "@khanacademy/wonder-blocks-core";
import IconButton from "@khanacademy/wonder-blocks-icon-button";
import {Strut} from "@khanacademy/wonder-blocks-layout";
import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
import {LabelLarge} from "@khanacademy/wonder-blocks-typography";
import xIcon from "@phosphor-icons/core/regular/x.svg";
import {action} from "@storybook/addon-actions";
import {StyleSheet} from "aphrodite";
import * as React from "react";

// eslint-disable-next-line import/no-relative-packages
import {mockStrings} from "../../../perseus/src/strings";
import EditorPage from "../editor-page";

import {flags} from "./flags-for-api-options";

type Props = {
apiOptions?: APIOptions;
question?: PerseusRenderer;
hints?: ReadonlyArray<Hint>;
};

const onChangeAction = action("onChange");

function EditorPageWithStorybookPreview(props: Props) {
const [previewDevice, setPreviewDevice] =
React.useState<DeviceType>("phone");
const [jsonMode, setJsonMode] = React.useState<boolean | undefined>(false);
const [answerArea, setAnswerArea] = React.useState<
PerseusAnswerArea | undefined | null
>();
const [question, setQuestion] = React.useState<PerseusRenderer | undefined>(
props.question,
);
const [hints, setHints] = React.useState<ReadonlyArray<Hint> | undefined>(
props.hints,
);

const [panelOpen, setPanelOpen] = React.useState<boolean>(true);

const apiOptions = props.apiOptions ?? {
isMobile: false,
flags,
};

return (
<View>
<EditorPage
apiOptions={apiOptions}
previewDevice={previewDevice}
onPreviewDeviceChange={(newDevice) =>
setPreviewDevice(newDevice)
}
developerMode={true}
jsonMode={jsonMode}
answerArea={answerArea}
question={question}
hints={hints}
frameSource="about:blank"
previewURL="about:blank"
itemId="1"
onChange={(props) => {
onChangeAction(props);

if ("jsonMode" in props) {
setJsonMode(props.jsonMode);
}
if ("answerArea" in props) {
setAnswerArea(props.answerArea);
}
if ("question" in props) {
setQuestion(props.question);
}
if ("hints" in props) {
setHints(props.hints);
}
}}
/>

{/* Button to open panel */}
{!panelOpen && (
<Button
onClick={() => setPanelOpen(!panelOpen)}
style={styles.openPanelButton}
>
Open preview (storybook only)
</Button>
)}

{/* Panel to show the question/hint previews */}
{panelOpen && (
<View style={styles.panel}>
{/* Close button */}
<IconButton
icon={xIcon}
onClick={() => setPanelOpen(!panelOpen)}
style={styles.closeButton}
/>

<View style={styles.panelInner}>
{/* Question preview */}
<Renderer
strings={mockStrings}
apiOptions={apiOptions}
{...question}
/>
</View>

{/* Hints preview */}
{hints?.map((hint, index) => (
<View key={index} style={styles.panelInner}>
<Strut size={spacing.medium_16} />
<LabelLarge>{`Hint ${index + 1}`}</LabelLarge>
<Renderer
strings={mockStrings}
apiOptions={apiOptions}
{...hint}
/>
</View>
))}
</View>
)}
</View>
);
}

const styles = StyleSheet.create({
panel: {
position: "fixed",
right: 0,
height: "90vh",
overflow: "auto",
flex: "none",
backgroundColor: color.fadedBlue16,
padding: spacing.medium_16,
borderRadius: spacing.small_12,
alignItems: "end",
},
panelInner: {
flex: "none",
backgroundColor: color.white,
borderRadius: spacing.xSmall_8,
marginTop: spacing.medium_16,
width: "100%",
padding: spacing.xSmall_8,
},
closeButton: {
margin: 0,
},
openPanelButton: {
position: "fixed",
right: spacing.medium_16,
// Extra space so it doesn't get covered up by storybook's
// "Style warnings" button.
bottom: spacing.xxxLarge_64,
},
});

export default EditorPageWithStorybookPreview;
171 changes: 7 additions & 164 deletions packages/perseus-editor/src/__stories__/editor-page.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {EditorPage} from "..";
import {segmentWithLockedFigures} from "../../../perseus/src/widgets/__testdata__/interactive-graph.testdata";
import {registerAllWidgetsAndEditorsForTesting} from "../util/register-all-widgets-and-editors-for-testing";

import EditorPageWithStorybookPreview from "./editor-page-with-storybook-preview";
import {flags} from "./flags-for-api-options";

import type {
Expand All @@ -28,67 +29,12 @@ export default {
const onChangeAction = action("onChange");

export const Demo = (): React.ReactElement => {
const [previewDevice, setPreviewDevice] =
React.useState<DeviceType>("phone");
const [jsonMode, setJsonMode] = React.useState<boolean | undefined>(false);
const [answerArea, setAnswerArea] = React.useState<
PerseusAnswerArea | undefined | null
>();
const [question, setQuestion] = React.useState<
PerseusRenderer | undefined
>();
const [hints, setHints] = React.useState<ReadonlyArray<Hint> | undefined>();

return (
<EditorPage
apiOptions={{
isMobile: false,
flags,
}}
previewDevice={previewDevice}
onPreviewDeviceChange={(newDevice) => setPreviewDevice(newDevice)}
developerMode={true}
jsonMode={jsonMode}
answerArea={answerArea}
question={question}
hints={hints}
frameSource="about:blank"
previewURL="about:blank"
itemId="1"
onChange={(props) => {
onChangeAction(props);

if ("jsonMode" in props) {
setJsonMode(props.jsonMode);
}
if ("answerArea" in props) {
setAnswerArea(props.answerArea);
}
if ("question" in props) {
setQuestion(props.question);
}
if ("hints" in props) {
setHints(props.hints);
}
}}
/>
);
return <EditorPageWithStorybookPreview />;
};

export const MafsWithLockedFiguresCurrent = (): React.ReactElement => {
const [previewDevice, setPreviewDevice] =
React.useState<DeviceType>("phone");
const [jsonMode, setJsonMode] = React.useState<boolean | undefined>(false);
const [answerArea, setAnswerArea] = React.useState<
PerseusAnswerArea | undefined | null
>();
const [question, setQuestion] = React.useState<PerseusRenderer | undefined>(
segmentWithLockedFigures,
);
const [hints, setHints] = React.useState<ReadonlyArray<Hint> | undefined>();

return (
<EditorPage
<EditorPageWithStorybookPreview
apiOptions={{
isMobile: false,
flags: {
Expand All @@ -99,32 +45,7 @@ export const MafsWithLockedFiguresCurrent = (): React.ReactElement => {
},
},
}}
previewDevice={previewDevice}
onPreviewDeviceChange={(newDevice) => setPreviewDevice(newDevice)}
developerMode={true}
jsonMode={jsonMode}
answerArea={answerArea}
question={question}
hints={hints}
frameSource="about:blank"
previewURL="about:blank"
itemId="1"
onChange={(props) => {
onChangeAction(props);

if ("jsonMode" in props) {
setJsonMode(props.jsonMode);
}
if ("answerArea" in props) {
setAnswerArea(props.answerArea);
}
if ("question" in props) {
setQuestion(props.question);
}
if ("hints" in props) {
setHints(props.hints);
}
}}
question={segmentWithLockedFigures}
/>
);
};
Expand All @@ -138,19 +59,8 @@ MafsWithLockedFiguresCurrent.parameters = {
};

export const MafsWithLockedFiguresM2Flag = (): React.ReactElement => {
const [previewDevice, setPreviewDevice] =
React.useState<DeviceType>("phone");
const [jsonMode, setJsonMode] = React.useState<boolean | undefined>(false);
const [answerArea, setAnswerArea] = React.useState<
PerseusAnswerArea | undefined | null
>();
const [question, setQuestion] = React.useState<PerseusRenderer | undefined>(
segmentWithLockedFigures,
);
const [hints, setHints] = React.useState<ReadonlyArray<Hint> | undefined>();

return (
<EditorPage
<EditorPageWithStorybookPreview
apiOptions={{
isMobile: false,
flags: {
Expand All @@ -161,32 +71,7 @@ export const MafsWithLockedFiguresM2Flag = (): React.ReactElement => {
},
},
}}
previewDevice={previewDevice}
onPreviewDeviceChange={(newDevice) => setPreviewDevice(newDevice)}
developerMode={true}
jsonMode={jsonMode}
answerArea={answerArea}
question={question}
hints={hints}
frameSource="about:blank"
previewURL="about:blank"
itemId="1"
onChange={(props) => {
onChangeAction(props);

if ("jsonMode" in props) {
setJsonMode(props.jsonMode);
}
if ("answerArea" in props) {
setAnswerArea(props.answerArea);
}
if ("question" in props) {
setQuestion(props.question);
}
if ("hints" in props) {
setHints(props.hints);
}
}}
question={segmentWithLockedFigures}
/>
);
};
Expand All @@ -200,50 +85,8 @@ MafsWithLockedFiguresM2Flag.parameters = {
};

export const MafsWithLockedFiguresM2bFlag = (): React.ReactElement => {
const [previewDevice, setPreviewDevice] =
React.useState<DeviceType>("phone");
const [jsonMode, setJsonMode] = React.useState<boolean | undefined>(false);
const [answerArea, setAnswerArea] = React.useState<
PerseusAnswerArea | undefined | null
>();
const [question, setQuestion] = React.useState<PerseusRenderer | undefined>(
segmentWithLockedFigures,
);
const [hints, setHints] = React.useState<ReadonlyArray<Hint> | undefined>();

return (
<EditorPage
apiOptions={{
isMobile: false,
flags,
}}
previewDevice={previewDevice}
onPreviewDeviceChange={(newDevice) => setPreviewDevice(newDevice)}
developerMode={true}
jsonMode={jsonMode}
answerArea={answerArea}
question={question}
hints={hints}
frameSource="about:blank"
previewURL="about:blank"
itemId="1"
onChange={(props) => {
onChangeAction(props);

if ("jsonMode" in props) {
setJsonMode(props.jsonMode);
}
if ("answerArea" in props) {
setAnswerArea(props.answerArea);
}
if ("question" in props) {
setQuestion(props.question);
}
if ("hints" in props) {
setHints(props.hints);
}
}}
/>
<EditorPageWithStorybookPreview question={segmentWithLockedFigures} />
);
};

Expand Down

0 comments on commit 8cbfeba

Please sign in to comment.