@khanacademy/perseus 10.0.1 → 11.0.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 (34) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/article-renderer.d.ts +5 -7
  3. package/dist/components/zoomable.d.ts +2 -0
  4. package/dist/es/index.css +26 -0
  5. package/dist/es/index.js +184 -203
  6. package/dist/es/index.js.map +1 -1
  7. package/dist/index.css +26 -0
  8. package/dist/index.js +184 -203
  9. package/dist/index.js.map +1 -1
  10. package/dist/perseus-api.d.ts +2 -2
  11. package/dist/server-item-renderer.d.ts +5 -4
  12. package/dist/types.d.ts +2 -1
  13. package/dist/widgets/expression.d.ts +12 -12
  14. package/package.json +5 -4
  15. package/src/__stories__/article-renderer.stories.tsx +22 -0
  16. package/src/__testdata__/article-renderer.testdata.ts +28 -0
  17. package/src/__tests__/__snapshots__/renderer.test.tsx.snap +1 -1
  18. package/src/__tests__/server-item-renderer.test.tsx +12 -4
  19. package/src/article-renderer.tsx +14 -27
  20. package/src/components/__stories__/zoomable.stories.tsx +9 -1
  21. package/src/components/zoomable-tex.tsx +2 -0
  22. package/src/components/zoomable.tsx +10 -3
  23. package/src/server-item-renderer.tsx +10 -7
  24. package/src/styles/widgets/image.less +32 -0
  25. package/src/types.ts +2 -4
  26. package/src/widgets/__stories__/expression.stories.tsx +3 -3
  27. package/src/widgets/__stories__/image.stories.tsx +37 -0
  28. package/src/widgets/__tests__/__snapshots__/categorizer.test.ts.snap +1 -1
  29. package/src/widgets/__tests__/__snapshots__/group.test.tsx.snap +2 -2
  30. package/src/widgets/__tests__/__snapshots__/image.test.ts.snap +9 -11
  31. package/src/widgets/__tests__/__snapshots__/matrix.test.ts.snap +1 -1
  32. package/src/widgets/__tests__/__snapshots__/number-line.test.ts.snap +1 -1
  33. package/src/widgets/image.tsx +8 -31
  34. package/tsconfig-build.tsbuildinfo +1 -1
