@khanacademy/math-input 14.1.1 → 14.2.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.
@@ -0,0 +1,115 @@
1
+ import {render, screen} from "@testing-library/react";
2
+ import * as React from "react";
3
+ import "@testing-library/jest-dom";
4
+
5
+ import MobileKeypad from "../mobile-keypad";
6
+
7
+ describe("mobile keypad", () => {
8
+ it("should render keypad when active", () => {
9
+ // Arrange
10
+ // Act
11
+ const {container} = render(
12
+ <MobileKeypad
13
+ onAnalyticsEvent={async () => undefined}
14
+ setKeypadActive={(keypadActive: boolean) => undefined}
15
+ keypadActive={true}
16
+ />,
17
+ );
18
+
19
+ // Assert
20
+ expect(container).toMatchSnapshot();
21
+ });
22
+
23
+ it("should not render the keypad when not active", () => {
24
+ // Arrange
25
+ // Act
26
+ const {container} = render(
27
+ <MobileKeypad
28
+ onAnalyticsEvent={async () => undefined}
29
+ setKeypadActive={(keypadActive: boolean) => undefined}
30
+ keypadActive={false}
31
+ />,
32
+ );
33
+
34
+ // Assert
35
+ expect(container).toMatchSnapshot();
36
+ });
37
+
38
+ it("should render the keypad when going from keypadActive=false to keypadActive=true", () => {
39
+ // Arrange
40
+ const {rerender} = render(
41
+ <MobileKeypad
42
+ onAnalyticsEvent={async () => undefined}
43
+ setKeypadActive={(keypadActive: boolean) => undefined}
44
+ keypadActive={false}
45
+ />,
46
+ );
47
+
48
+ expect(screen.queryAllByRole("button")).toHaveLength(0);
49
+
50
+ // Act
51
+ rerender(
52
+ <MobileKeypad
53
+ onAnalyticsEvent={async () => undefined}
54
+ setKeypadActive={(keypadActive: boolean) => undefined}
55
+ keypadActive={true}
56
+ />,
57
+ );
58
+
59
+ // Assert
60
+ expect(screen.queryAllByRole("tab")).not.toHaveLength(0);
61
+ });
62
+
63
+ it("should fire an 'opened' event when activated", () => {
64
+ // Arrange
65
+ const onAnalyticsEvent = jest.fn();
66
+
67
+ // Act
68
+ render(
69
+ <MobileKeypad
70
+ onAnalyticsEvent={onAnalyticsEvent}
71
+ setKeypadActive={(keypadActive: boolean) => undefined}
72
+ keypadActive={true}
73
+ />,
74
+ );
75
+
76
+ // Assert
77
+ expect(onAnalyticsEvent).toHaveBeenCalledWith({
78
+ type: "math-input:keypad-opened",
79
+ payload: {
80
+ virtualKeypadVersion: "MATH_INPUT_KEYPAD_V2",
81
+ },
82
+ });
83
+ });
84
+
85
+ it("should fire an 'closed' event when dismissed", async () => {
86
+ const onAnalyticsEvent = jest.fn();
87
+
88
+ // Arrange
89
+ const {rerender, unmount} = render(
90
+ <MobileKeypad
91
+ onAnalyticsEvent={onAnalyticsEvent}
92
+ setKeypadActive={(keypadActive: boolean) => undefined}
93
+ keypadActive={true}
94
+ />,
95
+ );
96
+
97
+ // Act
98
+ rerender(
99
+ <MobileKeypad
100
+ onAnalyticsEvent={onAnalyticsEvent}
101
+ setKeypadActive={(keypadActive: boolean) => undefined}
102
+ keypadActive={false}
103
+ />,
104
+ );
105
+ unmount();
106
+
107
+ // Assert
108
+ expect(onAnalyticsEvent).toHaveBeenCalledWith({
109
+ type: "math-input:keypad-closed",
110
+ payload: {
111
+ virtualKeypadVersion: "MATH_INPUT_KEYPAD_V2",
112
+ },
113
+ });
114
+ });
115
+ });
@@ -3,6 +3,7 @@ import * as React from "react";
3
3
  import ReactDOM from "react-dom";
4
4
 
5
5
  import {View} from "../../fake-react-native-web/index";
6
+ import AphroditeCssTransitionGroup from "../aphrodite-css-transition-group";
6
7
 
