From 138ea81f02af2f5b706f1a2eeefbec0c37e0746b Mon Sep 17 00:00:00 2001 From: Sarah Third Date: Wed, 22 Jan 2025 13:25:10 -0800 Subject: [PATCH 1/4] [its-a-numeric-story] Improving Numeric Input Storybook Stories Modernizing story, ensuring controls work, updating RendererWithDebugUI to also set customKeypad alongside isMobile, and updating SideBySide to hide messy JSON. Issue: LEMS-2449 Test Plan: Ensure all tests pass + manual testing --- .../numeric-input/numeric-input.stories.tsx | 221 ++++++++++++------ .../numeric-input/numeric-input.testdata.ts | 49 ++++ testing/renderer-with-debug-ui.tsx | 8 +- testing/side-by-side.tsx | 2 +- 4 files changed, 207 insertions(+), 73 deletions(-) diff --git a/packages/perseus/src/widgets/numeric-input/numeric-input.stories.tsx b/packages/perseus/src/widgets/numeric-input/numeric-input.stories.tsx index 0222b37690..5e9b5e9f77 100644 --- a/packages/perseus/src/widgets/numeric-input/numeric-input.stories.tsx +++ b/packages/perseus/src/widgets/numeric-input/numeric-input.stories.tsx @@ -1,99 +1,178 @@ -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, +const meta: Meta = { + component: NumericInput, + title: "Perseus/Widgets/Numeric Input", + args: { 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; - - return {...base, ...overwrite}; -} - -export default { - title: "Perseus/Widgets/NumericInput", - args: { - coefficient: false, - currentValue: "8675309", - rightAlign: false, + answers: [ + { + status: "correct", + maxError: null, + strict: false, + value: 1252, + simplify: "required", + message: "", + }, + ], + labelText: "What's the answer?", + answerForms: [], }, argTypes: { + answers: { + control: {type: "object"}, + description: + "A list of all the possible correct and incorrect answers", + type: { + name: "array", + value: { + name: "object", + value: { + message: {name: "string", required: true}, + value: {name: "number"}, + status: {name: "string", required: true}, + answerForms: { + name: "array", + value: {name: "string"}, + }, + strict: {name: "boolean", required: true}, + maxError: {name: "number"}, + simplify: {name: "string"}, + }, + }, + }, + }, + answerForms: { + control: {type: "object"}, + description: + "Used by examples, maybe not used and should be removed in the future", + type: { + name: "array", + value: { + name: "object", + value: { + simplify: {name: "string"}, + name: {name: "string"}, + }, + }, + }, + }, + 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 ; -}; +export default meta; -export const Interactive = (args: StoryArgs): React.ReactElement => { - const props = generateProps(args); - - return ; +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 ( -
- - -
- ); +export const Default = ( + args: PerseusNumericInputWidgetOptions, +): React.ReactElement => { + const question = updateWidgetOptions(question1, "numeric-input 1", args); + return ; }; +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 ( -
- - -
+export const WithExample = ( + args: PerseusNumericInputWidgetOptions, +): React.ReactElement => { + const question = updateWidgetOptions( + decimalProblem, + "numeric-input 1", + args, ); + return ; }; +WithExample.args = decimalProblem.widgets["numeric-input 1"].options; diff --git a/packages/perseus/src/widgets/numeric-input/numeric-input.testdata.ts b/packages/perseus/src/widgets/numeric-input/numeric-input.testdata.ts index f23d28f667..993395e345 100644 --- a/packages/perseus/src/widgets/numeric-input/numeric-input.testdata.ts +++ b/packages/perseus/src/widgets/numeric-input/numeric-input.testdata.ts @@ -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: {}, @@ -134,6 +175,7 @@ export const multipleAnswersWithDecimals: PerseusRenderer = { value: 12.2, simplify: "required", message: "", + answerForms: ["decimal"], }, { status: "correct", @@ -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, diff --git a/testing/renderer-with-debug-ui.tsx b/testing/renderer-with-debug-ui.tsx index 7db7fb17ef..c491520682 100644 --- a/testing/renderer-with-debug-ui.tsx +++ b/testing/renderer-with-debug-ui.tsx @@ -40,6 +40,12 @@ export const RendererWithDebugUI = ({ const [isMobile, setIsMobile] = React.useState(false); const {strings} = usePerseusI18n(); + const controlledAPIOptions = { + ...apiOptions, + isMobile, + customKeypad: isMobile, // Use the mobile keypad for mobile + }; + return ( From a07e3775388de2fad07030a263ac352ebe581653 Mon Sep 17 00:00:00 2001 From: Sarah Third Date: Wed, 22 Jan 2025 13:29:46 -0800 Subject: [PATCH 2/4] [its-a-numeric-story] docs(changeset): Cleanup of Numeric Input stories --- .changeset/six-cars-agree.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/six-cars-agree.md diff --git a/.changeset/six-cars-agree.md b/.changeset/six-cars-agree.md new file mode 100644 index 0000000000..837b86f16b --- /dev/null +++ b/.changeset/six-cars-agree.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Cleanup of Numeric Input stories From 882e953144358d98160ed47258339ec2c8aba996 Mon Sep 17 00:00:00 2001 From: Sarah Third Date: Wed, 22 Jan 2025 14:02:14 -0800 Subject: [PATCH 3/4] [its-a-numeric-story] Improving types --- .../numeric-input/numeric-input.stories.tsx | 62 +++++++++++-------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/packages/perseus/src/widgets/numeric-input/numeric-input.stories.tsx b/packages/perseus/src/widgets/numeric-input/numeric-input.stories.tsx index 5e9b5e9f77..4863b4772d 100644 --- a/packages/perseus/src/widgets/numeric-input/numeric-input.stories.tsx +++ b/packages/perseus/src/widgets/numeric-input/numeric-input.stories.tsx @@ -11,6 +11,27 @@ import type { } from "@khanacademy/perseus-core"; import type {Meta} from "@storybook/react"; +// 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; + } +]`; + +const answersArray: string = `[ + { + message: string; + value: number; + status: string; + answerForms: array; + strict: boolean; + maxError: number; + simplify: string; + } +]`; + const meta: Meta = { component: NumericInput, title: "Perseus/Widgets/Numeric Input", @@ -29,30 +50,23 @@ const meta: Meta = { message: "", }, ], - labelText: "What's the answer?", - answerForms: [], + 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", - type: { - name: "array", - value: { - name: "object", - value: { - message: {name: "string", required: true}, - value: {name: "number"}, - status: {name: "string", required: true}, - answerForms: { - name: "array", - value: {name: "string"}, - }, - strict: {name: "boolean", required: true}, - maxError: {name: "number"}, - simplify: {name: "string"}, - }, + table: { + type: { + summary: "array", + detail: answersArray, }, }, }, @@ -60,14 +74,10 @@ const meta: Meta = { control: {type: "object"}, description: "Used by examples, maybe not used and should be removed in the future", - type: { - name: "array", - value: { - name: "object", - value: { - simplify: {name: "string"}, - name: {name: "string"}, - }, + table: { + type: { + summary: "array", + detail: answerFormsArray, }, }, }, From 7ca1c5b0cf44892a5aa7a3446cb20f8d05ac5be3 Mon Sep 17 00:00:00 2001 From: Sarah Third Date: Wed, 22 Jan 2025 14:57:15 -0800 Subject: [PATCH 4/4] [its-a-numeric-story] Let's do it live! Changing SideBySide to SplitView --- .../src/__stories__/editor.stories.tsx | 10 ++--- testing/renderer-with-debug-ui.tsx | 8 ++-- .../server-item-renderer-with-debug-ui.tsx | 4 +- testing/side-by-side.tsx | 38 +++++++------------ 4 files changed, 25 insertions(+), 35 deletions(-) diff --git a/packages/perseus-editor/src/__stories__/editor.stories.tsx b/packages/perseus-editor/src/__stories__/editor.stories.tsx index e039eeada8..eafc799caf 100644 --- a/packages/perseus-editor/src/__stories__/editor.stories.tsx +++ b/packages/perseus-editor/src/__stories__/editor.stories.tsx @@ -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"; @@ -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.
- { /> } - rightTitle="Serialized Widget Options" + JSONTitle="Serialized Widget Options" jsonObject={options} />
diff --git a/testing/renderer-with-debug-ui.tsx b/testing/renderer-with-debug-ui.tsx index c491520682..20f58c4866 100644 --- a/testing/renderer-with-debug-ui.tsx +++ b/testing/renderer-with-debug-ui.tsx @@ -14,7 +14,7 @@ import {scorePerseusItem} from "../packages/perseus/src/renderer-util"; import {mockStrings} from "../packages/perseus/src/strings"; import {registerAllWidgetsForTesting} from "../packages/perseus/src/util/register-all-widgets-for-testing"; -import SideBySide from "./side-by-side"; +import SplitView from "./side-by-side"; import type {PerseusRenderer} from "@khanacademy/perseus-core"; import type {ComponentProps} from "react"; @@ -47,8 +47,8 @@ export const RendererWithDebugUI = ({ }; return ( - } - left={ + renderer={ { return ( - - {leftTitle} - {left} + + {rendererTitle} + {renderer} - - {rightTitle} + + {JSONTitle}