@khanacademy/math-input 0.4.1 → 0.5.2

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 (176) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/{build/math-input.css → dist/es/index.css} +0 -150
  4. package/dist/es/index.js +7798 -0
  5. package/dist/es/index.js.map +1 -0
  6. package/dist/index.css +586 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +7768 -0
  9. package/dist/index.js.flow +2 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/strings.js +71 -0
  12. package/index.html +20 -0
  13. package/less/echo.less +56 -0
  14. package/less/main.less +5 -0
  15. package/less/overrides.less +129 -0
  16. package/less/popover.less +22 -0
  17. package/less/tabbar.less +6 -0
  18. package/package.json +60 -89
  19. package/src/actions/index.js +57 -0
  20. package/src/components/__tests__/gesture-state-machine_test.js +437 -0
  21. package/src/components/__tests__/node-manager_test.js +89 -0
  22. package/src/components/__tests__/two-page-keypad_test.js +42 -0
  23. package/src/components/app.js +73 -0
  24. package/src/components/common-style.js +47 -0
  25. package/src/components/compute-layout-parameters.js +157 -0
  26. package/src/components/corner-decal.js +56 -0
  27. package/src/components/echo-manager.js +160 -0
  28. package/src/components/empty-keypad-button.js +49 -0
  29. package/src/components/expression-keypad.js +323 -0
  30. package/src/components/fraction-keypad.js +176 -0
  31. package/src/components/gesture-manager.js +226 -0
  32. package/src/components/gesture-state-machine.js +283 -0
  33. package/src/components/icon.js +74 -0
  34. package/src/components/iconography/arrow.js +22 -0
  35. package/src/components/iconography/backspace.js +29 -0
  36. package/src/components/iconography/cdot.js +29 -0
  37. package/src/components/iconography/cos.js +30 -0
  38. package/src/components/iconography/cube-root.js +36 -0
  39. package/src/components/iconography/dismiss.js +25 -0
  40. package/src/components/iconography/divide.js +34 -0
  41. package/src/components/iconography/down.js +16 -0
  42. package/src/components/iconography/equal.js +33 -0
  43. package/src/components/iconography/exp-2.js +29 -0
  44. package/src/components/iconography/exp-3.js +29 -0
  45. package/src/components/iconography/exp.js +29 -0
  46. package/src/components/iconography/frac.js +44 -0
  47. package/src/components/iconography/geq.js +33 -0
  48. package/src/components/iconography/gt.js +33 -0
  49. package/src/components/iconography/index.js +45 -0
  50. package/src/components/iconography/jump-into-numerator.js +41 -0
  51. package/src/components/iconography/jump-out-base.js +30 -0
  52. package/src/components/iconography/jump-out-denominator.js +41 -0
  53. package/src/components/iconography/jump-out-exponent.js +30 -0
  54. package/src/components/iconography/jump-out-numerator.js +41 -0
  55. package/src/components/iconography/jump-out-parentheses.js +33 -0
  56. package/src/components/iconography/left-paren.js +33 -0
  57. package/src/components/iconography/left.js +16 -0
  58. package/src/components/iconography/leq.js +33 -0
  59. package/src/components/iconography/ln.js +29 -0
  60. package/src/components/iconography/log-n.js +29 -0
  61. package/src/components/iconography/log.js +29 -0
  62. package/src/components/iconography/lt.js +33 -0
  63. package/src/components/iconography/minus.js +32 -0
  64. package/src/components/iconography/neq.js +33 -0
  65. package/src/components/iconography/parens.js +33 -0
  66. package/src/components/iconography/percent.js +49 -0
  67. package/src/components/iconography/period.js +26 -0
  68. package/src/components/iconography/plus.js +32 -0
  69. package/src/components/iconography/radical.js +36 -0
  70. package/src/components/iconography/right-paren.js +33 -0
  71. package/src/components/iconography/right.js +16 -0
  72. package/src/components/iconography/sin.js +30 -0
  73. package/src/components/iconography/sqrt.js +32 -0
  74. package/src/components/iconography/tan.js +30 -0
  75. package/src/components/iconography/times.js +33 -0
  76. package/src/components/iconography/up.js +16 -0
  77. package/src/components/input/__tests__/context-tracking_test.js +177 -0
  78. package/src/components/input/__tests__/math-wrapper.jsx +33 -0
  79. package/src/components/input/__tests__/mathquill_test.js +747 -0
  80. package/src/components/input/cursor-contexts.js +29 -0
  81. package/src/components/input/cursor-handle.js +137 -0
  82. package/src/components/input/drag-listener.js +75 -0
  83. package/src/components/input/math-input.js +924 -0
  84. package/src/components/input/math-wrapper.js +959 -0
  85. package/src/components/input/scroll-into-view.js +72 -0
  86. package/src/components/keypad/button-assets.js +492 -0
  87. package/src/components/keypad/button.js +106 -0
  88. package/src/components/keypad/button.stories.js +29 -0
  89. package/src/components/keypad/index.js +64 -0
  90. package/src/components/keypad/keypad-page-items.js +106 -0
  91. package/src/components/keypad/keypad-pages.stories.js +32 -0
  92. package/src/components/keypad/keypad.stories.js +35 -0
  93. package/src/components/keypad/numeric-input-page.js +100 -0
  94. package/src/components/keypad/pre-algebra-page.js +98 -0
  95. package/src/components/keypad/trigonometry-page.js +90 -0
  96. package/src/components/keypad-button.js +366 -0
  97. package/src/components/keypad-container.js +303 -0
  98. package/src/components/keypad.js +154 -0
  99. package/src/components/many-keypad-button.js +44 -0
  100. package/src/components/math-icon.js +65 -0
  101. package/src/components/multi-symbol-grid.js +182 -0
  102. package/src/components/multi-symbol-popover.js +59 -0
  103. package/src/components/navigation-pad.js +139 -0
  104. package/src/components/node-manager.js +129 -0
  105. package/src/components/popover-manager.js +76 -0
  106. package/src/components/popover-state-machine.js +173 -0
  107. package/src/components/prop-types.js +82 -0
  108. package/src/components/provided-keypad.js +103 -0
  109. package/src/components/styles.js +38 -0
  110. package/src/components/svg-icon.js +25 -0
  111. package/src/components/tabbar/__tests__/tabbar_test.js +65 -0
  112. package/src/components/tabbar/icons.js +69 -0
  113. package/src/components/tabbar/item.js +138 -0
  114. package/src/components/tabbar/tabbar.js +61 -0
  115. package/src/components/tabbar/tabbar.stories.js +60 -0
  116. package/src/components/tabbar/types.js +3 -0
  117. package/src/components/text-icon.js +52 -0
  118. package/src/components/touchable-keypad-button.js +146 -0
  119. package/src/components/two-page-keypad.js +99 -0
  120. package/src/components/velocity-tracker.js +76 -0
  121. package/src/components/z-indexes.js +9 -0
  122. package/src/consts.js +74 -0
  123. package/src/data/key-configs.js +349 -0
  124. package/src/data/keys.js +72 -0
  125. package/src/demo.js +8 -0
  126. package/src/fake-react-native-web/index.js +12 -0
  127. package/src/fake-react-native-web/text.js +56 -0
  128. package/src/fake-react-native-web/view.js +91 -0
  129. package/src/index.js +14 -0
  130. package/src/native-app.js +84 -0
  131. package/src/store/index.js +505 -0
  132. package/src/utils.js +18 -0
  133. package/tools/svg-to-react/convert.py +111 -0
  134. package/tools/svg-to-react/icons/math-keypad-icon-0.svg +32 -0
  135. package/tools/svg-to-react/icons/math-keypad-icon-1.svg +32 -0
  136. package/tools/svg-to-react/icons/math-keypad-icon-2.svg +32 -0
  137. package/tools/svg-to-react/icons/math-keypad-icon-3.svg +32 -0
  138. package/tools/svg-to-react/icons/math-keypad-icon-4.svg +32 -0
  139. package/tools/svg-to-react/icons/math-keypad-icon-5.svg +32 -0
  140. package/tools/svg-to-react/icons/math-keypad-icon-6.svg +32 -0
  141. package/tools/svg-to-react/icons/math-keypad-icon-7.svg +32 -0
  142. package/tools/svg-to-react/icons/math-keypad-icon-8.svg +32 -0
  143. package/tools/svg-to-react/icons/math-keypad-icon-9.svg +32 -0
  144. package/tools/svg-to-react/icons/math-keypad-icon-addition.svg +34 -0
  145. package/tools/svg-to-react/icons/math-keypad-icon-cos.svg +38 -0
  146. package/tools/svg-to-react/icons/math-keypad-icon-delete.svg +36 -0
  147. package/tools/svg-to-react/icons/math-keypad-icon-dismiss.svg +36 -0
  148. package/tools/svg-to-react/icons/math-keypad-icon-division.svg +36 -0
  149. package/tools/svg-to-react/icons/math-keypad-icon-equals-not.svg +50 -0
  150. package/tools/svg-to-react/icons/math-keypad-icon-equals.svg +48 -0
  151. package/tools/svg-to-react/icons/math-keypad-icon-exponent-2.svg +38 -0
  152. package/tools/svg-to-react/icons/math-keypad-icon-exponent-3.svg +38 -0
  153. package/tools/svg-to-react/icons/math-keypad-icon-exponent.svg +38 -0
  154. package/tools/svg-to-react/icons/math-keypad-icon-fraction.svg +42 -0
  155. package/tools/svg-to-react/icons/math-keypad-icon-greater-than.svg +46 -0
  156. package/tools/svg-to-react/icons/math-keypad-icon-jump-out-base.svg +44 -0
  157. package/tools/svg-to-react/icons/math-keypad-icon-jump-out-denominator.svg +48 -0
  158. package/tools/svg-to-react/icons/math-keypad-icon-jump-out-exponent.svg +44 -0
  159. package/tools/svg-to-react/icons/math-keypad-icon-jump-out-parentheses.svg +44 -0
  160. package/tools/svg-to-react/icons/math-keypad-icon-less-than.svg +46 -0
  161. package/tools/svg-to-react/icons/math-keypad-icon-log-10.svg +36 -0
  162. package/tools/svg-to-react/icons/math-keypad-icon-log-e.svg +36 -0
  163. package/tools/svg-to-react/icons/math-keypad-icon-log.svg +38 -0
  164. package/tools/svg-to-react/icons/math-keypad-icon-multiplication-cross.svg +40 -0
  165. package/tools/svg-to-react/icons/math-keypad-icon-multiplication-dot.svg +38 -0
  166. package/tools/svg-to-react/icons/math-keypad-icon-percent.svg +42 -0
  167. package/tools/svg-to-react/icons/math-keypad-icon-radical-2.svg +36 -0
  168. package/tools/svg-to-react/icons/math-keypad-icon-radical-3.svg +38 -0
  169. package/tools/svg-to-react/icons/math-keypad-icon-radical.svg +38 -0
  170. package/tools/svg-to-react/icons/math-keypad-icon-radix-character.svg +32 -0
  171. package/tools/svg-to-react/icons/math-keypad-icon-sin.svg +38 -0
  172. package/tools/svg-to-react/icons/math-keypad-icon-subtraction.svg +32 -0
  173. package/tools/svg-to-react/icons/math-keypad-icon-tan.svg +38 -0
  174. package/tools/svg-to-react/symbol_map.py +41 -0
  175. package/LICENSE.txt +0 -21
  176. package/build/math-input.js +0 -1
