From 1b8e2c59964375c4b6b636a0ee2fa73e919168ad Mon Sep 17 00:00:00 2001 From: Matthew Curtis Date: Wed, 26 Jun 2024 15:53:32 -0500 Subject: [PATCH 1/2] desktop --- .../perseus/src/components/math-input.tsx | 189 ++++++++++-------- .../perseus/src/styles/perseus-renderer.less | 3 + 2 files changed, 108 insertions(+), 84 deletions(-) diff --git a/packages/perseus/src/components/math-input.tsx b/packages/perseus/src/components/math-input.tsx index f5a0f79b44..775b4c301d 100644 --- a/packages/perseus/src/components/math-input.tsx +++ b/packages/perseus/src/components/math-input.tsx @@ -291,96 +291,117 @@ class InnerMathInput extends React.Component { className = className + " " + this.props.className; } + const textareaId = `math-input_${Date.now()}`; + if (this.__mathFieldWrapperRef) { + const textarea = + this.__mathFieldWrapperRef.getElementsByTagName("textarea"); + textarea[0].setAttribute("id", textareaId); + } + return ( - -
+ + +
{ + // Prevent the click into the input from registering + // so that the keypad popover doesn't close when + // switching focus to the input. + e.stopPropagation(); + + const mathField = this.mathField(); + if (!mathField) { + return; + } + this.setState({ + cursorContext: getCursorContext(mathField), + }); + }} > - {this.props.buttonsVisible === "never" ? ( - - ) : ( - - this.state.keypadOpen - ? this.closeKeypad() - : this.openKeypad() - } - > - {(props) => ( - (this.__mathFieldWrapperRef = ref)} + aria-label={this.props.labelText} + onFocus={() => this.focus()} + onBlur={() => this.blur()} + /> + this.closeKeypad()} + dismissEnabled + content={() => ( + + - )} - - )} - -
+ + )} + > + {this.props.buttonsVisible === "never" ? ( + + ) : ( + + this.state.keypadOpen + ? this.closeKeypad() + : this.openKeypad() + } + > + {(props) => ( + + )} + + )} + +
+
); } diff --git a/packages/perseus/src/styles/perseus-renderer.less b/packages/perseus/src/styles/perseus-renderer.less index 32e1129470..ae878f993d 100644 --- a/packages/perseus/src/styles/perseus-renderer.less +++ b/packages/perseus/src/styles/perseus-renderer.less @@ -102,6 +102,9 @@ font-size: 18px; line-height: 22px; margin: 22px 0px; + display: inline-flex; + flex-wrap: wrap; + align-items: end; } // HACK(yejia): Override the font size and line height for blurbs From 45e0506e4632132b68918a9c9d836392e80fe04b Mon Sep 17 00:00:00 2001 From: Matthew Curtis Date: Wed, 26 Jun 2024 16:18:30 -0500 Subject: [PATCH 2/2] POC mobile input --- .../src/components/input/math-input.tsx | 3 +- .../__stories__/expression.stories.tsx | 3 +- .../__testdata__/expression.testdata.ts | 114 ++++++++++++++++++ packages/perseus/src/widgets/expression.tsx | 65 ++++++---- 4 files changed, 161 insertions(+), 24 deletions(-) diff --git a/packages/math-input/src/components/input/math-input.tsx b/packages/math-input/src/components/input/math-input.tsx index 2f136f32f8..666c3d50da 100644 --- a/packages/math-input/src/components/input/math-input.tsx +++ b/packages/math-input/src/components/input/math-input.tsx @@ -1044,7 +1044,6 @@ const inputMaxWidth = 128; const numeralHeightPx = 20; const totalDesiredPadding = 12; const minHeightPx = numeralHeightPx + totalDesiredPadding * 2; -const minWidthPx = 64; const styles = StyleSheet.create({ input: { @@ -1068,7 +1067,7 @@ const inlineStyles = { innerContainer: { backgroundColor: "white", minHeight: minHeightPx, - minWidth: minWidthPx, + width: "100%", maxWidth: inputMaxWidth, boxSizing: "border-box", position: "relative", diff --git a/packages/perseus/src/widgets/__stories__/expression.stories.tsx b/packages/perseus/src/widgets/__stories__/expression.stories.tsx index 421706ca88..5a31888701 100644 --- a/packages/perseus/src/widgets/__stories__/expression.stories.tsx +++ b/packages/perseus/src/widgets/__stories__/expression.stories.tsx @@ -4,6 +4,7 @@ import * as React from "react"; import {ServerItemRendererWithDebugUI} from "../../../../../testing/server-item-renderer-with-debug-ui"; import { + complexExpressionWidget, expressionItem2, expressionItem3, } from "../__testdata__/expression.testdata"; @@ -113,7 +114,7 @@ export const Mobile = (args: StoryArgs): React.ReactElement => { to use the custom keypad.

