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}
+ />
+
);
}