From b6318dac0409e448c7180a3368cea6a294ff12df Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Mon, 12 Aug 2024 12:18:59 -0700 Subject: [PATCH 01/45] IframeContentRenderer: move to 'preview' subfolder --- packages/perseus-editor/src/article-editor.tsx | 2 +- packages/perseus-editor/src/hint-editor.tsx | 2 +- packages/perseus-editor/src/index.ts | 2 +- packages/perseus-editor/src/item-editor.tsx | 2 +- .../src/{ => preview}/iframe-content-renderer.tsx | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename packages/perseus-editor/src/{ => preview}/iframe-content-renderer.tsx (100%) diff --git a/packages/perseus-editor/src/article-editor.tsx b/packages/perseus-editor/src/article-editor.tsx index 980be9777e..f087719f41 100644 --- a/packages/perseus-editor/src/article-editor.tsx +++ b/packages/perseus-editor/src/article-editor.tsx @@ -13,7 +13,7 @@ import DeviceFramer from "./components/device-framer"; import JsonEditor from "./components/json-editor"; import SectionControlButton from "./components/section-control-button"; import Editor from "./editor"; -import IframeContentRenderer from "./iframe-content-renderer"; +import IframeContentRenderer from "./preview/iframe-content-renderer"; import type {APIOptions, Changeable, ImageUploader} from "@khanacademy/perseus"; diff --git a/packages/perseus-editor/src/hint-editor.tsx b/packages/perseus-editor/src/hint-editor.tsx index c9fa8fc345..79825f0aa4 100644 --- a/packages/perseus-editor/src/hint-editor.tsx +++ b/packages/perseus-editor/src/hint-editor.tsx @@ -10,7 +10,7 @@ import _ from "underscore"; import DeviceFramer from "./components/device-framer"; import Editor from "./editor"; -import IframeContentRenderer from "./iframe-content-renderer"; +import IframeContentRenderer from "./preview/iframe-content-renderer"; import type { APIOptions, diff --git a/packages/perseus-editor/src/index.ts b/packages/perseus-editor/src/index.ts index 1f3114764c..a6cedf9123 100644 --- a/packages/perseus-editor/src/index.ts +++ b/packages/perseus-editor/src/index.ts @@ -9,7 +9,7 @@ export {default as StructuredItemDiff} from "./diffs/structured-item-diff"; export {default as EditorPage} from "./editor-page"; export {default as Editor} from "./editor"; export {default as i18n} from "./i18n"; -export {default as IframeContentRenderer} from "./iframe-content-renderer"; +export {default as IframeContentRenderer} from "./preview/iframe-content-renderer"; export {default as MultiRendererEditor} from "./multirenderer-editor"; export {default as StatefulEditorPage} from "./stateful-editor-page"; diff --git a/packages/perseus-editor/src/item-editor.tsx b/packages/perseus-editor/src/item-editor.tsx index e3c6d6ab57..c316657b4c 100644 --- a/packages/perseus-editor/src/item-editor.tsx +++ b/packages/perseus-editor/src/item-editor.tsx @@ -4,8 +4,8 @@ import _ from "underscore"; import DeviceFramer from "./components/device-framer"; import Editor from "./editor"; -import IframeContentRenderer from "./iframe-content-renderer"; import ItemExtrasEditor from "./item-extras-editor"; +import IframeContentRenderer from "./preview/iframe-content-renderer"; import type { APIOptions, diff --git a/packages/perseus-editor/src/iframe-content-renderer.tsx b/packages/perseus-editor/src/preview/iframe-content-renderer.tsx similarity index 100% rename from packages/perseus-editor/src/iframe-content-renderer.tsx rename to packages/perseus-editor/src/preview/iframe-content-renderer.tsx From 08f346c507c315e8332d99b57b5414ae61cd9922 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Mon, 12 Aug 2024 12:19:13 -0700 Subject: [PATCH 02/45] Add vite aliases for `strings` exports --- dev/vite.config.ts | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/dev/vite.config.ts b/dev/vite.config.ts index 2185e291a7..6e79e12730 100644 --- a/dev/vite.config.ts +++ b/dev/vite.config.ts @@ -10,7 +10,41 @@ const packageAliases = {}; glob.sync(resolve(__dirname, "../packages/*/package.json")).forEach( (packageJsonPath) => { const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); - packageAliases[pkg.name] = join(dirname(packageJsonPath), pkg.source); + + // "exports" is the more modern way to declare package exports. Some, + // but not all, Perseus packages delcare "exports". + if ("exports" in pkg) { + // Not all packages export strings, but for those that do we need + // to set up an alias so Vite properly resolves them. + // Eg `import {strings, mockStrings} from "@khanacademy/perseus/strings";` + // And MOST IMPORTANTLY, this alias _must_ precede the main + // import, otherwise Vite will just use the main export and tack + // `/strings` onto the end, resulting in a path like this: + // `packages/perseus/src/index.ts/strings` + const stringsSource = pkg.exports["./strings"]?.source; + if (stringsSource != null) { + packageAliases[`${pkg.name}/strings`] = join( + dirname(packageJsonPath), + stringsSource, + ); + } + + const mainSource = pkg.exports["."]?.source; + if (mainSource == null) { + throw new Error( + `Package declares 'exports', but not provide a main export (exports["."])`, + ); + } + packageAliases[pkg.name] = join( + dirname(packageJsonPath), + mainSource, + ); + } else { + packageAliases[pkg.name] = join( + dirname(packageJsonPath), + pkg.source, + ); + } }, ); From e662bdc76f337d3d0d1c9cd13a11a7056ba98eef Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 17 May 2024 20:47:52 -0700 Subject: [PATCH 03/45] ContentRenderer: Initial basic content preview (no iframe) --- packages/perseus-editor/src/item-editor.tsx | 16 ++---- .../src/preview/content-renderer.tsx | 49 +++++++++++++++++++ 2 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 packages/perseus-editor/src/preview/content-renderer.tsx diff --git a/packages/perseus-editor/src/item-editor.tsx b/packages/perseus-editor/src/item-editor.tsx index c316657b4c..3bedb4ebc9 100644 --- a/packages/perseus-editor/src/item-editor.tsx +++ b/packages/perseus-editor/src/item-editor.tsx @@ -5,8 +5,9 @@ import _ from "underscore"; import DeviceFramer from "./components/device-framer"; import Editor from "./editor"; import ItemExtrasEditor from "./item-extras-editor"; -import IframeContentRenderer from "./preview/iframe-content-renderer"; +import ContentRenderer from "./preview/content-renderer"; +import type IframeContentRenderer from "./preview/iframe-content-renderer"; import type { APIOptions, ImageUploader, @@ -89,9 +90,6 @@ class ItemEditor extends React.Component { }; render(): React.ReactNode { - const isMobile = - this.props.deviceType === "phone" || - this.props.deviceType === "tablet"; return (
@@ -120,13 +118,9 @@ class ItemEditor extends React.Component { deviceType={this.props.deviceType} nochrome={true} > -
+ + + + {({setKeypadActive, setKeypadElement}) => { + return ( + Promise.resolve()} + onDismiss={() => setKeypadActive(false)} + onElementMounted={(element) => { + if (element) { + setKeypadElement(element); + } + }} + /> + ); + }} + + + + ); +} + +export default ComponentRenderer; From 0193db83edbd43f07e45b455959af1834f6b5659 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Wed, 22 May 2024 09:27:14 -0700 Subject: [PATCH 04/45] DeviceFramer: Tweak comment --- packages/perseus-editor/src/components/device-framer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/perseus-editor/src/components/device-framer.tsx b/packages/perseus-editor/src/components/device-framer.tsx index c654e551f0..e43b37f6a6 100644 --- a/packages/perseus-editor/src/components/device-framer.tsx +++ b/packages/perseus-editor/src/components/device-framer.tsx @@ -60,7 +60,7 @@ const DeviceFramer = ({ ); if (nochrome) { - // Render content inside a variable height iframe. Used on the + // Render content inside a variable height div. Used on the // "edit" table of the content editor. In this mode, PerseusFrame // will draw the border and reserve space on the right for // lint indicators. From 1263fbcc0475986626dc439c39ac816760598e78 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 24 May 2024 16:15:08 -0700 Subject: [PATCH 05/45] ContentRenderer: Get linting working somewhat --- .../src/preview/content-renderer.tsx | 8 +- packages/perseus/src/components/lint.tsx | 146 ++++-------------- 2 files changed, 36 insertions(+), 118 deletions(-) diff --git a/packages/perseus-editor/src/preview/content-renderer.tsx b/packages/perseus-editor/src/preview/content-renderer.tsx index 2b01c0bd9b..4fbabbc34a 100644 --- a/packages/perseus-editor/src/preview/content-renderer.tsx +++ b/packages/perseus-editor/src/preview/content-renderer.tsx @@ -19,11 +19,17 @@ function ComponentRenderer({ apiOptions?: APIOptions; }) { return ( - + diff --git a/packages/perseus/src/components/lint.tsx b/packages/perseus/src/components/lint.tsx index d53dadf9d9..1a8fbc1ff1 100644 --- a/packages/perseus/src/components/lint.tsx +++ b/packages/perseus/src/components/lint.tsx @@ -1,10 +1,11 @@ import {StyleSheet, css} from "aphrodite"; import * as React from "react"; -import ReactDOM from "react-dom"; +import Tooltip from "@khanacademy/wonder-blocks-tooltip"; import * as constants from "../styles/constants"; import InlineIcon from "./inline-icon"; +import {color, font} from "@khanacademy/wonder-blocks-tokens"; const exclamationIcon = { path: "M6 11a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0-9a1 1 0 0 1 1 1v4a1 1 0 1 1-2 0V3a1 1 0 0 1 1-1z", @@ -38,10 +39,6 @@ type Props = { severity?: Severity; }; -type State = { - tooltipAbove: boolean; -}; - /** * This component renders "lint" nodes in a markdown parse tree. Lint nodes * are inserted into the tree by the Perseus linter (see @@ -64,43 +61,12 @@ type State = { * that has a right margin (like anything blockquoted) the circle will appear * to the left of where it belongs. And if there is more **/ -class Lint extends React.Component { +class Lint extends React.Component { _positionTimeout: number | undefined; - state: State = { - tooltipAbove: true, - }; - - componentDidMount() { - // TODO(somewhatabstract): Use WB timing - // eslint-disable-next-line no-restricted-syntax - this._positionTimeout = window.setTimeout(this.getPosition); - } - - componentWillUnmount() { - // TODO(somewhatabstract): Use WB timing - // eslint-disable-next-line no-restricted-syntax - window.clearTimeout(this._positionTimeout); - } - - // We can't call setState in componentDidMount without risking a render - // thrash, and we can't call getBoundingClientRect in render, so we - // borrow a timeout approach from learnstorm-dashboard.jsx and set our - // state once the component has mounted and we can get what we need. - getPosition: () => void = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'getBoundingClientRect' does not exist on type 'Element | Text'. - const rect = ReactDOM.findDOMNode(this).getBoundingClientRect(); - // TODO(scottgrant): This is a magic number! We don't know the size - // of the tooltip at this point, so we're arbitrarily choosing a - // point at which to flip the tooltip's position. - this.setState({tooltipAbove: rect.top > 100}); - }; - // Render the element that holds the indicator icon and the tooltip // We pass different styles for the inline and block cases renderLink: (arg1: any) => React.ReactElement = (style) => { - const tooltipAbove = this.state.tooltipAbove; - let severityStyle; let warningText; let warningTextStyle; @@ -119,38 +85,33 @@ class Lint extends React.Component { } return ( - + {this.props.message.split("\n\n").map((m, i) => ( +

+ + {warningText}:{" "} + + {m} +

+ ))} + + } > - - {this.props.severity === 1 && ( - - )} - -
- {this.props.message.split("\n\n").map((m, i) => ( -

- - {warningText}:{" "} - - {m} -

- ))} -
+ {this.props.severity === 1 && ( + )} - /> -
-
+ + + ); }; @@ -386,60 +347,11 @@ const styles = StyleSheet.create({ backgroundColor: "#ffbe26", }, - // These are the styles for the tooltip - tooltip: { - // Absolute positioning relative to the lint indicator circle. - position: "absolute", - right: -12, - - // The tooltip is hidden by default; only displayed on hover - display: "none", - - // When it is displayed, it goes on top! - zIndex: 1000, - - // These styles control what the tooltip looks like - color: constants.white, - backgroundColor: constants.gray17, - opacity: 0.9, - fontFamily: constants.baseFontFamily, - fontSize: "12px", - lineHeight: "15px", - width: "320px", - borderRadius: "4px", - }, - // If we're going to render the tooltip above the warning circle, we use - // the previous rules in tooltip, but change the position slightly. - tooltipAbove: { - bottom: 32, - }, - - // We give the tooltip a little triangular "tail" that points down at - // the lint indicator circle. This is inside the tooltip and positioned - // relative to it. It also shares the opacity of the tooltip. We're using - // the standard CSS trick for drawing triangles with a thick border. - tail: { - position: "absolute", - top: -12, - right: 16, - width: 0, - height: 0, - - // This is the CSS triangle trick - borderLeft: "8px solid transparent", - borderRight: "8px solid transparent", - borderBottom: "12px solid " + constants.gray17, - }, - tailAbove: { - bottom: -12, - borderBottom: "none", - borderTop: "12px solid " + constants.gray17, - top: "auto", - }, - // Each warning in the tooltip is its own

. They are 12 pixels from // the edges of the tooltip and 12 pixels from each other. tooltipParagraph: { + fontFamily: font.family.sans, + color: color.white, margin: 12, }, From 2c9f10b53e8c8610efd791879b8cbc84fa93adc3 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 24 May 2024 16:24:00 -0700 Subject: [PATCH 06/45] Lint: Use phosphor icon --- packages/perseus/src/components/lint.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/perseus/src/components/lint.tsx b/packages/perseus/src/components/lint.tsx index 1a8fbc1ff1..7e996667ab 100644 --- a/packages/perseus/src/components/lint.tsx +++ b/packages/perseus/src/components/lint.tsx @@ -1,18 +1,11 @@ import {StyleSheet, css} from "aphrodite"; import * as React from "react"; import Tooltip from "@khanacademy/wonder-blocks-tooltip"; - +import ExclamationIcon from "@phosphor-icons/core/regular/warning-circle.svg"; +import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon"; import * as constants from "../styles/constants"; - -import InlineIcon from "./inline-icon"; import {color, font} from "@khanacademy/wonder-blocks-tokens"; -const exclamationIcon = { - path: "M6 11a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0-9a1 1 0 0 1 1 1v4a1 1 0 1 1-2 0V3a1 1 0 0 1 1-1z", - height: 12, - width: 12, -} as const; - enum Severity { Error = 1, Warning = 2, @@ -107,7 +100,7 @@ class Lint extends React.Component { > {this.props.severity === 1 && ( - + )} From 39231e57503529f249f0bf3e416876765c5a94bf Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 26 Jul 2024 14:47:10 -0700 Subject: [PATCH 07/45] ContentRenderer: Fix keypad connection for 'mobile' in previews --- .../src/preview/content-renderer.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/perseus-editor/src/preview/content-renderer.tsx b/packages/perseus-editor/src/preview/content-renderer.tsx index 4fbabbc34a..043217f935 100644 --- a/packages/perseus-editor/src/preview/content-renderer.tsx +++ b/packages/perseus-editor/src/preview/content-renderer.tsx @@ -21,9 +21,13 @@ function ComponentRenderer({ return ( + + {({setKeypadActive, keypadElement, setKeypadElement}) => ( + <> - - {({setKeypadActive, setKeypadElement}) => { - return ( + Promise.resolve()} onDismiss={() => setKeypadActive(false)} - onElementMounted={(element) => { - if (element) { - setKeypadElement(element); - } - }} + onElementMounted={setKeypadElement} /> - ); - }} + + )} From 2ef1dee0522b72a4e9df0f7dcd012e584c494827 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 26 Jul 2024 14:47:45 -0700 Subject: [PATCH 08/45] ContentRenderer: Implement nochrome --- .../src/preview/content-renderer.tsx | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/perseus-editor/src/preview/content-renderer.tsx b/packages/perseus-editor/src/preview/content-renderer.tsx index 043217f935..92e12a1401 100644 --- a/packages/perseus-editor/src/preview/content-renderer.tsx +++ b/packages/perseus-editor/src/preview/content-renderer.tsx @@ -3,10 +3,12 @@ import { MobileKeypad, StatefulKeypadContextProvider, } from "@khanacademy/math-input"; -import {Renderer} from "@khanacademy/perseus"; +import {Renderer, constants} from "@khanacademy/perseus"; // eslint-disable-next-line monorepo/no-internal-import import {mockStrings} from "@khanacademy/perseus/strings"; import {View} from "@khanacademy/wonder-blocks-core"; +import {spacing} from "@khanacademy/wonder-blocks-tokens"; +import {StyleSheet} from "aphrodite"; import * as React from "react"; import type {APIOptions, PerseusRenderer} from "@khanacademy/perseus"; @@ -14,28 +16,30 @@ import type {APIOptions, PerseusRenderer} from "@khanacademy/perseus"; function ComponentRenderer({ question, apiOptions, + seamless, }: { question?: PerseusRenderer; apiOptions?: APIOptions; + seamless?: boolean; }) { return ( - + {({setKeypadActive, keypadElement, setKeypadElement}) => ( <> - + linterContext={{ + contentType: "exercise", + highlightLint: true, + paths: [], + stack: [], + }} + {...question} + /> Promise.resolve()} @@ -50,4 +54,8 @@ function ComponentRenderer({ ); } +const styles = StyleSheet.create({ + container: {padding: spacing.xxxSmall_4}, + gutter: {marginRight: constants.lintGutterWidth}, +}); export default ComponentRenderer; From 1acd8e1cdb35d09d6f408e1cf4a217b9383bf6e2 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Tue, 30 Jul 2024 09:07:40 -0700 Subject: [PATCH 09/45] ContentRenderer: add linter props --- .../src/preview/content-renderer.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/perseus-editor/src/preview/content-renderer.tsx b/packages/perseus-editor/src/preview/content-renderer.tsx index 92e12a1401..c9636cea63 100644 --- a/packages/perseus-editor/src/preview/content-renderer.tsx +++ b/packages/perseus-editor/src/preview/content-renderer.tsx @@ -12,15 +12,20 @@ import {StyleSheet} from "aphrodite"; import * as React from "react"; import type {APIOptions, PerseusRenderer} from "@khanacademy/perseus"; +import type {LinterContextProps} from "@khanacademy/perseus-linter"; -function ComponentRenderer({ +function ContentRenderer({ question, apiOptions, seamless, + linterContext, + legacyPerseusLint, }: { question?: PerseusRenderer; apiOptions?: APIOptions; seamless?: boolean; + linterContext?: LinterContextProps; + legacyPerseusLint?: ReadonlyArray; }) { return ( @@ -32,12 +37,8 @@ function ComponentRenderer({ strings={mockStrings} apiOptions={apiOptions} keypadElement={keypadElement} - linterContext={{ - contentType: "exercise", - highlightLint: true, - paths: [], - stack: [], - }} + linterContext={linterContext} + legacyPerseusLint={legacyPerseusLint} {...question} /> @@ -58,4 +59,5 @@ const styles = StyleSheet.create({ container: {padding: spacing.xxxSmall_4}, gutter: {marginRight: constants.lintGutterWidth}, }); -export default ComponentRenderer; + +export default ContentRenderer; From 4a76f5e60acffe5b5a576acd30a69f9a3d975b8a Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Tue, 30 Jul 2024 08:57:53 -0700 Subject: [PATCH 10/45] ContentRenderer: Fix import of mock strings --- packages/perseus-editor/src/preview/content-renderer.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/perseus-editor/src/preview/content-renderer.tsx b/packages/perseus-editor/src/preview/content-renderer.tsx index c9636cea63..03ead81c69 100644 --- a/packages/perseus-editor/src/preview/content-renderer.tsx +++ b/packages/perseus-editor/src/preview/content-renderer.tsx @@ -4,7 +4,6 @@ import { StatefulKeypadContextProvider, } from "@khanacademy/math-input"; import {Renderer, constants} from "@khanacademy/perseus"; -// eslint-disable-next-line monorepo/no-internal-import import {mockStrings} from "@khanacademy/perseus/strings"; import {View} from "@khanacademy/wonder-blocks-core"; import {spacing} from "@khanacademy/wonder-blocks-tokens"; @@ -56,7 +55,11 @@ function ContentRenderer({ } const styles = StyleSheet.create({ - container: {padding: spacing.xxxSmall_4}, + container: { + padding: spacing.xxxSmall_4, + containerType: "inline-size", + containerName: "perseus-root", + }, gutter: {marginRight: constants.lintGutterWidth}, }); From 79b19d3965a3b77946cd48e45fef4fe62661d807 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Thu, 1 Aug 2024 11:55:37 -0700 Subject: [PATCH 11/45] ContentRenderer: set framework-perseus and, if relevant, perseus-mobile class names on wrapping View --- packages/perseus-editor/src/preview/content-renderer.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/perseus-editor/src/preview/content-renderer.tsx b/packages/perseus-editor/src/preview/content-renderer.tsx index 03ead81c69..b389d71dff 100644 --- a/packages/perseus-editor/src/preview/content-renderer.tsx +++ b/packages/perseus-editor/src/preview/content-renderer.tsx @@ -26,8 +26,13 @@ function ContentRenderer({ linterContext?: LinterContextProps; legacyPerseusLint?: ReadonlyArray; }) { + const className = apiOptions?.isMobile ? "perseus-mobile" : ""; + return ( - + {({setKeypadActive, keypadElement, setKeypadElement}) => ( From 4ee30b8296cce3a40afec578c0ea7b9f934cab80 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Wed, 7 Aug 2024 15:49:01 -0700 Subject: [PATCH 12/45] ContentRenderer: add story to demo the new preview system --- .../__stories__/content-renderer.stories.tsx | 152 ++++++++++++++++++ .../src/preview/content-renderer.tsx | 1 + 2 files changed, 153 insertions(+) create mode 100644 packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx diff --git a/packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx b/packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx new file mode 100644 index 0000000000..7111b2346d --- /dev/null +++ b/packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx @@ -0,0 +1,152 @@ +import {View} from "@khanacademy/wonder-blocks-core"; +import {spacing} from "@khanacademy/wonder-blocks-tokens"; +import {useState} from "react"; + +import {question} from "../../../../perseus/src/widgets/__testdata__/radio.testdata"; +import DeviceFramer from "../../components/device-framer"; +import ViewportResizer from "../../components/viewport-resizer"; +import ContentRenderer from "../content-renderer"; + +import type {DeviceType, PerseusRenderer} from "@khanacademy/perseus"; +import type {Meta, StoryObj} from "@storybook/react"; + +import "../../styles/perseus-editor.less"; + +const PreviewWrapper = (props) => { + const [previewDevice, setPreviewDevice] = useState("phone"); + + return ( + + + + + + + ); +}; + +const meta: Meta = { + title: "PerseusEditor/Preview/Content Renderer", + component: ContentRenderer, + decorators: [ + (Story) => ( + + + + ), + ], + render: (props) => , +}; + +export default meta; +type Story = StoryObj; + +const sampleArticleSection: PerseusRenderer = { + content: + "The word \"radiation\" sometimes gets a bad rap. People often associate radiation with something dangerous or scary, without really knowing what it is. In reality, we're surrounded by radiation all the time. \n\n**Radiation** is energy that travels through space (not just \"outer space\"—any space). Radiation can also interact with matter. How radiation interacts with matter depends on the type of radiation and the type of matter.\n\nRadiation comes in many forms, one of which is *electromagnetic radiation*. Without electromagnetic radiation life on Earth would not be possible, nor would most modern technologies.\n\n[[☃ image 13]]\n\nLet's take a closer look at this important and fascinating type of radiation.\n\n##Electromagnetic radiation\n\nAs the name suggests, **electromagnetic (EM) radiation** is energy transferred by *electromagnetic fields* oscillating through space.\n\nEM radiation is strange—it has both wave and particle properties. Let's take a look at both.\n\n###Electromagnetic waves\n\nAn animated model of an EM wave is shown below.\n[[☃ image 1]]\nThe electric field $(\\vec{\\textbf{E}})$ is shown in $\\color{blue}\\textbf{blue}$, and the magnetic field $(\\vec{\\textbf{B}})$ is shown in $\\color{red}\\textbf{red}$. They're perpendicular to each other.\n\nA changing electric field creates a magnetic field, and a changing magnetic field creates an electric field. So, once the EM wave is generated it propagates itself through space!\n\nAs with any wave, EM waves have wavelength, frequency, and speed. The wave model of EM radiation works best on large scales. But what about the atomic scale?\n\n###Photons\n\nAt the quantum level, EM radiation exists as particles. A particle of EM radiation is called a **photon**.\n\nWe can think of photons as wave *packets*—tiny bundles of EM radiation containing specific amounts of energy. Photons are visually represented using the following symbol.\n\n[[☃ image 3]]\n\nAll EM radiation, whether modeled as waves or photons, travels at the **speed of light** $\\textbf{(c)}$ in a vacuum: \n\n$\\text{c}=3\\times10^8\\space\\pu{m/s}=300{,}000{,}000\\space\\pu{m/s}$\n\nBut, EM radiation travels at a slower speed in matter, such as water or glass.", + images: {}, + widgets: { + "image 13": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [600, 254], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/358a87c20ab6ee70447f5fcb547010f69986828e.jpg", + width: 600, + height: 254, + }, + labels: [], + alt: "From space, the sun appears over Earth's horizon, illuminating the atmosphere as a blue layer above Earth. Above the atmosphere, space appears black.", + caption: + "*Sunrise photo from the International Space Station. Earth's atmosphere scatters electromagnetic radiation from the sun, producing a bright sky during the day.*", + }, + version: { + major: 0, + minor: 0, + }, + }, + "image 1": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [627, 522], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/8100369eaf3b581d4e7bfc9f1062625309def486.gif", + width: 627, + height: 522, + }, + labels: [], + alt: "An animation shows a blue electric field arrow oscillating up and down. Connected to the base of the electric field arrow is a magnetic field arrow, which oscillates from side to side. The two fields oscillate in unison: when one extends the other extends too, creating a repeating wave pattern.", + caption: "", + }, + version: { + major: 0, + minor: 0, + }, + }, + "image 3": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [350, 130], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/74edeeb6c6605a4e854e3a3e9db69c01dcf5508f.svg", + width: 350, + height: 130, + }, + labels: [], + alt: "A squiggly curve drawn from left to right. The right end of the curve has an arrow point. The curve begins with a small amount of wiggle on the left, which grows in amplitude in the middle and then decreases again on the right. The result is a small wave packet.", + caption: "", + }, + version: { + major: 0, + minor: 0, + }, + }, + }, +}; + +export const Exercise: Story = { + args: { + question, + }, +}; + +export const Article: Story = { + args: { + question: sampleArticleSection, + }, +}; diff --git a/packages/perseus-editor/src/preview/content-renderer.tsx b/packages/perseus-editor/src/preview/content-renderer.tsx index b389d71dff..6f52fb9d90 100644 --- a/packages/perseus-editor/src/preview/content-renderer.tsx +++ b/packages/perseus-editor/src/preview/content-renderer.tsx @@ -4,6 +4,7 @@ import { StatefulKeypadContextProvider, } from "@khanacademy/math-input"; import {Renderer, constants} from "@khanacademy/perseus"; +// eslint-disable-next-line monorepo/no-internal-import import {mockStrings} from "@khanacademy/perseus/strings"; import {View} from "@khanacademy/wonder-blocks-core"; import {spacing} from "@khanacademy/wonder-blocks-tokens"; From 5d191b09af9b3d49aee73ecddf5674dda24b653b Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Wed, 7 Aug 2024 16:18:13 -0700 Subject: [PATCH 13/45] ContentRenderer: move article sample data to .testdata.ts file --- .../__stories__/content-renderer.stories.tsx | 95 +------------------ .../__testdata__/article-renderer.testdata.ts | 90 ++++++++++++++++++ 2 files changed, 93 insertions(+), 92 deletions(-) diff --git a/packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx b/packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx index 7111b2346d..0903c9c099 100644 --- a/packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx +++ b/packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx @@ -2,12 +2,13 @@ import {View} from "@khanacademy/wonder-blocks-core"; import {spacing} from "@khanacademy/wonder-blocks-tokens"; import {useState} from "react"; +import {articleWithImages} from "../../../../perseus/src/__testdata__/article-renderer.testdata"; import {question} from "../../../../perseus/src/widgets/__testdata__/radio.testdata"; import DeviceFramer from "../../components/device-framer"; import ViewportResizer from "../../components/viewport-resizer"; import ContentRenderer from "../content-renderer"; -import type {DeviceType, PerseusRenderer} from "@khanacademy/perseus"; +import type {DeviceType} from "@khanacademy/perseus"; import type {Meta, StoryObj} from "@storybook/react"; import "../../styles/perseus-editor.less"; @@ -49,96 +50,6 @@ const meta: Meta = { export default meta; type Story = StoryObj; -const sampleArticleSection: PerseusRenderer = { - content: - "The word \"radiation\" sometimes gets a bad rap. People often associate radiation with something dangerous or scary, without really knowing what it is. In reality, we're surrounded by radiation all the time. \n\n**Radiation** is energy that travels through space (not just \"outer space\"—any space). Radiation can also interact with matter. How radiation interacts with matter depends on the type of radiation and the type of matter.\n\nRadiation comes in many forms, one of which is *electromagnetic radiation*. Without electromagnetic radiation life on Earth would not be possible, nor would most modern technologies.\n\n[[☃ image 13]]\n\nLet's take a closer look at this important and fascinating type of radiation.\n\n##Electromagnetic radiation\n\nAs the name suggests, **electromagnetic (EM) radiation** is energy transferred by *electromagnetic fields* oscillating through space.\n\nEM radiation is strange—it has both wave and particle properties. Let's take a look at both.\n\n###Electromagnetic waves\n\nAn animated model of an EM wave is shown below.\n[[☃ image 1]]\nThe electric field $(\\vec{\\textbf{E}})$ is shown in $\\color{blue}\\textbf{blue}$, and the magnetic field $(\\vec{\\textbf{B}})$ is shown in $\\color{red}\\textbf{red}$. They're perpendicular to each other.\n\nA changing electric field creates a magnetic field, and a changing magnetic field creates an electric field. So, once the EM wave is generated it propagates itself through space!\n\nAs with any wave, EM waves have wavelength, frequency, and speed. The wave model of EM radiation works best on large scales. But what about the atomic scale?\n\n###Photons\n\nAt the quantum level, EM radiation exists as particles. A particle of EM radiation is called a **photon**.\n\nWe can think of photons as wave *packets*—tiny bundles of EM radiation containing specific amounts of energy. Photons are visually represented using the following symbol.\n\n[[☃ image 3]]\n\nAll EM radiation, whether modeled as waves or photons, travels at the **speed of light** $\\textbf{(c)}$ in a vacuum: \n\n$\\text{c}=3\\times10^8\\space\\pu{m/s}=300{,}000{,}000\\space\\pu{m/s}$\n\nBut, EM radiation travels at a slower speed in matter, such as water or glass.", - images: {}, - widgets: { - "image 13": { - type: "image", - alignment: "block", - static: false, - graded: true, - options: { - static: false, - title: "", - range: [ - [0, 10], - [0, 10], - ], - box: [600, 254], - backgroundImage: { - url: "https://cdn.kastatic.org/ka-content-images/358a87c20ab6ee70447f5fcb547010f69986828e.jpg", - width: 600, - height: 254, - }, - labels: [], - alt: "From space, the sun appears over Earth's horizon, illuminating the atmosphere as a blue layer above Earth. Above the atmosphere, space appears black.", - caption: - "*Sunrise photo from the International Space Station. Earth's atmosphere scatters electromagnetic radiation from the sun, producing a bright sky during the day.*", - }, - version: { - major: 0, - minor: 0, - }, - }, - "image 1": { - type: "image", - alignment: "block", - static: false, - graded: true, - options: { - static: false, - title: "", - range: [ - [0, 10], - [0, 10], - ], - box: [627, 522], - backgroundImage: { - url: "https://cdn.kastatic.org/ka-content-images/8100369eaf3b581d4e7bfc9f1062625309def486.gif", - width: 627, - height: 522, - }, - labels: [], - alt: "An animation shows a blue electric field arrow oscillating up and down. Connected to the base of the electric field arrow is a magnetic field arrow, which oscillates from side to side. The two fields oscillate in unison: when one extends the other extends too, creating a repeating wave pattern.", - caption: "", - }, - version: { - major: 0, - minor: 0, - }, - }, - "image 3": { - type: "image", - alignment: "block", - static: false, - graded: true, - options: { - static: false, - title: "", - range: [ - [0, 10], - [0, 10], - ], - box: [350, 130], - backgroundImage: { - url: "https://cdn.kastatic.org/ka-content-images/74edeeb6c6605a4e854e3a3e9db69c01dcf5508f.svg", - width: 350, - height: 130, - }, - labels: [], - alt: "A squiggly curve drawn from left to right. The right end of the curve has an arrow point. The curve begins with a small amount of wiggle on the left, which grows in amplitude in the middle and then decreases again on the right. The result is a small wave packet.", - caption: "", - }, - version: { - major: 0, - minor: 0, - }, - }, - }, -}; - export const Exercise: Story = { args: { question, @@ -147,6 +58,6 @@ export const Exercise: Story = { export const Article: Story = { args: { - question: sampleArticleSection, + question: articleWithImages, }, }; diff --git a/packages/perseus/src/__testdata__/article-renderer.testdata.ts b/packages/perseus/src/__testdata__/article-renderer.testdata.ts index 57b446bd11..3783830d4b 100644 --- a/packages/perseus/src/__testdata__/article-renderer.testdata.ts +++ b/packages/perseus/src/__testdata__/article-renderer.testdata.ts @@ -33,6 +33,96 @@ export const singleSectionArticle: PerseusRenderer = { }, }; +export const articleWithImages: PerseusRenderer = { + content: + "The word \"radiation\" sometimes gets a bad rap. People often associate radiation with something dangerous or scary, without really knowing what it is. In reality, we're surrounded by radiation all the time. \n\n**Radiation** is energy that travels through space (not just \"outer space\"—any space). Radiation can also interact with matter. How radiation interacts with matter depends on the type of radiation and the type of matter.\n\nRadiation comes in many forms, one of which is *electromagnetic radiation*. Without electromagnetic radiation life on Earth would not be possible, nor would most modern technologies.\n\n[[☃ image 13]]\n\nLet's take a closer look at this important and fascinating type of radiation.\n\n##Electromagnetic radiation\n\nAs the name suggests, **electromagnetic (EM) radiation** is energy transferred by *electromagnetic fields* oscillating through space.\n\nEM radiation is strange—it has both wave and particle properties. Let's take a look at both.\n\n###Electromagnetic waves\n\nAn animated model of an EM wave is shown below.\n[[☃ image 1]]\nThe electric field $(\\vec{\\textbf{E}})$ is shown in $\\color{blue}\\textbf{blue}$, and the magnetic field $(\\vec{\\textbf{B}})$ is shown in $\\color{red}\\textbf{red}$. They're perpendicular to each other.\n\nA changing electric field creates a magnetic field, and a changing magnetic field creates an electric field. So, once the EM wave is generated it propagates itself through space!\n\nAs with any wave, EM waves have wavelength, frequency, and speed. The wave model of EM radiation works best on large scales. But what about the atomic scale?\n\n###Photons\n\nAt the quantum level, EM radiation exists as particles. A particle of EM radiation is called a **photon**.\n\nWe can think of photons as wave *packets*—tiny bundles of EM radiation containing specific amounts of energy. Photons are visually represented using the following symbol.\n\n[[☃ image 3]]\n\nAll EM radiation, whether modeled as waves or photons, travels at the **speed of light** $\\textbf{(c)}$ in a vacuum: \n\n$\\text{c}=3\\times10^8\\space\\pu{m/s}=300{,}000{,}000\\space\\pu{m/s}$\n\nBut, EM radiation travels at a slower speed in matter, such as water or glass.", + images: {}, + widgets: { + "image 13": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [600, 254], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/358a87c20ab6ee70447f5fcb547010f69986828e.jpg", + width: 600, + height: 254, + }, + labels: [], + alt: "From space, the sun appears over Earth's horizon, illuminating the atmosphere as a blue layer above Earth. Above the atmosphere, space appears black.", + caption: + "*Sunrise photo from the International Space Station. Earth's atmosphere scatters electromagnetic radiation from the sun, producing a bright sky during the day.*", + }, + version: { + major: 0, + minor: 0, + }, + }, + "image 1": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [627, 522], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/8100369eaf3b581d4e7bfc9f1062625309def486.gif", + width: 627, + height: 522, + }, + labels: [], + alt: "An animation shows a blue electric field arrow oscillating up and down. Connected to the base of the electric field arrow is a magnetic field arrow, which oscillates from side to side. The two fields oscillate in unison: when one extends the other extends too, creating a repeating wave pattern.", + caption: "", + }, + version: { + major: 0, + minor: 0, + }, + }, + "image 3": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [350, 130], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/74edeeb6c6605a4e854e3a3e9db69c01dcf5508f.svg", + width: 350, + height: 130, + }, + labels: [], + alt: "A squiggly curve drawn from left to right. The right end of the curve has an arrow point. The curve begins with a small amount of wiggle on the left, which grows in amplitude in the middle and then decreases again on the right. The result is a small wave packet.", + caption: "", + }, + version: { + major: 0, + minor: 0, + }, + }, + }, +}; + export const passageArticle: PerseusRenderer = { content: "###Group/Pair Activity \n\nThis passage is adapted from Ed Yong, “Turtles Use the Earth’s Magnetic Field as Global GPS.” ©2011 by Kalmbach Publishing Co.\n\n[[☃ passage 1]]\n\n**Question 9**\n\nThe passage most strongly suggests that Adelita used which of the following to navigate her 9,000-mile journey?\n\nA) The current of the North Atlantic gyre\n\nB) Cues from electromagnetic coils designed by Putman and Lohmann\n\nC) The inclination and intensity of Earth’s magnetic field\n\nD) A simulated “magnetic signature” configured by Lohmann\n\n10) Which choice provides the best evidence for the answer to the previous question?\n\nA) Lines 1–2 (“In 1996...way”)\n\nB) Lines 20–21 (“Using...surface”)\n\nC) Lines 36–37 (“In the wild...stars”)\n\nD) Lines 43–45 (“Neither...it is”)\n\n**Question 12** \n\nBased on the passage, which choice best describes the relationship between Putman’s and Lohmann’s research?\n\nA) Putman’s research contradicts Lohmann’s.\n\nB) Putman’s research builds on Lohmann’s.\n\nC) Lohmann’s research confirms Putman’s.\n\nD) Lohmann’s research corrects Putman’s.", From 642f9303311190ab1f90ad2ab75fad78d8199406 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Wed, 7 Aug 2024 16:38:04 -0700 Subject: [PATCH 14/45] ContentRenderer: add story showing lint support --- .../__stories__/content-renderer.stories.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx b/packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx index 0903c9c099..3e83fadc55 100644 --- a/packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx +++ b/packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx @@ -1,5 +1,7 @@ import {View} from "@khanacademy/wonder-blocks-core"; +import Switch from "@khanacademy/wonder-blocks-switch"; import {spacing} from "@khanacademy/wonder-blocks-tokens"; +import {LabelMedium} from "@khanacademy/wonder-blocks-typography"; import {useState} from "react"; import {articleWithImages} from "../../../../perseus/src/__testdata__/article-renderer.testdata"; @@ -61,3 +63,23 @@ export const Article: Story = { question: articleWithImages, }, }; + +export const WithLintErrors: Story = { + args: { + linterContext: { + contentType: "exercise", + highlightLint: true, + stack: [], + paths: [], + }, + question: { + content: `# H1s bad + +Here is some unclosed math: $1+1=3 + +We should use \`\\dfrac{}\` instead of \`\\frac{}\`: $\\frac{3}{5}$`, + widgets: {}, + images: {}, + }, + }, +}; From 53d0e40776fb850b654bb0ee5312ee89a0150906 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Wed, 7 Aug 2024 17:23:15 -0700 Subject: [PATCH 15/45] ContentRenderer: Extend linting story --- .../__stories__/content-renderer.stories.tsx | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx b/packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx index 3e83fadc55..edc02c8dd5 100644 --- a/packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx +++ b/packages/perseus-editor/src/preview/__stories__/content-renderer.stories.tsx @@ -77,8 +77,27 @@ export const WithLintErrors: Story = { Here is some unclosed math: $1+1=3 -We should use \`\\dfrac{}\` instead of \`\\frac{}\`: $\\frac{3}{5}$`, - widgets: {}, +We should use \`\\dfrac{}\` instead of \`\\frac{}\`: $\\frac{3}{5}$ + +What is the best color in the world? + +[[☃ radio 1]]`, + widgets: { + "radio 1": { + type: "radio", + options: { + choices: [ + {content: "Red"}, + {content: "# Green"}, + {content: "Blue", correct: true}, + { + content: "None of these!", + isNoneOfTheAbove: true, + }, + ], + }, + }, + }, images: {}, }, }, From 8b46b8b1b3607041110aab333b45bdbfb75ed165 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Wed, 7 Aug 2024 17:25:59 -0700 Subject: [PATCH 16/45] Lint: revert updates for now --- packages/perseus/src/components/lint.tsx | 157 ++++++++++++++++++----- 1 file changed, 126 insertions(+), 31 deletions(-) diff --git a/packages/perseus/src/components/lint.tsx b/packages/perseus/src/components/lint.tsx index 7e996667ab..d53dadf9d9 100644 --- a/packages/perseus/src/components/lint.tsx +++ b/packages/perseus/src/components/lint.tsx @@ -1,10 +1,16 @@ import {StyleSheet, css} from "aphrodite"; import * as React from "react"; -import Tooltip from "@khanacademy/wonder-blocks-tooltip"; -import ExclamationIcon from "@phosphor-icons/core/regular/warning-circle.svg"; -import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon"; +import ReactDOM from "react-dom"; + import * as constants from "../styles/constants"; -import {color, font} from "@khanacademy/wonder-blocks-tokens"; + +import InlineIcon from "./inline-icon"; + +const exclamationIcon = { + path: "M6 11a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0-9a1 1 0 0 1 1 1v4a1 1 0 1 1-2 0V3a1 1 0 0 1 1-1z", + height: 12, + width: 12, +} as const; enum Severity { Error = 1, @@ -32,6 +38,10 @@ type Props = { severity?: Severity; }; +type State = { + tooltipAbove: boolean; +}; + /** * This component renders "lint" nodes in a markdown parse tree. Lint nodes * are inserted into the tree by the Perseus linter (see @@ -54,12 +64,43 @@ type Props = { * that has a right margin (like anything blockquoted) the circle will appear * to the left of where it belongs. And if there is more **/ -class Lint extends React.Component { +class Lint extends React.Component { _positionTimeout: number | undefined; + state: State = { + tooltipAbove: true, + }; + + componentDidMount() { + // TODO(somewhatabstract): Use WB timing + // eslint-disable-next-line no-restricted-syntax + this._positionTimeout = window.setTimeout(this.getPosition); + } + + componentWillUnmount() { + // TODO(somewhatabstract): Use WB timing + // eslint-disable-next-line no-restricted-syntax + window.clearTimeout(this._positionTimeout); + } + + // We can't call setState in componentDidMount without risking a render + // thrash, and we can't call getBoundingClientRect in render, so we + // borrow a timeout approach from learnstorm-dashboard.jsx and set our + // state once the component has mounted and we can get what we need. + getPosition: () => void = () => { + // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'getBoundingClientRect' does not exist on type 'Element | Text'. + const rect = ReactDOM.findDOMNode(this).getBoundingClientRect(); + // TODO(scottgrant): This is a magic number! We don't know the size + // of the tooltip at this point, so we're arbitrarily choosing a + // point at which to flip the tooltip's position. + this.setState({tooltipAbove: rect.top > 100}); + }; + // Render the element that holds the indicator icon and the tooltip // We pass different styles for the inline and block cases renderLink: (arg1: any) => React.ReactElement = (style) => { + const tooltipAbove = this.state.tooltipAbove; + let severityStyle; let warningText; let warningTextStyle; @@ -78,33 +119,38 @@ class Lint extends React.Component { } return ( - - {this.props.message.split("\n\n").map((m, i) => ( -

- - {warningText}:{" "} - - {m} -

- ))} - - } + - + {this.props.severity === 1 && ( + + )} + +
- - {this.props.severity === 1 && ( - + {this.props.message.split("\n\n").map((m, i) => ( +

+ + {warningText}:{" "} + + {m} +

+ ))} +
- - + /> +
+ ); }; @@ -340,11 +386,60 @@ const styles = StyleSheet.create({ backgroundColor: "#ffbe26", }, + // These are the styles for the tooltip + tooltip: { + // Absolute positioning relative to the lint indicator circle. + position: "absolute", + right: -12, + + // The tooltip is hidden by default; only displayed on hover + display: "none", + + // When it is displayed, it goes on top! + zIndex: 1000, + + // These styles control what the tooltip looks like + color: constants.white, + backgroundColor: constants.gray17, + opacity: 0.9, + fontFamily: constants.baseFontFamily, + fontSize: "12px", + lineHeight: "15px", + width: "320px", + borderRadius: "4px", + }, + // If we're going to render the tooltip above the warning circle, we use + // the previous rules in tooltip, but change the position slightly. + tooltipAbove: { + bottom: 32, + }, + + // We give the tooltip a little triangular "tail" that points down at + // the lint indicator circle. This is inside the tooltip and positioned + // relative to it. It also shares the opacity of the tooltip. We're using + // the standard CSS trick for drawing triangles with a thick border. + tail: { + position: "absolute", + top: -12, + right: 16, + width: 0, + height: 0, + + // This is the CSS triangle trick + borderLeft: "8px solid transparent", + borderRight: "8px solid transparent", + borderBottom: "12px solid " + constants.gray17, + }, + tailAbove: { + bottom: -12, + borderBottom: "none", + borderTop: "12px solid " + constants.gray17, + top: "auto", + }, + // Each warning in the tooltip is its own

. They are 12 pixels from // the edges of the tooltip and 12 pixels from each other. tooltipParagraph: { - fontFamily: font.family.sans, - color: color.white, margin: 12, }, From f9ae119f2e30641c8bce11f7faa1281c6980cf83 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Wed, 17 Jul 2024 08:51:34 -0700 Subject: [PATCH 17/45] ItemEditor: Remove imperative API for preview updates --- packages/perseus-editor/src/editor-page.tsx | 72 --------------------- packages/perseus-editor/src/item-editor.tsx | 6 -- 2 files changed, 78 deletions(-) diff --git a/packages/perseus-editor/src/editor-page.tsx b/packages/perseus-editor/src/editor-page.tsx index 59b140e9f5..5084237e87 100644 --- a/packages/perseus-editor/src/editor-page.tsx +++ b/packages/perseus-editor/src/editor-page.tsx @@ -62,7 +62,6 @@ type State = { }; class EditorPage extends React.Component { - _isMounted: boolean; renderer: any; itemEditor = React.createRef(); @@ -94,33 +93,6 @@ class EditorPage extends React.Component { wasAnswered: false, highlightLint: true, }; - - this._isMounted = false; - } - - componentDidMount() { - // TODO(scottgrant): This is a hack to remove the deprecated call to - // this.isMounted() but is still considered an anti-pattern. - this._isMounted = true; - - this.updateRenderer(); - } - - componentDidUpdate() { - // NOTE: It is required to delay the preview update until after the - // current frame, to allow for ItemEditor to render its widgets. - // This then enables to serialize the widgets properties correctly, - // in order to send data to the preview iframe (IframeContentRenderer). - // Otherwise, widgets will render in an "empty" state in the preview. - // TODO(jeff, CP-3128): Use Wonder Blocks Timing API - // eslint-disable-next-line no-restricted-syntax - setTimeout(() => { - this.updateRenderer(); - }); - } - - componentWillUnmount() { - this._isMounted = false; } toggleJsonMode: () => void = () => { @@ -136,50 +108,6 @@ class EditorPage extends React.Component { ); }; - updateRenderer() { - // Some widgets (namely the image widget) like to call onChange before - // anything has actually been mounted, which causes problems here. We - // just ensure don't update until we've mounted - const hasEditor = !this.props.developerMode || !this.props.jsonMode; - if (!this._isMounted || !hasEditor) { - return; - } - - const touch = - this.props.previewDevice === "phone" || - this.props.previewDevice === "tablet"; - const deviceBasedApiOptions: APIOptionsWithDefaults = { - ...this.getApiOptions(), - customKeypad: touch, - isMobile: touch, - }; - - this.itemEditor.current?.triggerPreviewUpdate({ - type: "question", - data: _({ - item: this.serialize(), - apiOptions: deviceBasedApiOptions, - initialHintsVisible: 0, - device: this.props.previewDevice, - linterContext: { - contentType: "exercise", - highlightLint: this.state.highlightLint, - // TODO(CP-4838): is it okay to use [] as a default? - paths: this.props.contentPaths || [], - }, - reviewMode: true, - legacyPerseusLint: this.itemEditor.current?.getSaveWarnings(), - }).extend( - _(this.props).pick( - "workAreaSelector", - "solutionAreaSelector", - "hintsAreaSelector", - "problemNum", - ), - ), - }); - } - getApiOptions(): APIOptionsWithDefaults { return { ...ApiOptions.defaults, diff --git a/packages/perseus-editor/src/item-editor.tsx b/packages/perseus-editor/src/item-editor.tsx index 3bedb4ebc9..efc00aa9f1 100644 --- a/packages/perseus-editor/src/item-editor.tsx +++ b/packages/perseus-editor/src/item-editor.tsx @@ -7,7 +7,6 @@ import Editor from "./editor"; import ItemExtrasEditor from "./item-extras-editor"; import ContentRenderer from "./preview/content-renderer"; -import type IframeContentRenderer from "./preview/iframe-content-renderer"; import type { APIOptions, ImageUploader, @@ -45,7 +44,6 @@ class ItemEditor extends React.Component { answerArea: {}, }; - frame = React.createRef(); questionEditor = React.createRef(); itemExtrasEditor = React.createRef(); @@ -56,10 +54,6 @@ class ItemEditor extends React.Component { this.props.onChange(_(props).extend(newProps), cb, silent); }; - triggerPreviewUpdate: (newData?: any) => void = (newData: any) => { - this.frame.current?.sendNewData(newData); - }; - handleEditorChange: ChangeHandler = (newProps, cb, silent) => { const question = _.extend({}, this.props.question, newProps); this.updateProps({question}, cb, silent); From ad99a25e048b54577f28e5ba84c3a7b513195c29 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Tue, 30 Jul 2024 09:06:06 -0700 Subject: [PATCH 18/45] ItemEditor: provide linter context to new ContentRenderer (preview) --- packages/perseus-editor/src/item-editor.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/perseus-editor/src/item-editor.tsx b/packages/perseus-editor/src/item-editor.tsx index efc00aa9f1..12d43b262b 100644 --- a/packages/perseus-editor/src/item-editor.tsx +++ b/packages/perseus-editor/src/item-editor.tsx @@ -115,6 +115,12 @@ class ItemEditor extends React.Component {

Date: Wed, 7 Aug 2024 16:52:14 -0700 Subject: [PATCH 19/45] Remove Storybook-specific preview for EditorPage - real preview now works! --- .../editor-page-with-storybook-preview.tsx | 88 ------------------- 1 file changed, 88 deletions(-) diff --git a/packages/perseus-editor/src/__stories__/editor-page-with-storybook-preview.tsx b/packages/perseus-editor/src/__stories__/editor-page-with-storybook-preview.tsx index 5b358a0071..85f146c6a5 100644 --- a/packages/perseus-editor/src/__stories__/editor-page-with-storybook-preview.tsx +++ b/packages/perseus-editor/src/__stories__/editor-page-with-storybook-preview.tsx @@ -1,24 +1,15 @@ 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"; @@ -45,8 +36,6 @@ function EditorPageWithStorybookPreview(props: Props) { props.hints, ); - const [panelOpen, setPanelOpen] = React.useState(true); - const apiOptions = props.apiOptions ?? { isMobile: false, flags, @@ -85,85 +74,8 @@ function EditorPageWithStorybookPreview(props: Props) { } }} /> - - {/* Button to open panel */} - {!panelOpen && ( - - )} - - {/* Panel to show the question/hint previews */} - {panelOpen && ( - - {/* Close button */} - setPanelOpen(!panelOpen)} - style={styles.closeButton} - /> - - - {/* Question preview */} - - - - {/* Hints preview */} - {hints?.map((hint, index) => ( - - - {`Hint ${index + 1}`} - - - ))} - - )} ); } -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; From 84b23db2c3577e9e1e7006751ce3885b04a244f3 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Tue, 30 Jul 2024 09:04:06 -0700 Subject: [PATCH 20/45] HintEditor: migrate to iframe-less preview (ContentRenderer) --- packages/perseus-editor/src/hint-editor.tsx | 50 +++------------------ 1 file changed, 5 insertions(+), 45 deletions(-) diff --git a/packages/perseus-editor/src/hint-editor.tsx b/packages/perseus-editor/src/hint-editor.tsx index 79825f0aa4..34d204f384 100644 --- a/packages/perseus-editor/src/hint-editor.tsx +++ b/packages/perseus-editor/src/hint-editor.tsx @@ -10,7 +10,7 @@ import _ from "underscore"; import DeviceFramer from "./components/device-framer"; import Editor from "./editor"; -import IframeContentRenderer from "./preview/iframe-content-renderer"; +import ContentRenderer from "./preview/content-renderer"; import type { APIOptions, @@ -184,35 +184,6 @@ class CombinedHintEditor extends React.Component { }; editor = React.createRef(); - frame = React.createRef(); - - componentDidMount() { - this.updatePreview(); - } - - componentDidUpdate() { - this.updatePreview(); - } - - updatePreview = () => { - const shouldBold = - this.props.isLast && !/\*\*/.test(this.props.hint.content); - - this.frame.current?.sendNewData({ - type: "hint", - data: { - hint: this.props.hint, - bold: shouldBold, - pos: this.props.pos, - apiOptions: this.props.apiOptions, - linterContext: { - contentType: "hint", - highlightLint: this.props.highlightLint, - paths: this.props.contentPaths, - }, - }, - }); - }; getSaveWarnings = () => { return this.editor.current?.getSaveWarnings(); @@ -258,12 +229,9 @@ class CombinedHintEditor extends React.Component { deviceType={this.props.deviceType} nochrome={true} > -
@@ -398,7 +366,7 @@ class CombinedHintsEditor extends React.Component { const {itemId, hints} = this.props; const hintElems = _.map( hints, - function (hint, i) { + (hint, i) => { return ( { itemId={itemId} hint={hint} pos={i} - // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation. imageUploader={this.props.imageUploader} // eslint-disable-next-line react/jsx-no-bind - // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation. | TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation. onChange={this.handleHintChange.bind(this, i)} // eslint-disable-next-line react/jsx-no-bind - // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation. | TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation. onRemove={this.handleHintRemove.bind(this, i)} // eslint-disable-next-line react/jsx-no-bind - // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation. | TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation. onMove={this.handleHintMove.bind(this, i)} - // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation. deviceType={this.props.deviceType} - // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation. apiOptions={this.props.apiOptions} - // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation. highlightLint={this.props.highlightLint} - // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation. previewURL={this.props.previewURL} // TODO(CP-4838): what should be passed here? contentPaths={[]} From 2823360d230dd3d8674def7a6fbaf8c394d65988 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Tue, 6 Aug 2024 12:36:58 -0700 Subject: [PATCH 21/45] HintEditor: propogate preview device type to ContentRenderer --- packages/perseus-editor/src/hint-editor.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/perseus-editor/src/hint-editor.tsx b/packages/perseus-editor/src/hint-editor.tsx index 34d204f384..5cdff09c8b 100644 --- a/packages/perseus-editor/src/hint-editor.tsx +++ b/packages/perseus-editor/src/hint-editor.tsx @@ -232,6 +232,7 @@ class CombinedHintEditor extends React.Component {
From 67b9ee41955225e87809b5efec5503ea8cea9fce Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Tue, 30 Jul 2024 09:07:55 -0700 Subject: [PATCH 22/45] ArticleEditor: migrate to new preview (ContentRenderer) --- .../perseus-editor/src/article-editor.tsx | 147 +++++++----------- 1 file changed, 60 insertions(+), 87 deletions(-) diff --git a/packages/perseus-editor/src/article-editor.tsx b/packages/perseus-editor/src/article-editor.tsx index f087719f41..03ffefc5a3 100644 --- a/packages/perseus-editor/src/article-editor.tsx +++ b/packages/perseus-editor/src/article-editor.tsx @@ -13,20 +13,23 @@ import DeviceFramer from "./components/device-framer"; import JsonEditor from "./components/json-editor"; import SectionControlButton from "./components/section-control-button"; import Editor from "./editor"; -import IframeContentRenderer from "./preview/iframe-content-renderer"; +import ContentRenderer from "./preview/content-renderer"; -import type {APIOptions, Changeable, ImageUploader} from "@khanacademy/perseus"; +import type { + APIOptions, + Changeable, + ImageUploader, + PerseusRenderer, +} from "@khanacademy/perseus"; const {HUD, InlineIcon} = components; const {iconCircleArrowDown, iconCircleArrowUp, iconPlus, iconTrash} = icons; -type RendererProps = { - content?: string; - widgets?: any; - images?: any; -}; +type JsonType = + | ReadonlyArray> + | PerseusRenderer + | ReadonlyArray; -type JsonType = RendererProps | ReadonlyArray; type DefaultProps = { contentPaths?: ReadonlyArray; json: JsonType; @@ -61,62 +64,9 @@ export default class ArticleEditor extends React.Component { highlightLint: true, }; - componentDidMount() { - this._updatePreviewFrames(); - } - - componentDidUpdate() { - this._updatePreviewFrames(); - } - - _updatePreviewFrames() { - if (this.props.mode === "preview") { - // eslint-disable-next-line react/no-string-refs - // @ts-expect-error - TS2339 - Property 'sendNewData' does not exist on type 'ReactInstance'. - this.refs["frame-all"].sendNewData({ - type: "article-all", - data: this._sections().map((section, i) => { - return this._apiOptionsForSection(section, i); - }), - }); - } else if (this.props.mode === "edit") { - this._sections().forEach((section, i) => { - // eslint-disable-next-line react/no-string-refs - // @ts-expect-error - TS2339 - Property 'sendNewData' does not exist on type 'ReactInstance'. - this.refs["frame-" + i].sendNewData({ - type: "article", - data: this._apiOptionsForSection(section, i), - }); - }); - } - } - - _apiOptionsForSection(section: RendererProps, sectionIndex: number): any { - // eslint-disable-next-line react/no-string-refs - const editor = this.refs[`editor${sectionIndex}`]; - return { - apiOptions: { - ...ApiOptions.defaults, - ...this.props.apiOptions, - - // Alignment options are always available in article - // editors - showAlignmentOptions: true, - isArticle: true, - }, - json: section, - useNewStyles: this.props.useNewStyles, - linterContext: { - contentType: "article", - highlightLint: this.state.highlightLint, - paths: this.props.contentPaths, - }, - // @ts-expect-error - TS2339 - Property 'getSaveWarnings' does not exist on type 'ReactInstance'. - legacyPerseusLint: editor ? editor.getSaveWarnings() : [], - }; - } + editorRefs: Array = []; - _sections(): ReadonlyArray { + _sections(): ReadonlyArray { return Array.isArray(this.props.json) ? this.props.json : [this.props.json]; @@ -201,7 +151,9 @@ export default class ArticleEditor extends React.Component {
{ i, )} placeholder="Type your section text here..." - ref={"editor" + i} + ref={(editor) => { + this.editorRefs[i] = editor; + }} />
@@ -267,16 +221,36 @@ export default class ArticleEditor extends React.Component { const isMobile = this.props.screen === "phone" || this.props.screen === "tablet"; + const apiOptions = { + ...ApiOptions.defaults, + ...this.props.apiOptions, + isMobile, + // Alignment options are always available in article + // editors + showAlignmentOptions: true, + isArticle: true, + }; return ( - + {this._sections().map((section, i) => { + const editor = this.editorRefs[i]; + + return ( + + ); + })} ); } @@ -293,15 +267,14 @@ export default class ArticleEditor extends React.Component { this.props.onChange({json: newJson}); }; - _handleEditorChange: (i: number, newProps: RendererProps) => void = ( - i, - newProps, - ) => { - const sections = _.clone(this._sections()); - // @ts-expect-error - TS2542 - Index signature in type 'readonly RendererProps[]' only permits reading. - sections[i] = _.extend({}, sections[i], newProps); + _handleEditorChange( + sectionIndex: number, + newProps: Partial, + ) { + const sections = [...this._sections()]; + sections[sectionIndex] = {...sections[sectionIndex], ...newProps}; this.props.onChange({json: sections}); - }; + } _handleMoveSectionEarlier(i: number) { if (i === 0) { @@ -309,9 +282,9 @@ export default class ArticleEditor extends React.Component { } const sections = _.clone(this._sections()); const section = sections[i]; - // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly RendererProps[]'. Did you mean 'slice'? + // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly Renderer[]'. Did you mean 'slice'? sections.splice(i, 1); - // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly RendererProps[]'. Did you mean 'slice'? + // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly Renderer[]'. Did you mean 'slice'? sections.splice(i - 1, 0, section); this.props.onChange({ json: sections, @@ -324,9 +297,9 @@ export default class ArticleEditor extends React.Component { return; } const section = sections[i]; - // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly RendererProps[]'. Did you mean 'slice'? + // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly Renderer[]'. Did you mean 'slice'? sections.splice(i, 1); - // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly RendererProps[]'. Did you mean 'slice'? + // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly Renderer[]'. Did you mean 'slice'? sections.splice(i + 1, 0, section); this.props.onChange({ json: sections, @@ -358,7 +331,7 @@ export default class ArticleEditor extends React.Component { _handleRemoveSection(i: number) { const sections = _.clone(this._sections()); - // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly RendererProps[]'. Did you mean 'slice'? + // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly Renderer[]'. Did you mean 'slice'? sections.splice(i, 1); this.props.onChange({ json: sections, @@ -388,7 +361,7 @@ export default class ArticleEditor extends React.Component { * * This function can currently only be called in edit mode. */ - getSaveWarnings(): ReadonlyArray { + getSaveWarnings(): ReadonlyArray { if (this.props.mode !== "edit") { // TODO(joshuan): We should be able to get save warnings in // preview mode. From c5b39864b7194071e68b6dd99856f4501e7b4c5d Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Thu, 1 Aug 2024 10:56:35 -0700 Subject: [PATCH 23/45] ArticleEditor: restore section border to preview --- packages/perseus-editor/src/preview/content-renderer.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/perseus-editor/src/preview/content-renderer.tsx b/packages/perseus-editor/src/preview/content-renderer.tsx index 6f52fb9d90..443bb4de03 100644 --- a/packages/perseus-editor/src/preview/content-renderer.tsx +++ b/packages/perseus-editor/src/preview/content-renderer.tsx @@ -7,7 +7,7 @@ import {Renderer, constants} from "@khanacademy/perseus"; // eslint-disable-next-line monorepo/no-internal-import import {mockStrings} from "@khanacademy/perseus/strings"; import {View} from "@khanacademy/wonder-blocks-core"; -import {spacing} from "@khanacademy/wonder-blocks-tokens"; +import {color, spacing} from "@khanacademy/wonder-blocks-tokens"; import {StyleSheet} from "aphrodite"; import * as React from "react"; @@ -63,8 +63,9 @@ function ContentRenderer({ const styles = StyleSheet.create({ container: { padding: spacing.xxxSmall_4, - containerType: "inline-size", - containerName: "perseus-root", + borderStyle: "solid", + borderWidth: "1px", + borderColor: color.offBlack, }, gutter: {marginRight: constants.lintGutterWidth}, }); From 4e52482b5eae04e8b5570514d1e365f177285ff9 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Thu, 1 Aug 2024 13:43:22 -0700 Subject: [PATCH 24/45] ArticleEditor: handle rendering one or all sections --- .../perseus-editor/src/article-editor.tsx | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/perseus-editor/src/article-editor.tsx b/packages/perseus-editor/src/article-editor.tsx index 03ffefc5a3..bf30e411d1 100644 --- a/packages/perseus-editor/src/article-editor.tsx +++ b/packages/perseus-editor/src/article-editor.tsx @@ -215,7 +215,7 @@ export default class ArticleEditor extends React.Component { } _renderIframePreview( - i: number | string, + sectionIndex: number | "all", nochrome: boolean, ): React.ReactElement { const isMobile = @@ -230,27 +230,46 @@ export default class ArticleEditor extends React.Component { showAlignmentOptions: true, isArticle: true, }; + + const sections = this._sections(); return ( - {this._sections().map((section, i) => { - const editor = this.editorRefs[i]; - - return ( + {sectionIndex === "all" ? ( + sections.map((section, i) => ( - ); - })} + )) + ) : ( + + )} ); } From 9e6c8d6932aa58c4a2ee32aa2d3cccf29cda8ece Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Thu, 1 Aug 2024 13:43:56 -0700 Subject: [PATCH 25/45] ArticleEditor: build onChange handler safely (bind this as we pass a method which loses the this binding) --- packages/perseus-editor/src/article-editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/perseus-editor/src/article-editor.tsx b/packages/perseus-editor/src/article-editor.tsx index bf30e411d1..4b6d622c60 100644 --- a/packages/perseus-editor/src/article-editor.tsx +++ b/packages/perseus-editor/src/article-editor.tsx @@ -157,7 +157,7 @@ export default class ArticleEditor extends React.Component { apiOptions={apiOptions} imageUploader={imageUploader} onChange={_.partial( - this._handleEditorChange, + this._handleEditorChange.bind(this), i, )} placeholder="Type your section text here..." From dc9fff214d54590ce8391a2b1da5d6071e65394d Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Thu, 1 Aug 2024 13:51:21 -0700 Subject: [PATCH 26/45] ArticleEditor: add example article to article editor story --- .../__stories__/article-editor.stories.tsx | 590 +++++++++++++++++- 1 file changed, 589 insertions(+), 1 deletion(-) diff --git a/packages/perseus-editor/src/__stories__/article-editor.stories.tsx b/packages/perseus-editor/src/__stories__/article-editor.stories.tsx index 75dabe601e..4bd239dc5c 100644 --- a/packages/perseus-editor/src/__stories__/article-editor.stories.tsx +++ b/packages/perseus-editor/src/__stories__/article-editor.stories.tsx @@ -5,14 +5,602 @@ import {useRef, useState} from "react"; import ArticleEditor from "../article-editor"; import {registerAllWidgetsAndEditorsForTesting} from "../util/register-all-widgets-and-editors-for-testing"; +import type {PerseusRenderer} from "@khanacademy/perseus"; + registerAllWidgetsAndEditorsForTesting(); export default { title: "PerseusEditor/ArticleEditor", }; +const sampleArticle: ReadonlyArray = [ + { + content: + "The word \"radiation\" sometimes gets a bad rap. People often associate radiation with something dangerous or scary, without really knowing what it is. In reality, we're surrounded by radiation all the time. \n\n# Heading\n\n**Radiation** is energy that travels through space (not just \"outer space\"—any space). Radiation can also interact with matter. How radiation interacts with matter depends on the type of radiation and the type of matter.\n\nRadiation comes in many forms, one of which is *electromagnetic radiation*. Without electromagnetic radiation life on Earth would not be possible, nor would most modern technologies.\n\n[[☃ image 13]]\n\nLet's take a closer look at this important and fascinating type of radiation.\n\n##Electromagnetic radiation\n\nAs the name suggests, **electromagnetic (EM) radiation** is energy transferred by *electromagnetic fields* oscillating through space.\n\nEM radiation is strange—it has both wave and particle properties. Let's take a look at both.\n\n###Electromagnetic waves\n\nAn animated model of an EM wave is shown below.\n[[☃ image 1]]\nThe electric field $(\\vec{\\textbf{E}})$ is shown in $\\color{blue}\\textbf{blue}$, and the magnetic field $(\\vec{\\textbf{B}})$ is shown in $\\color{red}\\textbf{red}$. They're perpendicular to each other.\n\nA changing electric field creates a magnetic field, and a changing magnetic field creates an electric field. So, once the EM wave is generated it propagates itself through space!\n\nAs with any wave, EM waves have wavelength, frequency, and speed. The wave model of EM radiation works best on large scales. But what about the atomic scale?\n\n###Photons\n\nAt the quantum level, EM radiation exists as particles. A particle of EM radiation is called a **photon**.\n\nWe can think of photons as wave *packets*—tiny bundles of EM radiation containing specific amounts of energy. Photons are visually represented using the following symbol.\n\n[[☃ image 3]]\n\nAll EM radiation, whether modeled as waves or photons, travels at the **speed of light** $\\textbf{(c)}$ in a vacuum: \n\n$\\text{c}=3\\times10^8\\space\\pu{m/s}=300{,}000{,}000\\space\\pu{m/s}$\n\nBut, EM radiation travels at a slower speed in matter, such as water or glass.", + images: {}, + widgets: { + "image 13": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [600, 254], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/358a87c20ab6ee70447f5fcb547010f69986828e.jpg", + width: 600, + height: 254, + }, + labels: [], + alt: "From space, the sun appears over Earth's horizon, illuminating the atmosphere as a blue layer above Earth. Above the atmosphere, space appears black.", + caption: + "*Sunrise photo from the International Space Station. Earth's atmosphere scatters electromagnetic radiation from the sun, producing a bright sky during the day.*", + }, + version: { + major: 0, + minor: 0, + }, + }, + "image 1": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [627, 522], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/8100369eaf3b581d4e7bfc9f1062625309def486.gif", + width: 627, + height: 522, + }, + labels: [], + alt: "An animation shows a blue electric field arrow oscillating up and down. Connected to the base of the electric field arrow is a magnetic field arrow, which oscillates from side to side. The two fields oscillate in unison: when one extends the other extends too, creating a repeating wave pattern.", + caption: "", + }, + version: { + major: 0, + minor: 0, + }, + }, + "image 3": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [350, 130], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/74edeeb6c6605a4e854e3a3e9db69c01dcf5508f.svg", + width: 350, + height: 130, + }, + labels: [], + alt: "A squiggly curve drawn from left to right. The right end of the curve has an arrow point. The curve begins with a small amount of wiggle on the left, which grows in amplitude in the middle and then decreases again on the right. The result is a small wave packet.", + caption: "", + }, + version: { + major: 0, + minor: 0, + }, + }, + }, + }, + { + content: + "##The electromagnetic spectrum\n\nThe wavelength, frequency, and energy of EM radiation can fall within a wide range. We call that range the **electromagnetic (EM) spectrum**. \n\nA diagram of the EM spectrum is shown below.\n\n[[☃ image 3]]\n\nLet's analyze one piece of this diagram at a time.\n\n###Wavelength\n\nWavelength is the distance from one peak of a wave to the next peak. Notice how the red wave at the top of the diagram shows the wavelength *decreasing* from left to right—the peaks get closer together. \n\nThe numbers on the diagram tell us that EM wavelength ranges from $10^3\\space(1{,}000)$ meters to $10^{-12}\\space(0.00000000001)$ meters. \n\nSome EM waves have wavelengths longer than buildings while others are shorter than atoms. That's a big difference!\n\n###Frequency\n\nFrequency is the number of wave cycles in a period of time. We can't \"see\" frequency in a stationary diagram, since frequency involves change over time.\n\nBut, we can visualize frequency with a simple example. Imagine you're creating water waves by dripping water into a pool. The more frequent the drips, the higher the frequency of waves which will spread out.\n\n[[☃ image 6]]\n\nIf you release one drip per second, you'll create a wave with a frequency of $1$ hertz $(\\pu{Hz})$. $(1\\space\\pu{Hz}=1\\space\\text{cycle}/\\text{second})$ If you release three drips per second, you'll create a $3$ hertz wave.\n\nThe numbers on the diagram tell us that EM frequency ranges from $10^4\\space(10{,}000)$ hertz to $10^{20}\\space(100{,}000{,}000{,}000{,}000{,}000{,}000)$ hertz.\n\nImagine that many water drips every second!\n\n###Energy\n\nFinally, notice on the bottom of the EM spectrum diagram that **photon energy increases as frequency increases**. \n\nThe diagram does not show numerical values for energy, but the energy of EM radiation can be measured in various units.\n\nOne unit used to measure the energy of individual photons is the electronvolt $(\\pu{eV})$.\n\nTo summarize for EM waves:\n\nshorter wavelength $\\rightarrow$ higher frequency $\\rightarrow$ higher energy photons", + images: {}, + widgets: { + "image 3": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [650, 322], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/c91bbc26edeca8d28c474c22304ebff48ec4de52.svg", + width: 650, + height: 322, + }, + labels: [], + alt: "A diagram of the EM spectrum shows the following regions in order from lowest frequency and energy to highest frequency and energy. Each is accompanied by an object for comparison to its wavelength.\nRadio waves: wavelength on the scale of buildings\nMicrowaves: wavelength on the scale of humans to butterflies\nInfrared: wavelength on the scale of a needle point\nVisible light: wavelength on the scale of protozoans\nUltraviolet: wavelength on the scale of molecules\nX-rays: wavelength on the scale of atoms\nGamma rays: wavelength on the scale of nuclei", + caption: "", + }, + version: { + major: 0, + minor: 0, + }, + }, + "image 6": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [400, 267], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/ad740ef4cd0a31eb3e4e7e98a66cc907834a1eca.jpg", + width: 400, + height: 267, + }, + labels: [], + alt: "Water drips fall on a surface of water, creating circular waves that spread outward", + caption: "", + }, + version: { + major: 0, + minor: 0, + }, + }, + }, + }, + { + content: + "##Let's take a tour!\n\nEach region of the EM spectrum has it's own name. Let's explore each region in more detail.\n\n###Radio waves\n\nRadio waves are EM waves with the longest wavelength and lowest frequency. AM and FM radio signals are sent using radio waves.\n\n###Microwaves\n\nMicrowaves have higher frequency than radio waves. Microwaves are produced inside microwave ovens to warm food. Most WiFi signals are also in the microwave range.\n\n###Infrared\n\nInfrared radiation has higher frequency than microwaves. Objects at \"everyday\" temperatures we interact with regularly, including our bodies, radiate infrared waves. So, we commonly associate infrared radiation with warmth.\n\nInfrared cameras show the amount of infrared radiating from an area. For example, this infrared image of Rusty the dog shows that the greatest amount of infrared radiation escapes from his eyes, mouth, and ears. His fur traps much of the energy on the rest of his body, keeping him warm.\n\n[[☃ image 1]]\n\nInfrared waves also radiate from surfaces warmed by the sun, like soil and concrete. Some of that infrared radiation escapes into space, and some is trapped by Earth's atmosphere.\n\n###Visible light\n\nThe next highest frequency band is visible light. This band of EM radiation is called \"visible\" because it's what human eyes can see. \n\nSighted people can only see when visible light enters their eyes. Nothing is visible in a room without visible light.\n\nOur brains perceive different frequencies in the visible spectrum as different colors. We see the longest wavelength, lowest frequency visible light as $\\color{red}{\\textbf{red}}$, and the shortest wavelength, highest frequency visible light as $\\color{darkviolet}{\\textbf{violet}}$.\n\nAll the other colors of the \n$\\textcolor{red}{\\textbf{r}}\\textcolor{darkorange}{\\textbf{a}}\\textcolor{gold}{\\textbf{i}}\\textcolor{green}{\\textbf{n}}\\textcolor{blue}{\\textbf{b}}\\textcolor{indigo}{\\textbf{o}}\\textcolor{darkviolet}{\\textbf{w}}$ are associated with frequencies between red and violet. Other colors, like white, result from mixtures of these frequencies entering our eyes at the same time.\n\n[[☃ image 9]]\n\n[[☃ explanation 1]]\n\n###Ultraviolet (UV)\n\nAs EM frequency increases beyond violet light, we lose the ability to see it with our eyes. We've entered the ultraviolet—or UV—range.\n\nRemember that photon energy increases with frequency. The energy of UV photons is high enough that they can cause damage to living tissues.\n\nIn addition to visible light, UV radiation enters Earth's atmosphere from the sun. The ozone layer blocks much of it, but some UV makes it to Earth's surface.\n\nSo when you're outside in the sun, make sure that you use UV protection to avoid sunburns and eye damage.\n\n###X-rays\n\nAs EM frequency increases beyond UV, we arrive at X-rays.\n\nThe energy of X-ray photons is high enough to make it **ionizing** radiation. This means that if an X-ray photon enters matter, it can knock electrons out of (ionize) atoms in the matter.\n\nThis is dangerous for organisms, since repeated ionizations in cells can lead to damage and mutations.\n\nHowever, the small dose of X-ray radiation received during a medical scan is safe. If you've ever had an X-ray image taken of your body, X-ray photons were sent into you. Dense tissues like bone block more X-rays than soft tissues, creating a shadow image when the X-rays reach a detector on the other side.\n\nCan you tell what's wrong with this person's arm from the X-ray image? Ouch.\n\n[[☃ image 4]]\n\n###Gamma rays\n\nFinally, we reach gamma rays. Gamma $(\\gamma)$ radiation has the highest frequency and energy of any EM radiation. Like X-rays, gamma rays are **ionizing** radiation. They're the most hazardous to living things.\n\nGamma rays are produced by high energy interactions in space. Gamma rays are also produced by unstable nuclei during a type of radioactive decay called *gamma decay*.\n\n[[☃ image 7]]\n\nThough gamma rays are hazardous, they can also be used for good. For example, gamma rays are used in radiation therapy to kill cancer cells.", + images: {}, + widgets: { + "image 1": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [416, 212], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/386d98878f310b2c47b6ff9efa03bfb59d5a5448.jpg", + width: 416, + height: 212, + }, + labels: [], + alt: "An infrared image of a dog's face, represented in visible colors. Yellow regions emit more infrared, and purple regions emit less infrared. In the infrared image, the dog's open mouth, eyes, and ears are yellow, and the rest of his face is purple.", + caption: "", + }, + version: { + major: 0, + minor: 0, + }, + }, + "image 9": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [400, 314], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/84ab83cd86b18c94388b8247e853536d3230239e.jpg", + width: 400, + height: 314, + }, + labels: [], + alt: "A triangular glass prism spreads out different wavelengths of visible light. A beam of white light enters the prism on one side, and a rainbow of colors exits the other side.", + caption: "", + }, + version: { + major: 0, + minor: 0, + }, + }, + "explanation 1": { + type: "explanation", + alignment: "default", + static: false, + graded: true, + options: { + static: false, + showPrompt: 'Does "light" only refer to visible light?', + hidePrompt: "Hide explanation", + explanation: + 'It depends. Sometimes scientists use "light" to refer specifically to visible light. Other times they use "light" to refer to the entire EM spectrum.\n\nSo, you can do either, as long as you\'re clear about what you\'re referring to.', + widgets: {}, + }, + version: { + major: 0, + minor: 0, + }, + }, + "image 4": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [200, 446], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/ed08bb252bcc442e308e7a4de96ff1a312727065.jpg", + width: 200, + height: 446, + }, + labels: [], + alt: "A black and white X-ray image of a person's arm shows the arm bone in white, and the surrounding tissues in lighter gray. The bone is fractured near the elbow.", + caption: "", + }, + version: { + major: 0, + minor: 0, + }, + }, + "image 7": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [275, 187], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/e1dea0b94f32064bbb667aa882460092020afab5.png", + width: 275, + height: 187, + }, + labels: [], + alt: "A large nucleus is shown as a clump of red protons and blue neutrons. A gamma photon, shown as a squiggly wave packet, shoots out of the nucleus.", + caption: "", + }, + version: { + major: 0, + minor: 0, + }, + }, + }, + }, + { + content: + "##Summary\n\nElectromagnetic radiation consists of oscillating electric and magnetic fields. It can be modeled both as waves and as particles called *photons*. All EM radiation, from radio to gamma, travels at speed $\\text{c}$ in a vacuum. The higher the frequency of EM radiation, the higher the energy of the photons.\n\nHuman eyes can only see the visible light portion of the spectrum directly. However, we can use various detectors to make the other regions of the EM spectrum visible to us.\n\nLet's conclude by viewing the Crab Nebula in different wavelengths. A whole new universe is revealed when we view the cosmos across the electromagnetic spectrum!\n\n[[☃ image 1]]", + images: {}, + widgets: { + "image 1": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [650, 150], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/fa3133d6d23c0852e1a81a54d78946910523e365.png", + width: 650, + height: 150, + }, + labels: [], + alt: "The Crab Nebula appears different in the following wavelengths of the EM spectrum:\nRadio, infrared, and ultraviolet: the nebula is most intense in the middle and weaker around the outside.\nVisible light: the nebula shows several thin, fibrous features stretching toward the outside\nX-rays: a rotating spiral is visible in the middle of the nebula, with two jets shooting out\nGamma: a ball glows in the middle of the nebula", + caption: + "We can use detectors sensitive to different frequencies to produce images of the same object in different regions of the EM spectrum. The images are presented using visible light so we can see them.", + }, + version: { + major: 0, + minor: 0, + }, + }, + }, + }, + { + content: + "##Try it!\n\n[[☃ graded-group 1]]\n\n[[☃ graded-group 2]]\n\n[[☃ graded-group 3]]\n\n[[☃ explanation 1]]\n", + images: {}, + widgets: { + "graded-group 1": { + type: "graded-group", + alignment: "default", + static: false, + graded: true, + options: { + title: "", + content: + "\n**Problem 1**\n\nSelect the correct description for each point marked on the rainbow.\n\n[[☃ label-image 1]]", + images: {}, + widgets: { + "label-image 1": { + type: "label-image", + alignment: "default", + static: false, + graded: true, + options: { + static: false, + choices: [ + "long wavelength & low frequency", + "short wavelength & high frequency", + ], + imageAlt: + "Section of a rainbow in the sky. From left to right, the colors appear as violet, indigo, blue, green, yellow, orange, and red.", + imageUrl: + "https://cdn.kastatic.org/ka-content-images/227d402cb09ebc1b67f197467212fa4ab3ced5b3.jpg", + imageWidth: 400, + imageHeight: 289, + markers: [ + { + answers: [ + "short wavelength & high frequency", + ], + label: "Violet side of the rainbow", + x: 35.8, + y: 13, + }, + { + answers: [ + "long wavelength & low frequency", + ], + label: "Red side of the rainbow", + x: 71.8, + y: 50.5, + }, + ], + multipleAnswers: false, + hideChoicesFromInstructions: true, + }, + version: { + major: 0, + minor: 0, + }, + }, + }, + hint: { + content: + "We can use the electromagnetic spectrum to determine the properties of each end of the rainbow.\n\n[[☃ image 1]]\n\nThe spectrum shows that for visible light, red light has the longest wavelength and lowest frequency, and violet light has the shortest wavelength and highest frequency.", + images: {}, + widgets: { + "image 1": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [650, 322], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/c91bbc26edeca8d28c474c22304ebff48ec4de52.svg", + width: 650, + height: 322, + }, + labels: [], + alt: "", + caption: "", + }, + version: { + major: 0, + minor: 0, + }, + }, + }, + }, + }, + version: { + major: 0, + minor: 0, + }, + }, + "graded-group 2": { + type: "graded-group", + alignment: "default", + static: false, + graded: true, + options: { + title: "", + content: + "**Problem 2**\n\nThe image below shows laser beams of different colors.\n\n[[☃ image 1]]\n\n**Which laser beam contains the highest energy photons?**\n\n[[☃ radio 1]]", + images: {}, + widgets: { + "image 1": { + type: "image", + alignment: "block", + static: false, + graded: true, + options: { + static: false, + title: "", + range: [ + [0, 10], + [0, 10], + ], + box: [400, 300], + backgroundImage: { + url: "https://cdn.kastatic.org/ka-content-images/43a778bc143b510658c4be3cc447effe3d0018bf.jpg", + width: 400, + height: 300, + }, + labels: [], + alt: "Several laser beams of different colors: red, green, blue, and violet", + caption: "", + }, + version: { + major: 0, + minor: 0, + }, + }, + "radio 1": { + type: "radio", + alignment: "default", + static: false, + graded: true, + options: { + choices: [ + { + content: "red", + correct: false, + clue: "Red photons have the *lowest* frequency and energy on the visible spectrum.", + }, + { + content: "green", + correct: false, + clue: "Green photons have *lower* frequency and energy than blue photons on the visible spectrum.", + }, + { + isNoneOfTheAbove: false, + content: "blue", + correct: false, + clue: "Blue photons have *lower* frequency and energy than violet photons on the visible spectrum.", + }, + { + isNoneOfTheAbove: false, + content: "violet", + correct: true, + clue: "Violet photons have the highest frequency and energy on the visible spectrum.", + }, + ], + randomize: false, + multipleSelect: false, + countChoices: false, + displayCount: null, + hasNoneOfTheAbove: false, + deselectEnabled: false, + }, + version: { + major: 1, + minor: 0, + }, + }, + }, + }, + version: { + major: 0, + minor: 0, + }, + }, + "graded-group 3": { + type: "graded-group", + alignment: "default", + static: false, + graded: true, + options: { + title: "", + content: + "\n**Problem 3**\n\nWhich of these forms of EM radiation are **ionizing**?\n\n[[☃ radio 1]]", + images: {}, + widgets: { + "radio 1": { + type: "radio", + alignment: "default", + static: false, + graded: true, + options: { + choices: [ + { + content: "Radio waves", + clue: "Radio photons are *not* high enough energy to be ionizing.", + }, + { + content: "Infrared", + clue: "Infrared photons are *not* high enough energy to be ionizing.", + }, + { + isNoneOfTheAbove: false, + content: "X-rays", + clue: "X-ray photons are high enough energy to be ionizing.", + correct: true, + }, + { + isNoneOfTheAbove: false, + content: "Gamma rays", + clue: "Gamma photons are high enough energy to be ionizing.", + correct: true, + }, + ], + randomize: false, + multipleSelect: true, + countChoices: false, + displayCount: null, + hasNoneOfTheAbove: false, + deselectEnabled: false, + }, + version: { + major: 1, + minor: 0, + }, + }, + }, + }, + version: { + major: 0, + minor: 0, + }, + }, + "explanation 1": { + type: "explanation", + alignment: "default", + static: false, + graded: true, + options: { + static: false, + showPrompt: "Image credits", + hidePrompt: "Hide image credits", + explanation: + '"[An orbital sunrise above Shenzhen, China](https://www.nasa.gov/image-feature/an-orbital-sunrise-above-shenzhen-china)" by NASA, [Public domain](https://creativecommons.org/publicdomain/mark/1.0/)\n\n"[EM wave gif](https://commons.wikimedia.org/wiki/File:EM-Wave.gif)" by And1mu, [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en)\n\n"[Photon arrow](https://commons.wikimedia.org/wiki/File:Photon_arrow.svg)" by Napy1kenobi, [Public domain](https://creativecommons.org/publicdomain/zero/1.0/deed.en)\n\n"[EM spectrum properties](https://commons.wikimedia.org/wiki/File:EM_Spectrum_Properties_edit.svg)" by Inductiveload, [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/deed.en)\n\n"[Frozen](https://commons.wikimedia.org/wiki/File:Frozen_(86941053).jpeg)" by Ricardo Costa, [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/deed.en)\n\n"[Infrared dog](https://commons.wikimedia.org/wiki/File:Infrared_dog.jpg)" by NASA/IPAC, [Public domain](https://creativecommons.org/publicdomain/mark/1.0/)\n\n"[Prism flat rainbow](https://commons.wikimedia.org/wiki/File:Prism_flat_rainbow.jpg)" by Kelvinsong, [Public domain](https://creativecommons.org/publicdomain/mark/1.0/)\n\n"[Upper arm fracture from arm wrestling](https://commons.wikimedia.org/wiki/File:Oberarmfraktur_durch_Armdruecken_02.jpg)" by Hellerhoff, [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en)\n\n"[Gamma decay](https://commons.wikimedia.org/wiki/File:Gamma_Decay01.svg)" by And1mu, [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en)\n\n"[Crab Nebula in multiple wavelengths](https://commons.wikimedia.org/wiki/File:Crab_Nebula_in_Multiple_Wavelengths.png)" by Torres997, [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/deed.en)\n\n"[A rainbow viewed from Earl\'s Croome](https://commons.wikimedia.org/wiki/File:A_rainbow_viewed_from_Earl%27s_Croome_-_geograph.org.uk_-_5161205.jpg)" by Philip Halling, [CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/deed.en)\n\n"[LASER](https://commons.wikimedia.org/wiki/File:LASER.jpg)" by Peng Jiajie, [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/deed.en)\n', + widgets: {}, + }, + version: { + major: 0, + minor: 0, + }, + }, + }, + }, +]; + export const Base = (): React.ReactElement => { - const [state, setState] = useState(); + const [state, setState] = useState(sampleArticle); const articleEditorRef = useRef(); function handleChange(value) { From f8b21ef175ee7956d974d631240341f238409e09 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Tue, 6 Aug 2024 12:36:40 -0700 Subject: [PATCH 27/45] ArticleEditor: fix more types related to refs --- packages/perseus-editor/src/article-editor.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/perseus-editor/src/article-editor.tsx b/packages/perseus-editor/src/article-editor.tsx index 4b6d622c60..29e85e1e8a 100644 --- a/packages/perseus-editor/src/article-editor.tsx +++ b/packages/perseus-editor/src/article-editor.tsx @@ -359,10 +359,14 @@ export default class ArticleEditor extends React.Component { serialize(): JsonType { if (this.props.mode === "edit") { + // Note(jeremy): Fixing this type error involves some extensive + // re-working of the may component. Today it accepts a + // `replace` prop (I _think_ because it's also used for editing + // hints), but when I tried to re-work it I couldn't decipher which + // types to fix and which to not fix. + // @ts-expect-error - TS2322 - Type 'undefined' is not assignable to type 'PerseusRenderer'.ts return this._sections().map((section, i) => { - // eslint-disable-next-line react/no-string-refs - // @ts-expect-error - TS2339 - Property 'serialize' does not exist on type 'ReactInstance'. - return this.refs["editor" + i].serialize(); + return this.editorRefs[i]?.serialize(); }); } if (this.props.mode === "preview" || this.props.mode === "json") { @@ -375,12 +379,12 @@ export default class ArticleEditor extends React.Component { } /** - * Returns an array, with one element be section. + * Returns an array, with one element per section. * Each element is an array of lint warnings present in that section. * * This function can currently only be called in edit mode. */ - getSaveWarnings(): ReadonlyArray { + getSaveWarnings(): ReadonlyArray> { if (this.props.mode !== "edit") { // TODO(joshuan): We should be able to get save warnings in // preview mode. @@ -391,9 +395,7 @@ export default class ArticleEditor extends React.Component { } return this._sections().map((section, i) => { - // eslint-disable-next-line react/no-string-refs - // @ts-expect-error - TS2339 - Property 'getSaveWarnings' does not exist on type 'ReactInstance'. - return this.refs["editor" + i].getSaveWarnings(); + return this.editorRefs[i]?.getSaveWarnings() ?? []; }); } From e891ccaadfac96ccfe5015c636cf8269f1de5c58 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Tue, 30 Jul 2024 09:05:19 -0700 Subject: [PATCH 28/45] Remove part of a dependencies error message that makes no sense anymore --- packages/perseus/src/dependencies.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/perseus/src/dependencies.ts b/packages/perseus/src/dependencies.ts index 1ec83e83aa..1526b5cac7 100644 --- a/packages/perseus/src/dependencies.ts +++ b/packages/perseus/src/dependencies.ts @@ -17,7 +17,6 @@ export const getDependencies = (): PerseusDependencies => { [ "Perseus has not been provided required dependencies.", "setDependencies(dependencies) must be called first.", - "Make sure Perseus is being imported from javascript/perseus/perseus.js.", ].join("\n"), ); }; From 090b701130e5fe0eb6f8c183ba047875d5bfc4bf Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Wed, 31 Jul 2024 10:13:53 -0700 Subject: [PATCH 29/45] Spike on iframe dynamic rendering - failure as styles are very hard to manage/import --- .../__stories__/iframe-renderer.stories.tsx | 77 +++++++++++++++ .../perseus-editor/src/iframe-renderer.tsx | 95 +++++++++++++++++++ packages/perseus-editor/tsconfig-build.json | 2 + 3 files changed, 174 insertions(+) create mode 100644 packages/perseus-editor/src/__stories__/iframe-renderer.stories.tsx create mode 100644 packages/perseus-editor/src/iframe-renderer.tsx diff --git a/packages/perseus-editor/src/__stories__/iframe-renderer.stories.tsx b/packages/perseus-editor/src/__stories__/iframe-renderer.stories.tsx new file mode 100644 index 0000000000..d915ef485c --- /dev/null +++ b/packages/perseus-editor/src/__stories__/iframe-renderer.stories.tsx @@ -0,0 +1,77 @@ +import {Renderer} from "@khanacademy/perseus"; +// eslint-disable-next-line monorepo/no-internal-import +import {mockStrings} from "@khanacademy/perseus/strings"; + +import IFrameRenderer from "../iframe-renderer"; + +import type {PerseusRenderer} from "@khanacademy/perseus"; +import type {Meta, StoryObj} from "@storybook/react"; + +const meta: Meta = { + title: "PerseusEditor/IFrameRenderer", + component: IFrameRenderer, +}; + +export default meta; +type Story = StoryObj; + +const question: PerseusRenderer = { + content: + "Which of the following values of $x$ satisfies the equation $\\sqrt{64}=x$ ?\n\n[[\u2603 radio 1]]\n\n", + images: {}, + widgets: { + "radio 1": { + graded: true, + version: { + major: 1, + minor: 0, + }, + static: false, + type: "radio", + options: { + displayCount: null, + onePerLine: false, + choices: [ + { + content: "$-8$ and $8$", + correct: false, + clue: "The square root operation ($\\sqrt{\\phantom{x}}$) calculates *only* the positive square root when performed on a number, so $x$ is equal to *only* $8$.", + }, + { + content: "$-8$", + correct: false, + clue: "While $(-8)^2=64$, the square root operation ($\\sqrt{\\phantom{x}}$) calculates *only* the positive square root when performed on a number.", + }, + { + content: "$8$", + correct: true, + isNoneOfTheAbove: false, + clue: "$8$ is the positive square root of $64$.", + }, + { + content: "No value of $x$ satisfies the equation.", + correct: false, + isNoneOfTheAbove: false, + clue: "$8$ satisfies the equation.", + }, + ], + countChoices: false, + hasNoneOfTheAbove: false, + multipleSelect: false, + randomize: false, + deselectEnabled: false, + }, + alignment: "default", + }, + }, +}; + +export const Primary: Story = { + args: { + style: {width: "100%", height: "500px"}, + styleSelector: 'link, style[type="text/css"]', + children: ( + + ), + }, +}; diff --git a/packages/perseus-editor/src/iframe-renderer.tsx b/packages/perseus-editor/src/iframe-renderer.tsx new file mode 100644 index 0000000000..7f7496adb6 --- /dev/null +++ b/packages/perseus-editor/src/iframe-renderer.tsx @@ -0,0 +1,95 @@ +/* eslint-disable no-console */ +import {useEffect, useRef, useState} from "react"; +import {createPortal} from "react-dom"; + +import type {CSSProperties, PropsWithChildren} from "react"; + +type Props = PropsWithChildren<{style: CSSProperties; styleSelector: string}>; + +function copyStyleIntoIframe(target: HTMLHeadElement, node: Node) { + if ( + !(node instanceof HTMLLinkElement || node instanceof HTMLStyleElement) + ) { + console.log("[copyStyleIntoIframe] Ignoring invalid node:", node); + return; + } + + console.log("Copying:", node.outerHTML); + target.appendChild(node); +} + +/** + * A component that renders its children into an ` + ); +} + +export default IFrameRenderer; diff --git a/packages/perseus-editor/tsconfig-build.json b/packages/perseus-editor/tsconfig-build.json index f9d2f0b6ad..2807ed6f12 100644 --- a/packages/perseus-editor/tsconfig-build.json +++ b/packages/perseus-editor/tsconfig-build.json @@ -6,6 +6,8 @@ "paths": { "jsdiff": ["../../vendor/jsdiff/jsdiff.js"], "hubble": ["../../vendor/hubble/hubble.js"], + "@khanacademy/perseus/strings": ["../perseus/src/strings.ts"], + // NOTE(kevinb): We have to repeat this here because TS doesn't do // intelligent merge of tsconfig.json files when using `extends`. "@khanacademy/*": [ From 2229388dd32fb439597cb70995dcb27af49dfae5 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 9 Aug 2024 14:25:33 -0700 Subject: [PATCH 30/45] ArticleEditor: remove 'replace' prop passed to Editor - articles aren't hints --- packages/perseus-editor/src/article-editor.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/perseus-editor/src/article-editor.tsx b/packages/perseus-editor/src/article-editor.tsx index 29e85e1e8a..535f62e604 100644 --- a/packages/perseus-editor/src/article-editor.tsx +++ b/packages/perseus-editor/src/article-editor.tsx @@ -359,14 +359,8 @@ export default class ArticleEditor extends React.Component { serialize(): JsonType { if (this.props.mode === "edit") { - // Note(jeremy): Fixing this type error involves some extensive - // re-working of the may component. Today it accepts a - // `replace` prop (I _think_ because it's also used for editing - // hints), but when I tried to re-work it I couldn't decipher which - // types to fix and which to not fix. - // @ts-expect-error - TS2322 - Type 'undefined' is not assignable to type 'PerseusRenderer'.ts - return this._sections().map((section, i) => { - return this.editorRefs[i]?.serialize(); + return this._sections().map((_, i) => { + return this.editorRefs[i]!.serialize(); }); } if (this.props.mode === "preview" || this.props.mode === "json") { From f5f19dbae23670c2d0a797b23e7633154f920b04 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 9 Aug 2024 14:26:41 -0700 Subject: [PATCH 31/45] PerseusEditor: clarify types around serialize() and getSaveWarnings() --- packages/perseus-editor/src/editor-page.tsx | 23 +++++++++++++++------ packages/perseus-editor/src/editor.tsx | 2 +- packages/perseus-editor/src/hint-editor.tsx | 11 ++++------ packages/perseus-editor/src/item-editor.tsx | 4 ++-- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/perseus-editor/src/editor-page.tsx b/packages/perseus-editor/src/editor-page.tsx index 5084237e87..83efd632e7 100644 --- a/packages/perseus-editor/src/editor-page.tsx +++ b/packages/perseus-editor/src/editor-page.tsx @@ -1,5 +1,6 @@ import {components, ApiOptions, ClassNames} from "@khanacademy/perseus"; import * as React from "react"; +import invariant from "tiny-invariant"; import _ from "underscore"; import JsonEditor from "./components/json-editor"; @@ -115,19 +116,29 @@ class EditorPage extends React.Component { }; } - getSaveWarnings(): any { - const issues1 = this.itemEditor.current?.getSaveWarnings(); - const issues2 = this.hintsEditor.current?.getSaveWarnings(); + getSaveWarnings(): ReadonlyArray { + const issues1 = this.itemEditor.current?.getSaveWarnings() ?? []; + const issues2 = this.hintsEditor.current?.getSaveWarnings() ?? []; return issues1.concat(issues2); } - serialize(options?: {keepDeletedWidgets?: boolean}): any | PerseusItem { + serialize(options?: {keepDeletedWidgets?: boolean}): PerseusItem { if (this.props.jsonMode) { return this.state.json; } - return _.extend(this.itemEditor.current?.serialize(options), { + invariant(this.itemEditor.current != null); + invariant(this.hintsEditor.current != null); + return { + ...this.itemEditor.current?.serialize(options), hints: this.hintsEditor.current?.serialize(options), - }); + + // Note(jeremy): These two are to satisfy the fact that our + // PerseusItem type really should be a union between a multi item + // and a standard perseus item (also that the `answer` field, which + // is deprecated, is required). + _multi: undefined, + answer: undefined, + }; } handleChange: ChangeHandler = (toChange, cb, silent) => { diff --git a/packages/perseus-editor/src/editor.tsx b/packages/perseus-editor/src/editor.tsx index 0a976964a8..ecdda73ade 100644 --- a/packages/perseus-editor/src/editor.tsx +++ b/packages/perseus-editor/src/editor.tsx @@ -796,7 +796,7 @@ class Editor extends React.Component { this.props.onChange({content: newContent}, this.focusAndMoveToEnd); }; - getSaveWarnings: () => any = () => { + getSaveWarnings: () => ReadonlyArray = () => { // eslint-disable-next-line react/no-string-refs const widgetIds = _.intersection(this.widgetIds, _.keys(this.refs)); const warnings = _(widgetIds) diff --git a/packages/perseus-editor/src/hint-editor.tsx b/packages/perseus-editor/src/hint-editor.tsx index 5cdff09c8b..6d619fa5c2 100644 --- a/packages/perseus-editor/src/hint-editor.tsx +++ b/packages/perseus-editor/src/hint-editor.tsx @@ -348,20 +348,17 @@ class CombinedHintsEditor extends React.Component { .value(); }; - serialize: (options?: any) => ReadonlyArray = (options: any) => { + serialize(options?: any): ReadonlyArray { return this.props.hints.map((hint, i) => { return this.serializeHint(i, options); }); - }; + } - serializeHint: (index: number, options?: any) => string = ( - index: number, - options: any, - ): string => { + serializeHint(index: number, options?: any): Hint { // eslint-disable-next-line react/no-string-refs // @ts-expect-error - TS2339 - Property 'serialize' does not exist on type 'ReactInstance'. return this.refs["hintEditor" + index].serialize(options); - }; + } render(): React.ReactNode { const {itemId, hints} = this.props; diff --git a/packages/perseus-editor/src/item-editor.tsx b/packages/perseus-editor/src/item-editor.tsx index 12d43b262b..b2489d5242 100644 --- a/packages/perseus-editor/src/item-editor.tsx +++ b/packages/perseus-editor/src/item-editor.tsx @@ -64,8 +64,8 @@ class ItemEditor extends React.Component { this.updateProps({answerArea}, cb, silent); }; - getSaveWarnings: () => any = () => { - return this.questionEditor.current?.getSaveWarnings(); + getSaveWarnings: () => ReadonlyArray = () => { + return this.questionEditor.current?.getSaveWarnings() ?? []; }; serialize: (options?: any) => { From e260df81358edb68eb67289e9ad862f9629ec6b7 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 9 Aug 2024 15:08:02 -0700 Subject: [PATCH 32/45] Fonts: fix path to Lato-Regular.woff2 in Storybook --- .storybook/preview-head.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index 8b04d3a37d..2a794db981 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -1,7 +1,7 @@ Date: Fri, 9 Aug 2024 15:08:22 -0700 Subject: [PATCH 33/45] Lint: modernize stories --- .../components/__stories__/lint.stories.tsx | 82 ++++++------------- packages/perseus/src/components/lint.tsx | 23 +++--- 2 files changed, 34 insertions(+), 71 deletions(-) diff --git a/packages/perseus/src/components/__stories__/lint.stories.tsx b/packages/perseus/src/components/__stories__/lint.stories.tsx index e34d5a3140..ea3ba7a6c5 100644 --- a/packages/perseus/src/components/__stories__/lint.stories.tsx +++ b/packages/perseus/src/components/__stories__/lint.stories.tsx @@ -2,22 +2,29 @@ import * as React from "react"; import Lint from "../lint"; -import type {Meta} from "@storybook/react"; +import type {Meta, StoryObj} from "@storybook/react"; const meta: Meta = { title: "Perseus/Components/Lint", + component: Lint, + args: { + children:
This is the sample lint child
, + insideTable: false, + message: "Test message", + ruleName: "Test rule", + }, + decorators: [ + (Story) => ( + + + + ), + ], }; export default meta; -type StoryArgs = Record; - -const defaultObject = { - children:
This is the sample lint child
, - insideTable: false, - message: "Test message", - ruleName: "Test rule", -} as const; +type Story = StoryObj; const Container = ({children}: {children: React.ReactNode}) => { return ( @@ -34,53 +41,12 @@ const Container = ({children}: {children: React.ReactNode}) => { ); }; -export const DefaultLintContainerAndMessage = ( - args: StoryArgs, -): React.ReactElement => { - return ( - - - - ); -}; -export const LintSeverity1Error = (args: StoryArgs): React.ReactElement => { - return ( - - - - ); -}; -export const LintSeverity2Warning = (args: StoryArgs): React.ReactElement => { - return ( - - - - ); -}; -export const LintSeverity3Recommendation = ( - args: StoryArgs, -): React.ReactElement => { - return ( - - - - ); -}; -export const LintSeverity4OfflineReportingOnly = ( - args: StoryArgs, -): React.ReactElement => { - return ( - - - - ); -}; -export const InlineLintContainerAndMessage = ( - args: StoryArgs, -): React.ReactElement => { - return ( - - - - ); +export const DefaultLintContainerAndMessage: Story = {}; +export const LintSeverity1Error: Story = {args: {severity: 1}}; +export const LintSeverity2Error: Story = {args: {severity: 2}}; +export const LintSeverity3Error: Story = {args: {severity: 3}}; +export const LintSeverity4Error: Story = {args: {severity: 4}}; + +export const InlineLintContainerAndMessage: Story = { + args: {inline: true}, }; diff --git a/packages/perseus/src/components/lint.tsx b/packages/perseus/src/components/lint.tsx index d53dadf9d9..fa78a08254 100644 --- a/packages/perseus/src/components/lint.tsx +++ b/packages/perseus/src/components/lint.tsx @@ -45,24 +45,21 @@ type State = { /** * This component renders "lint" nodes in a markdown parse tree. Lint nodes * are inserted into the tree by the Perseus linter (see - * perseus-linter/src/index). + * `perseus-linter/src/index`). * - * This component serves multiple purposes - * - * 1) It renders a small circle in the right margin to indicate that there - * is lint on (or near) that line. - * - * 2) The area around the circle is hoverable: when the mouse moves over it - * the linty content is highlighted and a tooltip is displayed that explains - * what the problem is. - * - * 3) The hoverable area is also an HTML tag. Clicking on it opens - * a new tab and links to additional details about the specific lint rule. + * This component serves multiple purposes: + * 1. It renders a small circle in the right margin to indicate that there is + * lint on (or near) that line. + * 2. The area around the circle is hoverable: when the mouse moves over it + * the linty content is highlighted and a tooltip is displayed that + * explains what the problem is. + * 3. The hoverable area is also an HTML tag. Clicking on it opens + * a new tab and links to additional details about the specific lint rule. * * The CSS required to position the circles in the right margin is tricky * and it does not always work perfectly. When lint occurs on a block element * that has a right margin (like anything blockquoted) the circle will appear - * to the left of where it belongs. And if there is more + * to the left of where it belongs. And if there is more... **/ class Lint extends React.Component { _positionTimeout: number | undefined; From 011f727852759902dbbcb3baf38bfb2cf7ea6933 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Mon, 12 Aug 2024 11:52:24 -0700 Subject: [PATCH 34/45] Lint: Use for lint content as
is not supported --- packages/perseus/src/components/__stories__/lint.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/perseus/src/components/__stories__/lint.stories.tsx b/packages/perseus/src/components/__stories__/lint.stories.tsx index ea3ba7a6c5..23e8a9927f 100644 --- a/packages/perseus/src/components/__stories__/lint.stories.tsx +++ b/packages/perseus/src/components/__stories__/lint.stories.tsx @@ -8,7 +8,7 @@ const meta: Meta = { title: "Perseus/Components/Lint", component: Lint, args: { - children:
This is the sample lint child
, + children: This is the sample lint child, insideTable: false, message: "Test message", ruleName: "Test rule", From 9e87fc81d4375404e3fc4bc5cbb43694afc7dadd Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Mon, 12 Aug 2024 11:53:15 -0700 Subject: [PATCH 35/45] Lint: Hide children prop in stories - it's not useful --- packages/perseus/src/components/__stories__/lint.stories.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/perseus/src/components/__stories__/lint.stories.tsx b/packages/perseus/src/components/__stories__/lint.stories.tsx index 23e8a9927f..6aeadf179c 100644 --- a/packages/perseus/src/components/__stories__/lint.stories.tsx +++ b/packages/perseus/src/components/__stories__/lint.stories.tsx @@ -13,6 +13,9 @@ const meta: Meta = { message: "Test message", ruleName: "Test rule", }, + argTypes: { + children: {table: {disable: true}}, + }, decorators: [ (Story) => ( From 3cdd2467274dbbb8e320f58dea30ecebf344a72b Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Mon, 12 Aug 2024 12:09:53 -0700 Subject: [PATCH 36/45] Lint: Provide 'severity' dropdown in storybook --- .../components/__stories__/lint.stories.tsx | 26 +++++++++++++++---- packages/perseus/src/components/lint.tsx | 2 +- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/perseus/src/components/__stories__/lint.stories.tsx b/packages/perseus/src/components/__stories__/lint.stories.tsx index 6aeadf179c..8a23d9ea81 100644 --- a/packages/perseus/src/components/__stories__/lint.stories.tsx +++ b/packages/perseus/src/components/__stories__/lint.stories.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import Lint from "../lint"; +import Lint, {Severity} from "../lint"; import type {Meta, StoryObj} from "@storybook/react"; @@ -15,6 +15,18 @@ const meta: Meta = { }, argTypes: { children: {table: {disable: true}}, + severity: { + options: [1, 2, 3, 4], + control: { + type: "select", + labels: { + 1: "Error", + 2: "Warning", + 3: "Recommendation", + 4: "Offline Reporting Only", + }, + }, + }, }, decorators: [ (Story) => ( @@ -45,10 +57,14 @@ const Container = ({children}: {children: React.ReactNode}) => { }; export const DefaultLintContainerAndMessage: Story = {}; -export const LintSeverity1Error: Story = {args: {severity: 1}}; -export const LintSeverity2Error: Story = {args: {severity: 2}}; -export const LintSeverity3Error: Story = {args: {severity: 3}}; -export const LintSeverity4Error: Story = {args: {severity: 4}}; +export const SeverityError: Story = {args: {severity: Severity.Error}}; +export const SeverityWarning: Story = {args: {severity: Severity.Warning}}; +export const SeverityRecommendation: Story = { + args: {severity: Severity.Recommendation}, +}; +export const SeverityOfflineReportingOnly: Story = { + args: {severity: Severity.OfflineReportingOnly}, +}; export const InlineLintContainerAndMessage: Story = { args: {inline: true}, diff --git a/packages/perseus/src/components/lint.tsx b/packages/perseus/src/components/lint.tsx index fa78a08254..0565b94ab7 100644 --- a/packages/perseus/src/components/lint.tsx +++ b/packages/perseus/src/components/lint.tsx @@ -12,7 +12,7 @@ const exclamationIcon = { width: 12, } as const; -enum Severity { +export enum Severity { Error = 1, Warning = 2, Recommendation = 3, From 05fc776e8d1953cee18ee0c72fcc9c333735a439 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Mon, 12 Aug 2024 12:10:20 -0700 Subject: [PATCH 37/45] InteractiveGraphEditor: fix readonly typedness in story --- .../src/__stories__/interactive-graph-editor.stories.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx b/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx index f672376960..f45f0ff4c1 100644 --- a/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx +++ b/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx @@ -177,7 +177,9 @@ export const WithSaveWarnings = (): React.ReactElement => { segmentWithLockedFigures, ); const [hints, setHints] = React.useState | undefined>(); - const [saveWarnings, setSaveWarnings] = React.useState([]); + const [saveWarnings, setSaveWarnings] = React.useState( + [], + ); const editorPageRef = React.useRef(null); From b2ea199324676f3044ecc8b7f45f6d935e64af15 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Mon, 12 Aug 2024 15:58:38 -0700 Subject: [PATCH 38/45] ViewportResizer: Use Phosphor icons --- .../src/components/viewport-resizer.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/perseus-editor/src/components/viewport-resizer.tsx b/packages/perseus-editor/src/components/viewport-resizer.tsx index 7acc3a82b3..01a6ec9b65 100644 --- a/packages/perseus-editor/src/components/viewport-resizer.tsx +++ b/packages/perseus-editor/src/components/viewport-resizer.tsx @@ -2,14 +2,18 @@ * A component that displays controls for choosing a viewport size. * Renders three buttons: "Phone", "Tablet", and "Desktop". */ -import {components, constants, icons} from "@khanacademy/perseus"; +import {components, constants} from "@khanacademy/perseus"; +import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon"; +import {LabelMedium} from "@khanacademy/wonder-blocks-typography"; +import phosphorDesktop from "@phosphor-icons/core/regular/desktop.svg"; +import phosphorPhone from "@phosphor-icons/core/regular/device-mobile.svg"; +import phosphorTablet from "@phosphor-icons/core/regular/device-tablet.svg"; import * as React from "react"; import type {DeviceType} from "@khanacademy/perseus"; -const {ButtonGroup, InlineIcon} = components; +const {ButtonGroup} = components; const {devices} = constants; -const {iconDesktop, iconMobilePhone, iconTablet} = icons; type Props = { /** The current device type that is selected. */ @@ -24,23 +28,22 @@ type Props = { const ViewportResizer = (props: Props) => { const phoneButtonContents = ( - Phone + Phone ); const tabletButtonContents = ( - Tablet + Tablet ); const desktopButtonContents = ( - Desktop + Desktop ); - // TODO(david): Allow input of custom viewport sizes. return ( - + Viewport:{" "} { ]} onChange={props.onViewportSizeChanged} /> - + ); }; From e8acd31f737a6f95f1f0a8361976d7631c69c6bd Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Mon, 12 Aug 2024 16:17:12 -0700 Subject: [PATCH 39/45] EditorPage: tiny WB improvements for JSON mode and viewport resizer alignments --- packages/perseus-editor/src/editor-page.tsx | 36 +++++++++++++-------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/perseus-editor/src/editor-page.tsx b/packages/perseus-editor/src/editor-page.tsx index 83efd632e7..7b1107e844 100644 --- a/packages/perseus-editor/src/editor-page.tsx +++ b/packages/perseus-editor/src/editor-page.tsx @@ -1,4 +1,7 @@ import {components, ApiOptions, ClassNames} from "@khanacademy/perseus"; +import {View} from "@khanacademy/wonder-blocks-core"; +import {Checkbox} from "@khanacademy/wonder-blocks-form"; +import {spacing} from "@khanacademy/wonder-blocks-tokens"; import * as React from "react"; import invariant from "tiny-invariant"; import _ from "underscore"; @@ -180,19 +183,21 @@ class EditorPage extends React.Component { return (
-
+ {this.props.developerMode && ( - - {" "} - + )} {!this.props.jsonMode && ( @@ -205,6 +210,11 @@ class EditorPage extends React.Component { )} {!this.props.jsonMode && ( + // NOTE: This component positions itself using fixed + // positioning, so even though it appears here, near + // the JSON Mode and Viewport Resizer elements, it + // shows up in a completely different place on the page + // visually. { }} /> )} -
+ {this.props.developerMode && this.props.jsonMode && (
From dbd968e25caf7793bbea512b5e1214453ba25535 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Mon, 12 Aug 2024 16:25:36 -0700 Subject: [PATCH 40/45] GroupEditor: Handle ref being null - should not happen so using invariant() --- packages/perseus-editor/src/widgets/group-editor.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/perseus-editor/src/widgets/group-editor.tsx b/packages/perseus-editor/src/widgets/group-editor.tsx index 1b5f6b4dc5..bdd21c3c08 100644 --- a/packages/perseus-editor/src/widgets/group-editor.tsx +++ b/packages/perseus-editor/src/widgets/group-editor.tsx @@ -3,6 +3,7 @@ import {ApiOptions, Changeable} from "@khanacademy/perseus"; import PropTypes from "prop-types"; import * as React from "react"; +import invariant from "tiny-invariant"; import _ from "underscore"; import Editor from "../editor"; @@ -69,7 +70,8 @@ class GroupEditor extends React.Component { return Changeable.change.apply(this, args); }; - getSaveWarnings: () => ReadonlyArray = () => { + getSaveWarnings: () => ReadonlyArray = () => { + invariant(this.editor.current != null); return this.editor.current?.getSaveWarnings(); }; From a80c35ba90c3b237b9f63c720b90608850f11ecd Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Mon, 12 Aug 2024 17:04:13 -0700 Subject: [PATCH 41/45] EditorPage: Preview only desktop, but note phone and tablet checking using preview tab --- packages/perseus-editor/src/editor-page.tsx | 16 +++++----- packages/perseus-editor/src/item-editor.tsx | 34 +++++++-------------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/packages/perseus-editor/src/editor-page.tsx b/packages/perseus-editor/src/editor-page.tsx index 7b1107e844..867b7d9f54 100644 --- a/packages/perseus-editor/src/editor-page.tsx +++ b/packages/perseus-editor/src/editor-page.tsx @@ -2,6 +2,7 @@ import {components, ApiOptions, ClassNames} from "@khanacademy/perseus"; import {View} from "@khanacademy/wonder-blocks-core"; import {Checkbox} from "@khanacademy/wonder-blocks-form"; import {spacing} from "@khanacademy/wonder-blocks-tokens"; +import {LabelSmall} from "@khanacademy/wonder-blocks-typography"; import * as React from "react"; import invariant from "tiny-invariant"; import _ from "underscore"; @@ -194,6 +195,7 @@ class EditorPage extends React.Component { > {this.props.developerMode && ( { )} {!this.props.jsonMode && ( - + + + Note: Please ensure this exercise looks + correct on a phone and tablet by using the + "Preview" tab. + + )} {!this.props.jsonMode && ( @@ -247,7 +250,6 @@ class EditorPage extends React.Component { onChange={this.handleChange} wasAnswered={this.state.wasAnswered} gradeMessage={this.state.gradeMessage} - deviceType={this.props.previewDevice} apiOptions={deviceBasedApiOptions} previewURL={this.props.previewURL} /> diff --git a/packages/perseus-editor/src/item-editor.tsx b/packages/perseus-editor/src/item-editor.tsx index b2489d5242..1335f377aa 100644 --- a/packages/perseus-editor/src/item-editor.tsx +++ b/packages/perseus-editor/src/item-editor.tsx @@ -11,7 +11,6 @@ import type { APIOptions, ImageUploader, ChangeHandler, - DeviceType, PerseusRenderer, } from "@khanacademy/perseus"; @@ -19,7 +18,6 @@ const ITEM_DATA_VERSION = itemDataVersion; type Props = { apiOptions?: APIOptions; - deviceType?: DeviceType; gradeMessage?: string; imageUploader?: ImageUploader; wasAnswered?: boolean; @@ -107,28 +105,18 @@ class ItemEditor extends React.Component {
-
- - - -
+ -
+
From 9920bc7c3dc2b248abef743f40ef37bfa2b56750 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Mon, 12 Aug 2024 19:11:08 -0700 Subject: [PATCH 42/45] ArticleEditor: remove passing 'screen' to editor --- packages/perseus-editor/src/hint-editor.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/perseus-editor/src/hint-editor.tsx b/packages/perseus-editor/src/hint-editor.tsx index 6d619fa5c2..ea8bb60e2c 100644 --- a/packages/perseus-editor/src/hint-editor.tsx +++ b/packages/perseus-editor/src/hint-editor.tsx @@ -232,7 +232,6 @@ class CombinedHintEditor extends React.Component {
From 760fdcbc36cda9e020fdea5c9fea9932f6ddcd90 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Tue, 13 Aug 2024 11:49:05 -0700 Subject: [PATCH 43/45] EditorPage: Add note about preview being desktop-only now --- .../editor-page-with-storybook-preview.tsx | 5 +- .../src/__stories__/editor-page.stories.tsx | 13 ++-- packages/perseus-editor/src/editor-page.tsx | 60 +++++++++++++++---- 3 files changed, 59 insertions(+), 19 deletions(-) diff --git a/packages/perseus-editor/src/__stories__/editor-page-with-storybook-preview.tsx b/packages/perseus-editor/src/__stories__/editor-page-with-storybook-preview.tsx index 85f146c6a5..511bd18c5f 100644 --- a/packages/perseus-editor/src/__stories__/editor-page-with-storybook-preview.tsx +++ b/packages/perseus-editor/src/__stories__/editor-page-with-storybook-preview.tsx @@ -18,13 +18,14 @@ type Props = { apiOptions?: APIOptions; question?: PerseusRenderer; hints?: ReadonlyArray; + developerMode?: boolean; }; const onChangeAction = action("onChange"); function EditorPageWithStorybookPreview(props: Props) { const [previewDevice, setPreviewDevice] = - React.useState("phone"); + React.useState("desktop"); const [jsonMode, setJsonMode] = React.useState(false); const [answerArea, setAnswerArea] = React.useState< PerseusAnswerArea | undefined | null @@ -49,7 +50,7 @@ function EditorPageWithStorybookPreview(props: Props) { onPreviewDeviceChange={(newDevice) => setPreviewDevice(newDevice) } - developerMode={true} + developerMode={props.developerMode} jsonMode={jsonMode} answerArea={answerArea} question={question} diff --git a/packages/perseus-editor/src/__stories__/editor-page.stories.tsx b/packages/perseus-editor/src/__stories__/editor-page.stories.tsx index 33569fa99d..76d37d68ab 100644 --- a/packages/perseus-editor/src/__stories__/editor-page.stories.tsx +++ b/packages/perseus-editor/src/__stories__/editor-page.stories.tsx @@ -4,12 +4,17 @@ import {registerAllWidgetsAndEditorsForTesting} from "../util/register-all-widge import EditorPageWithStorybookPreview from "./editor-page-with-storybook-preview"; +import type {Meta, StoryObj} from "@storybook/react"; + registerAllWidgetsAndEditorsForTesting(); // SIDE_EFFECTY!!!! :cry: -export default { +const meta: Meta = { title: "PerseusEditor/EditorPage", + component: EditorPageWithStorybookPreview, + args: {developerMode: false}, }; +export default meta; -export const Demo = (): React.ReactElement => { - return ; -}; +type Story = StoryObj; + +export const Demo: Story = {}; diff --git a/packages/perseus-editor/src/editor-page.tsx b/packages/perseus-editor/src/editor-page.tsx index 867b7d9f54..40cf3f5c4c 100644 --- a/packages/perseus-editor/src/editor-page.tsx +++ b/packages/perseus-editor/src/editor-page.tsx @@ -1,14 +1,16 @@ import {components, ApiOptions, ClassNames} from "@khanacademy/perseus"; import {View} from "@khanacademy/wonder-blocks-core"; import {Checkbox} from "@khanacademy/wonder-blocks-form"; -import {spacing} from "@khanacademy/wonder-blocks-tokens"; +import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon"; +import {color, spacing} from "@khanacademy/wonder-blocks-tokens"; +import Tooltip from "@khanacademy/wonder-blocks-tooltip"; import {LabelSmall} from "@khanacademy/wonder-blocks-typography"; +import warning from "@phosphor-icons/core/bold/warning-circle-bold.svg"; import * as React from "react"; import invariant from "tiny-invariant"; import _ from "underscore"; import JsonEditor from "./components/json-editor"; -import ViewportResizer from "./components/viewport-resizer"; import CombinedHintsEditor from "./hint-editor"; import ItemEditor from "./item-editor"; @@ -193,21 +195,53 @@ class EditorPage extends React.Component { alignItems: "center", }} > - {this.props.developerMode && ( - - )} + + {" "} + {this.props.developerMode && ( + + )} + {!this.props.jsonMode && ( - Note: Please ensure this exercise looks - correct on a phone and tablet by using the - "Preview" tab. + Note: Don't forget to check how this + exercise looks on a phone and tablet by using + the "Preview" tab.{" "} + + + This preview is designed to give + you fast feedback when editing + the exercise. To be sure it + looks correct on all devices a + Khan Academy learner may view it + on, please use the "Preview" + tab. + + + } + > + + )} From 05b55215096606c8da6ff186d71c6b41f9886e46 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Tue, 13 Aug 2024 12:42:49 -0700 Subject: [PATCH 44/45] EditorPage: Don't change ApiOptions based on previewDevice --- packages/perseus-editor/src/editor-page.tsx | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/perseus-editor/src/editor-page.tsx b/packages/perseus-editor/src/editor-page.tsx index 40cf3f5c4c..955fd8815f 100644 --- a/packages/perseus-editor/src/editor-page.tsx +++ b/packages/perseus-editor/src/editor-page.tsx @@ -169,20 +169,9 @@ class EditorPage extends React.Component { } render(): React.ReactNode { - let className = "framework-perseus"; + const className = "framework-perseus"; - const touch = - this.props.previewDevice === "phone" || - this.props.previewDevice === "tablet"; - const deviceBasedApiOptions: APIOptionsWithDefaults = { - ...this.getApiOptions(), - customKeypad: touch, - isMobile: touch, - }; - - if (deviceBasedApiOptions.isMobile) { - className += " " + ClassNames.MOBILE; - } + const apiOptions = this.getApiOptions(); return (
@@ -284,7 +273,7 @@ class EditorPage extends React.Component { onChange={this.handleChange} wasAnswered={this.state.wasAnswered} gradeMessage={this.state.gradeMessage} - apiOptions={deviceBasedApiOptions} + apiOptions={apiOptions} previewURL={this.props.previewURL} /> )} @@ -297,7 +286,7 @@ class EditorPage extends React.Component { imageUploader={this.props.imageUploader} onChange={this.handleChange} deviceType={this.props.previewDevice} - apiOptions={deviceBasedApiOptions} + apiOptions={apiOptions} previewURL={this.props.previewURL} highlightLint={this.state.highlightLint} /> From 143f88c1b9167b9008cf3b1b3327a71556725a2b Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Tue, 13 Aug 2024 13:23:00 -0700 Subject: [PATCH 45/45] Changeset --- .changeset/hungry-seals-refuse.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/hungry-seals-refuse.md diff --git a/.changeset/hungry-seals-refuse.md b/.changeset/hungry-seals-refuse.md new file mode 100644 index 0000000000..b2a80347c4 --- /dev/null +++ b/.changeset/hungry-seals-refuse.md @@ -0,0 +1,7 @@ +--- +"@khanacademy/perseus-dev-ui": patch +"@khanacademy/perseus": patch +"@khanacademy/perseus-editor": patch +--- + +Migrate Perseus preview to use non-iframe approach