Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lovely-bugs-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

Remove unused code and usages of findDOMNode
102 changes: 51 additions & 51 deletions packages/perseus/src/components/simple-keypad-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,78 +15,78 @@ import * as React from "react";

import type {Focusable} from "../types";

export default class SimpleKeypadInput
extends React.Component<any>
implements Focusable
{
static contextType = KeypadContext;
declare context: React.ContextType<typeof KeypadContext>;
_isMounted = false;
inputRef = React.createRef<KeypadInput>();
type SimpleKeypadInputProps = {
keypadElement?: any;
onFocus: () => void;
onBlur: () => void;
onChange: (value: string, callback: any) => void;
value?: string | number | null;
ariaLabel?: string;
style?: React.CSSProperties;
};

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;
}
const SimpleKeypadInput = React.forwardRef<Focusable, SimpleKeypadInputProps>(
function SimpleKeypadInput(props, ref) {
const keypadInputRef = React.useRef<any>(null);
const context = React.useContext(KeypadContext);

componentWillUnmount() {
this._isMounted = false;
}
// Use imperative handle to expose the DOM node properties and custom methods
// required for consuming Perseus widgets to handle focus/blur events.
React.useImperativeHandle(ref, () => {
const keypadInstance = keypadInputRef.current;
if (!keypadInstance) {
return null;
}

focus() {
// The inputRef is a ref to a MathInput, which
// also controls the keypad state during focus events.
this.inputRef.current?.focus(this.context.setKeypadActive);
}
// Get the actual DOM node from the KeypadInput (MathInput) component's
// internal inputRef.
const inputElement = keypadInstance.inputRef;

blur() {
this.inputRef.current?.blur();
}
if (!inputElement) {
return null;
}

getValue(): string | number {
return this.props.value;
}
// Return the DOM node with focus/blur methods attached.
return {
...inputElement,
focus: () => {
keypadInstance.focus(context.setKeypadActive);
},
blur: () => {
keypadInstance.blur();
},
getValue: () => {
return props.value;
},
};
});

render(): React.ReactNode {
const _this = this;
// Intercept the `onFocus` prop, as we need to configure the keypad
// before continuing with the default focus logic for Perseus inputs.
// Intercept the `value` prop so as to map `null` to the empty string,
// as the `KeypadInput` does not support `null` values.
const {keypadElement, onFocus, value, ...rest} = _this.props;
const {keypadElement, onFocus, value, ...rest} = props;

return (
// @ts-expect-error - TS2769 - No overload matches this call.
<KeypadInput
ref={this.inputRef}
ref={keypadInputRef}
keypadElement={keypadElement}
onFocus={() => {
if (keypadElement) {
keypadElement.configure(
{
keypadType: "FRACTION",
},
() => {
if (_this._isMounted) {
onFocus?.();
}
},
);
} else {
onFocus?.();
keypadElement.configure({
keypadType: "FRACTION",
});
}
onFocus?.();
}}
value={value == null ? "" : "" + value}
ariaLabel={props.ariaLabel || ""}
{...rest}
/>
);
}
}
},
);

// @ts-expect-error - TS2339 - Property 'propTypes' does not exist on type 'typeof SimpleKeypadInput'.
SimpleKeypadInput.propTypes = {
keypadElement: keypadElementPropType,
onFocus: PropTypes.func,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};

export default SimpleKeypadInput;
30 changes: 4 additions & 26 deletions packages/perseus/src/renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
} from "@khanacademy/perseus-score";
import {entries} from "@khanacademy/wonder-stuff-core";
import classNames from "classnames";
import $ from "jquery";
import * as React from "react";
import ReactDOM from "react-dom";
import _ from "underscore";
Expand Down Expand Up @@ -311,9 +310,7 @@ class Renderer
componentDidMount() {
this._isMounted = true;

// figure out why we're passing an empty object
// @ts-expect-error - TS2345 - Argument of type '{}' is not assignable to parameter of type 'Props'.
this.handleRender({});
this.handleRender();
this._currentFocus = null;

this.props.initializeUserInput?.(
Expand Down Expand Up @@ -423,7 +420,7 @@ class Renderer
}

componentDidUpdate(prevProps: Props, prevState: State) {
this.handleRender(prevProps);
this.handleRender();
// We even do this if we did reuse the markdown because
// we might need to update the widget props on this render,
// even though we have the same widgets.
Expand Down Expand Up @@ -1421,27 +1418,8 @@ class Renderer
);
};

