From b2149d310775f4d98fae5395cb5cc1c8299e6283 Mon Sep 17 00:00:00 2001 From: Kevin Barabash 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 { 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 { { onFocus={() => this.focus()} onBlur={() => this.blur()} /> - this.closeKeypad()} - dismissEnabled - aria-label={this.context.strings.mathInputTitle} - aria-describedby={`popover-content-${popoverContentUniqueId}`} - content={() => ( - <> - - {this.context.strings.mathInputDescription} - - - this.closeKeypad()} + dismissEnabled + aria-label={this.context.strings.mathInputTitle} + aria-describedby={`popover-content-${popoverContentUniqueId}`} + content={() => ( + <> + + { + this.context.strings + .mathInputDescription } - {...(this.props.keypadButtonSets ?? - mapButtonSets( - this.props?.buttonSets, - ))} - /> - - - )} - > - {this.props.buttonsVisible === "never" ? ( - - ) : ( - - this.state.keypadOpen - ? this.closeKeypad() - : this.openKeypad() - } - > - {(props) => ( - - )} - - )} - + + + + + + )} + > + {this.props.buttonsVisible === "never" ? ( + + ) : ( + + this.state.keypadOpen + ? this.closeKeypad() + : this.openKeypad() + } + > + {(props) => ( + + )} + + )} + + )} ); @@ -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 = 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} > 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 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 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 ( - + {!!this.props.visibleLabel && ( {this.props.visibleLabel} From 6da54c3f0edf3acecbea79e2449677ef73d02f68 Mon Sep 17 00:00:00 2001 From: Kevin Barabash 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 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 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 { }; focus: () => void = () => { + if (this.props.disabled) { + return; + } this.mathField()?.focus(); this.setState({focused: true}); }; @@ -338,7 +341,12 @@ class InnerMathInput extends React.Component { (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 { } focus() { + if (this.props.disabled) { + return; + } this.inputRef.current?.focus(); } From b8b9f9de821093a06bc8014d0a12fdc5d3b9577b Mon Sep 17 00:00:00 2001 From: Kevin Barabash 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 { }; focus: () => void = () => { - if (this.props.disabled) { - return; - } this.mathField()?.focus(); this.setState({focused: true}); }; @@ -316,6 +313,7 @@ class InnerMathInput extends React.Component { !this.props.noBackground && styles.outerWrapperBackground, this.state.focused && styles.wrapperFocused, this.props.hasError && styles.wrapperError, + this.props.disabled && styles.disabled, ]} >
{ (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 { } 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();