@@ -65,7 +65,7 @@ export declare const ApiOptions: {
65
65
  readonly defaults: Readonly<Readonly<{
66
66
  isArticle?: boolean | undefined;
67
67
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
68
- onFocusChange?: ((newFocusPath: import("./types").FocusPath, oldFocusPath: import("./types").FocusPath, keypadElement?: HTMLElement | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
68
+ onFocusChange?: ((newFocusPath: import("./types").FocusPath, oldFocusPath: import("./types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
69
69
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
70
70
  showAlignmentOptions?: boolean | undefined;
71
71
  readOnly?: boolean | undefined;
@@ -108,7 +108,7 @@ export declare const ApiOptions: {
108
108
  isArticle: NonNullable<boolean | undefined>;
109
109
  isMobile: NonNullable<boolean | undefined>;
110
110
  useV2Keypad: NonNullable<boolean | undefined>;
111
- onFocusChange: (newFocusPath: import("./types").FocusPath, oldFocusPath: import("./types").FocusPath, keypadElement?: HTMLElement | undefined, focusedElement?: HTMLElement | undefined) => unknown;
111
+ onFocusChange: (newFocusPath: import("./types").FocusPath, oldFocusPath: import("./types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown;
112
112
  onInputError: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
113
113
  readOnly: NonNullable<boolean | undefined>;
114
114
  setDrawingAreaAvailable: (arg1: boolean) => unknown;
@@ -10,7 +10,8 @@ import * as React from "react";
10
10
  import Renderer from "./renderer";
11
11
  import type { KeypadProps } from "./mixins/provide-keypad";
12
12
  import type { APIOptions, FocusPath, PerseusDependenciesV2 } from "./types";
13
- import type { RendererInterface, KEScore } from "@khanacademy/perseus-core";
13
+ import type { KeypadAPI } from "@khanacademy/math-input";
14
+ import type { KeypadContextRendererInterface, RendererInterface, KEScore } from "@khanacademy/perseus-core";
14
15
  type OwnProps = // These props are used by the ProvideKeypad mixin.
15
16
  KeypadProps & {
16
17
  apiOptions: APIOptions;
@@ -21,7 +22,7 @@ KeypadProps & {
21
22
  };
22
23
  problemNum?: number;
23
24
  reviewMode?: boolean;
24
- keypadElement?: any | null | undefined;
25
+ keypadElement?: KeypadAPI | null | undefined;
25
26
  dependencies: PerseusDependenciesV2;
26
27
  };
27
28
  type HOCProps = {
@@ -43,7 +44,7 @@ type SerializedState = {
43
44
  question: any;
44
45
  hints: any;
45
46
  };
46
- export declare class ServerItemRenderer extends React.Component<Props, State> implements RendererInterface {
47
+ export declare class ServerItemRenderer extends React.Component<Props, State> implements RendererInterface, KeypadContextRendererInterface {
47
48
  questionRenderer: Renderer;
48
49
  hintsRenderer: any;
49
50
  _currentFocus: FocusPath;
@@ -110,7 +111,7 @@ declare const ref: React.ForwardRefExoticComponent<Omit<KeypadProps & {
110
111
  };
111
112
  problemNum?: number | undefined;
112
113
  reviewMode?: boolean | undefined;
113
- keypadElement?: any | null | undefined;
114
+ keypadElement?: KeypadAPI | null | undefined;
114
115
  dependencies: PerseusDependenciesV2;
115
116
  } & HOCProps, "onRendered"> & React.RefAttributes<ServerItemRenderer>>;
116
117
  export default ref;
package/dist/types.d.ts CHANGED
@@ -8,6 +8,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";
11
12
  export type FocusPath = ReadonlyArray<string> | null | undefined;
12
13
  type Empty = Record<never, never>;
13
14
  export type Dimensions = {
@@ -85,7 +86,7 @@ type TrackInteractionArgs = {
85
86
  export type APIOptions = Readonly<{
86
87
  isArticle?: boolean;
87
88
  onInputError?: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
88
- onFocusChange?: (newFocusPath: FocusPath, oldFocusPath: FocusPath, keypadElement?: HTMLElement, focusedElement?: HTMLElement) => unknown;
89
+ onFocusChange?: (newFocusPath: FocusPath, oldFocusPath: FocusPath, keypadElement?: ReturnType<typeof ReactDOM.findDOMNode>, focusedElement?: HTMLElement) => unknown;
89
90
  GroupMetadataEditor?: React.ComponentType<StubTagEditorType>;
90
91
  showAlignmentOptions?: boolean;
91
92
  readOnly?: boolean;
@@ -94,7 +94,7 @@ declare const _default: Readonly<{
94
94
  apiOptions: Readonly<Readonly<{
95
95
  isArticle?: boolean | undefined;
96
96
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
97
- onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: HTMLElement | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
97
+ onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
98
98
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
99
99
  showAlignmentOptions?: boolean | undefined;
100
100
  readOnly?: boolean | undefined;
@@ -137,7 +137,7 @@ declare const _default: Readonly<{
137
137
  isArticle: NonNullable<boolean | undefined>;
138
138
  isMobile: NonNullable<boolean | undefined>;
139
139
  useV2Keypad: NonNullable<boolean | undefined>;
140
- onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: HTMLElement | undefined, focusedElement?: HTMLElement | undefined) => unknown;
140
+ onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown;
141
141
  onInputError: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
142
142
  readOnly: NonNullable<boolean | undefined>;
143
143
  setDrawingAreaAvailable: (arg1: boolean) => unknown;
@@ -159,7 +159,7 @@ declare const _default: Readonly<{
159
159
  apiOptions: Readonly<Readonly<{
160
160
  isArticle?: boolean | undefined;
161
161
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
162
- onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: HTMLElement | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
162
+ onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
163
163
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
164
164
  showAlignmentOptions?: boolean | undefined;
165
165
  readOnly?: boolean | undefined;
@@ -202,7 +202,7 @@ declare const _default: Readonly<{
202
202
  isArticle: NonNullable<boolean | undefined>;
203
203
  isMobile: NonNullable<boolean | undefined>;
204
204
  useV2Keypad: NonNullable<boolean | undefined>;
205
- onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: HTMLElement | undefined, focusedElement?: HTMLElement | undefined) => unknown;
205
+ onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown;
206
206
  onInputError: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
207
207
  readOnly: NonNullable<boolean | undefined>;
208
208
  setDrawingAreaAvailable: (arg1: boolean) => unknown;
@@ -224,7 +224,7 @@ declare const _default: Readonly<{
224
224
  apiOptions?: Readonly<Readonly<{
225
225
  isArticle?: boolean | undefined;
226
226
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
227
- onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: HTMLElement | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
227
+ onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
228
228
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
229
229
  showAlignmentOptions?: boolean | undefined;
230
230
  readOnly?: boolean | undefined;
@@ -267,7 +267,7 @@ declare const _default: Readonly<{
267
267
  isArticle: NonNullable<boolean | undefined>;
268
268
  isMobile: NonNullable<boolean | undefined>;
269
269
  useV2Keypad: NonNullable<boolean | undefined>;
270
- onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: HTMLElement | undefined, focusedElement?: HTMLElement | undefined) => unknown;
270
+ onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown;
271
271
  onInputError: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
272
272
  readOnly: NonNullable<boolean | undefined>;
273
273
  setDrawingAreaAvailable: (arg1: boolean) => unknown;
@@ -300,7 +300,7 @@ declare const _default: Readonly<{
300
300
  apiOptions: Readonly<Readonly<{
301
301
  isArticle?: boolean | undefined;
302
302
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
303
- onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: HTMLElement | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
303
+ onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
304
304
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
305
305
  showAlignmentOptions?: boolean | undefined;
306
306
  readOnly?: boolean | undefined;
@@ -343,7 +343,7 @@ declare const _default: Readonly<{
343
343
  isArticle: NonNullable<boolean | undefined>;
344
344
  isMobile: NonNullable<boolean | undefined>;
345
345
  useV2Keypad: NonNullable<boolean | undefined>;
346
- onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: HTMLElement | undefined, focusedElement?: HTMLElement | undefined) => unknown;
346
+ onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown;
347
347
  onInputError: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
348
348
  readOnly: NonNullable<boolean | undefined>;
349
349
  setDrawingAreaAvailable: (arg1: boolean) => unknown;
@@ -365,7 +365,7 @@ declare const _default: Readonly<{
365
365
  apiOptions: Readonly<Readonly<{
366
366
  isArticle?: boolean | undefined;
367
367
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
368
- onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: HTMLElement | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
368
+ onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
369
369
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
370
370
  showAlignmentOptions?: boolean | undefined;
371
371
  readOnly?: boolean | undefined;
@@ -408,7 +408,7 @@ declare const _default: Readonly<{
408
408
  isArticle: NonNullable<boolean | undefined>;
409
409
  isMobile: NonNullable<boolean | undefined>;
410
410
  useV2Keypad: NonNullable<boolean | undefined>;
411
- onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: HTMLElement | undefined, focusedElement?: HTMLElement | undefined) => unknown;
411
+ onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown;
412
412
  onInputError: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
413
413
  readOnly: NonNullable<boolean | undefined>;
414
414
  setDrawingAreaAvailable: (arg1: boolean) => unknown;
@@ -430,7 +430,7 @@ declare const _default: Readonly<{
430
430
  apiOptions?: Readonly<Readonly<{
431
431
  isArticle?: boolean | undefined;
432
432
  onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
433
- onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: HTMLElement | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
433
+ onFocusChange?: ((newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown) | undefined;
434
434
  GroupMetadataEditor?: React.ComponentType<any> | undefined;
435
435
  showAlignmentOptions?: boolean | undefined;
436
436
  readOnly?: boolean | undefined;
@@ -473,7 +473,7 @@ declare const _default: Readonly<{
473
473
  isArticle: NonNullable<boolean | undefined>;
474
474
  isMobile: NonNullable<boolean | undefined>;
475
475
  useV2Keypad: NonNullable<boolean | undefined>;
476
- onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: HTMLElement | undefined, focusedElement?: HTMLElement | undefined) => unknown;
476
+ onFocusChange: (newFocusPath: import("../types").FocusPath, oldFocusPath: import("../types").FocusPath, keypadElement?: Element | Text | null | undefined, focusedElement?: HTMLElement | undefined) => unknown;
477
477
  onInputError: (widgetId: any, value: string, message?: string | null | undefined) => unknown;
478
478
  readOnly: NonNullable<boolean | undefined>;
479
479
  setDrawingAreaAvailable: (arg1: boolean) => unknown;
package/package.json CHANGED
@@ -3,13 +3,14 @@
3
3
  "description": "Core Perseus API (includes renderers and widgets)",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "10.0.1",
6
+ "version": "11.0.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "https://github.com/Khan/perseus.git"
12
+ "url": "https://github.com/Khan/perseus.git",
13
+ "directory": "packages/perseus"
13
14
  },
14
15
  "bugs": {
15
16
  "url": "https://github.com/Khan/perseus/issues"
@@ -23,8 +24,8 @@
23
24
  "dependencies": {
24
25
  "@khanacademy/kas": "^0.3.1",
25
26
  "@khanacademy/kmath": "^0.1.2",
26
- "@khanacademy/math-input": "^10.1.0",
27
- "@khanacademy/perseus-core": "0.2.0",
27
+ "@khanacademy/math-input": "^11.0.0",
28
+ "@khanacademy/perseus-core": "1.0.0",
28
29
  "@khanacademy/perseus-linter": "^0.3.2",
29
30
  "@khanacademy/pure-markdown": "^0.2.4",
30
31
  "@khanacademy/simple-markdown": "^0.10.0"
@@ -1,3 +1,4 @@
1
+ import {KeypadContext} from "@khanacademy/math-input";
1
2
  import React from "react";
2
3
 
3
4
  import {storybookDependenciesV2} from "../../../../testing/test-dependencies";
@@ -5,8 +6,10 @@ import {
5
6
  singleSectionArticle,
6
7
  multiSectionArticle,
7
8
  passageArticle,
9
+ articleWithExpression,
8
10
  } from "../__testdata__/article-renderer.testdata";
9
11
  import ArticleRenderer from "../article-renderer";
12
+ import TestKeypadContextWrapper from "../widgets/__stories__/test-keypad-context-wrapper";
10
13
 
11
14
  export default {
12
15
  title: "Perseus/Renderers/Article Renderer",
@@ -48,3 +51,22 @@ export const PassageArticle = ({useNewStyles}): any => (
48
51
  useNewStyles={useNewStyles}
49
52
  />
50
53
  );
54
+
55
+ export const ExpressionArticle = ({useNewStyles}): any => (
56
+ <TestKeypadContextWrapper>
57
+ <KeypadContext.Consumer>
58
+ {({keypadElement, setRenderer, scrollableElement}) => (
59
+ <ArticleRenderer
60
+ ref={(node) => {
61
+ setRenderer(node);
62
+ }}
63
+ json={articleWithExpression}
64
+ dependencies={storybookDependenciesV2}
65
+ useNewStyles={useNewStyles}
66
+ apiOptions={{isMobile: true, customKeypad: true}}
67
+ keypadElement={keypadElement}
68
+ />
69
+ )}
70
+ </KeypadContext.Consumer>
71
+ </TestKeypadContextWrapper>
72
+ );
@@ -58,6 +58,34 @@ export const passageArticle: PerseusRenderer = {
58
58
  },
59
59
  };
60
60
 
61
+ export const articleWithExpression: PerseusRenderer = {
62
+ content:
63
+ "### Practice Problem\n\n$8\\cdot(11i+2)=$ [[☃ expression 1]] \n*Your answer should be a complex number in the form $a+bi$ where $a$ and $b$ are real numbers.*",
64
+ images: {},
65
+ widgets: {
66
+ "expression 1": {
67
+ alignment: "default",
68
+ graded: true,
69
+ options: {
70
+ answerForms: [
71
+ {
72
+ considered: "correct",
73
+ form: true,
74
+ simplify: false,
75
+ value: "16+88i",
76
+ },
77
+ ],
78
+ buttonSets: ["basic"],
79
+ functions: ["f", "g", "h"],
80
+ times: false,
81
+ },
82
+ static: false,
83
+ type: "expression",
84
+ version: {major: 1, minor: 0},
85
+ },
86
+ },
87
+ };
88
+
61
89
  export const multiSectionArticle: ReadonlyArray<PerseusRenderer> = [
62
90
  {
63
91
  content:
@@ -230,7 +230,7 @@ exports[`renderer rendering should render block math on mobile 1`] = `
230
230
  style="overflow-x: auto; overflow-y: hidden; margin-top: -10px; margin-bottom: -10px; transform: translate3d(0,0,0); padding: 10px 16px 10px 16px;"
231
231
  >
232
232
  <span
233
- style="display: block; width: 100%; transform: scale(1, 1) translate(0, 8px); transform-origin: 0 0; opacity: 0;"
233
+ style="display: block; width: 100%; transform: scale(1, 1); transform-origin: 0 0; opacity: 1;"
234
234
  >
235
235
  <span
236
236
  class="mock-TeX"
@@ -28,6 +28,7 @@ import MockWidgetExport from "./mock-widget";
28
28
  import type {PerseusItem} from "../perseus-types";
29
29
  import type {APIOptions} from "../types";
30
30
  import type {MockAssetLoadingWidget} from "./mock-asset-loading-widget";
31
+ import type {KeypadAPI} from "@khanacademy/math-input";
31
32
  import type {PropsFor} from "@khanacademy/wonder-blocks-core";
32
33
 
33
34
  // This looks alot like `widgets/__tests__/renderQuestion.jsx', except we use
@@ -342,12 +343,16 @@ describe("server item renderer", () => {
342
343
  // Arranged
343
344
  const onFocusChange = jest.fn();
344
345
  const keypadElementDOMNode = <div />;
345
- const keypadElement = {
346
+ const keypadElement: KeypadAPI = {
346
347
  getDOMNode: jest
347
348
  .fn()
348
349
  .mockImplementation(() => keypadElementDOMNode),
349
350
  activate: jest.fn(),
350
- } as const;
351
+ dismiss: jest.fn(),
352
+ configure: jest.fn(),
353
+ setCursor: jest.fn(),
354
+ setKeyHandler: jest.fn(),
355
+ };
351
356
  const {renderer} = renderQuestion(
352
357
  itemWithInput,
353
358
  {onFocusChange, isMobile: true},
@@ -402,13 +407,16 @@ describe("server item renderer", () => {
402
407
  // Arranged
403
408
  const onFocusChange = jest.fn();
404
409
  const keypadElementDOMNode = <div />;
405
- const keypadElement = {
410
+ const keypadElement: KeypadAPI = {
406
411
  getDOMNode: jest
407
412
  .fn()
408
413
  .mockImplementation(() => keypadElementDOMNode),
409
414
  activate: jest.fn(),
410
415
  dismiss: jest.fn(),
411
- } as const;
416
+ configure: jest.fn(),
417
+ setCursor: jest.fn(),
418
+ setKeyHandler: jest.fn(),
419
+ };
412
420
  const {renderer} = renderQuestion(
413
421
  itemWithInput,
414
422
  {onFocusChange, isMobile: true},
@@ -8,7 +8,6 @@ import classNames from "classnames";
8
8
  import * as React from "react";
9
9
 
10
10
  import {DependenciesContext} from "./dependencies";
11
- import ProvideKeypad from "./mixins/provide-keypad";
12
11
  import {ClassNames as ApiClassNames, ApiOptions} from "./perseus-api";
13
12
  import Renderer from "./renderer";
14
13
  import Util from "./util";
@@ -16,6 +15,8 @@ import Util from "./util";
16
15
  import type {KeypadProps} from "./mixins/provide-keypad";
17
16
  import type {PerseusRenderer} from "./perseus-types";
18
17
  import type {APIOptions, PerseusDependenciesV2} from "./types";
18
+ import type {KeypadAPI} from "@khanacademy/math-input";
19
+ import type {KeypadContextRendererInterface} from "@khanacademy/perseus-core";
19
20
  import type {LinterContextProps} from "@khanacademy/perseus-linter";
20
21
 
21
22
  type Props = {
@@ -28,7 +29,7 @@ type Props = {
28
29
  useNewStyles: boolean;
29
30
  linterContext: LinterContextProps;
30
31
  legacyPerseusLint?: ReadonlyArray<string>;
31
-
32
+ keypadElement?: KeypadAPI | null | undefined;
32
33
  dependencies: PerseusDependenciesV2;
33
34
  } & KeypadProps;
34
35
 
@@ -38,11 +39,10 @@ type DefaultProps = {
38
39
  linterContext: Props["linterContext"];
39
40
  };
40
41
 
41
- type State = {
42
- keypadElement: any | null;
43
- };
44
-
45
- class ArticleRenderer extends React.Component<Props, State> {
42
+ class ArticleRenderer
43
+ extends React.Component<Props>
44
+ implements KeypadContextRendererInterface
45
+ {
46
46
  _currentFocus: any;
47
47
 
48
48
  static defaultProps: DefaultProps = {
@@ -53,26 +53,16 @@ class ArticleRenderer extends React.Component<Props, State> {
53
53
 
54
54
  constructor(props: Props) {
55
55
  super(props);
56
- this.state = ProvideKeypad.getInitialState.call(this);
57
56
  }
58
57
 
59
58
  componentDidMount() {
60
- ProvideKeypad.componentDidMount.call(this);
61
59
  this._currentFocus = null;
62
60
  }
63
61
 
64
- shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
65
- return nextProps !== this.props || nextState !== this.state;
62
+ shouldComponentUpdate(nextProps: Props): boolean {
63
+ return nextProps !== this.props;
66
64
  }
67
65
 
68
- componentWillUnmount() {
69
- ProvideKeypad.componentWillUnmount.call(this);
70
- }
71
-
72
- keypadElement: () => void = () => {
73
- return ProvideKeypad.keypadElement.call(this);
74
- };
75
-
76
66
  _handleFocusChange: (arg1: any, arg2: any) => void = (
77
67
  newFocusPath,
78
68
  oldFocusPath,
@@ -87,7 +77,8 @@ class ArticleRenderer extends React.Component<Props, State> {
87
77
  };
88
78
 
89
79
  _setCurrentFocus: (arg1: any) => void = (newFocusPath) => {
90
- const keypadElement = this.keypadElement();
80
+ const {keypadElement, apiOptions} = this.props;
81
+ const {isMobile} = apiOptions;
91
82
 
92
83
  const prevFocusPath = this._currentFocus;
93
84
  this._currentFocus = newFocusPath;
@@ -110,18 +101,14 @@ class ArticleRenderer extends React.Component<Props, State> {
110
101
  this.props.apiOptions.onFocusChange(
111
102
  this._currentFocus,
112
103
  prevFocusPath,
113
- // @ts-expect-error - TS2339 - Property 'getDOMNode' does not exist on type 'never'.
114
- didFocusInput && keypadElement && keypadElement.getDOMNode(),
104
+ didFocusInput ? keypadElement?.getDOMNode() : null,
115
105
  );
116
106
  }
117
107
 
118
- // @ts-expect-error - TS1345 - An expression of type 'void' cannot be tested for truthiness.
119
- if (keypadElement) {
108
+ if (keypadElement && isMobile) {
120
109
  if (didFocusInput) {
121
- // @ts-expect-error - TS2339 - Property 'activate' does not exist on type 'never'.
122
110
  keypadElement.activate();
123
111
  } else {
124
- // @ts-expect-error - TS2339 - Property 'dismiss' does not exist on type 'never'.
125
112
  keypadElement.dismiss();
126
113
  }
127
114
  }
@@ -189,7 +176,7 @@ class ArticleRenderer extends React.Component<Props, State> {
189
176
  ref={refForSection}
190
177
  key={i}
191
178
  key_={i}
192
- keypadElement={this.keypadElement()}
179
+ keypadElement={this.props.keypadElement}
193
180
  apiOptions={{
194
181
  ...apiOptions,
195
182
  onFocusChange: (newFocusPath, oldFocusPath) => {
@@ -10,6 +10,11 @@ type Story = {
10
10
 
11
11
  export default {
12
12
  title: "Perseus/Components/Zoomable",
13
+ argTypes: {
14
+ disableEntranceAnimation: {
15
+ control: {type: "boolean"},
16
+ },
17
+ },
13
18
  } as Story;
14
19
 
15
20
  type Bounds = {
@@ -29,7 +34,10 @@ const computeChildBounds = (
29
34
 
30
35
  export const ZoomableExample = (args: StoryArgs): React.ReactElement => {
31
36
  return (
32
- <Zoomable computeChildBounds={computeChildBounds}>
37
+ <Zoomable
38
+ computeChildBounds={computeChildBounds}
39
+ disableEntranceAnimation={!!args.disableEntranceAnimation}
40
+ >
33
41
  <span>
34
42
  Here's some zoomed-out content.
35
43
  <br />
@@ -59,6 +59,8 @@ export default class ZoomableTeX extends React.Component<Props, State> {
59
59
  <Zoomable
60
60
  readyToMeasure={this.state.isRendered}
61
61
  computeChildBounds={computeMathBounds}
62
+ animateHeight={false}
63
+ disableEntranceAnimation={true}
62
64
  >
63
65
  <AssetContext.Consumer>
64
66
  {({setAssetStatus}) => (
@@ -14,6 +14,8 @@ type Bounds = {
14
14
  type Props = {
15
15
  children: React.ReactNode;
16
16
  animateHeight: boolean;
17
+
18
+ disableEntranceAnimation: boolean;
17
19
  /**
18
20
  * Optional function that allows customizations in zooming.
19
21
  *
@@ -35,6 +37,7 @@ type DefaultProps = {
35
37
  animateHeight: Props["animateHeight"];
36
38
  computeChildBounds: Props["computeChildBounds"];
37
39
  readyToMeasure: Props["readyToMeasure"];
40
+ disableEntranceAnimation: Props["disableEntranceAnimation"];
38
41
  };
39
42
 
40
43
  type State = {
@@ -60,6 +63,7 @@ class Zoomable extends React.Component<Props, State> {
60
63
  static defaultProps: DefaultProps = {
61
64
  animateHeight: false,
62
65
  readyToMeasure: true,
66
+ disableEntranceAnimation: false,
63
67
  computeChildBounds: (parentNode) => {
64
68
  const firstChild = parentNode.firstElementChild;
65
69
 
@@ -245,7 +249,7 @@ class Zoomable extends React.Component<Props, State> {
245
249
  render(): React.ReactNode {
246
250
  const {visible, scale, compactHeight, expandedHeight, zoomed} =
247
251
  this.state;
248
- const {animateHeight} = this.props;
252
+ const {animateHeight, disableEntranceAnimation} = this.props;
249
253
 
250
254
  const property = animateHeight
251
255
  ? "opacity transform height"
@@ -261,7 +265,10 @@ class Zoomable extends React.Component<Props, State> {
261
265
  : {};
262
266
 
263
267
  // Do a fancy little slide as we fade the contents in the first time.
264
- const translateOffset = visible ? "" : " translate(0, 8px)";
268
+ const shouldSlide = !disableEntranceAnimation && !visible;
269
+ const translateOffset = shouldSlide ? " translate(0, 8px)" : "";
270
+ const opacity = shouldSlide ? 0 : 1;
271
+
265
272
  const scaleString = (scale ?? 1).toString();
266
273
  const transform = zoomed
267
274
  ? `scale(1, 1) ${translateOffset}`
@@ -273,7 +280,7 @@ class Zoomable extends React.Component<Props, State> {
273
280
  height: zoomed ? expandedHeight : compactHeight,
274
281
  transform: transform,
275
282
  transformOrigin: "0 0",
276
- opacity: visible ? 1 : 0,
283
+ opacity: opacity,
277
284
  WebkitTapHighlightColor: "transparent",
278
285
  ...transitionStyle,
279
286
  } as const;
@@ -22,7 +22,12 @@ import Util from "./util";
22
22
 
23
23
  import type {KeypadProps} from "./mixins/provide-keypad";
24
24
  import type {APIOptions, FocusPath, PerseusDependenciesV2} from "./types";
25
- import type {RendererInterface, KEScore} from "@khanacademy/perseus-core";
25
+ import type {KeypadAPI} from "@khanacademy/math-input";
26
+ import type {
27
+ KeypadContextRendererInterface,
28
+ RendererInterface,
29
+ KEScore,
30
+ } from "@khanacademy/perseus-core";
26
31
 
27
32
  const {mapObject} = Objective;
28
33
 
@@ -36,9 +41,7 @@ type OwnProps = // These props are used by the ProvideKeypad mixin.
36
41
  };
37
42
  problemNum?: number;
38
43
  reviewMode?: boolean;
39
- // from KeypadContext
40
- keypadElement?: any | null | undefined;
41
-
44
+ keypadElement?: KeypadAPI | null | undefined;
42
45
  dependencies: PerseusDependenciesV2;
43
46
  };
44
47
 
@@ -71,7 +74,7 @@ type SerializedState = {
71
74
  /* eslint-disable-next-line react/no-unsafe */
72
75
  export class ServerItemRenderer
73
76
  extends React.Component<Props, State>
74
- implements RendererInterface
77
+ implements RendererInterface, KeypadContextRendererInterface
75
78
  {
76
79
  // @ts-expect-error - TS2564 - Property 'questionRenderer' has no initializer and is not definitely assigned in the constructor.
77
80
  questionRenderer: Renderer;
@@ -176,8 +179,8 @@ export class ServerItemRenderer
176
179
  onFocusChange(
177
180
  this._currentFocus,
178
181
  prevFocus,
179
- didFocusInput && keypadElement && keypadElement.getDOMNode(),
180
- // @ts-expect-error - TS2345 - Argument of type 'false | Element | Text | null | undefined' is not assignable to parameter of type 'HTMLElement | undefined'.
182
+ didFocusInput ? keypadElement?.getDOMNode() : null,
183
+ // @ts-expect-error [FEI-5003] - TS2345 - Argument of type 'false | Element | Text | null | undefined' is not assignable to parameter of type 'HTMLElement | undefined'.
181
184
  didFocusInput &&
182
185
  this.questionRenderer.getDOMNodeForPath(newFocus),
183
186
  );
@@ -1,7 +1,39 @@
1
1
  .perseus-image-widget {
2
+ margin: 0;
3
+
2
4
  .perseus-image-title {
3
5
  text-align: center;
4
6
  }
7
+
8
+ .perseus-image-caption {
9
+ color: rgba(33, 36, 44, 0.7);
10
+ max-width: 420px;
11
+ padding-left: 16px;
12
+ position: relative;
13
+
14
+ // This offsets the paragraph's padding
15
+ // So that the caption sits much closer
16
+ // to the image
17
+ top: -12px;
18
+
19
+ &::before {
20
+ content: "";
21
+ position: absolute;
22
+ top: 0;
23
+ bottom: 0;
24
+ left: 0;
25
+ margin-left: 4px;
26
+ width: 1px;
27
+ background-color: rgba(33, 36, 44, 0.5);
28
+
29
+ border: 1px solid rgba(33, 36, 44, 0.5);
30
+ border-radius: 2px;
31
+ }
32
+ .paragraph {
33
+ font-size: 14px;
34
+ padding-right: 12px;
35
+ }
36
+ }
5
37
  }
6
38
 
7
39
  .perseus-image-editor {
package/src/types.ts CHANGED
@@ -8,6 +8,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";
11
12
 
12
13
  export type FocusPath = ReadonlyArray<string> | null | undefined;
13
14
 
@@ -143,10 +144,7 @@ export type APIOptions = Readonly<{
143
144
  onFocusChange?: (
144
145
  newFocusPath: FocusPath,
145
146
  oldFocusPath: FocusPath,
146
- // These two parameters are only used in library-exercise.jsx and
147
- // practice-exercise.jsx and in the mobile app
148
- // (javascript/perseus-package/perseus-mobile-bridge.js).
149
- keypadElement?: HTMLElement,
147
+ keypadElement?: ReturnType<typeof ReactDOM.findDOMNode>,
150
148
  focusedElement?: HTMLElement,
151
149
  ) => unknown;
152
150
  GroupMetadataEditor?: React.ComponentType<StubTagEditorType>;