diff --git a/packages/perseus-editor/src/__tests__/issues-panel.test.tsx b/packages/perseus-editor/src/__tests__/issues-panel.test.tsx index d9714828af8..571b78203b1 100644 --- a/packages/perseus-editor/src/__tests__/issues-panel.test.tsx +++ b/packages/perseus-editor/src/__tests__/issues-panel.test.tsx @@ -7,9 +7,9 @@ import {getFeatureFlags} from "../../../../testing/feature-flags-util"; import IssuesPanel from "../components/issues-panel"; import type {IssueImpact} from "../components/issues-panel"; -import type {APIOptions} from "@khanacademy/perseus"; +import type {APIOptionsWithDefaults} from "@khanacademy/perseus"; -const imageUpdateFFOptions: APIOptions = { +const imageUpdateFFOptions: APIOptionsWithDefaults = { ...ApiOptions.defaults, flags: getFeatureFlags({ "image-widget-upgrade": true, diff --git a/packages/perseus-editor/src/components/widget-editor.tsx b/packages/perseus-editor/src/components/widget-editor.tsx index 752bea230ff..401c7be2305 100644 --- a/packages/perseus-editor/src/components/widget-editor.tsx +++ b/packages/perseus-editor/src/components/widget-editor.tsx @@ -1,5 +1,9 @@ /* eslint-disable @khanacademy/ts-no-error-suppressions */ -import {Widgets, excludeDenylistKeys} from "@khanacademy/perseus"; +import { + APIOptionsContext, + Widgets, + excludeDenylistKeys, +} from "@khanacademy/perseus"; import { CoreWidgetRegistry, applyDefaultsToWidget, @@ -17,9 +21,10 @@ import SectionControlButton from "./section-control-button"; import ToggleableCaret from "./toggleable-caret"; import type Editor from "../editor"; -import type {APIOptions} from "@khanacademy/perseus"; import type {Alignment, PerseusWidget} from "@khanacademy/perseus-core"; +type EditorType = React.ElementRef; + type WidgetEditorProps = { // Unserialized props id: string; @@ -29,7 +34,6 @@ type WidgetEditorProps = { silent?: boolean, ) => unknown; onRemove: () => unknown; - apiOptions: APIOptions; widgetIsOpen?: boolean; } & Omit; @@ -54,7 +58,7 @@ class WidgetEditor extends React.Component< WidgetEditorProps, WidgetEditorState > { - widget: React.RefObject>; + widget: React.RefObject; constructor(props: WidgetEditorProps) { super(props); @@ -135,106 +139,120 @@ class WidgetEditor extends React.Component< alignment: widgetInfo.alignment, static: widgetInfo.static, graded: widgetInfo.graded, - // eslint-disable-next-line react/no-string-refs - options: this.widget.current?.serialize(), + options: this.widget.current?.serialize() ?? widgetInfo.options, version: widgetInfo.version, }; }; render(): React.ReactNode { - const widgetInfo = this.state.widgetInfo; - const isEditingDisabled = - this.props.apiOptions.editingDisabled ?? false; + return ( + + {(apiOptions) => { + const widgetInfo = this.state.widgetInfo; + const isEditingDisabled = + apiOptions.editingDisabled ?? false; - const Ed = Widgets.getEditor(widgetInfo.type); - let supportedAlignments: ReadonlyArray; - const imageUpgradeFF = isFeatureOn(this.props, "image-widget-upgrade"); + const Ed = Widgets.getEditor(widgetInfo.type); + let supportedAlignments: ReadonlyArray; + const imageUpgradeFF = isFeatureOn( + {apiOptions}, + "image-widget-upgrade", + ); - if (widgetInfo.type === "image" && !imageUpgradeFF) { - // TODO(LEMS-3520): Feature flag cleanup - supportedAlignments = ["block", "full-width"]; - } else if (this.props.apiOptions.showAlignmentOptions) { - supportedAlignments = CoreWidgetRegistry.getSupportedAlignments( - widgetInfo.type, - ); - } else { - // NOTE(kevinb): "default" is not one in `validAlignments` in widgets.js. - supportedAlignments = ["default"]; - } + if (widgetInfo.type === "image" && !imageUpgradeFF) { + // TODO(LEMS-3520): Feature flag cleanup + supportedAlignments = ["block", "full-width"]; + } else if (apiOptions.showAlignmentOptions) { + supportedAlignments = + CoreWidgetRegistry.getSupportedAlignments( + widgetInfo.type, + ); + } else { + // NOTE(kevinb): "default" is not one in `validAlignments` in widgets.js. + supportedAlignments = ["default"]; + } - const supportsStaticMode = Widgets.supportsStaticMode(widgetInfo.type); + const supportsStaticMode = Widgets.supportsStaticMode( + widgetInfo.type, + ); - return ( -
-
-
- - - {this.props.id} - -
+ return ( +
+
+
+ + + {this.props.id} + +
- {supportsStaticMode && ( - - )} - {supportedAlignments.length > 1 && ( - - )} - { - this.props.onRemove(); - }} - title="Remove image widget" - /> -
-
- {Ed && ( - - )} -
-
+ {supportsStaticMode && ( + + )} + {supportedAlignments.length > 1 && ( + + )} + { + this.props.onRemove(); + }} + title="Remove image widget" + /> +
+
+ {Ed && ( + + )} +
+
+ ); + }} +
); } } diff --git a/packages/perseus-editor/src/editor.tsx b/packages/perseus-editor/src/editor.tsx index 7364aa32449..9c88c62457a 100644 --- a/packages/perseus-editor/src/editor.tsx +++ b/packages/perseus-editor/src/editor.tsx @@ -163,7 +163,7 @@ type State = { }; // eslint-disable-next-line react/no-unsafe -class EditorInner extends React.Component { +class EditorClass extends React.Component { lastUserValue: string | null | undefined; deferredChange: any | null | undefined; widgetIds: any | null | undefined; @@ -1137,5 +1137,5 @@ class EditorInner extends React.Component { } } -const Editor = withAPIOptions(EditorInner); +const Editor = withAPIOptions(EditorClass); export default Editor; diff --git a/packages/perseus-editor/src/widgets/__docs__/categorizer-editor.stories.tsx b/packages/perseus-editor/src/widgets/__docs__/categorizer-editor.stories.tsx index b6fdea050cd..aed489b4cc2 100644 --- a/packages/perseus-editor/src/widgets/__docs__/categorizer-editor.stories.tsx +++ b/packages/perseus-editor/src/widgets/__docs__/categorizer-editor.stories.tsx @@ -1,4 +1,3 @@ -import {ApiOptions} from "@khanacademy/perseus"; import * as React from "react"; import {action} from "storybook/actions"; @@ -23,7 +22,6 @@ type Story = StoryObj; export const Default: Story = { args: { onChange: action("onChange"), - apiOptions: ApiOptions.defaults, }, }; diff --git a/packages/perseus-editor/src/widgets/__docs__/plotter-editor.stories.tsx b/packages/perseus-editor/src/widgets/__docs__/plotter-editor.stories.tsx index 403c19b5010..1c9286b7d84 100644 --- a/packages/perseus-editor/src/widgets/__docs__/plotter-editor.stories.tsx +++ b/packages/perseus-editor/src/widgets/__docs__/plotter-editor.stories.tsx @@ -1,4 +1,3 @@ -import {ApiOptions} from "@khanacademy/perseus"; import {action} from "storybook/actions"; import PlotterEditor from "../plotter-editor"; @@ -16,7 +15,6 @@ type Story = StoryObj; export const Default: Story = { args: { onChange: action("onChange"), - apiOptions: ApiOptions.defaults, categories: ["0", "1", "2"], plotDimensions: [300, 300], correct: [0, 1, 2], diff --git a/packages/perseus-editor/src/widgets/__docs__/radio-editor.stories.tsx b/packages/perseus-editor/src/widgets/__docs__/radio-editor.stories.tsx index c5bc0c5f3ae..867d4e23fe4 100644 --- a/packages/perseus-editor/src/widgets/__docs__/radio-editor.stories.tsx +++ b/packages/perseus-editor/src/widgets/__docs__/radio-editor.stories.tsx @@ -27,7 +27,6 @@ type Story = StoryObj; export const Default: Story = { args: { onChange: action("onChange"), - apiOptions: Object.freeze({}), static: false, }, }; diff --git a/packages/perseus-editor/src/widgets/__tests__/categorizer-editor.test.tsx b/packages/perseus-editor/src/widgets/__tests__/categorizer-editor.test.tsx index caf44934ef8..f98772a7975 100644 --- a/packages/perseus-editor/src/widgets/__tests__/categorizer-editor.test.tsx +++ b/packages/perseus-editor/src/widgets/__tests__/categorizer-editor.test.tsx @@ -1,4 +1,4 @@ -import {ApiOptions, Dependencies} from "@khanacademy/perseus"; +import {Dependencies} from "@khanacademy/perseus"; import {render, screen} from "@testing-library/react"; import {userEvent as userEventLib} from "@testing-library/user-event"; import * as React from "react"; @@ -21,12 +21,7 @@ describe("categorizer-editor", () => { }); it("should render", async () => { - render( - undefined} - apiOptions={ApiOptions.defaults} - />, - ); + render( undefined} />); expect( await screen.findByText("Randomize item order"), @@ -36,12 +31,7 @@ describe("categorizer-editor", () => { it("should be possible to change randomize item order", async () => { const onChangeMock = jest.fn(); - render( - , - ); + render(); await userEvent.click( screen.getByRole("checkbox", { diff --git a/packages/perseus-editor/src/widgets/__tests__/image-editor.test.tsx b/packages/perseus-editor/src/widgets/__tests__/image-editor.test.tsx index a7a525018c8..d3988334118 100644 --- a/packages/perseus-editor/src/widgets/__tests__/image-editor.test.tsx +++ b/packages/perseus-editor/src/widgets/__tests__/image-editor.test.tsx @@ -1,4 +1,9 @@ -import {ApiOptions, Dependencies, Util} from "@khanacademy/perseus"; +import { + ApiOptions, + APIOptionsContext, + Dependencies, + Util, +} from "@khanacademy/perseus"; import {act, render, screen, fireEvent} from "@testing-library/react"; import {userEvent as userEventLib} from "@testing-library/user-event"; import * as React from "react"; @@ -32,7 +37,9 @@ const apiOptions = { const ImageEditorWithDependencies = (props: PropsFor) => { return ( - + + + ); }; @@ -73,7 +80,6 @@ describe("image editor", () => { // Act render( {}} backgroundImage={backgroundImage} />, @@ -103,7 +109,6 @@ describe("image editor", () => { // Act render( { // Act render( {}} />, @@ -200,7 +204,6 @@ describe("image editor", () => { const onChangeMock = jest.fn(); render( , @@ -223,7 +226,6 @@ describe("image editor", () => { render( {}} @@ -246,7 +248,6 @@ describe("image editor", () => { render( {}} /> @@ -267,7 +268,6 @@ describe("image editor", () => { // Act render( {}} />, @@ -287,7 +287,6 @@ describe("image editor", () => { const onChangeMock = jest.fn(); render( , @@ -313,12 +312,7 @@ describe("image editor", () => { it("should call onChange with empty image url", async () => { // Arrange const onChangeMock = jest.fn(); - render( - , - ); + render(); // Act const urlField = screen.getByRole("textbox", {name: "Image URL"}); @@ -342,7 +336,6 @@ describe("image editor", () => { const onChangeMock = jest.fn(); render( , @@ -371,7 +364,6 @@ describe("image editor", () => { const onChangeMock = jest.fn(); render( { const onChangeMock = jest.fn(); render( { const onChangeMock = jest.fn(); render( { const onChangeMock = jest.fn(); render( , @@ -483,7 +472,6 @@ describe("image editor", () => { const onChangeMock = jest.fn(); render( , @@ -505,7 +493,6 @@ describe("image editor", () => { const onChangeMock = jest.fn(); render( { const onChangeMock = jest.fn(); render( , @@ -551,7 +537,6 @@ describe("image editor", () => { const onChangeMock = jest.fn(); render( { // Arrange const onChangeMock = jest.fn(); render( - , + + + + + , ); // Assert @@ -592,7 +580,6 @@ describe("image editor", () => { const onChangeMock = jest.fn(); render( , @@ -614,7 +601,6 @@ describe("image editor", () => { const onChangeMock = jest.fn(); render( { const onChangeMock = jest.fn(); render( , @@ -657,7 +642,6 @@ describe("image editor", () => { const onChangeMock = jest.fn(); render( { const onChangeMock = jest.fn(); render( , @@ -702,7 +685,6 @@ describe("image editor", () => { const onChangeMock = jest.fn(); render( , @@ -722,7 +704,6 @@ describe("image editor", () => { const onChangeMock = jest.fn(); render( { // Arrange & Act render( {}} />, @@ -766,14 +746,21 @@ describe("image editor", () => { it("should not render feature flag is disabled", () => { // Arrange & Act render( - {}} - />, + + + {}} + /> + + , ); // Assert @@ -789,7 +776,6 @@ describe("image editor", () => { // Arrange & Act render( {}} @@ -809,7 +795,6 @@ describe("image editor", () => { const onChangeMock = jest.fn(); render( , diff --git a/packages/perseus-editor/src/widgets/__tests__/interactive-graph-editor-locked-figures.test.tsx b/packages/perseus-editor/src/widgets/__tests__/interactive-graph-editor-locked-figures.test.tsx index eeee93d245d..8ff5fea9a64 100644 --- a/packages/perseus-editor/src/widgets/__tests__/interactive-graph-editor-locked-figures.test.tsx +++ b/packages/perseus-editor/src/widgets/__tests__/interactive-graph-editor-locked-figures.test.tsx @@ -1,4 +1,4 @@ -import {ApiOptions, Dependencies} from "@khanacademy/perseus"; +import {Dependencies} from "@khanacademy/perseus"; import {RenderStateRoot} from "@khanacademy/wonder-blocks-core"; import {render, screen} from "@testing-library/react"; import {userEvent as userEventLib} from "@testing-library/user-event"; @@ -16,7 +16,6 @@ const defaultLine = getDefaultFigureForType("line"); const defaultPolygon = getDefaultFigureForType("polygon"); const baseProps = { - apiOptions: ApiOptions.defaults, box: [288, 288] as [number, number], gridStep: [1, 1] as [number, number], snapStep: [1, 1] as [number, number], diff --git a/packages/perseus-editor/src/widgets/__tests__/interactive-graph-editor.test.tsx b/packages/perseus-editor/src/widgets/__tests__/interactive-graph-editor.test.tsx index d56a7edceff..a3dbae02f61 100644 --- a/packages/perseus-editor/src/widgets/__tests__/interactive-graph-editor.test.tsx +++ b/packages/perseus-editor/src/widgets/__tests__/interactive-graph-editor.test.tsx @@ -1,4 +1,4 @@ -import {ApiOptions, Dependencies} from "@khanacademy/perseus"; +import {Dependencies} from "@khanacademy/perseus"; import {RenderStateRoot} from "@khanacademy/wonder-blocks-core"; import {render, screen, waitFor} from "@testing-library/react"; import {userEvent as userEventLib} from "@testing-library/user-event"; @@ -13,7 +13,6 @@ import type {PropsFor} from "@khanacademy/wonder-blocks-core"; import type {UserEvent} from "@testing-library/user-event"; const baseProps = { - apiOptions: ApiOptions.defaults, box: [288, 288] as [number, number], gridStep: [1, 1] as [number, number], snapStep: [1, 1] as [number, number], diff --git a/packages/perseus-editor/src/widgets/__tests__/label-image-editor.test.tsx b/packages/perseus-editor/src/widgets/__tests__/label-image-editor.test.tsx index 31659019926..45f081f9f10 100644 --- a/packages/perseus-editor/src/widgets/__tests__/label-image-editor.test.tsx +++ b/packages/perseus-editor/src/widgets/__tests__/label-image-editor.test.tsx @@ -1,4 +1,8 @@ -import {Dependencies, ApiOptions} from "@khanacademy/perseus"; +import { + Dependencies, + ApiOptions, + APIOptionsContext, +} from "@khanacademy/perseus"; import {render, screen} from "@testing-library/react"; import {userEvent as userEventLib} from "@testing-library/user-event"; import * as React from "react"; @@ -10,7 +14,6 @@ import type {PreferredPopoverDirection} from "../label-image-editor/behavior"; import type {UserEvent} from "@testing-library/user-event"; const defaultProps = { - apiOptions: ApiOptions.defaults, choices: ["Choice 1", "Choice 2", "Choice 3"], imageAlt: "Test image alt text", imageUrl: "https://example.com/test-image.png", @@ -155,11 +158,11 @@ describe("label-image-editor", () => { const onChangeMock = jest.fn(); render( - , + + + , ); // Act diff --git a/packages/perseus-editor/src/widgets/__tests__/radio-editor.test.tsx b/packages/perseus-editor/src/widgets/__tests__/radio-editor.test.tsx index 2d7a79095c7..68390dba889 100644 --- a/packages/perseus-editor/src/widgets/__tests__/radio-editor.test.tsx +++ b/packages/perseus-editor/src/widgets/__tests__/radio-editor.test.tsx @@ -1,4 +1,4 @@ -import {Dependencies, ApiOptions} from "@khanacademy/perseus"; +import {Dependencies} from "@khanacademy/perseus"; import {RenderStateRoot} from "@khanacademy/wonder-blocks-core"; import {render, screen} from "@testing-library/react"; import {userEvent as userEventLib} from "@testing-library/user-event"; @@ -35,12 +35,7 @@ function renderRadioEditor( props: Partial = {}, ) { return render( - , + , {wrapper: RenderStateRoot}, ); } @@ -325,7 +320,6 @@ describe("radio-editor", () => { {}} - apiOptions={ApiOptions.defaults} static={false} choices={fourChoices} />, @@ -352,7 +346,6 @@ describe("radio-editor", () => { {}} - apiOptions={ApiOptions.defaults} static={false} choices={[ getCorrectChoice(), @@ -375,7 +368,6 @@ describe("radio-editor", () => { render( { render( { render( { render( { render( { {}} - apiOptions={ApiOptions.defaults} static={false} />, {wrapper: RenderStateRoot}, @@ -1343,7 +1330,6 @@ describe("radio-editor", () => { {}} - apiOptions={ApiOptions.defaults} static={false} />, {wrapper: RenderStateRoot}, @@ -1366,7 +1352,6 @@ describe("radio-editor", () => { {}} - apiOptions={ApiOptions.defaults} static={false} />, {wrapper: RenderStateRoot}, @@ -1390,7 +1375,6 @@ describe("radio-editor", () => { {}} - apiOptions={ApiOptions.defaults} static={false} />, {wrapper: RenderStateRoot}, diff --git a/packages/perseus-editor/src/widgets/categorizer-editor.tsx b/packages/perseus-editor/src/widgets/categorizer-editor.tsx index 82c9d5fac84..1f23c665d37 100644 --- a/packages/perseus-editor/src/widgets/categorizer-editor.tsx +++ b/packages/perseus-editor/src/widgets/categorizer-editor.tsx @@ -1,13 +1,12 @@ import { components, - ApiOptions, Categorizer as CategorizerWidget, Changeable, EditorJsonify, + APIOptionsContext, } from "@khanacademy/perseus"; import {categorizerLogic} from "@khanacademy/perseus-core"; import {Checkbox} from "@khanacademy/wonder-blocks-form"; -import PropTypes from "prop-types"; import * as React from "react"; import _ from "underscore"; @@ -17,22 +16,19 @@ import type {PropsFor} from "@khanacademy/wonder-blocks-core"; const {TextListEditor} = components; const Categorizer = CategorizerWidget.widget; -type Props = any; +type Props = { + items: string[]; + categories: string[]; + values: number[]; + randomizeItems: boolean; + onChange: (options: any) => void; +}; // JSDoc will be shown in Storybook widget editor description /** * An editor for adding a categorizer widget that allows users to sort items into categories. */ class CategorizerEditor extends React.Component { - static propTypes = { - ...Changeable.propTypes, - apiOptions: ApiOptions.propTypes, - items: PropTypes.arrayOf(PropTypes.string), - categories: PropTypes.arrayOf(PropTypes.string), - values: PropTypes.arrayOf(PropTypes.number), - randomizeItems: PropTypes.bool, - }; - static widgetName = "categorizer" as const; static defaultProps: CategorizerDefaultWidgetOptions = @@ -54,57 +50,68 @@ class CategorizerEditor extends React.Component { handleUserInput: (userInput) => { this.props.onChange({values: userInput.values}); }, - apiOptions: this.props.apiOptions, trackInteraction: function () {}, }; return ( -
-
- { - this.props.onChange({randomizeItems: value}); - }} - /> -
- Categories: - { - // @ts-expect-error - TS2554 - Expected 3 arguments, but got 2. - this.change("categories", cat); - }} - layout="horizontal" - /> - Items: - { - // @ts-expect-error - TS2554 - Expected 3 arguments, but got 1. - this.change({ - items: items, - // TODO(eater): This truncates props.values so there - // are never more correct answers than items, - // ensuring the widget is possible to answer - // correctly. It doesn't necessarly keep each - // answer with its corresponding item if an item - // is deleted from the middle. Inconvenient, but - // it's at least possible for content creators to - // catch and fix. - values: _.first(this.props.values, items.length), - }); - }} - layout="vertical" - /> - {/* There are a bunch of props that renderer.jsx passes to each widget + + {(apiOptions) => ( +
+
+ { + this.props.onChange({ + randomizeItems: value, + }); + }} + /> +
+ Categories: + { + // @ts-expect-error - TS2554 - Expected 3 arguments, but got 2. + this.change("categories", cat); + }} + layout="horizontal" + /> + Items: + { + // @ts-expect-error - TS2554 - Expected 3 arguments, but got 1. + this.change({ + items: items, + // TODO(eater): This truncates props.values so there + // are never more correct answers than items, + // ensuring the widget is possible to answer + // correctly. It doesn't necessarly keep each + // answer with its corresponding item if an item + // is deleted from the middle. Inconvenient, but + // it's at least possible for content creators to + // catch and fix. + values: _.first( + this.props.values, + items.length, + ), + }); + }} + layout="vertical" + /> + {/* There are a bunch of props that renderer.jsx passes to each widget via widget-container.jsx that we aren't passing to Categorizer here. See perseus-all-package/types.js#WidgetProps for details. */} - )} - /> -
+ )} + apiOptions={apiOptions} + /> +
+ )} + ); } } diff --git a/packages/perseus-editor/src/widgets/definition-editor.tsx b/packages/perseus-editor/src/widgets/definition-editor.tsx index 4850550851d..c30a4ad6aa7 100644 --- a/packages/perseus-editor/src/widgets/definition-editor.tsx +++ b/packages/perseus-editor/src/widgets/definition-editor.tsx @@ -1,17 +1,21 @@ /* eslint-disable @khanacademy/ts-no-error-suppressions */ import {components, Changeable, EditorJsonify} from "@khanacademy/perseus"; -import {definitionLogic} from "@khanacademy/perseus-core"; -import PropTypes from "prop-types"; +import { + definitionLogic, + type DefinitionDefaultWidgetOptions, +} from "@khanacademy/perseus-core"; import * as React from "react"; import _ from "underscore"; import Editor from "../editor"; -import type {DefinitionDefaultWidgetOptions} from "@khanacademy/perseus-core"; - const {TextInput} = components; -type Props = any; +type Props = { + togglePrompt: string; + definition: string; + onChange: (options: any) => void; +}; // JSDoc will be shown in Storybook widget editor description /** @@ -19,13 +23,6 @@ type Props = any; * editors to embed clickable terms with expandable explanations within content. */ class DefinitionEditor extends React.Component { - static propTypes = { - ...Changeable.propTypes, - togglePrompt: PropTypes.string, - definition: PropTypes.string, - apiOptions: PropTypes.any, - }; - static widgetName = "definition" as const; static defaultProps: DefinitionDefaultWidgetOptions = diff --git a/packages/perseus-editor/src/widgets/explanation-editor.tsx b/packages/perseus-editor/src/widgets/explanation-editor.tsx index 9943cfb9668..64dfba4e7ca 100644 --- a/packages/perseus-editor/src/widgets/explanation-editor.tsx +++ b/packages/perseus-editor/src/widgets/explanation-editor.tsx @@ -2,7 +2,6 @@ /* eslint-disable react/forbid-prop-types */ import {components, Changeable, EditorJsonify} from "@khanacademy/perseus"; import {explanationLogic} from "@khanacademy/perseus-core"; -import PropTypes from "prop-types"; import * as React from "react"; import _ from "underscore"; @@ -12,30 +11,24 @@ import type {ExplanationDefaultWidgetOptions} from "@khanacademy/perseus-core"; const {TextInput} = components; -type Props = any; -type State = any; +type Props = { + showPrompt: string; + hidePrompt: string; + explanation: string; + widgets: any; + onChange: (options: any) => void; +}; // JSDoc will be shown in Storybook widget editor description /** * An editor for adding an explanation widget that provides supplementary information to users. */ -class ExplanationEditor extends React.Component { - static propTypes = { - ...Changeable.propTypes, - showPrompt: PropTypes.string, - hidePrompt: PropTypes.string, - explanation: PropTypes.string, - widgets: PropTypes.object, - apiOptions: PropTypes.any, - }; - +class ExplanationEditor extends React.Component { static widgetName = "explanation" as const; static defaultProps: ExplanationDefaultWidgetOptions = explanationLogic.defaultWidgetOptions; - state: State = {}; - change: (arg1: any, arg2: any, arg3: any) => any = (...args) => { return Changeable.change.apply(this, args); }; diff --git a/packages/perseus-editor/src/widgets/graded-group-editor.tsx b/packages/perseus-editor/src/widgets/graded-group-editor.tsx index 62cbb46ce15..83484333684 100644 --- a/packages/perseus-editor/src/widgets/graded-group-editor.tsx +++ b/packages/perseus-editor/src/widgets/graded-group-editor.tsx @@ -1,38 +1,29 @@ /* eslint-disable @khanacademy/ts-no-error-suppressions */ /* eslint-disable react/forbid-prop-types */ +import {components, Changeable, iconTrash} from "@khanacademy/perseus"; import { - components, - ApiOptions, - Changeable, - iconTrash, -} from "@khanacademy/perseus"; -import {gradedGroupLogic} from "@khanacademy/perseus-core"; + gradedGroupLogic, + type GradedGroupDefaultWidgetOptions, + type PerseusRenderer, +} from "@khanacademy/perseus-core"; import {StyleSheet, css} from "aphrodite"; -import PropTypes from "prop-types"; import * as React from "react"; import Editor from "../editor"; import {iconPlus} from "../styles/icon-paths"; -import type { - GradedGroupDefaultWidgetOptions, - PerseusRenderer, -} from "@khanacademy/perseus-core"; - const {InlineIcon, TextInput} = components; -type Props = any; +type Props = { + title: string; + content: string; + widgets: any; + images: any; + hint: PerseusRenderer | null | undefined; + onChange: (options: any, callback?: () => void) => void; +}; class GradedGroupEditor extends React.Component { - static propTypes = { - ...Changeable.propTypes, - title: PropTypes.string, - content: PropTypes.string, - widgets: PropTypes.object, - images: PropTypes.object, - apiOptions: ApiOptions.propTypes, - }; - static widgetName = "graded-group" as const; static defaultProps: GradedGroupDefaultWidgetOptions = @@ -111,13 +102,9 @@ class GradedGroupEditor extends React.Component {
Hint
{ diff --git a/packages/perseus-editor/src/widgets/graded-group-set-editor.tsx b/packages/perseus-editor/src/widgets/graded-group-set-editor.tsx index bb28171fe77..2d01917555f 100644 --- a/packages/perseus-editor/src/widgets/graded-group-set-editor.tsx +++ b/packages/perseus-editor/src/widgets/graded-group-set-editor.tsx @@ -1,26 +1,22 @@ /* eslint-disable react/forbid-prop-types, react/no-unsafe */ -import {ApiOptions, Changeable} from "@khanacademy/perseus"; -import {gradedGroupSetLogic} from "@khanacademy/perseus-core"; -import PropTypes from "prop-types"; +import {Changeable} from "@khanacademy/perseus"; +import { + gradedGroupSetLogic, + type GradedGroupSetDefaultWidgetOptions, +} from "@khanacademy/perseus-core"; import * as React from "react"; import GradedGroupEditor from "./graded-group-editor"; -import type {GradedGroupSetDefaultWidgetOptions} from "@khanacademy/perseus-core"; - -type Props = any; +type Props = { + gradedGroups: Array; + onChange: (options: any) => void; +}; class GradedGroupSetEditor extends React.Component { // @ts-expect-error - TS2564 - Property '_editors' has no initializer and is not definitely assigned in the constructor. _editors: Array; - static propTypes = { - ...Changeable.propTypes, - apiOptions: ApiOptions.propTypes, - gradedGroups: PropTypes.array, - onChange: PropTypes.func.isRequired, - }; - static widgetName = "graded-group-set" as const; static defaultProps: GradedGroupSetDefaultWidgetOptions = @@ -51,16 +47,12 @@ class GradedGroupSetEditor extends React.Component { }; }; - renderGroups: () => React.ReactElement = () => { - if (!this.props.gradedGroups) { - return null; - } + renderGroups: () => React.ReactElement[] = () => { return this.props.gradedGroups.map((group, i) => ( (this._editors[i] = el)} {...group} - apiOptions={this.props.apiOptions} widgetEnabled={true} immutableWidgets={false} onChange={(data) => @@ -78,7 +70,7 @@ class GradedGroupSetEditor extends React.Component { }; addGroup: () => void = () => { - const groups = this.props.gradedGroups || []; + const groups = this.props.gradedGroups ?? []; // @ts-expect-error - TS2554 - Expected 3 arguments, but got 2. this.change( "gradedGroups", diff --git a/packages/perseus-editor/src/widgets/grapher-editor.tsx b/packages/perseus-editor/src/widgets/grapher-editor.tsx index 6635a9c3168..b5a086f5bdc 100644 --- a/packages/perseus-editor/src/widgets/grapher-editor.tsx +++ b/packages/perseus-editor/src/widgets/grapher-editor.tsx @@ -6,6 +6,7 @@ import { GrapherWidget, containerSizeClass, getInteractiveBoxFromSizeClass, + APIOptionsContext, } from "@khanacademy/perseus"; import { GrapherUtil as CoreGrapherUtil, @@ -27,13 +28,14 @@ const Grapher = GrapherWidget.widget; const {chooseType, defaultPlotProps, getEquationString, typeToButton} = GrapherUtil; -type Props = any; +type Props = { + availableTypes: Array; + correct: any; + graph: any; + onChange: (options: any, callback?: () => void) => void; +}; class GrapherEditor extends React.Component { - static propTypes = { - ...Changeable.propTypes, - }; - static widgetName = "grapher" as const; static defaultProps: GrapherDefaultWidgetOptions = @@ -67,90 +69,106 @@ class GrapherEditor extends React.Component { }; render(): React.ReactNode { - const sizeClass = containerSizeClass.SMALL; - let equationString; - let graph; - if (this.props.graph.valid === true) { - const graphProps: Partial> = { - apiOptions: this.props.apiOptions, - containerSizeClass: sizeClass, - graph: this.props.graph, - userInput: this.props.correct, - correct: this.props.correct, - handleUserInput: (userInput, cb) => { - let correct = this.props.correct; - if (correct.type === userInput?.type) { - correct = _.extend({}, correct, userInput); + return ( + + {(apiOptions) => { + const sizeClass = containerSizeClass.SMALL; + let equationString; + let graph; + if (this.props.graph.valid === true) { + const graphProps: Partial> = { + apiOptions: apiOptions, + containerSizeClass: sizeClass, + graph: this.props.graph, + userInput: this.props.correct, + correct: this.props.correct, + handleUserInput: (userInput, cb) => { + let correct = this.props.correct; + if (correct.type === userInput?.type) { + correct = _.extend({}, correct, userInput); + } else { + // Clear options from previous graph + correct = userInput; + } + this.props.onChange({correct: correct}, cb); + }, + availableTypes: this.props.availableTypes, + trackInteraction: function () {}, + // Set the "correct answer" graph to static when editing is disabled + static: apiOptions.editingDisabled, + }; + + graph = ( + // NOTE(jeremy): This editor doesn't pass in a bunch of + // standard props that the Renderer provides normally (eg. + // alignment, findWidgets, etc). + )} + /> + ); + equationString = getEquationString( + graphProps.userInput as GrapherAnswerTypes, + ); } else { - // Clear options from previous graph - correct = userInput; + graph = ( +
+ {this.props.graph.valid} +
+ ); } - this.props.onChange({correct: correct}, cb); - }, - availableTypes: this.props.availableTypes, - trackInteraction: function () {}, - // Set the "correct answer" graph to static when editing is disabled - static: this.props.apiOptions.editingDisabled, - }; - - graph = ( - // NOTE(jeremy): This editor doesn't pass in a bunch of - // standard props that the Renderer provides normally (eg. - // alignment, findWidgets, etc). - )} /> - ); - equationString = getEquationString( - graphProps.userInput as GrapherAnswerTypes, - ); - } else { - graph = ( -
{this.props.graph.valid}
- ); - } - - return ( -
-
- Correct answer{" "} - -

- Graph the correct answer in the graph below and - ensure the equation or point coordinates displayed - represent the correct answer. -

-
{" "} - : {equationString} -
- -
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control -- TODO(LEMS-2871): Address a11y error */} - - -
- {graph} -
+ return ( +
+
+ Correct answer{" "} + +

+ Graph the correct answer in the graph + below and ensure the equation or point + coordinates displayed represent the + correct answer. +

+
{" "} + : {equationString} +
+ + +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control -- TODO(LEMS-2871): Address a11y error */} + + +
+ {graph} +
+ ); + }} +
); } } diff --git a/packages/perseus-editor/src/widgets/group-editor.tsx b/packages/perseus-editor/src/widgets/group-editor.tsx index 2cbb2abff50..0d3ee06c0aa 100644 --- a/packages/perseus-editor/src/widgets/group-editor.tsx +++ b/packages/perseus-editor/src/widgets/group-editor.tsx @@ -1,26 +1,23 @@ /* eslint-disable @khanacademy/ts-no-error-suppressions */ /* eslint-disable react/forbid-prop-types */ -import {ApiOptions} from "@khanacademy/perseus"; import { groupLogic, type GroupDefaultWidgetOptions, + type PerseusWidgetsMap, } from "@khanacademy/perseus-core"; -import PropTypes from "prop-types"; import * as React from "react"; import _ from "underscore"; import Editor from "../editor"; -type Props = any; +type Props = { + content: string; + widgets: PerseusWidgetsMap; + images: any; + onChange: (options: any) => void; +}; class GroupEditor extends React.Component { - static propTypes = { - content: PropTypes.string, - widgets: PropTypes.object, - images: PropTypes.object, - apiOptions: ApiOptions.propTypes, - }; - static widgetName = "group" as const; static defaultProps: GroupDefaultWidgetOptions = diff --git a/packages/perseus-editor/src/widgets/image-editor/components/image-settings.tsx b/packages/perseus-editor/src/widgets/image-editor/components/image-settings.tsx index 1b3ea063a94..931e89397be 100644 --- a/packages/perseus-editor/src/widgets/image-editor/components/image-settings.tsx +++ b/packages/perseus-editor/src/widgets/image-editor/components/image-settings.tsx @@ -1,4 +1,4 @@ -import {components} from "@khanacademy/perseus"; +import {components, useAPIOptionsContext} from "@khanacademy/perseus"; import {isFeatureOn} from "@khanacademy/perseus-core"; import {LabeledField} from "@khanacademy/wonder-blocks-labeled-field"; import * as React from "react"; @@ -23,13 +23,13 @@ const altTextTooShortError = export default function ImageSettings({ alt, backgroundImage, - apiOptions, caption, decorative, longDescription, title, onChange, }: Props) { + const apiOptions = useAPIOptionsContext(); const imageUpgradeFF = isFeatureOn({apiOptions}, "image-widget-upgrade"); const [altFieldError, setAltFieldError] = React.useState( null, diff --git a/packages/perseus-editor/src/widgets/image-editor/image-editor.tsx b/packages/perseus-editor/src/widgets/image-editor/image-editor.tsx index 514075157e0..35daf91e40f 100644 --- a/packages/perseus-editor/src/widgets/image-editor/image-editor.tsx +++ b/packages/perseus-editor/src/widgets/image-editor/image-editor.tsx @@ -9,10 +9,7 @@ import * as React from "react"; import ImageSettings from "./components/image-settings"; import ImageUrlInput from "./components/image-url-input"; -import type {APIOptions} from "@khanacademy/perseus"; - export interface Props extends PerseusImageWidgetOptions { - apiOptions: APIOptions; onChange: (newValues: Partial) => void; } diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/interactive-graph-editor.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/interactive-graph-editor.tsx index cab7b33e0fe..59635ff2ec0 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/interactive-graph-editor.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/interactive-graph-editor.tsx @@ -1,5 +1,6 @@ import {vector as kvector} from "@khanacademy/kmath"; import { + APIOptionsContext, containerSizeClass, getInteractiveBoxFromSizeClass, InteractiveGraphWidget, @@ -37,7 +38,6 @@ import LockedFiguresSection from "./locked-figures/locked-figures-section"; import StartCoordsSettings from "./start-coords/start-coords-settings"; import {getStartCoords, shouldShowStartCoordsUI} from "./start-coords/util"; -import type {APIOptionsWithDefaults} from "@khanacademy/perseus"; import type {PropsFor} from "@khanacademy/wonder-blocks-core"; const InteractiveGraph = InteractiveGraphWidget.widget; @@ -47,8 +47,6 @@ type InteractiveGraphProps = PropsFor; type Range = [min: number, max: number]; export type Props = { - apiOptions: APIOptionsWithDefaults; - /** * The labels for the x and y axes. */ @@ -167,6 +165,8 @@ class InteractiveGraphEditor extends React.Component { lockedFigures: [], }; + graphRef = React.createRef>(); + changeStartCoords = (coords) => { if (!this.props.graph?.type) { return; @@ -202,11 +202,9 @@ class InteractiveGraphEditor extends React.Component { "fullGraphAriaDescription", ); - // eslint-disable-next-line react/no-string-refs - const graph = this.refs.graph; + const graph = this.graphRef.current; // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (graph) { - // @ts-expect-error TS2339 Property 'getUserInput' does not exist on type 'ReactInstance'. Property 'getUserInput' does not exist on type 'Component'. // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions const correct = graph && graph.getUserInput(); _.extend(json, { @@ -268,204 +266,234 @@ class InteractiveGraphEditor extends React.Component { }; render() { - let graph; - let equationString; - - const gridStep = - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - this.props.gridStep || - Util.getGridStep( - this.props.range, - this.props.step, - interactiveSizes.defaultBoxSize, - ); - const snapStep = - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - this.props.snapStep || Util.snapStepFromGridStep(gridStep); - - const sizeClass = containerSizeClass.SMALL; - - const editingDisabled = this.props.apiOptions?.editingDisabled ?? false; - - if (this.props.valid === true) { - const correct = this.props.correct; - - // TODO(aria): send these down all at once - const graphProps = { - ref: "graph", - box: this.props.box, - range: this.props.range, - showAxisArrows: this.props.showAxisArrows, - labels: this.props.labels, - labelLocation: this.props.labelLocation, - step: this.props.step, - gridStep: gridStep, - snapStep: snapStep, - backgroundImage: this.props.backgroundImage, - markings: this.props.markings, - showProtractor: this.props.showProtractor, - showTooltips: this.props.showTooltips, - lockedFigures: this.props.lockedFigures, - fullGraphAriaLabel: this.props.fullGraphAriaLabel, - fullGraphAriaDescription: this.props.fullGraphAriaDescription, - // Set the "correct answer" graph to static when editing is disabled - static: editingDisabled, - trackInteraction: function () {}, - userInput: correct, - handleUserInput: ( - newGraph: InteractiveGraphProps["userInput"], - ) => { - let correct = this.props.correct; - // TODO(benchristel): can we improve the type of onChange - // so this invariant isn't necessary? - invariant(newGraph != null); - if (correct.type === newGraph.type) { - correct = mergeGraphs(correct, newGraph); - } else { - // Clear options from previous graph - correct = newGraph; - } - this.props.onChange({ - correct: correct, - graph: this.props.graph, - }); - }, - } as const; - - graph = ( - // There are a bunch of props that renderer.jsx passes to widgets via - // getWidgetProps() and widget-container.jsx that the editors don't - // bother passing. - // @ts-expect-error - TS2769 - No overload matches this call. - - ); - // TODO(kevinb): Update getEquationString to only accept the data it actually - // needs to compute the equation string. - // @ts-expect-error - TS2345 - Argument of type '{ readonly ref: "graph"; readonly box: any; readonly range: any; readonly labels: any; readonly step: any; readonly gridStep: any; readonly snapStep: any; readonly graph: any; readonly backgroundImage: any; ... 6 more ...; readonly onChange: (newProps: Pick<...> & ... 1 more ... & InexactPartial<...>) => void; }' is not assignable to parameter of type 'Props'. - equationString = InteractiveGraph.getEquationString(graphProps); - } else { - graph =
{this.props.valid}
; - } - return ( - - {(graphId) => ( - - - ["userInput"]["type"], - ) => { - this.props.onChange({ - graph: {type}, - correct: {type}, - }); - }) as any + + {(apiOptions) => { + let graph; + let equationString; + + const gridStep = + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + this.props.gridStep || + Util.getGridStep( + this.props.range, + this.props.step, + interactiveSizes.defaultBoxSize, + ); + const snapStep = + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + this.props.snapStep || + Util.snapStepFromGridStep(gridStep); + + const sizeClass = containerSizeClass.SMALL; + + const editingDisabled = + apiOptions?.editingDisabled ?? false; + + if (this.props.valid === true) { + const correct = this.props.correct; + + // TODO(aria): send these down all at once + const graphProps = { + ref: this.graphRef, + box: this.props.box, + range: this.props.range, + showAxisArrows: this.props.showAxisArrows, + labels: this.props.labels, + labelLocation: this.props.labelLocation, + step: this.props.step, + gridStep: gridStep, + snapStep: snapStep, + backgroundImage: this.props.backgroundImage, + markings: this.props.markings, + showProtractor: this.props.showProtractor, + showTooltips: this.props.showTooltips, + lockedFigures: this.props.lockedFigures, + fullGraphAriaLabel: this.props.fullGraphAriaLabel, + fullGraphAriaDescription: + this.props.fullGraphAriaDescription, + // Set the "correct answer" graph to static when editing is disabled + static: editingDisabled, + trackInteraction: function () {}, + userInput: correct, + handleUserInput: ( + newGraph: InteractiveGraphProps["userInput"], + ) => { + let correct = this.props.correct; + // TODO(benchristel): can we improve the type of onChange + // so this invariant isn't necessary? + invariant(newGraph != null); + if (correct.type === newGraph.type) { + correct = mergeGraphs(correct, newGraph); + } else { + // Clear options from previous graph + correct = newGraph; } + this.props.onChange({ + correct: correct, + graph: this.props.graph, + }); + }, + } as const; + + graph = ( + // There are a bunch of props that renderer.jsx passes to widgets via + // getWidgetProps() and widget-container.jsx that the editors don't + // bother passing. + // @ts-expect-error - TS2769 - No overload matches this call. + - - - - {graph} - - - {this.props.correct?.type === "angle" && ( - - )} - {this.props.correct?.type === "point" && ( - - )} - {this.props.correct?.type === "polygon" && ( - - )} - {this.props.correct?.type === "segment" && ( - - )} - - {this.props.graph?.type && - shouldShowStartCoordsUI( - this.props.graph, - this.props.static, - ) && ( - + ); + // TODO(kevinb): Update getEquationString to only accept the data it actually + // needs to compute the equation string. + equationString = + // @ts-expect-error - TS2345 - Argument of type '{ readonly ref: "graph"; readonly box: any; readonly range: any; readonly labels: any; readonly step: any; readonly gridStep: any; readonly snapStep: any; readonly graph: any; readonly backgroundImage: any; ... 6 more ...; readonly onChange: (newProps: Pick<...> & ... 1 more ... & InexactPartial<...>) => void; }' is not assignable to parameter of type 'Props'. + InteractiveGraph.getEquationString(graphProps); + } else { + graph = ( +
+ {this.props.valid} +
+ ); + } + + return ( + + {(graphId) => ( + + + ["userInput"]["type"], + ) => { + this.props.onChange({ + graph: {type}, + correct: {type}, + }); + }) as any + } + /> + + + + {graph} + + + {this.props.correct?.type === "angle" && ( + + )} + {this.props.correct?.type === "point" && ( + + )} + {this.props.correct?.type === "polygon" && ( + + )} + {this.props.correct?.type === "segment" && ( + + )} + + {this.props.graph?.type && + shouldShowStartCoordsUI( + this.props.graph, + this.props.static, + ) && ( + + )} + + + + )} - - - -
- )} -
+ + ); + }} + ); } } diff --git a/packages/perseus-editor/src/widgets/label-image-editor/label-image-editor.tsx b/packages/perseus-editor/src/widgets/label-image-editor/label-image-editor.tsx index c043dea1cfc..caa9b9dd33e 100644 --- a/packages/perseus-editor/src/widgets/label-image-editor/label-image-editor.tsx +++ b/packages/perseus-editor/src/widgets/label-image-editor/label-image-editor.tsx @@ -1,4 +1,4 @@ -import {EditorJsonify, Util} from "@khanacademy/perseus"; +import {APIOptionsContext, EditorJsonify, Util} from "@khanacademy/perseus"; import {labelImageLogic} from "@khanacademy/perseus-core"; import {StyleSheet, css} from "aphrodite"; import * as React from "react"; @@ -11,14 +11,12 @@ import QuestionMarkers from "./question-markers"; import SelectImage from "./select-image"; import type {PreferredPopoverDirection} from "./behavior"; -import type {APIOptions} from "@khanacademy/perseus"; import type { PerseusLabelImageWidgetOptions, LabelImageDefaultWidgetOptions, } from "@khanacademy/perseus-core"; type Props = { - apiOptions: APIOptions; // List of answer choices to label question image with. choices: string[]; // The question image properties. @@ -183,69 +181,86 @@ class LabelImageEditor extends React.Component { }; render(): React.ReactNode { - const { - choices, - imageAlt, - imageUrl, - imageWidth, - imageHeight, - markers, - multipleAnswers, - hideChoicesFromInstructions, - preferredPopoverDirection, - } = this.props; - - const editingDisabled = this.props.apiOptions?.editingDisabled ?? false; - - const imageSelected = imageUrl && imageWidth > 0 && imageHeight > 0; - return ( -
- - -
- - {/* eslint-disable-next-line @typescript-eslint/strict-boolean-expressions */} - {imageSelected && ( - this.handleAltChange(e.target.value)} - value={imageAlt} - width="100%" - /> - )} - -
- - (this._questionMarkers = node)} - /> - -
- - - -
- - -
+ + {(apiOptions) => { + const { + choices, + imageAlt, + imageUrl, + imageWidth, + imageHeight, + markers, + multipleAnswers, + hideChoicesFromInstructions, + preferredPopoverDirection, + } = this.props; + + const editingDisabled = + apiOptions?.editingDisabled ?? false; + + const imageSelected = + imageUrl && imageWidth > 0 && imageHeight > 0; + + return ( +
+ + +
+ + {/* eslint-disable-next-line @typescript-eslint/strict-boolean-expressions */} + {imageSelected && ( + + this.handleAltChange(e.target.value) + } + value={imageAlt} + width="100%" + /> + )} + +
+ + (this._questionMarkers = node)} + /> + +
+ + + +
+ + +
+ ); + }} + ); } } diff --git a/packages/perseus-editor/src/widgets/plotter-editor.tsx b/packages/perseus-editor/src/widgets/plotter-editor.tsx index e31b6752c0a..7a1f42ca1ea 100644 --- a/packages/perseus-editor/src/widgets/plotter-editor.tsx +++ b/packages/perseus-editor/src/widgets/plotter-editor.tsx @@ -9,7 +9,6 @@ import _ from "underscore"; import BlurInput from "../components/blur-input"; -import type {APIOptions} from "@khanacademy/perseus"; import type { PerseusPlotterWidgetOptions, PlotterDefaultWidgetOptions, @@ -42,7 +41,6 @@ const editorDefaults = { } as const; type Props = { - apiOptions: APIOptions; type: PerseusPlotterWidgetOptions["type"]; labels: Array; categories: ReadonlyArray; diff --git a/packages/perseus-editor/src/widgets/radio/editor.tsx b/packages/perseus-editor/src/widgets/radio/editor.tsx index dd0c8226c12..fa989fdaa4d 100644 --- a/packages/perseus-editor/src/widgets/radio/editor.tsx +++ b/packages/perseus-editor/src/widgets/radio/editor.tsx @@ -1,4 +1,5 @@ /* eslint-disable jsx-a11y/anchor-is-valid */ +import {type Changeable, APIOptionsContext} from "@khanacademy/perseus"; import {radioLogic, deriveNumCorrect} from "@khanacademy/perseus-core"; import Button from "@khanacademy/wonder-blocks-button"; import Link from "@khanacademy/wonder-blocks-link"; @@ -14,7 +15,6 @@ import {RadioOptionSettings} from "./radio-option-settings"; import {getMovedChoices} from "./utils"; import type {ChoiceMovementType} from "./radio-option-settings-actions"; -import type {Changeable, APIOptions} from "@khanacademy/perseus"; import type { PerseusRadioWidgetOptions, PerseusRadioChoice, @@ -23,7 +23,6 @@ import type { // Exported for testing export interface RadioEditorProps extends Changeable.ChangeableProps { - apiOptions: APIOptions; countChoices: boolean; choices: PerseusRadioChoice[]; randomize: boolean; @@ -276,99 +275,110 @@ class RadioEditor extends React.Component { } render(): React.ReactNode { - const numCorrect = deriveNumCorrect(this.props.choices); - const isEditingDisabled = this.props.apiOptions.editingDisabled; - return ( -
- - Multiple choice best practices - -
- { - this.props.onChange({randomize: value}); - }} - style={{marginBlockEnd: sizing.size_060}} - /> - { - this.onMultipleSelectChange({ - multipleSelect: value, - }); - }} - style={{marginBlockEnd: sizing.size_060}} - /> - {this.props.multipleSelect && ( - <> - { - this.onCountChoicesChange({ - countChoices: value, - }); - }} - style={{marginBlockEnd: sizing.size_060}} - /> - - Current number correct: {numCorrect} - - - )} -
- - {this.props.choices.map((choice, index) => ( - = 2} - showMove={ - this.props.choices.length > 1 && - !choice.isNoneOfTheAbove - } - onDelete={() => this.onDelete(index)} - onMove={this.handleMove} - /> - ))} - -
- - {!this.props.hasNoneOfTheAbove && ( - - )} -
-
+ + {(apiOptions) => { + const numCorrect = deriveNumCorrect(this.props.choices); + const isEditingDisabled = apiOptions.editingDisabled; + + return ( +
+ + Multiple choice best practices + +
+ { + this.props.onChange({randomize: value}); + }} + style={{marginBlockEnd: sizing.size_060}} + /> + { + this.onMultipleSelectChange({ + multipleSelect: value, + }); + }} + style={{marginBlockEnd: sizing.size_060}} + /> + {this.props.multipleSelect && ( + <> + { + this.onCountChoicesChange({ + countChoices: value, + }); + }} + style={{ + marginBlockEnd: sizing.size_060, + }} + /> + + Current number correct: {numCorrect} + + + )} +
+ + {this.props.choices.map((choice, index) => ( + = 2} + showMove={ + this.props.choices.length > 1 && + !choice.isNoneOfTheAbove + } + onDelete={() => this.onDelete(index)} + onMove={this.handleMove} + /> + ))} + +
+ + {!this.props.hasNoneOfTheAbove && ( + + )} +
+
+ ); + }} +
); } } diff --git a/packages/perseus-editor/src/widgets/table-editor.tsx b/packages/perseus-editor/src/widgets/table-editor.tsx index 6ae29b5f771..b3a6fb1d0b5 100644 --- a/packages/perseus-editor/src/widgets/table-editor.tsx +++ b/packages/perseus-editor/src/widgets/table-editor.tsx @@ -1,9 +1,13 @@ -import {components, TableWidget, Util} from "@khanacademy/perseus"; +import { + APIOptionsContext, + components, + TableWidget, + Util, +} from "@khanacademy/perseus"; import { tableLogic, type TableDefaultWidgetOptions, } from "@khanacademy/perseus-core"; -import PropTypes from "prop-types"; import * as React from "react"; import _ from "underscore"; @@ -14,16 +18,15 @@ import type {PropsFor} from "@khanacademy/wonder-blocks-core"; const {InfoTip, NumberInput} = components; const Table = TableWidget.widget; -type Props = any; +type Props = { + rows: number; + columns: number; + headers: string[]; + answers: string[][]; + onChange: (options: any) => void; +}; class TableEditor extends React.Component { - static propTypes = { - rows: PropTypes.number, - columns: PropTypes.number, - headers: PropTypes.arrayOf(PropTypes.string), - answers: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), - }; - static widgetName = "table" as const; static defaultProps: TableDefaultWidgetOptions = @@ -90,75 +93,90 @@ class TableEditor extends React.Component { }; render(): React.ReactNode { - const tableProps: Partial> = { - headers: this.props.headers, - onChange: this.props.onChange, - userInput: this.props.answers, - handleUserInput: (userInput) => { - // Disable table input changes when editing is disabled - if (this.props.apiOptions?.editingDisabled) { - return; - } - // In the editing experience, - // user input is actually editing answers - this.props.onChange({answers: userInput}); - }, - apiOptions: this.props.apiOptions, - editableHeaders: true, - onFocus: () => {}, - onBlur: () => {}, - trackInteraction: () => {}, - Editor: Editor, - }; - return ( -
-
- -
-
- -
-
- {" "} - Table of answers:{" "} - -

- The student has to fill out all cells in the table. - For partially filled tables create a table using the - template, and insert text input boxes as desired. -

-
-
-
- )} /> - - + + {(apiOptions) => { + const tableProps: Partial> = { + headers: this.props.headers, + onChange: this.props.onChange, + userInput: this.props.answers, + handleUserInput: (userInput) => { + // Disable table input changes when editing is disabled + if (apiOptions?.editingDisabled) { + return; + } + // In the editing experience, + // user input is actually editing answers + this.props.onChange({answers: userInput}); + }, + apiOptions: apiOptions, + editableHeaders: true, + onFocus: () => {}, + onBlur: () => {}, + trackInteraction: () => {}, + Editor: Editor, + }; + + return ( +
+
+ +
+
+ +
+
+ {" "} + Table of answers:{" "} + +

+ The student has to fill out all cells in + the table. For partially filled tables + create a table using the template, and + insert text input boxes as desired. +

+
+
+
+
)} + /> + + + ); + }} + ); } } diff --git a/packages/perseus/src/components/api-options-context.tsx b/packages/perseus/src/components/api-options-context.tsx index 0be209dd121..09607e490a4 100644 --- a/packages/perseus/src/components/api-options-context.tsx +++ b/packages/perseus/src/components/api-options-context.tsx @@ -2,12 +2,12 @@ import * as React from "react"; import {ApiOptions} from "../perseus-api"; -import type {APIOptions} from "../types"; +import type {APIOptionsWithDefaults} from "../types"; -export const APIOptionsContext = React.createContext({ +export const APIOptionsContext = React.createContext({ ...ApiOptions.defaults, }); -export function useAPIOptionsContext(): APIOptions { +export function useAPIOptionsContext(): APIOptionsWithDefaults { return React.useContext(APIOptionsContext); }