@khanacademy/perseus-editor 2.15.7 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/components/blur-input.d.ts +2 -0
  3. package/dist/components/widget-editor.d.ts +1 -1
  4. package/dist/editor-page.d.ts +4 -1
  5. package/dist/editor.d.ts +3 -1
  6. package/dist/es/index.js +170 -260
  7. package/dist/es/index.js.map +1 -1
  8. package/dist/hint-editor.d.ts +2 -0
  9. package/dist/iframe-content-renderer.d.ts +1 -0
  10. package/dist/index.js +137 -256
  11. package/dist/index.js.map +1 -1
  12. package/dist/item-editor.d.ts +6 -0
  13. package/dist/widgets/example-widget-editor.d.ts +1 -0
  14. package/dist/widgets/example-widget.d.ts +7 -0
  15. package/dist/widgets/graded-group-editor.d.ts +3 -0
  16. package/dist/widgets/group-editor.d.ts +2 -0
  17. package/dist/widgets/input-number-editor.d.ts +2 -0
  18. package/dist/widgets/simple-markdown-tester-editor.d.ts +7 -0
  19. package/dist/widgets/table-editor.d.ts +2 -0
  20. package/package.json +13 -13
  21. package/src/components/__tests__/blur-input.test.tsx +67 -0
  22. package/src/components/blur-input.tsx +7 -0
  23. package/src/editor-page.tsx +11 -25
  24. package/src/editor.tsx +37 -65
  25. package/src/hint-editor.tsx +15 -27
  26. package/src/iframe-content-renderer.tsx +19 -20
  27. package/src/item-editor.tsx +11 -18
  28. package/src/util/register-all-widgets-and-editors-for-testing.test.ts +3 -1
  29. package/src/widgets/example-widget-editor.tsx +4 -5
  30. package/src/widgets/example-widget.tsx +8 -10
  31. package/src/widgets/graded-group-editor.tsx +9 -19
  32. package/src/widgets/group-editor.tsx +5 -8
  33. package/src/widgets/input-number-editor.tsx +4 -5
  34. package/src/widgets/simple-markdown-tester-editor.tsx +4 -5
  35. package/src/widgets/table-editor.tsx +4 -5
  36. package/tsconfig-build.tsbuildinfo +1 -1
@@ -1,4 +1,7 @@
1
1
  import * as React from "react";
2
+ import Editor from "./editor";
3
+ import IframeContentRenderer from "./iframe-content-renderer";
4
+ import ItemExtrasEditor from "./item-extras-editor";
2
5
  import type { APIOptions, ImageUploader, ChangeHandler } from "@khanacademy/perseus";