7
8
  import Keypad from "./keypad";
8
9
  import {expandedViewThreshold} from "./utils";
@@ -17,16 +18,7 @@ import type {
17
18
  import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core";
18
19
  import type {StyleType} from "@khanacademy/wonder-blocks-core";
19
20
 
20
- /**
21
- * This is the v2 equivalent of v1's ProvidedKeypad. It follows the same
22
- * external API so that it can be hot-swapped with the v1 keypad and
23
- * is responsible for connecting the keypad with MathInput and the Renderer.
24
- *
25
- * Ideally this strategy of attaching methods on the class component for
26
- * other components to call will be replaced props/callbacks since React
27
- * doesn't support this type of code anymore (functional components
28
- * can't have methods attached to them).
29
- */
21
+ const AnimationDurationInMS = 200;
30
22
 
31
23
  type Props = {
32
24
  onElementMounted?: (arg1: any) => void;
@@ -39,21 +31,28 @@ type Props = {
39
31
 
40
32
  type State = {
41
33
  containerWidth: number;
42
- hasBeenActivated: boolean;
43
34
  keypadConfig?: KeypadConfiguration;
44
35
  keyHandler?: KeyHandler;
45
36
  cursor?: Cursor;
46
37
  };
47
38
 
39
+ /**
40
+ * This is the v2 equivalent of v1's ProvidedKeypad. It follows the same
41
+ * external API so that it can be hot-swapped with the v1 keypad and
42
+ * is responsible for connecting the keypad with MathInput and the Renderer.
43
+ *
44
+ * Ideally this strategy of attaching methods on the class component for
45
+ * other components to call will be replaced props/callbacks since React
46
+ * doesn't support this type of code anymore (functional components
47
+ * can't have methods attached to them).
48
+ */
48
49
  class MobileKeypad extends React.Component<Props, State> implements KeypadAPI {
49
50
  _containerRef = React.createRef<HTMLDivElement>();
50
51
  _containerResizeObserver: ResizeObserver | null = null;
51
52
  _throttleResize = false;
52
- hasMounted = false;
53
53
 
54
54
  state: State = {
55
55
  containerWidth: 0,
56
- hasBeenActivated: false,
57
56
  };
58
57
 
59
58
  componentDidMount() {
@@ -78,6 +77,15 @@ class MobileKeypad extends React.Component<Props, State> implements KeypadAPI {
78
77
  );
79
78
  }
80
79
  }
80
+
81
+ this.props.onElementMounted?.({
82
+ activate: this.activate,
83
+ dismiss: this.dismiss,
84
+ configure: this.configure,
85
+ setCursor: this.setCursor,
86
+ setKeyHandler: this.setKeyHandler,
87
+ getDOMNode: this.getDOMNode,
88
+ });
81
89
  }
82
90
 
83
91
  componentWillUnmount() {
@@ -109,9 +117,6 @@ class MobileKeypad extends React.Component<Props, State> implements KeypadAPI {
109
117
 
110
118
  activate: () => void = () => {
111
119
  this.props.setKeypadActive(true);
112
- this.setState({
113
- hasBeenActivated: true,
114
- });
115
120
  };
116
121
 
117
122
  dismiss: () => void = () => {
@@ -161,74 +166,64 @@ class MobileKeypad extends React.Component<Props, State> implements KeypadAPI {
161
166
 
162
167
  render(): React.ReactNode {
163
168
  const {keypadActive, style} = this.props;
164
- const {hasBeenActivated, containerWidth, cursor, keypadConfig} =
165
- this.state;
169
+ const {containerWidth, cursor, keypadConfig} = this.state;
166
170
 
167
171
  const containerStyle = [
168
- // internal styles
169
172
  styles.keypadContainer,
170
- keypadActive && styles.activeKeypadContainer,
171
173
  // styles passed as props
172
174
  ...(Array.isArray(style) ? style : [style]),
173
175
  ];
174
176
 
175
- // If the keypad is yet to have ever been activated, we keep it invisible
176
- // so as to avoid, e.g., the keypad flashing at the bottom of the page
177
- // during the initial render.
178
- // Done inline (dynamicStyle) since stylesheets might not be loaded yet.
179
- let dynamicStyle = {};
180
- if (!keypadActive && !hasBeenActivated) {
181
- dynamicStyle = {visibility: "hidden"};
182
- }
183
-
184
177
  const isExpression = keypadConfig?.keypadType === "EXPRESSION";
185
178
  const convertDotToTimes = keypadConfig?.times;
186
179
 
187
180
  return (
188
- <View
189
- style={containerStyle}
190
- dynamicStyle={dynamicStyle}
191
- forwardRef={this._containerRef}
192
- ref={(element) => {
193
- if (!this.hasMounted && element) {
194
- // TODO(matthewc)[LC-1081]: clean up this weird
195
- // object and type the onElementMounted callback
196
- // Append the dispatch methods that we want to expose
197
- // externally to the returned React element.
198
- const elementWithDispatchMethods = {
199
- ...element,
200
- activate: this.activate,
201
- dismiss: this.dismiss,
202
- configure: this.configure,
203
- setCursor: this.setCursor,
204
- setKeyHandler: this.setKeyHandler,
205
- getDOMNode: this.getDOMNode,
206
- } as const;
207
-
208
- this.hasMounted = true;
209
- this.props.onElementMounted?.(
210
- elementWithDispatchMethods,
211
- );
212
- }
181
+ <AphroditeCssTransitionGroup
182
+ transitionEnterTimeout={AnimationDurationInMS}
183
+ transitionLeaveTimeout={AnimationDurationInMS}
184
+ transitionStyle={{
185
+ enter: {
186
+ transform: "translate3d(0, 100%, 0)",
187
+ transition: `${AnimationDurationInMS}ms ease-out`,
188
+ },
189
+ enterActive: {
190
+ transform: "translate3d(0, 0, 0)",
191
+ },
192
+ leave: {
193
+ transform: "translate3d(0, 0, 0)",
194
+ transition: `${AnimationDurationInMS}ms ease-out`,
195
+ },
196
+ leaveActive: {
197
+ transform: "translate3d(0, 100%, 0)",
198
+ },
213
199
  }}
214
200
  >
215
- <Keypad
216
- onAnalyticsEvent={this.props.onAnalyticsEvent}
217
- extraKeys={keypadConfig?.extraKeys}
218
- onClickKey={(key) => this._handleClickKey(key)}
219
- cursorContext={cursor?.context}
220
- fractionsOnly={!isExpression}
221
- convertDotToTimes={convertDotToTimes}
222
- divisionKey={isExpression}
223
- trigonometry={isExpression}
224
- preAlgebra={isExpression}
225
- logarithms={isExpression}
226
- basicRelations={isExpression}
227
- advancedRelations={isExpression}
228
- expandedView={containerWidth > expandedViewThreshold}
229
- showDismiss
230
- />
231
- </View>
201
+ {keypadActive ? (
202
+ <View
203
+ style={containerStyle}
204
+ forwardRef={this._containerRef}
205
+ >
206
+ <Keypad
207
+ onAnalyticsEvent={this.props.onAnalyticsEvent}
208
+ extraKeys={keypadConfig?.extraKeys}
209
+ onClickKey={(key) => this._handleClickKey(key)}
210
+ cursorContext={cursor?.context}
211
+ fractionsOnly={!isExpression}
212
+ convertDotToTimes={convertDotToTimes}
213
+ divisionKey={isExpression}
214
+ trigonometry={isExpression}
215
+ preAlgebra={isExpression}
216
+ logarithms={isExpression}
217
+ basicRelations={isExpression}
218
+ advancedRelations={isExpression}
219
+ expandedView={
220
+ containerWidth > expandedViewThreshold
221
+ }
222
+ showDismiss
223
+ />
224
+ </View>
225
+ ) : null}
226
+ </AphroditeCssTransitionGroup>
232
227
  );
233
228
  }
234
229
  }
@@ -239,14 +234,6 @@ const styles = StyleSheet.create({
239
234
  left: 0,
240
235
  right: 0,
241
236
  position: "fixed",
242
- transitionProperty: "all",
243
- transition: `200ms ease-out`,
244
- visibility: "hidden",
245
- transform: "translate3d(0, 100%, 0)",
246
- },
247
- activeKeypadContainer: {
248
- transform: "translate3d(0, 0, 0)",
249
- visibility: "visible",
250
237
  },
251
238
  });
252
239