@khanacademy/math-input 13.1.0 → 14.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.
package/dist/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { CursorContext } from "./components/input/cursor-contexts";
2
2
  import type Key from "./data/keys";
3
3
  import type { BorderDirection, EchoAnimationType, IconType, KeyType, KeypadType } from "./enums";
4
+ import type { KeypadContextRendererInterface } from "@khanacademy/perseus-core";
4
5
  import type * as React from "react";
5
6
  import type ReactDOM from "react-dom";
6
7
  export type Border = Partial<ReadonlyArray<BorderDirection>>;
@@ -74,4 +75,14 @@ export interface KeypadAPI {
74
75
  setKeyHandler: (keyHandler: KeyHandler) => void;
75
76
  getDOMNode: () => ReturnType<typeof ReactDOM.findDOMNode>;
76
77
  }
78
+ export type KeypadContextType = {
79
+ setKeypadActive: (keypadActive: boolean) => void;
80
+ keypadActive: boolean;
81
+ setKeypadElement: (keypadElement?: KeypadAPI) => void;
82
+ keypadElement: KeypadAPI | null | undefined;
83
+ setRenderer: (renderer?: KeypadContextRendererInterface | null | undefined) => void;
84
+ renderer: KeypadContextRendererInterface | null | undefined;
85
+ setScrollableElement: (scrollableElement?: HTMLElement | null | undefined) => void;
86
+ scrollableElement: HTMLElement | null | undefined;
87
+ };
77
88
  export {};
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Khan Academy's new expression editor for the mobile web.",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "13.1.0",
6
+ "version": "14.0.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -12,10 +12,10 @@ import React, {useState} from "react";
12
12
 
13
13
  import {KeypadType} from "../../enums";
14
14
  import MathInput from "../input/math-input";
15
- import KeypadContext from "../keypad-context";
15
+ import {KeypadContext, StatefulKeypadContextProvider} from "../keypad-context";
16
16
  import KeypadSwitch from "../keypad-switch";
17
17
 
18
- import type {KeypadAPI, KeypadConfiguration} from "../../types";
18
+ import type {KeypadConfiguration} from "../../types";
19
19
 
20
20
  const MQ = MathQuill.getInterface(2);
21
21
 
@@ -73,25 +73,11 @@ function KeypadWithContext() {
73
73
  }
74
74
 
75
75
  function ConnectedMathInput({keypadConfiguration = defaultConfiguration}) {
76
- const [keypadElement, setKeypadElement] = useState<KeypadAPI | null>();
77
- const [renderer, setRenderer] = useState<any>(null);
78
- const [scrollableElement, setScrollableElement] =
79
- useState<HTMLElement | null>();
80
-
81
76
  return (
82
- <KeypadContext.Provider
83
- value={{
84
- setKeypadElement,
85
- keypadElement,
86
- setRenderer,
87
- renderer,
88
- setScrollableElement,
89
- scrollableElement,
90
- }}
91
- >
77
+ <StatefulKeypadContextProvider>
92
78
  <InputWithContext keypadConfiguration={keypadConfiguration} />
93
79
  <KeypadWithContext />
94
- </KeypadContext.Provider>
80
+ </StatefulKeypadContextProvider>
95
81
  );
96
82
  }
97
83
 
@@ -4,6 +4,7 @@ import ReactDOM from "react-dom";
4
4
 
5
5
  import {View} from "../../fake-react-native-web/index";
6
6
 
7
+ import Keypad from "./keypad";
7
8
  import {expandedViewThreshold} from "./utils";
8
9
 
9
10
  import type Key from "../../data/keys";
