@khanacademy/perseus 17.3.2 → 17.4.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.
@@ -64,7 +64,7 @@ export declare const ApiOptions: {
64
64
  readonly defaults: Readonly<Readonly<{
65
65
  isArticle?: boolean | undefined;
66
66
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
67
- onFocusChange?: ((newFocusPath: import("./types").FocusPath, oldFocusPath: import("./types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
67
+ onFocusChange?: ((newFocusPath: import("./types").FocusPath, oldFocusPath: import("./types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
68
68
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
69
69
  showAlignmentOptions?: boolean | undefined;
70
70
  readOnly?: boolean | undefined;
@@ -105,7 +105,7 @@ export declare const ApiOptions: {
105
105
  inModal: NonNullable<boolean | undefined>;
106
106
  isArticle: NonNullable<boolean | undefined>;
107
107
  isMobile: NonNullable<boolean | undefined>;
108
- onFocusChange: (newFocusPath: import("./types").FocusPath, oldFocusPath: import("./types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown;
108
+ onFocusChange: (newFocusPath: import("./types").FocusPath, oldFocusPath: import("./types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown;
109
109
  onInputError: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
110
110
  readOnly: NonNullable<boolean | undefined>;
111
111
  setDrawingAreaAvailable: (arg1: boolean) => unknown;
@@ -1,11 +1,11 @@
1
1
  import * as PerseusLinter from "@khanacademy/perseus-linter";
2
2
  /**
3
- * A copy of the ItemRenderer which renders its question renderer and hints
4
- * renderer normally instead of ReactDOM.render()ing them into elements in the
5
- * DOM.
3
+ * The main item (aka Exercise) renderer in Perseus. This component renders its
4
+ * question renderer and hints renderer using standard React practices (an
5
+ * earlier ItemRenderer component used to use jQuery and ReactDOM.render()ing
6
+ * to render into elements it didn't own.
6
7
  *
7
- * This allows this component to be used in server-rendering of a perseus
8
- * exercise.
8
+ * This component is compatible with server-rendering of a perseus exercise.
9
9
  */
10
10
  import * as React from "react";
11
11
  import Renderer from "./renderer";
@@ -109,7 +109,7 @@ declare const ref: React.ForwardRefExoticComponent<Omit<Pick<OwnProps & HOCProps
109
109
  apiOptions?: Readonly<{
110
110
  isArticle?: boolean | undefined;
111
111
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
112
- onFocusChange?: ((newFocusPath: FocusPath, oldFocusPath: FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
112
+ onFocusChange?: ((newFocusPath: FocusPath, oldFocusPath: FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
113
113
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
114
114
  showAlignmentOptions?: boolean | undefined;
115
115
  readOnly?: boolean | undefined;
package/dist/types.d.ts CHANGED
@@ -1,14 +1,13 @@
1
1
  import type { SerializedHighlightSet } from "./components/highlighting/types";
2
2
  import type { ILogger } from "./logging/log";
3
3
  import type { Item } from "./multi-items/item-types";
4
- import type { PerseusWidget } from "./perseus-types";
4
+ import type { PerseusAnswerArea, PerseusRenderer, PerseusWidget } from "./perseus-types";
5
5
  import type { SizeClass } from "./util/sizing-utils";
6
6
  import type { KeypadAPI } from "@khanacademy/math-input";
7
7
  import type { AnalyticsEventHandlerFn } from "@khanacademy/perseus-core";
8
8
  import type { LinterContextProps } from "@khanacademy/perseus-linter";
9
9
  import type { Result } from "@khanacademy/wonder-blocks-data";
10
10
  import type * as React from "react";
11
- import type ReactDOM from "react-dom";
12
11
  export type FocusPath = ReadonlyArray<string> | null | undefined;
13
12
  type Empty = Record<never, never>;
14
13
  export type Dimensions = {
@@ -33,10 +32,7 @@ export type PerseusScore = {
33
32
  total: number;
34
33
  message?: string | null | undefined;
35
34
  };
36
- export type Hint = {
37
- widgets: WidgetDict;
38
- content: string;
39
- images: ImageDict;
35
+ export type Hint = PerseusRenderer & {
40
36
  replace?: boolean;
41
37
  };
42
38
  export type Version = {
@@ -60,7 +56,7 @@ export type ChangeHandler = (arg1: {
60
56
  widgets?: WidgetDict;
61
57
  images?: ImageDict;
62
58
  question?: any;
63
- answerArea?: any;
59
+ answerArea?: PerseusAnswerArea | null;
64
60
  itemDataVersion?: Version;
65
61
  item?: Item;
66
62
  editorMode?: EditorMode;
@@ -86,7 +82,7 @@ type TrackInteractionArgs = {
86
82
  export type APIOptions = Readonly<{
87
83
  isArticle?: boolean;
88
84
  onInputError?: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
89
- onFocusChange?: (newFocusPath: FocusPath, oldFocusPath: FocusPath, keypadElement?: ReturnType<typeof ReactDOM.findDOMNode>, focusedElement?: HTMLElement) => unknown;
85
+ onFocusChange?: (newFocusPath: FocusPath, oldFocusPath: FocusPath, keypadHeight?: number, focusedElement?: HTMLElement) => unknown;
90
86
  GroupMetadataEditor?: React.ComponentType<StubTagEditorType>;
91
87
  showAlignmentOptions?: boolean;
92
88
  readOnly?: boolean;
@@ -92,7 +92,7 @@ declare const _default: Readonly<{
92
92
  apiOptions: Readonly<Readonly<{
93
93
  isArticle?: boolean | undefined;
94
94
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
95
- onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
95
+ onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
96
96
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
97
97
  showAlignmentOptions?: boolean | undefined;
98
98
  readOnly?: boolean | undefined;
@@ -133,7 +133,7 @@ declare const _default: Readonly<{
133
133
  inModal: NonNullable<boolean | undefined>;
134
134
  isArticle: NonNullable<boolean | undefined>;
135
135
  isMobile: NonNullable<boolean | undefined>;
136
- onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown;
136
+ onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown;
137
137
  onInputError: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
138
138
  readOnly: NonNullable<boolean | undefined>;
139
139
  setDrawingAreaAvailable: (arg1: boolean) => unknown;
@@ -155,7 +155,7 @@ declare const _default: Readonly<{
155
155
  apiOptions: Readonly<Readonly<{
156
156
  isArticle?: boolean | undefined;
157
157
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
158
- onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
158
+ onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
159
159
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
160
160
  showAlignmentOptions?: boolean | undefined;
161
161
  readOnly?: boolean | undefined;
@@ -196,7 +196,7 @@ declare const _default: Readonly<{
196
196
  inModal: NonNullable<boolean | undefined>;
197
197
  isArticle: NonNullable<boolean | undefined>;
198
198
  isMobile: NonNullable<boolean | undefined>;
199
- onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown;
199
+ onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown;
200
200
  onInputError: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
201
201
  readOnly: NonNullable<boolean | undefined>;
202
202
  setDrawingAreaAvailable: (arg1: boolean) => unknown;
@@ -218,7 +218,7 @@ declare const _default: Readonly<{
218
218
  apiOptions?: Readonly<Readonly<{
219
219
  isArticle?: boolean | undefined;
220
220
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
221
- onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
221
+ onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
222
222
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
223
223
  showAlignmentOptions?: boolean | undefined;
224
224
  readOnly?: boolean | undefined;
@@ -259,7 +259,7 @@ declare const _default: Readonly<{
259
259
  inModal: NonNullable<boolean | undefined>;
260
260
  isArticle: NonNullable<boolean | undefined>;
261
261
  isMobile: NonNullable<boolean | undefined>;
262
- onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown;
262
+ onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown;
263
263
  onInputError: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
264
264
  readOnly: NonNullable<boolean | undefined>;
265
265
  setDrawingAreaAvailable: (arg1: boolean) => unknown;
@@ -292,7 +292,7 @@ declare const _default: Readonly<{
292
292
  apiOptions: Readonly<Readonly<{
293
293
  isArticle?: boolean | undefined;
294
294
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
295
- onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
295
+ onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
296
296
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
297
297
  showAlignmentOptions?: boolean | undefined;
298
298
  readOnly?: boolean | undefined;
@@ -333,7 +333,7 @@ declare const _default: Readonly<{
333
333
  inModal: NonNullable<boolean | undefined>;
334
334
  isArticle: NonNullable<boolean | undefined>;
335
335
  isMobile: NonNullable<boolean | undefined>;
336
- onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown;
336
+ onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown;
337
337
  onInputError: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
338
338
  readOnly: NonNullable<boolean | undefined>;
339
339
  setDrawingAreaAvailable: (arg1: boolean) => unknown;
@@ -355,7 +355,7 @@ declare const _default: Readonly<{
355
355
  apiOptions: Readonly<Readonly<{
356
356
  isArticle?: boolean | undefined;
357
357
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
358
- onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
358
+ onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
359
359
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
360
360
  showAlignmentOptions?: boolean | undefined;
361
361
  readOnly?: boolean | undefined;
@@ -396,7 +396,7 @@ declare const _default: Readonly<{
396
396
  inModal: NonNullable<boolean | undefined>;
397
397
  isArticle: NonNullable<boolean | undefined>;
398
398
  isMobile: NonNullable<boolean | undefined>;
399
- onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown;
399
+ onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown;
400
400
  onInputError: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
401
401
  readOnly: NonNullable<boolean | undefined>;
402
402
  setDrawingAreaAvailable: (arg1: boolean) => unknown;
@@ -418,7 +418,7 @@ declare const _default: Readonly<{
418
418
  apiOptions?: Readonly<Readonly<{
419
419
  isArticle?: boolean | undefined;
420
420
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
421
- onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
421
+ onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
422
422
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
423
423
  showAlignmentOptions?: boolean | undefined;
424
424
  readOnly?: boolean | undefined;
@@ -459,7 +459,7 @@ declare const _default: Readonly<{
459
459
  inModal: NonNullable<boolean | undefined>;
460
460
  isArticle: NonNullable<boolean | undefined>;
461
461
  isMobile: NonNullable<boolean | undefined>;
462
- onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown;
462
+ onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown;
463
463
  onInputError: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
464
464
  readOnly: NonNullable<boolean | undefined>;
465
465
  setDrawingAreaAvailable: (arg1: boolean) => unknown;
@@ -98,7 +98,7 @@ declare const _default: Readonly<{
98
98
  apiOptions: Readonly<{
99
99
  isArticle?: boolean | undefined;
100
100
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
101
- onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
101
+ onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
102
102
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
103
103
  showAlignmentOptions?: boolean | undefined;
104
104
  readOnly?: boolean | undefined;
@@ -157,7 +157,7 @@ declare const _default: Readonly<{
157
157
  apiOptions: Readonly<{
158
158
  isArticle?: boolean | undefined;
159
159
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
160
- onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
160
+ onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadHeight?: number | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
161
161
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
162
162
  showAlignmentOptions?: boolean | undefined;
163
163
  readOnly?: boolean | undefined;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Core Perseus API (includes renderers and widgets)",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "17.3.2",
6
+ "version": "17.4.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -335,7 +335,7 @@ describe("server item renderer", () => {
335
335
  expect(onFocusChange).toHaveBeenCalledWith(
336
336
  ["input-number 1"],
337
337
  null,
338
- undefined,
338
+ 0,
339
339
  expect.any(Object),
340
340
  );
341
341
  });
@@ -343,7 +343,15 @@ describe("server item renderer", () => {
343
343
  it("activates the keypadElement when focusing the renderer on mobile", () => {
344
344
  // Arranged
345
345
  const onFocusChange = jest.fn();
346
- const keypadElementDOMNode = <div />;
346
+ const keypadElementDOMNode = document.createElement("div");
347
+
348
+ // We need to mock the getBoundingClientRect() method for our
349
+ // onFocusChange() callback to work properly.
350
+ keypadElementDOMNode.getBoundingClientRect = () =>
351
+ ({
352
+ height: 250,
353
+ } as DOMRect);
354
+
347
355
  const keypadElement: KeypadAPI = {
348
356
  getDOMNode: jest
349
357
  .fn()
@@ -372,7 +380,7 @@ describe("server item renderer", () => {
372
380
  expect(onFocusChange).toHaveBeenCalledWith(
373
381
  ["input-number 1"],
374
382
  null,
375
- keypadElementDOMNode,
383
+ 250,
376
384
  expect.any(Object),
377
385
  );
378
386
  });
@@ -399,7 +407,7 @@ describe("server item renderer", () => {
399
407
  expect(onFocusChange).toHaveBeenLastCalledWith(
400
408
  null,
401
409
  ["input-number 1"],
402
- null,
410
+ 0,
403
411
  null,
404
412
  );
405
413
  });
@@ -407,7 +415,15 @@ describe("server item renderer", () => {
407
415
  it("dismisses the keypadElement when blurring the renderer on mobile", () => {
408
416
  // Arranged
409
417
  const onFocusChange = jest.fn();
410
- const keypadElementDOMNode = <div />;
418
+ const keypadElementDOMNode = document.createElement("div");
419
+
420
+ // We need to mock the getBoundingClientRect() method for our
421
+ // onFocusChange() callback to work properly.
422
+ keypadElementDOMNode.getBoundingClientRect = () =>
423
+ ({
424
+ height: 250,
425
+ } as DOMRect);
426
+
411
427
  const keypadElement: KeypadAPI = {
412
428
  getDOMNode: jest
413
429
  .fn()
@@ -440,7 +456,7 @@ describe("server item renderer", () => {
440
456
  expect(onFocusChange).toHaveBeenLastCalledWith(
441
457
  null,
442
458
  ["input-number 1"],
443
- null,
459
+ 0,
444
460
  null,
445
461
  );
446
462
  });
@@ -459,7 +475,7 @@ describe("server item renderer", () => {
459
475
  expect(onFocusChange).toHaveBeenCalledWith(
460
476
  ["input-number 1"],
461
477
  null,
462
- undefined,
478
+ 0,
463
479
  expect.any(Object),
464
480
  );
465
481
  });
@@ -104,15 +104,6 @@ class ArticleRenderer
104
104
  );
105
105
  }
106
106
 
107
- if (this.props.apiOptions.onFocusChange != null) {
108
- this.props.apiOptions.onFocusChange(
109
- this._currentFocus,
110
- prevFocusPath,
111
- didFocusInput ? keypadElement?.getDOMNode() : null,
112
- didFocusInput ? focusedInput : null,
113
- );
114
- }
115
-
116
107
  if (keypadElement && isMobile) {
117
108
  if (didFocusInput) {
118
109
  keypadElement.activate();
@@ -120,6 +111,21 @@ class ArticleRenderer
120
111
  keypadElement.dismiss();
121
112
  }
122
113
  }
114
+
115
+ if (this.props.apiOptions.onFocusChange != null) {
116
+ const keypadDomNode = keypadElement?.getDOMNode() as HTMLElement;
117
+ const keypadHeight =
118
+ keypadDomNode && didFocusInput
119
+ ? keypadDomNode.getBoundingClientRect().height
120
+ : 0;
121
+
122
+ this.props.apiOptions.onFocusChange(
123
+ this._currentFocus,
124
+ prevFocusPath,
125
+ keypadHeight,
126
+ didFocusInput ? focusedInput : null,
127
+ );
128
+ }
123
129
  };
124
130
 
125
131
  _onRendererBlur: (arg1: any) => void = (blurPath) => {
package/src/index.ts CHANGED
@@ -120,15 +120,16 @@ export type {
120
120
  } from "./types";
121
121
  export type {ParsedValue} from "./util";
122
122
  export type {
123
- MathFormat,
124
123
  InputNumberWidget,
124
+ MathFormat,
125
+ PerseusAnswerArea,
126
+ PerseusExpressionWidgetOptions,
125
127
  PerseusImageBackground,
126
128
  PerseusInteractiveGraphWidgetOptions,
127
- PerseusRadioWidgetOptions,
128
- PerseusExpressionWidgetOptions,
129
+ PerseusItem,
129
130
  PerseusPlotterWidgetOptions,
131
+ PerseusRadioWidgetOptions,
130
132
  PerseusRenderer,
131
- PerseusAnswerArea,
132
133
  } from "./perseus-types";
133
134
  export type {Coord} from "./interactive2/types";
134
135
  export type {MarkerType} from "./widgets/label-image/types";
@@ -2,12 +2,12 @@
2
2
  import * as PerseusLinter from "@khanacademy/perseus-linter";
3
3
  import {StyleSheet, css} from "aphrodite";
4
4
  /**
5
- * A copy of the ItemRenderer which renders its question renderer and hints
6
- * renderer normally instead of ReactDOM.render()ing them into elements in the
7
- * DOM.
5
+ * The main item (aka Exercise) renderer in Perseus. This component renders its
6
+ * question renderer and hints renderer using standard React practices (an
7
+ * earlier ItemRenderer component used to use jQuery and ReactDOM.render()ing
8
+ * to render into elements it didn't own.
8
9
  *
9
- * This allows this component to be used in server-rendering of a perseus
10
- * exercise.
10
+ * This component is compatible with server-rendering of a perseus exercise.
11
11
  */
12
12
  import * as React from "react";
13
13
  import _ from "underscore";
@@ -180,24 +180,33 @@ export class ServerItemRenderer
180
180
  return Util.inputPathsEqual(inputPath, this._currentFocus);
181
181
  });
182
182
 
183
+ if (keypadElement && isMobile) {
184
+ if (didFocusInput) {
185
+ keypadElement.activate();
186
+ } else {
187
+ keypadElement.dismiss();
188
+ }
189
+ }
190
+
183
191
  if (onFocusChange != null) {
192
+ // First, calculate the current keypad height
193
+ const keypadDomNode: HTMLElement =
194
+ keypadElement?.getDOMNode() as HTMLElement;
195
+ const keypadHeight =
196
+ keypadDomNode && didFocusInput
197
+ ? keypadDomNode.getBoundingClientRect().height
198
+ : 0;
199
+
200
+ // Then call the callback
184
201
  onFocusChange(
185
202
  this._currentFocus,
186
203
  prevFocus,
187
- didFocusInput ? keypadElement?.getDOMNode() : null,
204
+ keypadHeight,
188
205
  // @ts-expect-error [FEI-5003] - TS2345 - Argument of type 'false | Element | Text | null | undefined' is not assignable to parameter of type 'HTMLElement | undefined'.
189
206
  didFocusInput &&
190
207
  this.questionRenderer.getDOMNodeForPath(newFocus),
191
208
  );
192
209
  }
193
-
194
- if (keypadElement && isMobile) {
195
- if (didFocusInput) {
196
- keypadElement.activate();
197
- } else {
198
- keypadElement.dismiss();
199
- }
200
- }
201
210
  }
202
211
 
203
212
  _onRendererBlur(blurPath: FocusPath) {
@@ -2,7 +2,24 @@
2
2
  @import "./variables.less";
3
3
  @import "./util.less";
4
4
 
5
- // Article Renderer
5
+ // Common styles for _all_ of Perseus
6
+ .framework-perseus {
7
+ code {
8
+ font-family: Courier, monospace;
9
+ }
10
+
11
+ pre {
12
+ background-color: @gray95;
13
+ border-radius: 4px;
14
+ color: @gray17;
15
+ font-size: 18px;
16
+ padding: 16px;
17
+ white-space: pre;
18
+ overflow: auto;
19
+ }
20
+ }
21
+
22
+ // Desktop Articles
6
23
  .framework-perseus.perseus-article:not(.perseus-mobile) {
7
24
  @textColor: #21242c;
8
25
  .body-text() {
@@ -190,25 +207,12 @@
190
207
  }
191
208
  }
192
209
 
193
- code {
194
- font-family: Courier, monospace;
195
- }
196
-
197
210
  pre {
198
- background-color: @gray95;
199
- border-radius: 4px;
200
- color: @gray17;
201
- font-size: 18px;
202
- padding: 16px;
203
- white-space: pre;
204
- overflow: auto;
205
211
  margin: 0 -16px 32px -16px;
206
212
  }
207
213
  }
208
214
 
209
- // HACK(emily): This is a copy of a bunch of the mixins from the block of
210
- // styles down below. However, those styles are limited to mobile devices
211
- // only, so this block is for non-mobile devices.
215
+ // Desktop Exercises
212
216
  .framework-perseus:not(.perseus-article):not(.perseus-mobile) {
213
217
  // Mixins
214
218
  .text(@fontSize; @lineHeight; @color) {
@@ -251,6 +255,7 @@
251
255
  .choice-text(14px, 1.25);
252
256
  }
253
257
 
258
+ // Mobile Articles and Exercises
254
259
  .framework-perseus.perseus-mobile {
255
260
  .perseus-article {
256
261
  .perseus-widget-container {
package/src/types.ts CHANGED
@@ -1,14 +1,17 @@
1
1
  import type {SerializedHighlightSet} from "./components/highlighting/types";
2
2
  import type {ILogger} from "./logging/log";
3
3
  import type {Item} from "./multi-items/item-types";
4
- import type {PerseusWidget} from "./perseus-types";
4
+ import type {
5
+ PerseusAnswerArea,
6
+ PerseusRenderer,
7
+ PerseusWidget,
8
+ } from "./perseus-types";
5
9
  import type {SizeClass} from "./util/sizing-utils";
6
10
  import type {KeypadAPI} from "@khanacademy/math-input";
7
11
  import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core";
8
12
  import type {LinterContextProps} from "@khanacademy/perseus-linter";
9
13
  import type {Result} from "@khanacademy/wonder-blocks-data";
10
14
  import type * as React from "react";
11
- import type ReactDOM from "react-dom";
12
15
 
13
16
  export type FocusPath = ReadonlyArray<string> | null | undefined;
14
17
 
@@ -45,10 +48,7 @@ export type PerseusScore =
45
48
  message?: string | null | undefined;
46
49
  };
47
50
 
48
- export type Hint = {
49
- widgets: WidgetDict;
50
- content: string; // JSON string,
51
- images: ImageDict;
51
+ export type Hint = PerseusRenderer & {
52
52
  replace?: boolean;
53
53
  };
54
54
 
@@ -78,7 +78,7 @@ export type ChangeHandler = (
78
78
  images?: ImageDict;
79
79
  // used only in EditorPage
80
80
  question?: any;
81
- answerArea?: any;
81
+ answerArea?: PerseusAnswerArea | null;
82
82
  itemDataVersion?: Version;
83
83
  // used in MutirenderEditor
84
84
  item?: Item;
@@ -144,7 +144,7 @@ export type APIOptions = Readonly<{
144
144
  onFocusChange?: (
145
145
  newFocusPath: FocusPath,
146
146
  oldFocusPath: FocusPath,
147
- keypadElement?: ReturnType<typeof ReactDOM.findDOMNode>,
147
+ keypadHeight?: number,
148
148
  focusedElement?: HTMLElement,
149
149
  ) => unknown;
150
150
  GroupMetadataEditor?: React.ComponentType<StubTagEditorType>;
@@ -1,4 +1,5 @@
1
1
  import {KeypadContext, KeypadType} from "@khanacademy/math-input";
2
+ import {action} from "@storybook/addon-actions";
2
3
  import * as React from "react";
3
4
 
4
5
  import {ServerItemRendererWithDebugUI} from "../../../../../testing/server-item-renderer-with-debug-ui";
@@ -46,6 +47,7 @@ const WrappedKeypadContext = ({
46
47
  apiOptions={{
47
48
  isMobile: isMobile,
48
49
  customKeypad: customKeypad,
50
+ onFocusChange: action("onFocusChange"),
49
51
  }}
50
52
  />
51
53
  );