Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[its-a-numeric-story] Improving Numeric Input Storybook Stories #2138

Draft
wants to merge 4 commits into
base: feature/numeric-dx-refactor
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/six-cars-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": patch
---

Cleanup of Numeric Input stories
10 changes: 5 additions & 5 deletions packages/perseus-editor/src/__stories__/editor.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {action} from "@storybook/addon-actions";
import * as React from "react";

import {Editor} from "..";
import SideBySide from "../../../../testing/side-by-side";
import SplitView from "../../../../testing/side-by-side";
import {question1} from "../__testdata__/numeric-input.testdata";
import {registerAllWidgetsAndEditorsForTesting} from "../util/register-all-widgets-and-editors-for-testing";

Expand Down Expand Up @@ -85,9 +85,9 @@ export const DemoInteractiveGraph = (): React.ReactElement => {
// class to be above it.
// TODO: Refactor to aphrodite styles instead of scoped CSS in Less.
<div className="framework-perseus">
<SideBySide
leftTitle="Editor"
left={
<SplitView
rendererTitle="Editor"
renderer={
<View style={{width: "360px", margin: "20px"}}>
<Editor
ref={editorRef}
Expand Down Expand Up @@ -128,7 +128,7 @@ export const DemoInteractiveGraph = (): React.ReactElement => {
/>
</View>
}
rightTitle="Serialized Widget Options"
JSONTitle="Serialized Widget Options"
jsonObject={options}
/>
</div>
Expand Down
229 changes: 159 additions & 70 deletions packages/perseus/src/widgets/numeric-input/numeric-input.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,99 +1,188 @@
import {action} from "@storybook/addon-actions";
import * as React from "react";

import {RendererWithDebugUI} from "../../../../../testing/renderer-with-debug-ui";

import {NumericInput} from "./numeric-input.class";
import {question1} from "./numeric-input.testdata";
import {decimalProblem, question1} from "./numeric-input.testdata";

type StoryArgs = {
coefficient: boolean;
currentValue: string;
rightAlign: boolean;
size: "normal" | "small";
};
import type {
PerseusNumericInputWidgetOptions,
PerseusRenderer,
} from "@khanacademy/perseus-core";
import type {Meta} from "@storybook/react";

function generateProps(overwrite) {
const base = {
alignment: "",
answers: [],
containerSizeClass: "medium",
isLastUsedWidget: true,
coefficient: false,
currentValue: "",
problemNum: 0,
rightAlign: false,
size: "normal",
static: false,
widgetId: "widgetId",
findWidgets: action("findWidgets"),
onBlur: action("onBlur"),
onChange: action("onChange"),
onFocus: action("onFocus"),
trackInteraction: action("trackInteraction"),
} as const;
// We're using this format as storybook was not able to infer the type of the options.
// It also gives us a lovely hover view of the JSON structure.
const answerFormsArray: string = `[
{
simplify: string;
name: string;
}
]`;

return {...base, ...overwrite};
}
const answersArray: string = `[
{
message: string;
value: number;
status: string;
answerForms: array<string>;
strict: boolean;
maxError: number;
simplify: string;
}
]`;

export default {
title: "Perseus/Widgets/NumericInput",
const meta: Meta<typeof NumericInput> = {
component: NumericInput,
title: "Perseus/Widgets/Numeric Input",
args: {
coefficient: false,
currentValue: "8675309",
currentValue: "",
rightAlign: false,
size: "normal",
answers: [
{
status: "correct",
maxError: null,
strict: false,
value: 1252,
simplify: "required",
message: "",
},
],
answerForms: [
{simplify: "required", name: "decimal"},
{simplify: "required", name: "integer"},
{simplify: "required", name: "mixed"},
{simplify: "required", name: "percent"},
{simplify: "required", name: "pi"},
],
},
argTypes: {
answers: {
control: {type: "object"},
description:
"A list of all the possible correct and incorrect answers",
table: {
type: {
summary: "array",
detail: answersArray,
},
},
},
answerForms: {
control: {type: "object"},
description:
"Used by examples, maybe not used and should be removed in the future",
table: {
type: {
summary: "array",
detail: answerFormsArray,
},
},
},
currentValue: {
control: {type: "text"},
description: "The current value of the input field",
table: {
type: {summary: "string"},
},
},
coefficient: {
control: {type: "boolean"},
description:
"A coefficient style number allows the student to use - for -1 and an empty string to mean 1.",
table: {
type: {summary: "boolean"},
},
},
labelText: {
control: {type: "text"},
description:
" Translatable Text; Text to describe this input. This will be shown to users using screenreaders.",
value: "What's the answer?",
table: {
type: {summary: "string"},
},
},
rightAlign: {
control: {type: "boolean"},
description: "Whether to right-align the text or not",
table: {
type: {summary: "boolean"},
},
},
size: {
options: ["normal", "small"],
control: {type: "radio"},
defaultValue: "normal",
description:
"Use size 'Normal' for all text boxes, unless there are multiple text boxes in one line and the answer area is too narrow to fit them.",
table: {
type: {summary: "string"},
defaultValue: {summary: "normal"},
},
},
static: {
control: {type: "boolean"},
description: "Always false. Not used for this widget",
table: {
type: {summary: "boolean"},
},
},
// ApiOptions and linterContext are large objects and not particularly applicable to this story,
// so we're hiding them from view to simplify the UI.
apiOptions: {
table: {
disable: true,
},
},
linterContext: {
table: {
disable: true,
},
},
},
};

export const Question1 = (): React.ReactElement => {
return <RendererWithDebugUI question={question1} />;
};
export default meta;

export const Interactive = (args: StoryArgs): React.ReactElement => {
const props = generateProps(args);

return <NumericInput {...props} />;
const updateWidgetOptions = (
question: PerseusRenderer,
widgetId: string,
options: PerseusNumericInputWidgetOptions,
): PerseusRenderer => {
const widget = question.widgets[widgetId];
return {
...question,
widgets: {
[widgetId]: {
...widget,
options: {
...widget.options,
...options,
},
},
},
};
};

export const Sizes = (args: StoryArgs): React.ReactElement => {
const smallProps = generateProps({...args, size: "small"});
const normalProps = generateProps({...args, size: "normal"});

return (
<div>
<label>
Small:
<NumericInput {...smallProps} />
</label>
<label>
Normal:
<NumericInput {...normalProps} />
</label>
</div>
);
export const Default = (
args: PerseusNumericInputWidgetOptions,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After setting up all the Storybook controls, I wasn't really sure if we needed any more stories as we can control most of these options on the fly now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! Perhaps a story for each answerForm, as it IS a little frustrating to edit the json views for that. If possible, it'd be good to get an example of the pi answerForm with a note about how to trigger the "It seems like you've approximated pi" message.

): React.ReactElement => {
const question = updateWidgetOptions(question1, "numeric-input 1", args);
return <RendererWithDebugUI question={question} />;
};
Default.args = question1.widgets["numeric-input 1"].options;

export const TextAlignment = (args: StoryArgs): React.ReactElement => {
const leftProps = generateProps({...args, rightAlign: false});
const rightProps = generateProps({...args, rightAlign: true});

return (
<div>
<label>
Left:
<NumericInput {...leftProps} />
</label>
<label>
Right:
<NumericInput {...rightProps} />
</label>
</div>
export const WithExample = (
args: PerseusNumericInputWidgetOptions,
): React.ReactElement => {
const question = updateWidgetOptions(
decimalProblem,
"numeric-input 1",
args,
);
return <RendererWithDebugUI question={question} />;
};
WithExample.args = decimalProblem.widgets["numeric-input 1"].options;
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,47 @@ export const question1: PerseusRenderer = {
},
};

export const decimalProblem: PerseusRenderer = {
// Added a floating question mark to keep enough space to show the examples.
content: "$12 + 0.52 =$ [[\u2603 numeric-input 1]] \n\n\n\n\n ?",
images: {},
widgets: {
"numeric-input 1": {
graded: true,
version: {
major: 0,
minor: 0,
},
static: false,
type: "numeric-input",
options: {
coefficient: false,
static: false,
answers: [
{
status: "correct",
maxError: null,
strict: false,
value: 12.52,
simplify: "required",
message: "",
answerForms: ["decimal"],
},
],
labelText: "",
size: "normal",
answerForms: [
{
simplify: "required",
name: "decimal",
},
],
},
alignment: "default",
} as NumericInputWidget,
},
};

export const percentageProblem: PerseusRenderer = {
content: "$5008 \\div 4 =$ [[\u2603 numeric-input 1]] ",
images: {},
Expand Down Expand Up @@ -134,6 +175,7 @@ export const multipleAnswersWithDecimals: PerseusRenderer = {
value: 12.2,
simplify: "required",
message: "",
answerForms: ["decimal"],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realized that these should be specifying their answerForms, just for consistency.

},
{
status: "correct",
Expand All @@ -142,10 +184,17 @@ export const multipleAnswersWithDecimals: PerseusRenderer = {
value: 13.4,
simplify: "required",
message: "",
answerForms: ["decimal"],
},
],
labelText: "What's the answer?",
size: "normal",
answerforms: [
{
simplify: "required",
name: "decimal",
},
],
},
alignment: "default",
} as NumericInputWidget,
Expand Down
Loading
Loading