From b2149d310775f4d98fae5395cb5cc1c8299e6283 Mon Sep 17 00:00:00 2001
From: Kevin Barabash <kevinbarabash@khanacademy.org>
Date: Sun, 20 Oct 2024 12:19:43 -0400
Subject: [PATCH 1/8] Update the Expression widget to support the 'Show Your
 Work' hackathon project

---
 .../perseus/src/components/math-input.tsx     | 155 ++++++++++--------
 .../src/widgets/expression/expression.tsx     |  10 +-
 2 files changed, 95 insertions(+), 70 deletions(-)

diff --git a/packages/perseus/src/components/math-input.tsx b/packages/perseus/src/components/math-input.tsx
index f1a6b776c7..8dc19eea8f 100644
--- a/packages/perseus/src/components/math-input.tsx
+++ b/packages/perseus/src/components/math-input.tsx
@@ -69,6 +69,8 @@ type Props = {
      */
     buttonsVisible?: ButtonsVisibleType;
     analytics: PerseusDependenciesV2["analytics"];
+    disabled?: boolean;
+    noBackground?: boolean;
 };
 
 type InnerProps = Props & {
@@ -165,6 +167,13 @@ class InnerMathInput extends React.Component<InnerProps, State> {
         input?.focus();
     };
 
+    // TODO(kevinb): Port this to @khanacademy/math-input
+    setValue = (value: string) => {
+        const input = this.mathField();
+        input?.select();
+        input?.write(value);
+    };
+
     mathField: () => MathFieldInterface | null = () => {
         if (!this.__mathField && this.__mathFieldWrapperRef) {
             const {locale} = this.context;
@@ -301,6 +310,7 @@ class InnerMathInput extends React.Component<InnerProps, State> {
             <View
                 style={[
                     styles.outerWrapper,
+                    !this.props.noBackground && styles.outerWrapperBackground,
                     this.state.focused && styles.wrapperFocused,
                     this.props.hasError && styles.wrapperError,
                 ]}
@@ -331,74 +341,81 @@ class InnerMathInput extends React.Component<InnerProps, State> {
                         onFocus={() => this.focus()}
                         onBlur={() => this.blur()}
                     />
-                    <Popover
-                        rootBoundary="document"
-                        opened={this.state.keypadOpen}
-                        onClose={() => this.closeKeypad()}
-                        dismissEnabled
-                        aria-label={this.context.strings.mathInputTitle}
-                        aria-describedby={`popover-content-${popoverContentUniqueId}`}
-                        content={() => (
-                            <>
-                                <HeadingMedium
-                                    id={`popover-content-${popoverContentUniqueId}`}
-                                    style={a11y.srOnly}
-                                >
-                                    {this.context.strings.mathInputDescription}
-                                </HeadingMedium>
-                                <PopoverContentCore
-                                    closeButtonVisible
-                                    style={styles.popoverContent}
-                                >
-                                    <DesktopKeypad
-                                        onAnalyticsEvent={
-                                            this.props.analytics
-                                                .onAnalyticsEvent
-                                        }
-                                        extraKeys={this.props.extraKeys}
-                                        onClickKey={this.handleKeypadPress}
-                                        cursorContext={this.state.cursorContext}
-                                        convertDotToTimes={
-                                            this.props.convertDotToTimes
+                    {!this.props.disabled && (
+                        <Popover
+                            rootBoundary="document"
+                            opened={this.state.keypadOpen}
+                            onClose={() => this.closeKeypad()}
+                            dismissEnabled
+                            aria-label={this.context.strings.mathInputTitle}
+                            aria-describedby={`popover-content-${popoverContentUniqueId}`}
+                            content={() => (
+                                <>
+                                    <HeadingMedium
+                                        id={`popover-content-${popoverContentUniqueId}`}
+                                        style={a11y.srOnly}
+                                    >
+                                        {
+                                            this.context.strings
+                                                .mathInputDescription
                                         }
-                                        {...(this.props.keypadButtonSets ??
-                                            mapButtonSets(
-                                                this.props?.buttonSets,
-                                            ))}
-                                    />
-                                </PopoverContentCore>
-                            </>
-                        )}
-                    >
-                        {this.props.buttonsVisible === "never" ? (
-                            <MathInputIcon
-                                hovered={false}
-                                focused={false}
-                                active={false}
-                            />
-                        ) : (
-                            <Clickable
-                                aria-label={
-                                    this.state.keypadOpen
-                                        ? this.context.strings.closeKeypad
-                                        : this.context.strings.openKeypad
-                                }
-                                role="button"
-                                onClick={() =>
-                                    this.state.keypadOpen
-                                        ? this.closeKeypad()
-                                        : this.openKeypad()
-                                }
-                            >
-                                {(props) => (
-                                    <MathInputIcon
-                                        active={this.state.keypadOpen}
-                                        {...props}
-                                    />
-                                )}
-                            </Clickable>
-                        )}
-                    </Popover>
+                                    </HeadingMedium>
+                                    <PopoverContentCore
+                                        closeButtonVisible
+                                        style={styles.popoverContent}
+                                    >
+                                        <DesktopKeypad
+                                            onAnalyticsEvent={
+                                                this.props.analytics
+                                                    .onAnalyticsEvent
+                                            }
+                                            extraKeys={this.props.extraKeys}
+                                            onClickKey={this.handleKeypadPress}
+                                            cursorContext={
+                                                this.state.cursorContext
+                                            }
+                                            convertDotToTimes={
+                                                this.props.convertDotToTimes
+                                            }
+                                            {...(this.props.keypadButtonSets ??
+                                                mapButtonSets(
+                                                    this.props?.buttonSets,
+                                                ))}
+                                        />
+                                    </PopoverContentCore>
+                                </>
+                            )}
+                        >
+                            {this.props.buttonsVisible === "never" ? (
+                                <MathInputIcon
+                                    hovered={false}
+                                    focused={false}
+                                    active={false}
+                                />
+                            ) : (
+                                <Clickable
+                                    aria-label={
+                                        this.state.keypadOpen
+                                            ? this.context.strings.closeKeypad
+                                            : this.context.strings.openKeypad
+                                    }
+                                    role="button"
+                                    onClick={() =>
+                                        this.state.keypadOpen
+                                            ? this.closeKeypad()
+                                            : this.openKeypad()
+                                    }
+                                >
+                                    {(props) => (
+                                        <MathInputIcon
+                                            active={this.state.keypadOpen}
+                                            {...props}
+                                        />
+                                    )}
+                                </Clickable>
+                            )}
+                        </Popover>
+                    )}
                 </div>
             </View>
         );
@@ -531,9 +548,11 @@ const styles = StyleSheet.create({
         borderWidth: 1,
         borderColor: color.offBlack50,
         borderRadius: 3,
-        background: color.white,
         ":hover": inputFocused,
     },
+    outerWrapperBackground: {
+        background: color.white,
+    },
     wrapperFocused: inputFocused,
     wrapperError: {
         borderColor: color.red,
diff --git a/packages/perseus/src/widgets/expression/expression.tsx b/packages/perseus/src/widgets/expression/expression.tsx
index 2dc06dc088..2292f223c0 100644
--- a/packages/perseus/src/widgets/expression/expression.tsx
+++ b/packages/perseus/src/widgets/expression/expression.tsx
@@ -110,6 +110,7 @@ export class Expression
 
     _textareaId = `expression_textarea_${Date.now()}`;
     _isMounted = false;
+    _mathInput: React.MutableRefObject<null | MathInput> = React.createRef();
 
     static getUserInputFromProps(props: Props): PerseusExpressionUserInput {
         return normalizeTex(props.value);
@@ -281,6 +282,12 @@ export class Expression
     }
 
     setInputValue(path: FocusPath, newValue: string, cb: () => void) {
+        if (this._mathInput.current) {
+            const inputRef = this._mathInput.current.inputRef;
+            if (inputRef.current) {
+                inputRef.current.setValue(newValue);
+            }
+        }
         this.props.onChange(
             {
                 value: newValue,
@@ -371,8 +378,7 @@ export class Expression
                         content={ERROR_MESSAGE}
                     >
                         <MathInput
-                            // eslint-disable-next-line react/no-string-refs
-                            ref="input"
+                            ref={this._mathInput}
                             className={ApiClassNames.INTERACTIVE}
                             value={this.props.value}
                             onChange={this.changeAndTrack}

From 0b4334e145155d1f1c8298689f687df53a5dc8fc Mon Sep 17 00:00:00 2001
From: Kevin Barabash <kevinbarabash@khanacademy.org>
Date: Sun, 20 Oct 2024 12:38:09 -0400
Subject: [PATCH 2/8] Feed 'disabled' prop through to MathInput

---
 packages/perseus/src/widgets/expression/expression.tsx | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/packages/perseus/src/widgets/expression/expression.tsx b/packages/perseus/src/widgets/expression/expression.tsx
index 2292f223c0..dafc6db69d 100644
--- a/packages/perseus/src/widgets/expression/expression.tsx
+++ b/packages/perseus/src/widgets/expression/expression.tsx
@@ -81,6 +81,7 @@ export type Props = ExternalProps &
         visibleLabel: PerseusExpressionWidgetOptions["visibleLabel"];
         ariaLabel: PerseusExpressionWidgetOptions["ariaLabel"];
         value: string;
+        disabled?: boolean;
     };
 
 export type ExpressionState = {
@@ -384,6 +385,7 @@ export class Expression
                             onChange={this.changeAndTrack}
                             convertDotToTimes={this.props.times}
                             buttonSets={this.props.buttonSets}
+                            disabled={this.props.disabled}
                             onFocus={this._handleFocus}
                             onBlur={this._handleBlur}
                             hasError={this.state.showErrorStyle}

From 2fb214794adc1c73a50813004eb30e8710043c9c Mon Sep 17 00:00:00 2001
From: Kevin Barabash <kevinbarabash@khanacademy.org>
Date: Sun, 20 Oct 2024 12:50:46 -0400
Subject: [PATCH 3/8] Feed 'noBackground' prop through to MathInput

---
 packages/perseus/src/widgets/expression/expression.tsx | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/packages/perseus/src/widgets/expression/expression.tsx b/packages/perseus/src/widgets/expression/expression.tsx
index dafc6db69d..5bbabb0a61 100644
--- a/packages/perseus/src/widgets/expression/expression.tsx
+++ b/packages/perseus/src/widgets/expression/expression.tsx
@@ -82,6 +82,7 @@ export type Props = ExternalProps &
         ariaLabel: PerseusExpressionWidgetOptions["ariaLabel"];
         value: string;
         disabled?: boolean;
+        noBackground?: boolean;
     };
 
 export type ExpressionState = {
@@ -386,6 +387,7 @@ export class Expression
                             convertDotToTimes={this.props.times}
                             buttonSets={this.props.buttonSets}
                             disabled={this.props.disabled}
+                            noBackground={this.props.noBackground}
                             onFocus={this._handleFocus}
                             onBlur={this._handleBlur}
                             hasError={this.state.showErrorStyle}

From d072b86d0c44e12055ca158a113f4b80181cb2e3 Mon Sep 17 00:00:00 2001
From: Kevin Barabash <kevinbarabash@khanacademy.org>
Date: Sun, 20 Oct 2024 12:56:59 -0400
Subject: [PATCH 4/8] Add 'noWrapper' prop to Expression widget

---
 packages/perseus/src/widgets/expression/expression.tsx | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/packages/perseus/src/widgets/expression/expression.tsx b/packages/perseus/src/widgets/expression/expression.tsx
index 5bbabb0a61..0f44f9296b 100644
--- a/packages/perseus/src/widgets/expression/expression.tsx
+++ b/packages/perseus/src/widgets/expression/expression.tsx
@@ -83,6 +83,7 @@ export type Props = ExternalProps &
         value: string;
         disabled?: boolean;
         noBackground?: boolean;
+        noWrapper?: boolean;
     };
 
 export type ExpressionState = {
@@ -344,7 +345,13 @@ export class Expression
         const {ERROR_MESSAGE, ERROR_TITLE} = this.context.strings;
 
         return (
-            <View className={css(styles.desktopLabelInputWrapper)}>
+            <View
+                className={
+                    this.props.noWrapper
+                        ? undefined
+                        : css(styles.desktopLabelInputWrapper)
+                }
+            >
                 {!!this.props.visibleLabel && (
                     <LabelSmall htmlFor={this._textareaId} tag="label">
                         {this.props.visibleLabel}

From 6da54c3f0edf3acecbea79e2449677ef73d02f68 Mon Sep 17 00:00:00 2001
From: Kevin Barabash <kevinbarabash@khanacademy.org>
Date: Thu, 24 Oct 2024 20:50:32 -0400
Subject: [PATCH 5/8] If props.disable is true, don't allow the input field to
 be focused

---
 .../perseus/src/widgets/expression/expression.tsx    | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/packages/perseus/src/widgets/expression/expression.tsx b/packages/perseus/src/widgets/expression/expression.tsx
index 0f44f9296b..b8a16f1a7b 100644
--- a/packages/perseus/src/widgets/expression/expression.tsx
+++ b/packages/perseus/src/widgets/expression/expression.tsx
@@ -221,6 +221,10 @@ export class Expression
     };
 
     _handleFocus: () => void = () => {
+        if (this.props.disabled) {
+            return;
+        }
+
         this.props.analytics?.onAnalyticsEvent({
             type: "perseus:expression-focused",
             payload: null,
@@ -236,6 +240,10 @@ export class Expression
     };
 
     focus: () => boolean = () => {
+        if (this.props.disabled) {
+            return false;
+        }
+
         if (this.props.apiOptions.customKeypad) {
             // eslint-disable-next-line react/no-string-refs
             // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
@@ -249,6 +257,10 @@ export class Expression
     };
 
     focusInputPath(inputPath: InputPath) {
+        if (this.props.disabled) {
+            return;
+        }
+
         // eslint-disable-next-line react/no-string-refs
         // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
         this.refs.input.focus();

From 520b449b813310850fb0d5366a35114faaa79c56 Mon Sep 17 00:00:00 2001
From: Kevin Barabash <kevinbarabash@khanacademy.org>
Date: Thu, 24 Oct 2024 21:04:39 -0400
Subject: [PATCH 6/8] add changeset file

---
 .changeset/chilly-singers-decide.md | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 .changeset/chilly-singers-decide.md

diff --git a/.changeset/chilly-singers-decide.md b/.changeset/chilly-singers-decide.md
new file mode 100644
index 0000000000..d6c5c3bced
--- /dev/null
+++ b/.changeset/chilly-singers-decide.md
@@ -0,0 +1,5 @@
+---
+"@khanacademy/perseus": patch
+---
+
+Add features to support show-your-work widget

From 6b285b582e7d71f31706b0b6412956d8fd4f90e6 Mon Sep 17 00:00:00 2001
From: Kevin Barabash <kevinbarabash@khanacademy.org>
Date: Thu, 24 Oct 2024 21:13:06 -0400
Subject: [PATCH 7/8] also prevent MathInput from gaining focus when disabled

---
 packages/perseus/src/components/math-input.tsx | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/packages/perseus/src/components/math-input.tsx b/packages/perseus/src/components/math-input.tsx
index 09bb3e9f9e..b999e4ce07 100644
--- a/packages/perseus/src/components/math-input.tsx
+++ b/packages/perseus/src/components/math-input.tsx
@@ -258,6 +258,9 @@ class InnerMathInput extends React.Component<InnerProps, State> {
     };
 
     focus: () => void = () => {
+        if (this.props.disabled) {
+            return;
+        }
         this.mathField()?.focus();
         this.setState({focused: true});
     };
@@ -338,7 +341,12 @@ class InnerMathInput extends React.Component<InnerProps, State> {
                     <span
                         className={className}
                         ref={(ref) => (this.__mathFieldWrapperRef = ref)}
-                        onFocus={() => this.focus()}
+                        onFocus={() => {
+                            if (this.props.disabled) {
+                                return;
+                            }
+                            this.focus();
+                        }}
                         onBlur={() => this.blur()}
                     />
                     {!this.props.disabled && (
@@ -439,6 +447,9 @@ class MathInput extends React.Component<Props, State> {
     }
 
     focus() {
+        if (this.props.disabled) {
+            return;
+        }
         this.inputRef.current?.focus();
     }
 

From b8b9f9de821093a06bc8014d0a12fdc5d3b9577b Mon Sep 17 00:00:00 2001
From: Kevin Barabash <kevinbarabash@khanacademy.org>
Date: Thu, 24 Oct 2024 21:25:42 -0400
Subject: [PATCH 8/8] revert focus() method changes and use 'pointer-events:
 none' to disable the expression widget

---
 packages/perseus/src/components/math-input.tsx  | 17 +++++------------
 .../src/widgets/expression/expression.tsx       | 12 ------------
 2 files changed, 5 insertions(+), 24 deletions(-)

diff --git a/packages/perseus/src/components/math-input.tsx b/packages/perseus/src/components/math-input.tsx
index b999e4ce07..533defd041 100644
--- a/packages/perseus/src/components/math-input.tsx
+++ b/packages/perseus/src/components/math-input.tsx
@@ -258,9 +258,6 @@ class InnerMathInput extends React.Component<InnerProps, State> {
     };
 
     focus: () => void = () => {
-        if (this.props.disabled) {
-            return;
-        }
         this.mathField()?.focus();
         this.setState({focused: true});
     };
@@ -316,6 +313,7 @@ class InnerMathInput extends React.Component<InnerProps, State> {
                     !this.props.noBackground && styles.outerWrapperBackground,
                     this.state.focused && styles.wrapperFocused,
                     this.props.hasError && styles.wrapperError,
+                    this.props.disabled && styles.disabled,
                 ]}
             >
                 <div
@@ -341,12 +339,7 @@ class InnerMathInput extends React.Component<InnerProps, State> {
                     <span
                         className={className}
                         ref={(ref) => (this.__mathFieldWrapperRef = ref)}
-                        onFocus={() => {
-                            if (this.props.disabled) {
-                                return;
-                            }
-                            this.focus();
-                        }}
+                        onFocus={() => this.focus()}
                         onBlur={() => this.blur()}
                     />
                     {!this.props.disabled && (
@@ -447,9 +440,6 @@ class MathInput extends React.Component<Props, State> {
     }
 
     focus() {
-        if (this.props.disabled) {
-            return;
-        }
         this.inputRef.current?.focus();
     }
 
@@ -577,6 +567,9 @@ const styles = StyleSheet.create({
         paddingBottom: spacing.xxSmall_6,
         maxWidth: "initial",
     },
+    disabled: {
+        pointerEvents: "none",
+    },
 });
 
 export default MathInput;
diff --git a/packages/perseus/src/widgets/expression/expression.tsx b/packages/perseus/src/widgets/expression/expression.tsx
index 13d75e3704..643cfe6123 100644
--- a/packages/perseus/src/widgets/expression/expression.tsx
+++ b/packages/perseus/src/widgets/expression/expression.tsx
@@ -221,10 +221,6 @@ export class Expression
     };
 
     _handleFocus: () => void = () => {
-        if (this.props.disabled) {
-            return;
-        }
-
         this.props.analytics?.onAnalyticsEvent({
             type: "perseus:expression-focused",
             payload: null,
@@ -240,10 +236,6 @@ export class Expression
     };
 
     focus: () => boolean = () => {
-        if (this.props.disabled) {
-            return false;
-        }
-
         if (this.props.apiOptions.customKeypad) {
             // eslint-disable-next-line react/no-string-refs
             // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
@@ -257,10 +249,6 @@ export class Expression
     };
 
     focusInputPath(inputPath: InputPath) {
-        if (this.props.disabled) {
-            return;
-        }
-
         // eslint-disable-next-line react/no-string-refs
         // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
         this.refs.input.focus();