handleRender: (prevProps: Props) => void = (prevProps: Props) => {
const onRender = this.props.onRender;
const oldOnRender = prevProps.onRender;

// In the common case of no callback specified, avoid this work.
if (onRender !== noopOnRender || oldOnRender !== noopOnRender) {
// @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'find' does not exist on type 'JQueryStatic'.
const $images = $(ReactDOM.findDOMNode(this)).find("img");

// Fire callback on image load...
if (oldOnRender !== noopOnRender) {
$images.off("load", oldOnRender);
}

if (onRender !== noopOnRender) {
$images.on("load", onRender);
}
}

// ...as well as right now (non-image, non-TeX or image from cache)
onRender();
handleRender: () => void = () => {
this.props.onRender();
};

// Sets the current focus path
Expand Down
38 changes: 18 additions & 20 deletions packages/perseus/src/widgets/matrix/matrix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {linterContextDefault} from "@khanacademy/perseus-linter";
import {StyleSheet} from "aphrodite";
import classNames from "classnames";
import * as React from "react";
import ReactDOM from "react-dom";
import _ from "underscore";

import {PerseusI18nContext} from "../../components/i18n-context";
Expand Down Expand Up @@ -115,6 +114,7 @@ type State = {
class Matrix extends React.Component<Props, State> implements Widget {
static contextType = PerseusI18nContext;
declare context: React.ContextType<typeof PerseusI18nContext>;
answerRefs: Record<string, HTMLInputElement> = {};

// @ts-expect-error - TS2564 - Property 'cursorPosition' has no initializer and is not definitely assigned in the constructor.
cursorPosition: [number, number];
Expand Down Expand Up @@ -170,9 +170,8 @@ class Matrix extends React.Component<Props, State> implements Widget {

focusInputPath: (arg1: any) => void = (path) => {
const inputID = getRefForPath(path);
// eslint-disable-next-line react/no-string-refs
// @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
this.refs[inputID].focus();
const inputComponent = this.answerRefs[inputID];
inputComponent.focus();
};

blurInputPath: (arg1: any) => void = (path) => {
Expand All @@ -181,15 +180,14 @@ class Matrix extends React.Component<Props, State> implements Widget {
}

const inputID = getRefForPath(path);
// eslint-disable-next-line react/no-string-refs
// @ts-expect-error - TS2339 - Property 'blur' does not exist on type 'ReactInstance'.
this.refs[inputID].blur();
const inputComponent = this.answerRefs[inputID];
inputComponent.blur();
};

getDOMNodeForPath(path: FocusPath) {
const inputID = getRefForPath(path);
// eslint-disable-next-line react/no-string-refs
return ReactDOM.findDOMNode(this.refs[inputID]);
const inputRef = this.answerRefs[inputID];
return inputRef;
}

handleKeyDown: (arg1: any, arg2: any, arg3: any) => void = (
Expand All @@ -201,8 +199,8 @@ class Matrix extends React.Component<Props, State> implements Widget {
const maxCol = this.props.matrixBoardSize[1];
let enterTheMatrix = null;

// eslint-disable-next-line react/no-string-refs
const curInput = this.refs[getRefForPath(getInputPath(row, col))];
const inputID = getRefForPath(getInputPath(row, col));
const curInput = this.answerRefs[inputID];
// @ts-expect-error - TS2339 - Property 'getStringValue' does not exist on type 'ReactInstance'.
const curValueString = curInput.getStringValue();
Copy link
Contributor Author

@SonicScrewdriver SonicScrewdriver Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't remove these ts-expect errors as the SimpleKeypadInput never supported these methods, although our TextInput does. (getStringValue, getSelectionStart, getSelectionEnd).

This means that none of this keyboard logic was ever working for our mobile users. However, they were also very unlikely to hit this as it requires hitting specific keys on their mobile device's keyboard.

// @ts-expect-error - TS2339 - Property 'getSelectionStart' does not exist on type 'ReactInstance'.
Expand Down Expand Up @@ -243,22 +241,19 @@ class Matrix extends React.Component<Props, State> implements Widget {
e.preventDefault();

// Focus the input and move the cursor to the end of it.
// eslint-disable-next-line react/no-string-refs
const input = this.refs[getRefForPath(nextPath)];
const inputID = getRefForPath(nextPath);
const input = this.answerRefs[inputID];

// Multiply by 2 to ensure the cursor always ends up at the end;
// Opera sometimes sees a carriage return as 2 characters.
// @ts-expect-error - TS2339 - Property 'getStringValue' does not exist on type 'ReactInstance'.
const inputValString = input.getStringValue();
const valueLength = inputValString.length * 2;

// @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
input.focus();
if (e.key === "ArrowRight") {
// @ts-expect-error - TS2339 - Property 'setSelectionRange' does not exist on type 'ReactInstance'.
input.setSelectionRange(0, 0);
} else {
// @ts-expect-error - TS2339 - Property 'setSelectionRange' does not exist on type 'ReactInstance'.
input.setSelectionRange(valueLength, valueLength);
}
}
Expand Down Expand Up @@ -377,9 +372,13 @@ class Matrix extends React.Component<Props, State> implements Widget {
className: outside
? "outside"
: "inside",
ref: getRefForPath(
getInputPath(row, col),
),
ref: (ref) => {
this.answerRefs[
getRefForPath(
getInputPath(row, col),
)
] = ref;
},
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
value: rowVals ? rowVals[col] : null,
style: {
Expand Down Expand Up @@ -462,7 +461,6 @@ class Matrix extends React.Component<Props, State> implements Widget {
<SimpleKeypadInput
{...inputProps}
style={style}
scrollable={true}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nonsense property that doesn't exist, so I've removed it! We simply didn't catch it before as SimpleKeypadInput wasn't very specific about props

keypadElement={
this.props.keypadElement
}
Expand Down
28 changes: 14 additions & 14 deletions packages/perseus/src/widgets/number-line/number-line.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {number as knumber, KhanMath} from "@khanacademy/kmath";
import * as React from "react";
import ReactDOM from "react-dom";
import _ from "underscore";

import Graphie from "../../components/graphie";
Expand Down Expand Up @@ -247,6 +246,8 @@ class NumberLine extends React.Component<Props, State> implements Widget {
static contextType = PerseusI18nContext;
declare context: React.ContextType<typeof PerseusI18nContext>;

tickControlRef: HTMLInputElement | null = null;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's only ever one input in NumberLine, so I figured I'd simplify this accordingly.


static defaultProps: DefaultProps = {
range: [0, 10],
labelStyle: "decimal",
Expand Down Expand Up @@ -346,41 +347,39 @@ class NumberLine extends React.Component<Props, State> implements Widget {

focus() {
if (this.props.isTickCtrl) {
// eslint-disable-next-line react/no-string-refs
// @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
this.refs["tick-ctrl"].focus();
this.tickControlRef?.focus();
return true;
}
return false;
}

focusInputPath: (arg1: any) => void = (path) => {
if (path.length === 1) {
// eslint-disable-next-line react/no-string-refs
// @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
this.refs[path[0]].focus();
this.tickControlRef?.focus();
}
};

blurInputPath: (arg1: any) => void = (path) => {
if (path.length === 1) {
// eslint-disable-next-line react/no-string-refs
// @ts-expect-error - TS2339 - Property 'blur' does not exist on type 'ReactInstance'.
this.refs[path[0]].blur();
this.tickControlRef?.blur();
}
};

// There's only one input path for the tick control, but the renderer
// expects this method to be implemented.
getInputPaths: () => ReadonlyArray<ReadonlyArray<string>> = () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these methods are part of our our main renderer logic, I didn't want to mess with them too much.

I think improving this will require a more holistic and modern approach to our focus handling across Perseus (perhaps a context?).

if (this.props.isTickCtrl) {
return [["tick-ctrl"]];
}
return [];
};

// This consumes the input path returned by getInputPaths,
// and returns the DOM node for the tick control input.
getDOMNodeForPath(inputPath: FocusPath) {
// If we have a tick control, return the DOM node for the tick control input.
if (inputPath?.length === 1) {
// eslint-disable-next-line react/no-string-refs
return ReactDOM.findDOMNode(this.refs[inputPath[0]]);
return this.tickControlRef;
}
return null;
}
Expand Down Expand Up @@ -679,8 +678,9 @@ class NumberLine extends React.Component<Props, State> implements Widget {
<label>
{strings.numDivisions}{" "}
<Input
// eslint-disable-next-line react/no-string-refs
ref="tick-ctrl"
ref={(ref) => {
this.tickControlRef = ref;
}}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooo I can simplify this

value={
this.state.numDivisionsEmpty
? null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const NumericInputComponent = forwardRef<Focusable, NumericInputProps>(
return (
<div className={alignmentClass}>
<SimpleKeypadInput
ref={inputRef as React.RefObject<SimpleKeypadInput>}
ref={inputRef}
value={props.userInput.currentValue}
keypadElement={props.keypadElement}
onChange={handleChange}
Expand Down
Loading
Loading