Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
30 changes: 0 additions & 30 deletions packages/perseus/src/__tests__/renderer-api.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {describe, beforeEach, it} from "@jest/globals";
import {act, render, screen} from "@testing-library/react";
import {userEvent as userEventLib} from "@testing-library/user-event";
import $ from "jquery";
import * as React from "react";
import ReactDOM from "react-dom";
import _ from "underscore";

import {testDependencies} from "../../../../testing/test-dependencies";
Expand Down Expand Up @@ -52,34 +50,6 @@ describe("Perseus API", function () {
});
});

describe("getDOMNodeForPath", function () {
it("should find one DOM node per <input>", function () {
const {renderer} = renderQuestion(mockWidget2Item.question);
const inputPaths = renderer.getInputPaths();

const allInputs = screen.queryAllByRole("textbox");

expect(inputPaths).toHaveLength(allInputs.length);
});

it("should find the right DOM nodes for the <input>s", function () {
const {renderer} = renderQuestion(mockWidget2Item.question);
const inputPaths = renderer.getInputPaths();

const allInputs = screen.queryAllByRole("textbox");

inputPaths.forEach((inputPath, i) => {
// @ts-expect-error - TS2769 - No overload matches this call.
const $node = $(renderer.getDOMNodeForPath(inputPath));
// @ts-expect-error - TS2769 - No overload matches this call.
const $input = $(ReactDOM.findDOMNode(allInputs[i]));
// @ts-expect-error - TS2339 - Property 'closest' does not exist on type 'JQueryStatic'.
// eslint-disable-next-line testing-library/no-node-access
expect($input.closest($node).length).toBeTruthy();
});
});
});

describe("CSS ClassNames", function () {
describe("perseus-focused", function () {
it("should be on a mock-widget exactly when focused", async function () {
Expand Down
30 changes: 1 addition & 29 deletions packages/perseus/src/__tests__/renderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
generateTestPerseusItem,
splitPerseusItem,
} from "@khanacademy/perseus-core";
import {act, screen, waitFor, within} from "@testing-library/react";
import {act, screen, waitFor} from "@testing-library/react";
import {userEvent as userEventLib} from "@testing-library/user-event";
import * as React from "react";

Expand Down Expand Up @@ -1478,34 +1478,6 @@ describe("renderer", () => {
});
});

describe("getDOMNodeForPath", () => {
it("should return the DOM node for the widget at requested FocusPath", () => {
// Arrange
const {renderer} = renderQuestion(question1);

// Act
const node = renderer.getDOMNodeForPath(["dropdown 1"]);

// Assert
// @ts-expect-error - TS2345 - Argument of type 'Element | Text | null | undefined' is not assignable to parameter of type 'HTMLElement'.
expect(within(node).queryAllByRole("combobox")).toHaveLength(1);
});

it("should return the widget's getDOMNodeForPath() result for the widget at requested FocusPath", () => {
// Arrange
const {renderer} = renderQuestion(definitionItem);
const widget2DOMNode = <span />;
const [widget2] = renderer.findWidgets("definition 2");
widget2.getDOMNodeForPath = jest.fn(() => widget2DOMNode);

// Act
const node = renderer.getDOMNodeForPath(["definition 2"]);

// Assert
expect(node).toBe(widget2DOMNode);
});
});

describe("getInputPaths", () => {
it("should return all input paths for all rendererd widgets", () => {
// Arrange
Expand Down
19 changes: 1 addition & 18 deletions packages/perseus/src/__tests__/server-item-renderer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
import {within, render, screen, act} from "@testing-library/react";
import {render, screen, act} from "@testing-library/react";
import {userEvent as userEventLib} from "@testing-library/user-event";
import * as React from "react";

Expand Down Expand Up @@ -108,18 +108,6 @@ describe("server item renderer", () => {
});
});

it("should return the DOM node for the requested focus path", async () => {
// Arrange
const {renderer} = renderQuestion(itemWithMockWidget);

// Act
const node = renderer.getDOMNodeForPath(["mock-widget 1"]);

// Assert
// @ts-expect-error - TS2345 - Argument of type 'Element | Text | null | undefined' is not assignable to parameter of type 'HTMLElement'.
expect(await within(node).findAllByRole("textbox")).toHaveLength(1);
});

it("should return the number of hints available", () => {
// Arrange
const {renderer} = renderQuestion({
Expand Down Expand Up @@ -304,7 +292,6 @@ describe("server item renderer", () => {
["mock-widget 1"],
null,
0,
expect.any(Object),
);
});

Expand Down Expand Up @@ -349,7 +336,6 @@ describe("server item renderer", () => {
["numeric-input 1"],
null,
250,
expect.any(Object),
);
});

Expand All @@ -373,7 +359,6 @@ describe("server item renderer", () => {
null,
["mock-widget 1"],
0,
null,
);
});

Expand Down Expand Up @@ -419,7 +404,6 @@ describe("server item renderer", () => {
null,
["numeric-input 1"],
0,
null,
);
});

Expand All @@ -441,7 +425,6 @@ describe("server item renderer", () => {
["mock-widget 1"],
null,
0,
expect.any(Object),
);
});
});
Expand Down
12 changes: 1 addition & 11 deletions packages/perseus/src/article-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ class ArticleRenderer
// paths, so as to check whether the focused path represents an
// input.
let didFocusInput = false;
let focusedInput;

if (this._currentFocus) {
const [sectionIndex, ...focusPath] = this._currentFocus;
Expand All @@ -105,10 +104,6 @@ class ArticleRenderer
didFocusInput = inputPaths.some((inputPath) => {
return Util.inputPathsEqual(inputPath, focusPath);
});
focusedInput =
this.sectionRenderers[sectionIndex].getDOMNodeForPath(
focusPath,
);
}

const {onFocusChange} = this.props.apiOptions;
Expand All @@ -123,12 +118,7 @@ class ArticleRenderer
? keypadDomNode.getBoundingClientRect().height
: 0;

onFocusChange(
this._currentFocus,
prevFocusPath,
keypadHeight,
didFocusInput ? focusedInput : null,
);
onFocusChange(this._currentFocus, prevFocusPath, keypadHeight);
}, 0);
}

Expand Down
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;
Loading
Loading