@@ -0,0 +1,437 @@
1
+ import GestureStateMachine from "../gesture-state-machine.js";
2
+
3
+ const swipeThresholdPx = 5;
4
+ const longPressWaitTimeMs = 5;
5
+ const holdIntervalMs = 5;
6
+
7
+ // Generates a set of handlers, to be passed to a GestureStateMachine instance,
8
+ // that track any callbacks, along with their arguments, by pushing to the
9
+ // provided buffer on call.
10
+ const eventTrackers = (buffer) => {
11
+ const handlers = {};
12
+ const callbackNames = [
13
+ "onBlur",
14
+ "onFocus",
15
+ "onTrigger",
16
+ "onTouchEnd",
17
+ "onLongPress",
18
+ "onSwipeChange",
19
+ "onSwipeEnd",
20
+ ];
21
+ callbackNames.forEach((callbackName) => {
22
+ handlers[callbackName] = function () {
23
+ buffer.push([callbackName, ...arguments]);
24
+ };
25
+ });
26
+ return handlers;
27
+ };
28
+
29
+ // Arbitrary node IDs (representative of arbitrary keys) to be used in testing.
30
+ const NodeIds = {
31
+ first: "first",
32
+ second: "second",
33
+ third: "third",
34
+ swipeDisabled: "swipeDisabled",
35
+ multiPressable: "multiPressable",
36
+ };
37
+
38
+ describe("GestureStateMachine", () => {
39
+ let eventBuffer;
40
+ let stateMachine;
41
+
42
+ beforeEach(() => {
43
+ eventBuffer = [];
44
+ stateMachine = new GestureStateMachine(
45
+ eventTrackers(eventBuffer),
46
+ {
47
+ swipeThresholdPx,
48
+ longPressWaitTimeMs,
49
+ holdIntervalMs,
50
+ },
51
+ [NodeIds.swipeDisabled],
52
+ [NodeIds.multiPressable],
53
+ );
54
+ });
55
+
56
+ const assertEvents = (expectedEvents) => {
57
+ expect(eventBuffer).toStrictEqual(expectedEvents);
58
+ };
59
+
60
+ it("should trigger a tap on a simple button", () => {
61
+ const touchId = 1;
62
+
63
+ // Trigger a touch start, followed immediately by a touch end.
64
+ stateMachine.onTouchStart(() => NodeIds.first, touchId, 0);
65
+ stateMachine.onTouchEnd(() => NodeIds.first, touchId, 0);
66
+
67
+ // Assert that we saw a focus and a touch end, in that order.
68
+ const expectedEvents = [
69
+ ["onFocus", NodeIds.first],
70
+ ["onTouchEnd", NodeIds.first],
71
+ ];
72
+ assertEvents(expectedEvents);
73
+ });
74
+
75
+ it("should shift focus to a new button on move", () => {
76
+ const touchId = 1;
77
+
78
+ // Trigger a touch start on one node before moving over another node and
79
+ // releasing.
80
+ stateMachine.onTouchStart(() => NodeIds.first, touchId, 0);
81
+ stateMachine.onTouchMove(() => NodeIds.second, touchId, 0);
82
+ stateMachine.onTouchEnd(() => NodeIds.second, touchId, 0);
83
+
84
+ // Assert that we saw a focus on both nodes.
85
+ const expectedEvents = [
86
+ ["onFocus", NodeIds.first],
87
+ ["onFocus", NodeIds.second],
88
+ ["onTouchEnd", NodeIds.second],
89
+ ];
90
+ assertEvents(expectedEvents);
91
+ });
92
+
93
+ it("should trigger a long press on hold", () => {
94
+ const touchId = 1;
95
+
96
+ /// Trigger a touch start.
97
+ stateMachine.onTouchStart(() => NodeIds.first, touchId, 0);
98
+
99
+ // Assert that we see a focus event immediately.
100
+ const initialExpectedEvents = [["onFocus", NodeIds.first]];
101
+ assertEvents(initialExpectedEvents);
102
+
103
+ jest.advanceTimersByTime(longPressWaitTimeMs);
104
+
105
+ const expectedEventsAfterLongPress = [
106
+ ...initialExpectedEvents,
107
+ ["onLongPress", NodeIds.first],
108
+ ];
109
+ assertEvents(expectedEventsAfterLongPress);
110
+
111
+ // Finish up the interaction.
112
+ stateMachine.onTouchEnd(() => NodeIds.first, touchId, 0);
113
+
114
+ // Assert that we still see a touch-end.
115
+ const expectedEventsAfterRelease = [
116
+ ...expectedEventsAfterLongPress,
117
+ ["onTouchEnd", NodeIds.first],
118
+ ];
119
+ assertEvents(expectedEventsAfterRelease);
120
+ });
121
+
122
+ it("should trigger multiple presses on hold", () => {
123
+ const touchId = 1;
124
+
125
+ // Trigger a touch start on the multi-pressable node.
126
+ stateMachine.onTouchStart(() => NodeIds.multiPressable, touchId, 0);
127
+
128
+ // Assert that we see an immediate focus and trigger.
129
+ const initialExpectedEvents = [
130
+ ["onFocus", NodeIds.multiPressable],
131
+ ["onTrigger", NodeIds.multiPressable],
132
+ ];
133
+ assertEvents(initialExpectedEvents);
134
+
135
+ jest.advanceTimersByTime(holdIntervalMs);
136
+
137
+ // Assert that we see an additional trigger after the delay.
138
+ const expectedEventsAfterHold = [
139
+ ...initialExpectedEvents,
140
+ ["onTrigger", NodeIds.multiPressable],
141
+ ];
142
+ assertEvents(expectedEventsAfterHold);
143
+
144
+ // Now release, and verify that we see a blur, but no touch-end.
145
+ stateMachine.onTouchEnd(() => NodeIds.multiPressable, touchId, 0);
146
+ const expectedEventsAfterRelease = [
147
+ ...expectedEventsAfterHold,
148
+ ["onBlur"],
149
+ ];
150
+ assertEvents(expectedEventsAfterRelease);
151
+ });
152
+
153
+ it("should be robust to multiple touch starts", () => {
154
+ const touchId = 1;
155
+
156
+ // Trigger a touch start on the multi-pressable node twice, because
157
+ // the webview was acting up.
158
+ stateMachine.onTouchStart(() => NodeIds.multiPressable, touchId, 0);
159
+ stateMachine.onTouchStart(() => NodeIds.multiPressable, touchId, 0);
160
+
161
+ // Assert that we see only one set of focus and triggers.
162
+ const initialExpectedEvents = [
163
+ ["onFocus", NodeIds.multiPressable],
164
+ ["onTrigger", NodeIds.multiPressable],
165
+ ];
166
+ assertEvents(initialExpectedEvents);
167
+
168
+ jest.advanceTimersByTime(holdIntervalMs);
169
+
170
+ // Assert that we see an additional trigger after the delay.
171
+ const expectedEventsAfterHold = [
172
+ ...initialExpectedEvents,
173
+ ["onTrigger", NodeIds.multiPressable],
174
+ ];
175
+ assertEvents(expectedEventsAfterHold);
176
+
177
+ // Now release, and verify that we see a blur, but no touch-end.
178
+ stateMachine.onTouchEnd(() => NodeIds.multiPressable, touchId, 0);
179
+ const expectedEventsAfterRelease = [
180
+ ...expectedEventsAfterHold,
181
+ ["onBlur"],
182
+ ];
183
+ assertEvents(expectedEventsAfterRelease);
184
+
185
+ jest.advanceTimersByTime(holdIntervalMs);
186
+ // Ensure the touch end cleaned it up, and that we didn't
187
+ // create multiple listeners.
188
+ assertEvents(expectedEventsAfterRelease);
189
+ });
190
+
191
+ /* Swiping. */
192
+
193
+ it("should transition to a swipe", () => {
194
+ const touchId = 1;
195
+
196
+ // Trigger a touch start, followed by a move past the swipe threshold.
197
+ const startX = 0;
198
+ const swipeDistancePx = swipeThresholdPx + 1;
199
+ stateMachine.onTouchStart(() => NodeIds.first, touchId, startX);
200
+ stateMachine.onTouchMove(
201
+ () => NodeIds.first,
202
+ touchId,
203
+ startX + swipeDistancePx,
204
+ true,
205
+ );
206
+ stateMachine.onTouchEnd(
207
+ () => NodeIds.first,
208
+ touchId,
209
+ startX + swipeDistancePx,
210
+ );
211
+
212
+ // Assert that the node is focused and then blurred before transitioning
213
+ // to a swipe.
214
+ const expectedEvents = [
215
+ ["onFocus", NodeIds.first],
216
+ ["onBlur"],
217
+ ["onSwipeChange", swipeDistancePx],
218
+ ["onSwipeEnd", swipeDistancePx],
219
+ ];
220
+ assertEvents(expectedEvents);
221
+ });
222
+
223
+ it("should not transition to a swipe when swiping is diabled", () => {
224
+ const touchId = 1;
225
+
226
+ // Trigger a touch start, followed by a move past the swipe threshold.
227
+ const startX = 0;
228
+ const swipeDistancePx = swipeThresholdPx + 1;
229
+ stateMachine.onTouchStart(() => NodeIds.first, touchId, startX);
230
+ stateMachine.onTouchMove(
231
+ () => NodeIds.first,
232
+ touchId,
233
+ startX + swipeDistancePx,
234
+ false,
235
+ );
236
+
237
+ // Assert that the node is focused but never blurred.
238
+ const expectedEvents = [["onFocus", NodeIds.first]];
239
+ assertEvents(expectedEvents);
240
+ });
241
+
242
+ it("should not transition to a swipe on drag from a locked key", () => {
243
+ const touchId = 1;
244
+
245
+ // Trigger a touch start, followed by a move past the swipe threshold.
246
+ const startX = 0;
247
+ const swipeDistancePx = swipeThresholdPx + 1;
248
+ stateMachine.onTouchStart(() => NodeIds.swipeDisabled, touchId, startX);
249
+ stateMachine.onTouchMove(
250
+ () => NodeIds.swipeDisabled,
251
+ touchId,
252
+ startX + swipeDistancePx,
253
+ true,
254
+ );
255
+
256
+ // Assert that the node is focused but never blurred.
257
+ const expectedEvents = [["onFocus", NodeIds.swipeDisabled]];
258
+ assertEvents(expectedEvents);
259
+ });
260
+
261
+ /* Multi-touch. */
262
+
263
+ it("should respect simultaneous taps by two fingers", () => {
264
+ const firstTouchId = 1;
265
+ const secondTouchId = 2;
266
+
267
+ // Tap down on the first node, then on the second node; then release
268
+ // on the second, and then the first.
269
+ stateMachine.onTouchStart(() => NodeIds.first, firstTouchId, 0);
270
+ stateMachine.onTouchStart(() => NodeIds.second, secondTouchId, 0);
271
+ stateMachine.onTouchEnd(() => NodeIds.second, secondTouchId, 0);
272
+ stateMachine.onTouchEnd(() => NodeIds.first, firstTouchId, 0);
273
+
274
+ // Assert that we saw a focus and a touch end, in that order.
275
+ const expectedEvents = [
276
+ ["onFocus", NodeIds.first],
277
+ ["onFocus", NodeIds.second],
278
+ ["onTouchEnd", NodeIds.second],
279
+ ["onTouchEnd", NodeIds.first],
280
+ ];
281
+ assertEvents(expectedEvents);
282
+ });
283
+
284
+ it("should ignore any additional touches when swiping", () => {
285
+ const firstTouchId = 1;
286
+ const secondTouchId = 2;
287
+ const thirdTouchId = 3;
288
+
289
+ // Tap down on the first node, then on the second node. Then use the
290
+ const startX = 0;
291
+ stateMachine.onTouchStart(() => NodeIds.first, firstTouchId, startX);
292
+ stateMachine.onTouchStart(() => NodeIds.second, secondTouchId, startX);
293
+
294
+ // Now, swipe with the second finger.
295
+ const swipeDistancePx = swipeThresholdPx + 1;
296
+ stateMachine.onTouchMove(
297
+ () => NodeIds.second,
298
+ secondTouchId,
299
+ startX + swipeDistancePx,
300
+ true,
301
+ );
302
+
303
+ const expectedEventsAfterSwipeStart = [
304
+ ["onFocus", NodeIds.first],
305
+ ["onFocus", NodeIds.second],
306
+ ["onBlur"],
307
+ ["onSwipeChange", startX + swipeDistancePx],
308
+ ];
309
+ assertEvents(expectedEventsAfterSwipeStart);
310
+
311
+ // Send some touch events via the non-swiping but active touch,
312
+ // simulating moving the finger over another node, and even moving it
313
+ // enough to swipe, before releasing.
314
+ stateMachine.onTouchMove(() => NodeIds.first, firstTouchId, 0);
315
+ stateMachine.onTouchMove(() => NodeIds.third, firstTouchId, 0);
316
+ stateMachine.onTouchMove(
317
+ () => NodeIds.third,
318
+ firstTouchId,
319
+ startX + swipeDistancePx,
320
+ true,
321
+ );
322
+ stateMachine.onTouchEnd(() => NodeIds.third, firstTouchId, 0);
323
+
324
+ // Assert that we see no new events.
325
+ assertEvents(expectedEventsAfterSwipeStart);
326
+
327
+ // Start a new touch event, over any node.
328
+ stateMachine.onTouchStart(() => NodeIds.first, thirdTouchId, 0);
329
+
330
+ // Assert that we still see no new events.
331
+ assertEvents(expectedEventsAfterSwipeStart);
332
+
333
+ // Finally, release with the second finger, which is mid-swipe.
334
+ stateMachine.onTouchEnd(
335
+ () => NodeIds.second,
336
+ secondTouchId,
337
+ startX + swipeDistancePx,
338
+ );
339
+ const expectedEventsAfterSwipeEnd = [
340
+ ...expectedEventsAfterSwipeStart,
341
+ ["onSwipeEnd", startX + swipeDistancePx],
342
+ ];
343
+ assertEvents(expectedEventsAfterSwipeEnd);
344
+ });
345
+
346
+ it("should track swipe displacement on a per-finger basis", () => {
347
+ const firstTouchId = 1;
348
+ const firstTouchStartX = 15;
349
+ const secondTouchId = 2;
350
+ const secondTouchStartX = firstTouchStartX + 2 * swipeThresholdPx;
351
+
352
+ // Kick off two separate touch gestures at positions separated by more
353
+ // than the swipe displacement.
354
+ stateMachine.onTouchStart(
355
+ () => NodeIds.first,
356
+ firstTouchId,
357
+ firstTouchStartX,
358
+ );
359
+ stateMachine.onTouchStart(
360
+ () => NodeIds.second,
361
+ secondTouchId,
362
+ secondTouchStartX,
363
+ );
364
+
365
+ // Move less than the swipe threshold with both fingers.
366
+ stateMachine.onTouchMove(
367
+ () => NodeIds.first,
368
+ firstTouchId,
369
+ firstTouchStartX + swipeThresholdPx - 1,
370
+ true,
371
+ );
372
+ stateMachine.onTouchMove(
373
+ () => NodeIds.second,
374
+ secondTouchId,
375
+ secondTouchStartX + swipeThresholdPx - 1,
376
+ true,
377
+ );
378
+
379
+ // Assert that we haven't started swiping--all we've done is focused the
380
+ // various nodes.
381
+ const initialExpectedEvents = [
382
+ ["onFocus", NodeIds.first],
383
+ ["onFocus", NodeIds.second],
384
+ ];
385
+ assertEvents(initialExpectedEvents);
386
+
387
+ // Swipe past the threshold with one finger.
388
+ const swipeDistancePx = swipeThresholdPx + 1;
389
+ stateMachine.onTouchMove(
390
+ () => NodeIds.first,
391
+ firstTouchId,
392
+ firstTouchStartX + swipeDistancePx,
393
+ true,
394
+ );
395
+ const expectedEventsAfterSwipeStart = [
396
+ ...initialExpectedEvents,
397
+ ["onBlur"],
398
+ ["onSwipeChange", swipeDistancePx],
399
+ ];
400
+ assertEvents(expectedEventsAfterSwipeStart);
401
+ });
402
+
403
+ it("should be robust to extraneous fingers", () => {
404
+ const firstTouchId = 1;
405
+ const firstTouchStartX = 15;
406
+ const secondTouchId = 2;
407
+ const secondTouchStartX = firstTouchStartX + 2 * swipeThresholdPx;
408
+
409
+ // The first finger initiates a gesture, but the second finger starts
410
+ // elsewhere on the screen and doesn't register a start...
411
+ stateMachine.onTouchStart(
412
+ () => NodeIds.first,
413
+ firstTouchId,
414
+ firstTouchStartX,
415
+ );
416
+
417
+ // Move the first finger, but less than the swipe threshold, and then
418
+ // start showing move events from the second finger (as it slides into
419
+ // the components we care about on screen)
420
+ stateMachine.onTouchMove(
421
+ () => NodeIds.first,
422
+ firstTouchId,
423
+ firstTouchStartX + swipeThresholdPx - 1,
424
+ true,
425
+ );
426
+ stateMachine.onTouchMove(
427
+ () => NodeIds.second,
428
+ secondTouchId,
429
+ secondTouchStartX,
430
+ true,
431
+ );
432
+
433
+ // Assert we've started focusing but haven't blown up.
434
+ const initialExpectedEvents = [["onFocus", NodeIds.first]];
435
+ assertEvents(initialExpectedEvents);
436
+ });
437
+ });
@@ -0,0 +1,89 @@
1
+ import NodeManager from "../node-manager.js";
2
+
3
+ describe("NodeManager", () => {
4
+ let nodeManager;
5
+
6
+ beforeEach(() => {
7
+ nodeManager = new NodeManager();
8
+ });
9
+
10
+ it("should register a single node with no children", () => {
11
+ const nodeId = "1";
12
+ nodeManager.registerDOMNode(nodeId, {}, []);
13
+
14
+ expect(nodeManager._nodesById[nodeId]).toBeTruthy();
15
+ expect(nodeManager._orderedIds.includes(nodeId)).toBeTruthy();
16
+ });
17
+
18
+ it("should register a single node with children", () => {
19
+ const nodeId = "1";
20
+ const childNodeIds = ["2", "3"];
21
+ nodeManager.registerDOMNode(nodeId, {}, childNodeIds);
22
+
23
+ expect(nodeManager._orderedIds.includes(nodeId)).toBeTruthy();
24
+ expect(nodeManager._nodesById[nodeId]).toBeTruthy();
25
+
26
+ for (const childId of childNodeIds) {
27
+ // The children should appear in the list of ordered IDs, but not
28
+ // in the list of registered nodes.
29
+ expect(!nodeManager._nodesById[childId]).toBeTruthy();
30
+ expect(nodeManager._orderedIds.includes(childId)).toBeTruthy();
31
+ }
32
+ });
33
+
34
+ it("should order children ahead of their parents", () => {
35
+ const nodeId = "1";
36
+ const childNodeIds = ["2", "3"];
37
+ nodeManager.registerDOMNode(nodeId, {}, childNodeIds);
38
+
39
+ const parentIndex = nodeManager._orderedIds.indexOf(nodeId);
40
+ for (const childId of childNodeIds) {
41
+ // The children should appear ahead of the parent in the ordered
42
+ // list.
43
+ const childIndex = nodeManager._orderedIds.indexOf(childId);
44
+ expect(childIndex < parentIndex).toBeTruthy();
45
+ }
46
+ });
47
+
48
+ it("should de-dupe the list of node IDs", () => {
49
+ const nodeId = "1";
50
+ const childNodeId = "2";
51
+
52
+ // Register both nodes.
53
+ nodeManager.registerDOMNode(nodeId, {}, [childNodeId]);
54
+ nodeManager.registerDOMNode(childNodeId, {}, []);
55
+
56
+ // Verify that both were added to the list of DOM nodes.
57
+ for (const id of [nodeId, childNodeId]) {
58
+ expect(nodeManager._nodesById[id]).toBeTruthy();
59
+ }
60
+
61
+ // Verify that the child is ahead of the parent, and only appears once.
62
+ expect(nodeManager._orderedIds).toStrictEqual([childNodeId, nodeId]);
63
+ });
64
+
65
+ it("should handle multiple sets of children", () => {
66
+ const firstNodeId = "1";
67
+ const firstNodeChildIds = ["2", "3"];
68
+ const secondNodeId = "4";
69
+ const secondNodeChildIds = ["5", "6"];
70
+ const nodeChildIdPairs = [
71
+ [firstNodeId, firstNodeChildIds],
72
+ [secondNodeId, secondNodeChildIds],
73
+ ];
74
+
75
+ for (const [nodeId, childNodeIds] of nodeChildIdPairs) {
76
+ nodeManager.registerDOMNode(nodeId, {}, childNodeIds);
77
+ }
78
+
79
+ for (const [nodeId, childNodeIds] of nodeChildIdPairs) {
80
+ const parentIndex = nodeManager._orderedIds.indexOf(nodeId);
81
+ for (const childId of childNodeIds) {
82
+ // The children should appear ahead of the parent in the
83
+ // ordered list.
84
+ const childIndex = nodeManager._orderedIds.indexOf(childId);
85
+ expect(childIndex < parentIndex).toBeTruthy();
86
+ }
87
+ }
88
+ });
89
+ });
@@ -0,0 +1,42 @@
1
+ import {mount} from "enzyme";
2
+ import * as React from "react";
3
+
4
+ import TwoPageKeypad from "../two-page-keypad.js";
5
+
6
+ describe("<TwoPageKeyPage />", () => {
7
+ xit("defaults to selecting the right page", () => {
8
+ // Arrange
9
+ const wrapper = mount(
10
+ <TwoPageKeypad
11
+ paginationEnabled={true}
12
+ currentPage={1}
13
+ leftPage={<p>Left Page</p>}
14
+ rightPage={<p>Right Page</p>}
15
+ />,
16
+ );
17
+
18
+ // Assert
19
+ const secondItem = wrapper.find("TabbarItem").at(0);
20
+ expect(secondItem).toHaveProp("itemState", "active");
21
+ });
22
+
23
+ xit("selects the second item", () => {
24
+ // Arrange
25
+ const wrapper = mount(
26
+ <TwoPageKeypad
27
+ paginationEnabled={true}
28
+ currentPage={0}
29
+ leftPage={<p>Left Page</p>}
30
+ rightPage={<p>Right Page</p>}
31
+ />,
32
+ );
33
+
34
+ // Act
35
+ let secondItem = wrapper.find("TabbarItem").at(1);
36
+ secondItem.simulate("click");
37
+
38
+ // Assert
39
+ secondItem = wrapper.find("TabbarItem").at(1);
40
+ expect(secondItem).toHaveProp("itemState", "active");
41
+ });
42
+ });
@@ -0,0 +1,73 @@
1
+ import {StyleSheet} from "aphrodite";
2
+ import * as React from "react";
3
+
4
+ import {View} from "../fake-react-native-web/index.js";
5
+ import {Keypad, KeypadInput, KeypadTypes} from "../index.js";
6
+
7
+ class App extends React.Component {
8
+ state = {
9
+ keypadElement: null,
10
+ value: "",
11
+ keypadType: KeypadTypes.EXPRESSION,
12
+ };
13
+
14
+ handleChange = (e) => {
15
+ this.state.keypadElement.configure({
16
+ keypadType: e.target.value,
17
+ extraKeys: ["x", "y", "PI", "THETA"],
18
+ });
19
+ this.setState({keypadType: e.target.value});
20
+ };
21
+
22
+ render() {
23
+ return (
24
+ <View>
25
+ <View style={styles.container}>
26
+ <KeypadInput
27
+ value={this.state.value}
28
+ keypadElement={this.state.keypadElement}
29
+ onChange={(value, cb) => this.setState({value}, cb)}
30
+ onFocus={() => this.state.keypadElement.activate()}
31
+ onBlur={() => this.state.keypadElement.dismiss()}
32
+ />
33
+ <View style={styles.selectContainer}>
34
+ Keypad type:
35
+ <select
36
+ onChange={this.handleChange}
37
+ value={this.state.keypadType}
38
+ >
39
+ <option value={KeypadTypes.FRACTION}>
40
+ FRACTION
41
+ </option>
42
+ <option value={KeypadTypes.EXPRESSION}>
43
+ EXPRESSION
44
+ </option>
45
+ </select>
46
+ </View>
47
+ </View>
48
+ <Keypad
49
+ onElementMounted={(node) => {
50
+ if (node && !this.state.keypadElement) {
51
+ this.setState({keypadElement: node});
52
+ }
53
+ }}
54
+ />
55
+ </View>
56
+ );
57
+ }
58
+ }
59
+
60
+ const styles = StyleSheet.create({
61
+ container: {
62
+ marginTop: 10,
63
+ marginLeft: 20,
64
+ marginRight: 20,
65
+ marginBottom: 40,
66
+ },
67
+ selectContainer: {
68
+ marginTop: 16,
69
+ flexDirection: "row",
70
+ },
71
+ });
72
+
73
+ export default App;
@@ -0,0 +1,47 @@
1
+ // @flow
2
+ /**
3
+ * Common parameters used to style components.
4
+ */
5
+ import Color from "@khanacademy/wonder-blocks-color";
6
+
7
+ export const wonderBlocksBlue = Color.blue;
8
+ export const offBlack = Color.offBlack;
9
+ export const offBlack32 = Color.offBlack32;
10
+ export const offBlack16 = Color.offBlack16;
11
+ export const offBlack8 = Color.offBlack8;
12
+
13
+ export const iconSizeHeightPx = 48;
14
+ export const iconSizeWidthPx = 48;
15
+ export const compactKeypadBorderRadiusPx = 4;
16
+ export const cursorHandleRadiusPx = 11;
17
+
18
+ // The amount to multiply the radius by to get the distance from the
19
+ // center to the tip of the cursor handle. The cursor is a circle with
20
+ // one quadrant replace with a square. The hypotenuse of the square is
21
+ // 1.045 times the radius of the circle.
22
+ export const cursorHandleDistanceMultiplier = 1.045;
23
+
24
+ // Keypad button colors
25
+ export const valueGrey = "#FFF";
26
+ export const operatorGrey = "#FAFAFA";
27
+ export const controlGrey = "#F6F7F7";
28
+ export const emptyGrey = "#F0F1F2";
29
+
30
+ // Constants defining any borders between elements in the keypad.
31
+ export const innerBorderColor = offBlack16;
32
+ export const innerBorderStyle = "solid";
33
+ export const innerBorderWidthPx = 1;
34
+
35
+ // The width at which a device is classified as a "tablet" for the purposes
36
+ // of the keypad layout.
37
+ export const tabletCutoffPx = 600;
38
+
39
+ // The dimensions that define various components in the tree, which may be
40
+ // needed outside of those components in order to determine various layout
41
+ // parameters.
42
+ export const pageIndicatorHeightPx = 16;
43
+ export const navigationPadWidthPx = 192;
44
+ // HACK(charlie): This should be injected by webapp somehow.
45
+ // TODO(charlie): Add a link to the webapp location as soon as the footer
46
+ // has settled down.
47
+ export const toolbarHeightPx = 60;