diff --git a/packages/perseus/src/widgets/__testdata__/expression.testdata.ts b/packages/perseus/src/widgets/__testdata__/expression.testdata.ts index a3eb60bdd8..3926632757 100644 --- a/packages/perseus/src/widgets/__testdata__/expression.testdata.ts +++ b/packages/perseus/src/widgets/__testdata__/expression.testdata.ts @@ -196,3 +196,117 @@ export const randomExpressionGenerator = (): PerseusRenderer => { }, }; }; + +export const complexExpressionWidget: PerseusItem = { + answerArea: Object.fromEntries( + ItemExtras.map((extra) => [extra, false]), + ) as PerseusAnswerArea, + _multi: null, + answer: null, + hints: [ + { + content: + "$f(x, y, z) = (f_0, f_1, f_2)$\n\nThe curl of $f$:\n\n$\\begin{align}\n\\text{curl}(f) &= \\det \\begin{bmatrix}\n{\\hat{\\imath}} & \\hat{\\jmath} & \\hat{k} \\\\ \\\\\n\\dfrac{\\partial}{\\partial x} & \\dfrac{\\partial}{\\partial y} & \\dfrac{\\partial}{\\partial z} \\\\ \\\\\nf_0 & f_1 & f_2\n\\end{bmatrix} \\\\ \\\\\n&= \\left( \\dfrac{\\partial f_2}{\\partial y} - \\dfrac{\\partial f_1}{\\partial z} \\right) \\hat{\\imath} \\\\ \\\\\n&+ \\left( \\dfrac{\\partial f_0}{\\partial z} - \\dfrac{\\partial f_2}{\\partial x} \\right) \\hat{\\jmath} \\\\ \\\\\n&+ \\left( \\dfrac{\\partial f_1}{\\partial x} - \\dfrac{\\partial f_0}{\\partial y} \\right) \\hat{k}\n\\end{align}$", + images: {}, + replace: false, + widgets: {}, + }, + { + content: + "$\\begin{align}\nf_0(x, y, z) &= \\tan(y) \\\\ \\\\\nf_1(x, y, z) &= z\\sin(x) \\\\ \\\\\nf_2(x, y, z) &= 2\\cos(x)\n\\end{align}$\n\nLet's calculate all the partial derivatives we'll need.\n\n | $f_0$ | $f_1$ | $f_2$\n:-: | :-: | :-: | :-:\n$\\dfrac{\\partial}{\\partial x}$ | | $z\\cos(x)$ | $-2\\sin(x)$\n$\\dfrac{\\partial}{\\partial y}$| $\\sec^2(y)$ | | $0$\n$\\dfrac{\\partial}{\\partial z}$ | $0$ | $\\sin(x)$ | \n\nNow we can put it all together.\n\n$\\begin{align}\n\\text{curl}(f) &= \\left( \\dfrac{\\partial f_2}{\\partial y} - \\dfrac{\\partial f_1}{\\partial z} \\right) \\hat{\\imath} \\\\ \\\\\n&+ \\left( \\dfrac{\\partial f_0}{\\partial z} - \\dfrac{\\partial f_2}{\\partial x} \\right) \\hat{\\jmath} \\\\ \\\\\n&+ \\left( \\dfrac{\\partial f_1}{\\partial x} - \\dfrac{\\partial f_0}{\\partial y} \\right) \\hat{k} \\\\ \\\\\n&= (0 - \\sin(x)) \\hat{\\imath} + (0 + 2\\sin(x)) \\hat{\\jmath} \\\\ \\\\\n&+ (z\\cos(x) - \\sec^2(y)) \\hat{k} \\\\ \\\\\n&= -\\sin(x) \\hat{\\imath} + 2\\sin(x) \\hat{\\jmath} + (z\\cos(x) - \\sec^2(y)) \\hat{k}\n\\end{align}$ \n\nWe could also write the $\\hat{k}$-component as $z\\cos(x) - \\dfrac{1}{\\cos^2(y)}$ because $\\sec(y) = \\dfrac{1}{\\cos(y)}$.", + images: {}, + replace: false, + widgets: {}, + }, + { + content: + "In conclusion:\n\n$\\text{curl}(f) = -\\sin(x) \\hat{\\imath} + 2\\sin(x) \\hat{\\jmath} + (z\\cos(x) - \\sec^2(y)) \\hat{k}$", + images: {}, + replace: false, + widgets: {}, + }, + ], + itemDataVersion: { + major: 0, + minor: 1, + }, + question: { + content: + "$f(x, y, z) = (\\tan(y), z\\sin(x), 2\\cos(x))$\n\n$\\text{curl}(f) = $ [[☃ expression 1]] $\\hat{\\imath} + $ [[☃ expression 2]] $\\hat{\\jmath} + $ [[☃ expression 3]] $\\hat{k}$", + images: {}, + widgets: { + "expression 1": { + alignment: "default", + graded: true, + options: { + answerForms: [ + { + considered: "correct", + form: false, + key: "0", + simplify: false, + value: "-\\sin\\left(x\\right)", + }, + ], + buttonSets: ["basic", "trig", "prealgebra"], + functions: ["f", "g", "h"], + times: false, + }, + static: false, + type: "expression", + version: { + major: 1, + minor: 0, + }, + }, + "expression 2": { + alignment: "default", + graded: true, + options: { + answerForms: [ + { + considered: "correct", + form: false, + key: "0", + simplify: false, + value: "2\\sin\\left(x\\right)", + }, + ], + buttonSets: ["basic", "trig", "prealgebra"], + functions: ["f", "g", "h"], + times: false, + }, + static: false, + type: "expression", + version: { + major: 1, + minor: 0, + }, + }, + "expression 3": { + alignment: "default", + graded: true, + options: { + answerForms: [ + { + considered: "correct", + form: false, + key: "0", + simplify: false, + value: "z\\cos\\left(x\\right)-\\sec^{2}\\left(y\\right)", + }, + ], + buttonSets: ["basic", "trig", "prealgebra"], + functions: ["f", "g", "h"], + times: false, + }, + static: false, + type: "expression", + version: { + major: 1, + minor: 0, + }, + }, + }, + }, +}; diff --git a/packages/perseus/src/widgets/expression.tsx b/packages/perseus/src/widgets/expression.tsx index 8318ae7097..6d14e1d7de 100644 --- a/packages/perseus/src/widgets/expression.tsx +++ b/packages/perseus/src/widgets/expression.tsx @@ -5,6 +5,7 @@ import {View} from "@khanacademy/wonder-blocks-core"; import Tooltip from "@khanacademy/wonder-blocks-tooltip"; import classNames from "classnames"; import * as React from "react"; +import ReactDOM from "react-dom"; import _ from "underscore"; import {PerseusI18nContext} from "../components/i18n-context"; @@ -540,28 +541,50 @@ export class Expression extends React.Component { | React.ReactNode | React.ReactElement> { if (this.props.apiOptions.customKeypad) { + const inputId = `expression-input_${Date.now()}`; + const labelId = `expression-label_${Date.now()}`; + if (this.refs.input) { + const root = ReactDOM.findDOMNode(this.refs.input) as Element; + const textarea = root.querySelector( + '[aria-label="Math input box:"]', + ); + textarea?.setAttribute("id", inputId); + textarea?.setAttribute("aria-labelledby", labelId); + } return ( - { - // this.props.keypadElement should always be set - // when apiOptions.customKeypad is set, but how - // to convince TypeScript of this? - this.props.keypadElement?.configure( - this.props.keypadConfiguration, - () => { - if (this._isMounted) { - this._handleFocus(); - } - }, - ); - }} - onBlur={this._handleBlur} - /> + + + { + // this.props.keypadElement should always be set + // when apiOptions.customKeypad is set, but how + // to convince TypeScript of this? + this.props.keypadElement?.configure( + this.props.keypadConfiguration, + () => { + if (this._isMounted) { + this._handleFocus(); + } + }, + ); + }} + onBlur={this._handleBlur} + /> + ); }