@@ -16,8 +17,6 @@ import type {
16
17
  import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core";
17
18
  import type {StyleType} from "@khanacademy/wonder-blocks-core";
18
19
 
19
- import Keypad from "./index";
20
-
21
20
  /**
22
21
  * This is the v2 equivalent of v1's ProvidedKeypad. It follows the same
23
22
  * external API so that it can be hot-swapped with the v1 keypad and
@@ -34,10 +33,11 @@ type Props = {
34
33
  onDismiss?: () => void;
35
34
  style?: StyleType;
36
35
  onAnalyticsEvent: AnalyticsEventHandlerFn;
36
+ setKeypadActive: (keypadActive: boolean) => void;
37
+ keypadActive: boolean;
37
38
  };
38
39
 
39
40
  type State = {
40
- active: boolean;
41
41
  containerWidth: number;
42
42
  hasBeenActivated: boolean;
43
43
  keypadConfig?: KeypadConfiguration;
@@ -52,7 +52,6 @@ class MobileKeypad extends React.Component<Props, State> implements KeypadAPI {
52
52
  hasMounted = false;
53
53
 
54
54
  state: State = {
55
- active: false,
56
55
  containerWidth: 0,
57
56
  hasBeenActivated: false,
58
57
  };
@@ -109,16 +108,15 @@ class MobileKeypad extends React.Component<Props, State> implements KeypadAPI {
109
108
  };
110
109
 
111
110
  activate: () => void = () => {
111
+ this.props.setKeypadActive(true);
112
112
  this.setState({
113
- active: true,
114
113
  hasBeenActivated: true,
115
114
  });
116
115
  };
117
116
 
118
117
  dismiss: () => void = () => {
119
- this.setState({active: false}, () => {
120
- this.props.onDismiss?.();
121
- });
118
+ this.props.setKeypadActive(false);
119
+ this.props.onDismiss?.();
122
120
  };
123
121
 
124
122
  configure: (configuration: KeypadConfiguration, cb: () => void) => void = (
@@ -162,14 +160,14 @@ class MobileKeypad extends React.Component<Props, State> implements KeypadAPI {
162
160
  }
163
161
 
164
162
  render(): React.ReactNode {
165
- const {style} = this.props;
166
- const {active, hasBeenActivated, containerWidth, cursor, keypadConfig} =
163
+ const {keypadActive, style} = this.props;
164
+ const {hasBeenActivated, containerWidth, cursor, keypadConfig} =
167
165
  this.state;
168
166
 
169
167
  const containerStyle = [
170
168
  // internal styles
171
169
  styles.keypadContainer,
172
- active && styles.activeKeypadContainer,
170
+ keypadActive && styles.activeKeypadContainer,
173
171
  // styles passed as props
174
172
  ...(Array.isArray(style) ? style : [style]),
175
173
  ];
@@ -179,7 +177,7 @@ class MobileKeypad extends React.Component<Props, State> implements KeypadAPI {
179
177
  // during the initial render.
180
178
  // Done inline (dynamicStyle) since stylesheets might not be loaded yet.
181
179
  let dynamicStyle = {};
182
- if (!active && !hasBeenActivated) {
180
+ if (!keypadActive && !hasBeenActivated) {
183
181
  dynamicStyle = {visibility: "hidden"};
184
182
  }
185
183
 
@@ -0,0 +1,61 @@
1
+ /**
2
+ * KeypadContext provides a way to the Keypad and Perseus Renderers to
3
+ * communicate.
4
+ *
5
+ * The StatefulKeypadContextProvider wraps the application
6
+ * while KeypadContext.Consumer wraps things that need this state:
7
+ * - mobile keypad usages
8
+ * - Perseus Renderers (Server/Item/Article)
9
+ */
10
+ import * as React from "react";
11
+ import {useState} from "react";
12
+
13
+ import type {KeypadAPI, KeypadContextType} from "../types";
14
+ import type {KeypadContextRendererInterface} from "@khanacademy/perseus-core";
15
+
16
+ // @ts-expect-error - TS2322 - Type 'Context<{ setKeypadElement: (keypadElement: HTMLElement | null | undefined) => void; keypadElement: null; setRenderer: (renderer: RendererInterface | null | undefined) => void; renderer: null; setScrollableElement: (scrollableElement: HTMLElement | ... 1 more ... | undefined) => void; scrollableElement: null; }>' is not assignable to type 'Context<KeypadContext>'.
17
+ export const KeypadContext: React.Context<KeypadContextType> =
18
+ React.createContext({
19
+ setKeypadActive: (keypadActive) => {},
20
+ keypadActive: false,
21
+ setKeypadElement: (keypadElement) => {},
22
+ keypadElement: null,
23
+ setRenderer: (renderer) => {},
24
+ renderer: null,
25
+ setScrollableElement: (scrollableElement) => {},
26
+ scrollableElement: null,
27
+ });
28
+
29
+ type Props = React.PropsWithChildren<unknown>;
30
+
31
+ export function StatefulKeypadContextProvider(props: Props) {
32
+ // whether or not to display the keypad
33
+ const [keypadActive, setKeypadActive] = useState<boolean>(false);
34
+ // used to communicate between the keypad and the Renderer
35
+ const [keypadElement, setKeypadElement] = useState<KeypadAPI | null>();
36
+ // this is a KeypadContextRendererInterface from Perseus
37
+ const [renderer, setRenderer] =
38
+ useState<KeypadContextRendererInterface | null>();
39
+ const [scrollableElement, setScrollableElement] =
40
+ useState<HTMLElement | null>();
41
+
42
+ return (
43
+ <KeypadContext.Provider
44
+ value={{
45
+ setKeypadActive,
46
+ keypadActive,
47
+ setKeypadElement,
48
+ keypadElement,
49
+ setRenderer,
50
+ renderer,
51
+ // The scrollableElement options can likely be removed after
52
+ // the exercises-package is officially deprecated. They don't appear
53
+ // to be used anywhere except for the exercises-package and tests.
54
+ setScrollableElement,
55
+ scrollableElement,
56
+ }}
57
+ >
58
+ {props.children}
59
+ </KeypadContext.Provider>
60
+ );
61
+ }
@@ -22,6 +22,8 @@ import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core";
22
22
  import type {StyleType} from "@khanacademy/wonder-blocks-core";
23
23
 
24
24
  type Props = {
25
+ setKeypadActive: (keypadActive: boolean) => void;
26
+ keypadActive: boolean;
25
27
  onElementMounted?: (arg1: any) => void;
26
28
  onDismiss?: () => void;
27
29
  style?: StyleType;
@@ -37,12 +39,22 @@ class ProvidedKeypad extends React.Component<Props> implements KeypadAPI {
37
39
  this.store = createStore();
38
40
  }
39
41
 
42
+ componentDidUpdate(prevProps) {
43
+ if (this.props.keypadActive && !prevProps.keypadActive) {
44
+ this.store.dispatch(activateKeypad());
45
+ }
46
+
47
+ if (!this.props.keypadActive && prevProps.keypadActive) {
48
+ this.store.dispatch(dismissKeypad());
49
+ }
50
+ }
51
+
40
52
  activate: () => void = () => {
41
- this.store.dispatch(activateKeypad());
53
+ this.props.setKeypadActive(true);
42
54
  };
43
55
 
44
56
  dismiss: () => void = () => {
45
- this.store.dispatch(dismissKeypad());
57
+ this.props.setKeypadActive(false);
46
58
  };
47
59
 
48
60
  configure: (configuration: KeypadConfiguration, cb: () => void) => void = (
@@ -1,6 +1,7 @@
1
1
  import * as React from "react";
2
2
 
3
3
  import {MobileKeypad} from "./keypad";
4
+ import {KeypadContext} from "./keypad-context";
4
5
  import LegacyKeypad from "./keypad-legacy";
5
6
 
6
7
  import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core";
@@ -23,7 +24,19 @@ function KeypadSwitch(props: Props) {
23
24
  // Note: Although we pass the "onAnalyticsEvent" to both keypad components,
24
25
  // only the current one uses it. There's no point in instrumenting the
25
26
  // legacy keypad given that it's on its way out the door.
26
- return <KeypadComponent {...rest} />;
27
+ return (
28
+ <KeypadContext.Consumer>
29
+ {({setKeypadActive, keypadActive}) => {
30
+ return (
31
+ <KeypadComponent
32
+ {...rest}
33
+ keypadActive={keypadActive}
34
+ setKeypadActive={setKeypadActive}
35
+ />
36
+ );
37
+ }}
38
+ </KeypadContext.Consumer>
39
+ );
27
40
  }
28
41
 
29
42
  export default KeypadSwitch;
@@ -1,9 +1,13 @@
1
1
  import {action} from "@storybook/addon-actions";
2
2
  import * as React from "react";
3
3
 
4
- import type {KeypadAPI} from "./types";
5
-
6
- import {KeypadInput, KeypadType, MobileKeypad} from "./index";
4
+ import {
5
+ KeypadInput,
6
+ KeypadType,
7
+ MobileKeypad,
8
+ StatefulKeypadContextProvider,
9
+ KeypadContext,
10
+ } from "./index";
7
11
 
8
12
  export default {
9
13
  title: "math-input/Full Mobile MathInput",
@@ -19,32 +23,19 @@ export default {
19
23
  },
20
24
  };
21
25
 
22
- export const Basic = () => {
26
+ const Basic = ({keypadElement, setKeypadElement}) => {
23
27
  const [value, setValue] = React.useState("");
24
- // Reference to the keypad
25
- const [keypadElement, setKeypadElement] = React.useState<KeypadAPI>();
26
28
  // Whether to use Expression or Fraction keypad
27
29
  const [expression, setExpression] = React.useState<boolean>(false);
28
30
  // Whether to use CDOT or TIMES
29
31
  const [times, setTimes] = React.useState<boolean>(true);
30
32
  // Whether to use v1 or v2 keypad
31
33
  const [v2Keypad, setV2Keypad] = React.useState<boolean>(true);
32
- // Whether the keypad is open or not
33
- const [keypadOpen, setKeypadOpen] = React.useState<boolean>(false);
34
34
 
35
35
  const input = React.useRef<KeypadInput>(null);
36
36
 
37
37
  const timesLabel = times ? "CDOT" : "TIMES";
38
38
 
39
- const toggleKeypad = () => {
40
- if (keypadOpen) {
41
- keypadElement?.dismiss();
42
- } else {
43
- keypadElement?.activate();
44
- }
45
- setKeypadOpen(!keypadOpen);
46
- };
47
-
48
39
  React.useEffect(() => {
49
40
  keypadElement?.configure(
50
41
  {
@@ -73,9 +64,6 @@ export const Basic = () => {
73
64
  <button onClick={() => setV2Keypad(!v2Keypad)}>
74
65
  {`Use ${v2Keypad ? "Legacy" : "New"} Keypad`}
75
66
  </button>
76
- <button onClick={() => toggleKeypad()}>
77
- {`Toggle Keypad`}
78
- </button>
79
67
  <button onClick={() => setTimes(!times)}>
80
68
  {`Toggle to ` + timesLabel}
81
69
  </button>
@@ -111,3 +99,18 @@ export const Basic = () => {
111
99
  </div>
112
100
  );
113
101
  };
102
+
103
+ export function Wrapped() {
104
+ return (
105
+ <StatefulKeypadContextProvider>
106
+ <KeypadContext.Consumer>
107
+ {({keypadElement, setKeypadElement}) => (
108
+ <Basic
109
+ keypadElement={keypadElement}
110
+ setKeypadElement={setKeypadElement}
111
+ />
112
+ )}
113
+ </KeypadContext.Consumer>
114
+ </StatefulKeypadContextProvider>
115
+ );
116
+ }
package/src/index.ts CHANGED
@@ -30,7 +30,10 @@ export {default as MobileKeypad} from "./components/keypad-switch";
30
30
  export {default as DesktopKeypad} from "./components/keypad";
31
31
 
32
32
  // Context used to pass data/refs around
33
- export {default as KeypadContext} from "./components/keypad-context";
33
+ export {
34
+ KeypadContext,
35
+ StatefulKeypadContextProvider,
36
+ } from "./components/keypad-context";
34
37
 
35
38
  // External API of the "Provided" keypad component
36
39
  export {keypadElementPropType} from "./components/prop-types";
package/src/types.ts CHANGED
@@ -7,6 +7,7 @@ import type {
7
7
  KeyType,
8
8
  KeypadType,
9
9
  } from "./enums";
10
+ import type {KeypadContextRendererInterface} from "@khanacademy/perseus-core";
10
11
  import type * as React from "react";
11
12
  import type ReactDOM from "react-dom";
12
13
 
@@ -105,3 +106,18 @@ export interface KeypadAPI {
105
106
  setKeyHandler: (keyHandler: KeyHandler) => void;
106
107
  getDOMNode: () => ReturnType<typeof ReactDOM.findDOMNode>;
107
108
  }
109
+
110
+ export type KeypadContextType = {
111
+ setKeypadActive: (keypadActive: boolean) => void;
112
+ keypadActive: boolean;
113
+ setKeypadElement: (keypadElement?: KeypadAPI) => void;
114
+ keypadElement: KeypadAPI | null | undefined;
115
+ setRenderer: (
116
+ renderer?: KeypadContextRendererInterface | null | undefined,
117
+ ) => void;
118
+ renderer: KeypadContextRendererInterface | null | undefined;
119
+ setScrollableElement: (
120
+ scrollableElement?: HTMLElement | null | undefined,
121
+ ) => void;
122
+ scrollableElement: HTMLElement | null | undefined;
123
+ };