3
6
  type Props = {
4
7
  apiOptions?: APIOptions;
@@ -18,6 +21,9 @@ declare class ItemEditor extends React.Component<Props> {
18
21
  onChange: () => void;
19
22
  question: Record<any, any>;
20
23
  };
24
+ frame: React.RefObject<IframeContentRenderer>;
25
+ questionEditor: React.RefObject<Editor>;
26
+ itemExtrasEditor: React.RefObject<ItemExtrasEditor>;
21
27
  updateProps: ChangeHandler;
22
28
  triggerPreviewUpdate: (newData?: any) => void;
23
29
  handleEditorChange: ChangeHandler;
@@ -10,6 +10,7 @@ declare class ExampleWidgetEditor extends React.Component<Props> {
10
10
  };
11
11
  static widgetName: "example-widget";
12
12
  static defaultProps: Props;
13
+ input: React.RefObject<HTMLInputElement>;
13
14
  handleAnswerChange: (arg1: React.ChangeEvent<HTMLInputElement>) => void;
14
15
  render(): React.ReactNode;
15
16
  change: (arg1: any, arg2: any, arg3: any) => any;
@@ -1,5 +1,11 @@
1
1
  import PropTypes from "prop-types";
2
2
  import * as React from "react";
3
+ declare class TextInput extends React.Component<any> {
4
+ input: React.RefObject<HTMLInputElement>;
5
+ render(): React.ReactNode;
6
+ focus: () => boolean;
7
+ changeValue: (e: any) => void;
8
+ }
3
9
  /**
4
10
  * This is the widget's renderer. It shows up in the right column
5
11
  * in the demo, and is what is visible to users, and where
@@ -11,6 +17,7 @@ declare class ExampleWidget extends React.Component<any> {
11
17
  onChange: PropTypes.Validator<(...args: any[]) => any>;
12
18
  };
13
19
  static defaultProps: any;
20
+ input: React.RefObject<TextInput>;
14
21
  /**
15
22
  * This is the widget's grading function. simpleValidate generally
16
23
  * defers to this function.
@@ -1,5 +1,6 @@
1
1
  import PropTypes from "prop-types";
2
2
  import * as React from "react";
3
+ import Editor from "../editor";
3
4
  type Props = any;
4
5
  declare class GradedGroupEditor extends React.Component<Props> {
5
6
  static propTypes: {
@@ -40,6 +41,8 @@ declare class GradedGroupEditor extends React.Component<Props> {
40
41
  };
41
42
  static widgetName: "graded-group";
42
43
  static defaultProps: Props;
44
+ editor: React.RefObject<Editor>;
45
+ hintEditor: React.RefObject<Editor>;
43
46
  change: (arg1: any, arg2: any, arg3: any) => any;
44
47
  handleAddHint: () => void;
45
48
  handleRemoveHint: (arg1: React.MouseEvent) => void;
@@ -1,5 +1,6 @@
1
1
  import PropTypes from "prop-types";
2
2
  import * as React from "react";
3
+ import Editor from "../editor";
3
4
  type Props = any;
4
5
  declare class GroupEditor extends React.Component<Props> {
5
6
  static propTypes: {
@@ -40,6 +41,7 @@ declare class GroupEditor extends React.Component<Props> {
40
41
  };
41
42
  static widgetName: "group";
42
43
  static defaultProps: Props;
44
+ editor: React.RefObject<Editor>;
43
45
  render(): React.ReactNode;
44
46
  _renderMetadataEditor: () => React.ReactElement;
45
47
  change: (arg1: any, arg2: any, arg3: any) => any;
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import BlurInput from "../components/blur-input";
2
3
  import type { ParsedValue, InputNumber } from "@khanacademy/perseus";
3
4
  import type { PropsFor } from "@khanacademy/wonder-blocks-core";
4
5
  type Props = {
@@ -31,6 +32,7 @@ type DefaultProps = {
31
32
  declare class InputNumberEditor extends React.Component<Props> {
32
33
  static widgetName: "input-number";
33
34
  static defaultProps: DefaultProps;
35
+ input: React.RefObject<BlurInput>;
34
36
  handleAnswerChange: (arg1: string) => void;
35
37
  render(): React.ReactNode;
36
38
  focus: () => boolean;
@@ -1,4 +1,10 @@
1
1
  import * as React from "react";
2
+ type TextAreaProps = any;
3
+ declare class TextArea extends React.Component<TextAreaProps> {
4
+ render(): React.ReactNode;
5
+ focus: () => boolean;
6
+ changeValue: (e: any) => void;
7
+ }
2
8
  type SimpleMarkdownTesterEditorProps = any;
3
9
  declare class SimpleMarkdownTesterEditor extends React.Component<SimpleMarkdownTesterEditorProps> {
4
10
  static propTypes: {
@@ -6,6 +12,7 @@ declare class SimpleMarkdownTesterEditor extends React.Component<SimpleMarkdownT
6
12
  };
7
13
  static widgetName: "simple-markdown-tester";
8
14
  static defaultProps: SimpleMarkdownTesterEditorProps;
15
+ input: React.RefObject<TextArea>;
9
16
  render(): React.ReactNode;
10
17
  change: (arg1: any, arg2: any, arg3: any) => any;
11
18
  focus: () => boolean;
@@ -1,3 +1,4 @@
1
+ import { components } from "@khanacademy/perseus";
1
2
  import PropTypes from "prop-types";
2
3
  import * as React from "react";
3
4
  type Props = any;
@@ -10,6 +11,7 @@ declare class TableEditor extends React.Component<Props> {
10
11
  };
11
12
  static widgetName: "table";
12
13
  static defaultProps: Props;
14
+ numberOfColumns: React.RefObject<components.NumberInput>;
13
15
  focus: () => void;
14
16
  render(): React.ReactNode;
15
17
  onSizeInput: (arg1: number, arg2: number) => void;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Perseus editors",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "2.15.7",
6
+ "version": "2.16.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -24,19 +24,19 @@
24
24
  "dependencies": {
25
25
  "@khanacademy/kas": "^0.3.7",
26
26
  "@khanacademy/kmath": "^0.1.8",
27
- "@khanacademy/math-input": "^16.4.0",
28
- "@khanacademy/perseus": "^17.6.0",
27
+ "@khanacademy/math-input": "^16.4.1",
28
+ "@khanacademy/perseus": "^17.6.1",
29
29
  "@khanacademy/perseus-core": "1.4.1"
30
30
  },
31
31
  "devDependencies": {
32
- "@khanacademy/wonder-blocks-button": "^6.2.4",
33
- "@khanacademy/wonder-blocks-clickable": "^4.0.11",
32
+ "@khanacademy/wonder-blocks-button": "^6.2.5",
33
+ "@khanacademy/wonder-blocks-clickable": "^4.0.12",
34
34
  "@khanacademy/wonder-blocks-color": "^3.0.0",
35
- "@khanacademy/wonder-blocks-core": "^6.3.0",
35
+ "@khanacademy/wonder-blocks-core": "^6.3.1",
36
36
  "@khanacademy/wonder-blocks-i18n": "^2.0.2",
37
- "@khanacademy/wonder-blocks-icon": "4.0.0",
37
+ "@khanacademy/wonder-blocks-icon": "4.0.1",
38
38
  "@khanacademy/wonder-blocks-spacing": "^4.0.1",
39
- "@khanacademy/wonder-blocks-typography": "^2.1.9",
39
+ "@khanacademy/wonder-blocks-typography": "^2.1.10",
40
40
  "@khanacademy/wonder-stuff-core": "^1.5.1",
41
41
  "@phosphor-icons/core": "^2.0.2",
42
42
  "aphrodite": "^1.2.5",
@@ -51,14 +51,14 @@
51
51
  "underscore": "^1.4.4"
52
52
  },
53
53
  "peerDependencies": {
54
- "@khanacademy/wonder-blocks-button": "^6.2.4",
55
- "@khanacademy/wonder-blocks-clickable": "^4.0.11",
54
+ "@khanacademy/wonder-blocks-button": "^6.2.5",
55
+ "@khanacademy/wonder-blocks-clickable": "^4.0.12",
56
56
  "@khanacademy/wonder-blocks-color": "^3.0.0",
57
- "@khanacademy/wonder-blocks-core": "^6.3.0",
57
+ "@khanacademy/wonder-blocks-core": "^6.3.1",
58
58
  "@khanacademy/wonder-blocks-i18n": "^2.0.2",
59
- "@khanacademy/wonder-blocks-icon": "4.0.0",
59
+ "@khanacademy/wonder-blocks-icon": "4.0.1",
60
60
  "@khanacademy/wonder-blocks-spacing": "^4.0.1",
61
- "@khanacademy/wonder-blocks-typography": "^2.1.9",
61
+ "@khanacademy/wonder-blocks-typography": "^2.1.10",
62
62
  "@khanacademy/wonder-stuff-core": "^1.5.1",
63
63
  "@phosphor-icons/core": "^2.0.2",
64
64
  "aphrodite": "^1.2.5",
@@ -0,0 +1,67 @@
1
+ import {render, screen} from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import * as React from "react";
4
+
5
+ import BlurInput from "../blur-input";
6
+
7
+ import "@testing-library/jest-dom"; // Imports custom matchers
8
+
9
+ describe("BlurInput", () => {
10
+ it("should render", () => {
11
+ render(<BlurInput value="Hello world!" onChange={() => {}} />);
12
+ });
13
+
14
+ it("should call onChange prop on blur", () => {
15
+ // Arrange
16
+ const onChange = jest.fn();
17
+ render(<BlurInput value="Hello world!" onChange={onChange} />);
18
+
19
+ // Act
20
+ userEvent.type(screen.getByRole("textbox"), "Hello Khan Academy!");
21
+ userEvent.tab();
22
+
23
+ // Assert
24
+ expect(onChange).toHaveBeenCalledTimes(1);
25
+ });
26
+
27
+ it("should call onChange prop when typing into input", () => {
28
+ // Arrange
29
+ const onChange = jest.fn();
30
+ render(<BlurInput value="Hello world!" onChange={onChange} />);
31
+
32
+ // Act
33
+ userEvent.type(screen.getByRole("textbox"), "Hello Khan Academy!");
34
+ // NO TAB
35
+
36
+ // Assert
37
+ expect(onChange).not.toHaveBeenCalledTimes(1);
38
+ });
39
+
40
+ it("should call onChange prop when pasting into input", () => {
41
+ // Arrange
42
+ const onChange = jest.fn();
43
+ render(<BlurInput value="Hello world!" onChange={onChange} />);
44
+
45
+ // Act
46
+ userEvent.paste(screen.getByRole("textbox"), "Hello Khan Academy!");
47
+ // NO TAB
48
+
49
+ // Assert
50
+ expect(onChange).not.toHaveBeenCalledTimes(1);
51
+ });
52
+
53
+ it("should focus input through focus function", () => {
54
+ const ref = React.createRef<BlurInput>();
55
+
56
+ // Arrange
57
+ render(
58
+ <BlurInput ref={ref} value="Hello world!" onChange={() => {}} />,
59
+ );
60
+
61
+ // Act
62
+ ref.current?.focus();
63
+
64
+ // Assert
65
+ expect(screen.getByRole("textbox")).toHaveFocus();
66
+ });
67
+ });
@@ -27,6 +27,8 @@ type State = {
27
27
  */
28
28
  // eslint-disable-next-line react/no-unsafe
29
29
  class BlurInput extends React.Component<Props, State> {
30
+ input = React.createRef<HTMLInputElement>();
31
+
30
32
  constructor(props: Props) {
31
33
  super(props);
32
34
  this.state = {value: this.props.value};
@@ -45,9 +47,14 @@ class BlurInput extends React.Component<Props, State> {
45
47
  this.props.onChange(e.target.value);
46
48
  };
47
49
 
50
+ focus() {
51
+ this.input.current?.focus();
52
+ }
53
+
48
54
  render(): React.ReactNode {
49
55
  return (
50
56
  <input
57
+ ref={this.input}
51
58
  className={this.props.className}
52
59
  style={this.props.style}
53
60
  type="text"
@@ -63,10 +63,11 @@ type State = {
63
63
 
64
64
  class EditorPage extends React.Component<Props, State> {
65
65
  _isMounted: boolean;
66
- // @ts-expect-error - TS2564 - Property 'rendererMountNode' has no initializer and is not definitely assigned in the constructor.
67
- rendererMountNode: HTMLDivElement;
68
66
  renderer: any;
69
67
 
68
+ itemEditor = React.createRef<ItemEditor>();
69
+ hintsEditor = React.createRef<CombinedHintsEditor>();
70
+
70
71
  static defaultProps: {
71
72
  developerMode: boolean;
72
73
  jsonMode: boolean;
@@ -102,7 +103,6 @@ class EditorPage extends React.Component<Props, State> {
102
103
  // this.isMounted() but is still considered an anti-pattern.
103
104
  this._isMounted = true;
104
105
 
105
- this.rendererMountNode = document.createElement("div");
106
106
  this.updateRenderer();
107
107
  }
108
108
 
@@ -154,9 +154,7 @@ class EditorPage extends React.Component<Props, State> {
154
154
  isMobile: touch,
155
155
  };
156
156
 
157
- // eslint-disable-next-line react/no-string-refs
158
- // @ts-expect-error - TS2339 - Property 'triggerPreviewUpdate' does not exist on type 'ReactInstance'.
159
- this.refs.itemEditor.triggerPreviewUpdate({
157
+ this.itemEditor.current?.triggerPreviewUpdate({
160
158
  type: "question",
161
159
  data: _({
162
160
  item: this.serialize(),
@@ -170,9 +168,7 @@ class EditorPage extends React.Component<Props, State> {
170
168
  paths: this.props.contentPaths || [],
171
169
  },
172
170
  reviewMode: true,
173
- // eslint-disable-next-line react/no-string-refs
174
- // @ts-expect-error - TS2339 - Property 'getSaveWarnings' does not exist on type 'ReactInstance'.
175
- legacyPerseusLint: this.refs.itemEditor.getSaveWarnings(),
171
+ legacyPerseusLint: this.itemEditor.current?.getSaveWarnings(),
176
172
  }).extend(
177
173
  _(this.props).pick(
178
174
  "workAreaSelector",
@@ -192,12 +188,8 @@ class EditorPage extends React.Component<Props, State> {
192
188
  }
193
189
 
194
190
  getSaveWarnings(): any {
195
- // eslint-disable-next-line react/no-string-refs
196
- // @ts-expect-error - TS2339 - Property 'getSaveWarnings' does not exist on type 'ReactInstance'.
197
- const issues1 = this.refs.itemEditor.getSaveWarnings();
198
- // eslint-disable-next-line react/no-string-refs
199
- // @ts-expect-error - TS2339 - Property 'getSaveWarnings' does not exist on type 'ReactInstance'.
200
- const issues2 = this.refs.hintsEditor.getSaveWarnings();
191
+ const issues1 = this.itemEditor.current?.getSaveWarnings();
192
+ const issues2 = this.hintsEditor.current?.getSaveWarnings();
201
193
  return issues1.concat(issues2);
202
194
  }
203
195
 
@@ -205,12 +197,8 @@ class EditorPage extends React.Component<Props, State> {
205
197
  if (this.props.jsonMode) {
206
198
  return this.state.json;
207
199
  }
208
- // eslint-disable-next-line react/no-string-refs
209
- // @ts-expect-error - TS2339 - Property 'serialize' does not exist on type 'ReactInstance'.
210
- return _.extend(this.refs.itemEditor.serialize(options), {
211
- // eslint-disable-next-line react/no-string-refs
212
- // @ts-expect-error - TS2339 - Property 'serialize' does not exist on type 'ReactInstance'.
213
- hints: this.refs.hintsEditor.serialize(options),
200
+ return _.extend(this.itemEditor.current?.serialize(options), {
201
+ hints: this.hintsEditor.current?.serialize(options),
214
202
  });
215
203
  }
216
204
 
@@ -302,8 +290,7 @@ class EditorPage extends React.Component<Props, State> {
302
290
 
303
291
  {(!this.props.developerMode || !this.props.jsonMode) && (
304
292
  <ItemEditor
305
- // eslint-disable-next-line react/no-string-refs
306
- ref="itemEditor"
293
+ ref={this.itemEditor}
307
294
  itemId={this.props.itemId}
308
295
  question={this.props.question}
309
296
  answerArea={this.props.answerArea}
@@ -319,8 +306,7 @@ class EditorPage extends React.Component<Props, State> {
319
306
 
320
307
  {(!this.props.developerMode || !this.props.jsonMode) && (
321
308
  <CombinedHintsEditor
322
- // eslint-disable-next-line react/no-string-refs
323
- ref="hintsEditor"
309
+ ref={this.hintsEditor}
324
310
  itemId={this.props.itemId}
325
311
  hints={this.props.hints}
326
312
  imageUploader={this.props.imageUploader}
package/src/editor.tsx CHANGED
@@ -18,7 +18,6 @@ import katex from "katex";
18
18
  // eslint-disable-next-line import/no-unassigned-import
19
19
  import "./katex-mhchem";
20
20
  import * as React from "react";
21
- import * as ReactDOM from "react-dom";
22
21
  import _ from "underscore";
23
22
 
24
23
  import DragTarget from "./components/drag-target";
@@ -88,8 +87,7 @@ const IMAGE_REGEX = /!\[[^\]]*\]\(([^\s)]+)[^)]*\)/g;
88
87
  * ignores captures. If you don't need captures, use String::match
89
88
  */
90
89
  const allMatches = function (regex: RegExp, str: string) {
91
- // @ts-expect-error - TS2702 - 'RegExp' only refers to a type, but is being used as a namespace here.
92
- const result: Array<any | RegExp.matchResult> = [];
90
+ const result: Array<RegExpExecArray> = [];
93
91
  // eslint-disable-next-line no-constant-condition
94
92
  while (true) {
95
93
  const match = regex.exec(str);
@@ -106,7 +104,7 @@ const allMatches = function (regex: RegExp, str: string) {
106
104
  * markdown.
107
105
  */
108
106
  const imageUrlsFromContent = function (content: string) {
109
- return _.map(allMatches(IMAGE_REGEX, content), (capture) => capture[1]);
107
+ return allMatches(IMAGE_REGEX, content).map((capture) => capture[1]);
110
108
  };
111
109
 
112
110
  type Props = Readonly<{
@@ -157,6 +155,9 @@ class Editor extends React.Component<Props, State> {
157
155
  deferredChange: any | null | undefined;
158
156
  widgetIds: any | null | undefined;
159
157
 
158
+ underlay = React.createRef<HTMLDivElement>();
159
+ textarea = React.createRef<HTMLTextAreaElement>();
160
+
160
161
  static defaultProps: DefaultProps = {
161
162
  content: "",
162
163
  placeholder: "",
@@ -183,11 +184,14 @@ class Editor extends React.Component<Props, State> {
183
184
  // this.props.onChange during that, since it calls our parent's
184
185
  // setState
185
186
  this._sizeImages(this.props);
186
- // eslint-disable-next-line react/no-string-refs
187
- // @ts-expect-error - TS2769 - No overload matches this call.
188
- $(ReactDOM.findDOMNode(this.refs.textarea))
187
+ // NOTE(jeremy): We use the non-null assertion here (!) because refs
188
+ // are guaranteed to be up-to-date before componentDidMount or
189
+ // componentDidUpdate fires.
190
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
191
+ $(this.textarea.current!)
189
192
  // @ts-expect-error - TS2339 - Property 'on' does not exist on type 'JQueryStatic'.
190
193
  .on("copy cut", this._maybeCopyWidgets)
194
+ // @ts-expect-error - TS2769 - No overload matches this call.
191
195
  .on("paste", this._maybePasteWidgets);
192
196
  }
193
197
 
@@ -200,9 +204,7 @@ class Editor extends React.Component<Props, State> {
200
204
  }
201
205
 
202
206
  componentDidUpdate(prevProps: Props) {
203
- // TODO(alpert): Maybe fix React so this isn't necessary
204
- // eslint-disable-next-line react/no-string-refs
205
- const textarea = ReactDOM.findDOMNode(this.refs.textarea);
207
+ const textarea = this.textarea.current;
206
208
 
207
209
  // Slightly unorthodox method to ensure that programmatic text changes
208
210
  // are in the browser's undo stack.
@@ -211,20 +213,11 @@ class Editor extends React.Component<Props, State> {
211
213
  // will return false. However at least in Firefox setting `value` on a
212
214
  // textbox clears the undo stack so we don't get unexpected undo
213
215
  // behavior.
214
- if (this.lastUserValue !== null && textarea) {
215
- /**
216
- * TODO(somewhatabstract, JIRA-XXXX):
217
- * textarea should be refined with an instanceof check to
218
- * HTMLTextAreaElement so that these props are available.
219
- */
220
- // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'Element | Text'.
216
+ if (this.lastUserValue != null && textarea) {
221
217
  textarea.focus();
222
- // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'.
223
218
  textarea.value = this.lastUserValue;
224
- // @ts-expect-error - TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'.
225
219
  textarea.selectionStart = 0;
226
- // @ts-expect-error - TS2339 - Property 'select' does not exist on type 'Element | Text'.
227
- textarea.select(0, prevProps.content.length);
220
+ textarea.setSelectionRange(0, prevProps.content.length);
228
221
  if (
229
222
  document.execCommand(
230
223
  "insertText",
@@ -234,7 +227,6 @@ class Editor extends React.Component<Props, State> {
234
227
  ) {
235
228
  // This command is not implemented. Fall back to setting `value`
236
229
  // directly.
237
- // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'.
238
230
  textarea.value = this.props.content;
239
231
  }
240
232
  this.lastUserValue = null;
@@ -293,11 +285,9 @@ class Editor extends React.Component<Props, State> {
293
285
  };
294
286
 
295
287
  _handleWidgetEditorRemove: (id: string) => void = (id: string) => {
296
- // eslint-disable-next-line react/no-string-refs
297
- const textarea = this.refs.textarea;
288
+ const textarea = this.textarea.current;
298
289
  const re = new RegExp(widgetRegExp.replace("{id}", id), "gm");
299
- // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'ReactInstance'.
300
- this.props.onChange({content: textarea.value.replace(re, "")});
290
+ this.props.onChange({content: textarea?.value.replace(re, "")});
301
291
  };
302
292
 
303
293
  /**
@@ -450,11 +440,12 @@ class Editor extends React.Component<Props, State> {
450
440
  // Tab-completion of widgets. For example, to insert an image:
451
441
  // type `[[im`, then tab.
452
442
  if (e.key === "Tab") {
453
- // eslint-disable-next-line react/no-string-refs
454
- const textarea = ReactDOM.findDOMNode(this.refs.textarea);
443
+ // We're in an event handler attached to the textarea, so the ref
444
+ // can't be empty/undefined! (which is why its safe to use the
445
+ // non-null-assertion here. aka the `!` suffix)
446
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
447
+ const textarea = this.textarea.current!;
455
448
 
456
- // findDOMNode can also return Text, but we know it's an element.
457
- // @ts-expect-error - TS2345 - Argument of type 'Element | Text | null' is not assignable to parameter of type 'HTMLTextAreaElement'.
458
449
  const word = Util.textarea.getWordBeforeCursor(textarea);
459
450
  const matches = word.string.toLowerCase().match(shortcutRegexp);
460
451
 
@@ -645,9 +636,6 @@ class Editor extends React.Component<Props, State> {
645
636
  cursorRange: ReadonlyArray<number>,
646
637
  widgetType: string,
647
638
  ) => {
648
- // eslint-disable-next-line react/no-string-refs
649
- const textarea = ReactDOM.findDOMNode(this.refs.textarea);
650
-
651
639
  // Note: we have to use _.map here instead of Array::map
652
640
  // because the results of a .match might be null if no
653
641
  // widgets were found.
@@ -710,11 +698,13 @@ class Editor extends React.Component<Props, State> {
710
698
  content: newContent,
711
699
  widgets: newWidgets,
712
700
  },
713
- function () {
701
+ () => {
702
+ if (!this.textarea.current) {
703
+ return;
704
+ }
705
+
714
706
  Util.textarea.moveCursor(
715
- // findDOMNode can return Text but we know this is Element
716
- // @ts-expect-error - TS2345 - Argument of type 'Element | Text | null' is not assignable to parameter of type 'HTMLTextAreaElement'.
717
- textarea,
707
+ this.textarea.current,
718
708
  // We want to put the cursor after the widget
719
709
  // and after any added newlines
720
710
  newContent.length - postlude.length,
@@ -724,21 +714,21 @@ class Editor extends React.Component<Props, State> {
724
714
  };
725
715
 
726
716
  _addWidget: (widgetType: string) => void = (widgetType: string) => {
727
- // eslint-disable-next-line react/no-string-refs
728
- const textarea = this.refs.textarea;
717
+ const textarea = this.textarea.current;
718
+ if (!textarea) {
719
+ return;
720
+ }
729
721
 
730
722
  this._addWidgetToContent(
731
723
  this.props.content,
732
- // @ts-expect-error - TS2339 - Property 'selectionStart' does not exist on type 'ReactInstance'. | TS2339 - Property 'selectionEnd' does not exist on type 'ReactInstance'.
733
724
  [textarea.selectionStart, textarea.selectionEnd],
734
725
  widgetType,
735
726
  );
736
- // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
737
727
  textarea.focus();
738
728
  };
739
729
 
740
- addTemplate: (e: React.SyntheticEvent<HTMLTextAreaElement>) => void = (
741
- e: React.SyntheticEvent<HTMLTextAreaElement>,
730
+ addTemplate: (e: React.SyntheticEvent<HTMLSelectElement>) => void = (
731
+ e: React.SyntheticEvent<HTMLSelectElement>,
742
732
  ) => {
743
733
  const templateType = e.currentTarget.value;
744
734
  if (templateType === "") {
@@ -820,32 +810,17 @@ class Editor extends React.Component<Props, State> {
820
810
  };
821
811
 
822
812
  focus: () => void = () => {
823
- // eslint-disable-next-line react/no-string-refs
824
- const textarea = ReactDOM.findDOMNode(this.refs.textarea);
813
+ const textarea = this.textarea.current;
825
814
  if (textarea) {
826
- /**
827
- * TODO(somewhatabstract, JIRA-XXXX):
828
- * textarea should be refined with an instanceof check to
829
- * HTMLTextAreaElement so that these props are available.
830
- */
831
- // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'Element | Text'.
832
815
  textarea.focus();
833
816
  }
834
817
  };
835
818
 
836
819
  focusAndMoveToEnd: () => void = () => {
837
820
  this.focus();
838
- // eslint-disable-next-line react/no-string-refs
839
- const textarea = ReactDOM.findDOMNode(this.refs.textarea);
821
+ const textarea = this.textarea.current;
840
822
  if (textarea) {
841
- /**
842
- * TODO(somewhatabstract, JIRA-XXXX):
843
- * textarea should be refined with an instanceof check to
844
- * HTMLTextAreaElement so that these props are available.
845
- */
846
- // @ts-expect-error - TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'. | TS2339 - Property 'value' does not exist on type 'Element | Text'.
847
823
  textarea.selectionStart = textarea.value.length;
848
- // @ts-expect-error - TS2339 - Property 'selectionEnd' does not exist on type 'Element | Text'. | TS2339 - Property 'value' does not exist on type 'Element | Text'.
849
824
  textarea.selectionEnd = textarea.value.length;
850
825
  }
851
826
  };
@@ -1003,7 +978,6 @@ class Editor extends React.Component<Props, State> {
1003
978
 
1004
979
  const insertTemplateString = "Insert template\u2026";
1005
980
  templatesDropDown = (
1006
- // @ts-expect-error - TS2322 - Type '(e: SyntheticEvent<HTMLTextAreaElement, Event>) => void' is not assignable to type 'ChangeEventHandler<HTMLSelectElement>'.
1007
981
  <select onChange={this.addTemplate}>
1008
982
  <option value="">{insertTemplateString}</option>
1009
983
  <option disabled>--</option>
@@ -1044,15 +1018,13 @@ class Editor extends React.Component<Props, State> {
1044
1018
  const completeTextarea = [
1045
1019
  <div
1046
1020
  className="perseus-textarea-underlay"
1047
- // eslint-disable-next-line react/no-string-refs
1048
- ref="underlay"
1021
+ ref={this.underlay}
1049
1022
  key="underlay"
1050
1023
  >
1051
1024
  {underlayPieces}
1052
1025
  </div>,
1053
1026
  <textarea
1054
- // eslint-disable-next-line react/no-string-refs
1055
- ref="textarea"
1027
+ ref={this.textarea}
1056
1028
  key="textarea"
1057
1029
  onChange={this.handleChange}
1058
1030
  onKeyDown={this._handleKeyDown}