@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,226 @@
1
+ /**
2
+ * A high-level manager for our gesture system. In particular, this class
3
+ * connects our various bits of logic for managing gestures and interactions,
4
+ * and links them together.
5
+ */
6
+
7
+ import GestureStateMachine from "./gesture-state-machine.js";
8
+ import NodeManager from "./node-manager.js";
9
+ import PopoverStateMachine from "./popover-state-machine.js";
10
+
11
+ const coordsForEvent = (evt) => {
12
+ return [evt.changedTouches[0].clientX, evt.changedTouches[0].clientY];
13
+ };
14
+
15
+ class GestureManager {
16
+ constructor(options, handlers, disabledSwipeKeys, multiPressableKeys) {
17
+ const {swipeEnabled} = options;
18
+
19
+ this.swipeEnabled = swipeEnabled;
20
+
21
+ // Events aren't tracked until event tracking is enabled.
22
+ this.trackEvents = false;
23
+
24
+ this.nodeManager = new NodeManager();
25
+ this.popoverStateMachine = new PopoverStateMachine({
26
+ onActiveNodesChanged: (activeNodes) => {
27
+ const {popover, ...rest} = activeNodes;
28
+ handlers.onActiveNodesChanged({
29
+ popover: popover && {
30
+ parentId: popover.parentId,
31
+ bounds: this.nodeManager.layoutPropsForId(
32
+ popover.parentId,
33
+ ).initialBounds,
34
+ childKeyIds: popover.childIds,
35
+ },
36
+ ...rest,
37
+ });
38
+ },
39
+ /**
40
+ * `onClick` takes two arguments:
41
+ *
42
+ * @param {string} keyId - the identifier key that should initiate
43
+ * a click
44
+ * @param {string} domNodeId - the identifier of the DOM node on
45
+ * which the click should be considered
46
+ * to have occurred
47
+ * @param {bool} inPopover - whether the key was contained within a
48
+ * popover
49
+ *
50
+ * These two parameters will often be equivalent. They will differ,
51
+ * though, when a popover button is itself clicked, in which case
52
+ * we need to mimic the effects of clicking on its 'primary' child
53
+ * key, but animate the click on the popover button.
54
+ */
55
+ onClick: (keyId, domNodeId, inPopover) => {
56
+ handlers.onClick(
57
+ keyId,
58
+ this.nodeManager.layoutPropsForId(domNodeId),
59
+ inPopover,
60
+ );
61
+ },
62
+ });
63
+ this.gestureStateMachine = new GestureStateMachine(
64
+ {
65
+ onFocus: (id) => {
66
+ this.popoverStateMachine.onFocus(id);
67
+ },
68
+ onLongPress: (id) => {
69
+ this.popoverStateMachine.onLongPress(id);
70
+ },
71
+ onTouchEnd: (id) => {
72
+ this.popoverStateMachine.onTouchEnd(id);
73
+ },
74
+ onBlur: () => {
75
+ this.popoverStateMachine.onBlur();
76
+ },
77
+ onSwipeChange: handlers.onSwipeChange,
78
+ onSwipeEnd: handlers.onSwipeEnd,
79
+ onTrigger: (id) => {
80
+ this.popoverStateMachine.onTrigger(id);
81
+ },
82
+ },
83
+ {},
84
+ disabledSwipeKeys,
85
+ multiPressableKeys,
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Handle a touch-start event that originated in a node registered with the
91
+ * gesture system.
92
+ *
93
+ * @param {TouchEvent} evt - the raw touch event from the browser
94
+ * @param {string} id - the identifier of the DOM node in which the touch
95
+ * occurred
96
+ */
97
+ onTouchStart(evt, id) {
98
+ if (!this.trackEvents) {
99
+ return;
100
+ }
101
+
102
+ const [x] = coordsForEvent(evt);
103
+
104
+ // TODO(charlie): It doesn't seem to be guaranteed that every touch
105
+ // event on `changedTouches` originates from the node through which this
106
+ // touch event was sent. In that case, we'd be inappropriately reporting
107
+ // the starting node ID.
108
+ for (let i = 0; i < evt.changedTouches.length; i++) {
109
+ this.gestureStateMachine.onTouchStart(
110
+ () => id,
111
+ evt.changedTouches[i].identifier,
112
+ x,
113
+ );
114
+ }
115
+
116
+ // If an event started in a view that we're managing, we'll handle it
117
+ // all the way through.
118
+ evt.preventDefault();
119
+ }
120
+
121
+ /**
122
+ * Handle a touch-move event that originated in a node registered with the
123
+ * gesture system.
124
+ *
125
+ * @param {TouchEvent} evt - the raw touch event from the browser
126
+ */
127
+ onTouchMove(evt) {
128
+ if (!this.trackEvents) {
129
+ return;
130
+ }
131
+
132
+ const swipeLocked = this.popoverStateMachine.isPopoverVisible();
133
+ const swipeEnabled = this.swipeEnabled && !swipeLocked;
134
+ const [x, y] = coordsForEvent(evt);
135
+ for (let i = 0; i < evt.changedTouches.length; i++) {
136
+ this.gestureStateMachine.onTouchMove(
137
+ () => this.nodeManager.idForCoords(x, y),
138
+ evt.changedTouches[i].identifier,
139
+ x,
140
+ swipeEnabled,
141
+ );
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Handle a touch-end event that originated in a node registered with the
147
+ * gesture system.
148
+ *
149
+ * @param {TouchEvent} evt - the raw touch event from the browser
150
+ */
151
+ onTouchEnd(evt) {
152
+ if (!this.trackEvents) {
153
+ return;
154
+ }
155
+
156
+ const [x, y] = coordsForEvent(evt);
157
+ for (let i = 0; i < evt.changedTouches.length; i++) {
158
+ this.gestureStateMachine.onTouchEnd(
159
+ () => this.nodeManager.idForCoords(x, y),
160
+ evt.changedTouches[i].identifier,
161
+ x,
162
+ );
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Handle a touch-cancel event that originated in a node registered with the
168
+ * gesture system.
169
+ *
170
+ * @param {TouchEvent} evt - the raw touch event from the browser
171
+ */
172
+ onTouchCancel(evt) {
173
+ if (!this.trackEvents) {
174
+ return;
175
+ }
176
+
177
+ for (let i = 0; i < evt.changedTouches.length; i++) {
178
+ this.gestureStateMachine.onTouchCancel(
179
+ evt.changedTouches[i].identifier,
180
+ );
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Register a DOM node with a given identifier.
186
+ *
187
+ * @param {string} id - the identifier of the given node
188
+ * @param {node} domNode - the DOM node linked to the identifier
189
+ * @param {string[]} childIds - the identifiers of any DOM nodes that
190
+ * should be considered children of this node,
191
+ * in that they should take priority when
192
+ * intercepting touch events
193
+ * @param {object} borders - an opaque object describing the node's borders
194
+ */
195
+ registerDOMNode(id, domNode, childIds, borders) {
196
+ this.nodeManager.registerDOMNode(id, domNode, childIds, borders);
197
+ this.popoverStateMachine.registerPopover(id, childIds);
198
+ }
199
+
200
+ /**
201
+ * Unregister the DOM node with the given identifier.
202
+ *
203
+ * @param {string} id - the identifier of the node to unregister
204
+ */
205
+ unregisterDOMNode(id) {
206
+ this.nodeManager.unregisterDOMNode(id);
207
+ this.popoverStateMachine.unregisterPopover(id);
208
+ }
209
+
210
+ /**
211
+ * Enable event tracking for the gesture manager.
212
+ */
213
+ enableEventTracking() {
214
+ this.trackEvents = true;
215
+ }
216
+
217
+ /**
218
+ * Disable event tracking for the gesture manager. When called, the gesture
219
+ * manager will drop any events received by managed nodes.
220
+ */
221
+ disableEventTracking() {
222
+ this.trackEvents = false;
223
+ }
224
+ }
225
+
226
+ export default GestureManager;
@@ -0,0 +1,283 @@
1
+ /**
2
+ * The state machine that backs our gesture system. In particular, this state
3
+ * machine manages the interplay between focuses, touch ups, and swiping.
4
+ * It is entirely ignorant of the existence of popovers and the positions of
5
+ * DOM nodes, operating solely on IDs. The state machine does accommodate for
6
+ * multi-touch interactions, tracking gesture state on a per-touch basis.
7
+ */
8
+
9
+ const defaults = {
10
+ longPressWaitTimeMs: 50,
11
+ swipeThresholdPx: 20,
12
+ holdIntervalMs: 250,
13
+ };
14
+
15
+ class GestureStateMachine {
16
+ constructor(handlers, options, swipeDisabledNodeIds, multiPressableKeys) {
17
+ this.handlers = handlers;
18
+ this.options = {
19
+ ...defaults,
20
+ ...options,
21
+ };
22
+ this.swipeDisabledNodeIds = swipeDisabledNodeIds || [];
23
+ this.multiPressableKeys = multiPressableKeys || [];
24
+
25
+ // TODO(charlie): Flow-type this file. It's not great that we're now
26
+ // passing around these opaque state objects.
27
+ this.touchState = {};
28
+ this.swipeState = null;
29
+ }
30
+
31
+ _maybeCancelLongPressForTouch(touchId) {
32
+ const {longPressTimeoutId} = this.touchState[touchId];
33
+ if (longPressTimeoutId) {
34
+ clearTimeout(longPressTimeoutId);
35
+ this.touchState[touchId] = {
36
+ ...this.touchState[touchId],
37
+ longPressTimeoutId: null,
38
+ };
39
+ }
40
+ }
41
+
42
+ _maybeCancelPressAndHoldForTouch(touchId) {
43
+ const {pressAndHoldIntervalId} = this.touchState[touchId];
44
+ if (pressAndHoldIntervalId) {
45
+ // If there was an interval set to detect holds, clear it out.
46
+ clearInterval(pressAndHoldIntervalId);
47
+ this.touchState[touchId] = {
48
+ ...this.touchState[touchId],
49
+ pressAndHoldIntervalId: null,
50
+ };
51
+ }
52
+ }
53
+
54
+ _cleanupTouchEvent(touchId) {
55
+ this._maybeCancelLongPressForTouch(touchId);
56
+ this._maybeCancelPressAndHoldForTouch(touchId);
57
+ delete this.touchState[touchId];
58
+ }
59
+
60
+ /**
61
+ * Handle a focus event on the node with the given identifier, which may be
62
+ * `null` to indicate that the user has dragged their finger off of any
63
+ * registered nodes, but is still in the middle of a gesture.
64
+ *
65
+ * @param {string|null} id - the identifier of the newly focused node, or
66
+ * `null` if no node is focused
67
+ * @param {number} touchId - a unique identifier associated with the touch
68
+ */
69
+ _onFocus(id, touchId) {
70
+ // If we're in the middle of a long-press, cancel it.
71
+ this._maybeCancelLongPressForTouch(touchId);
72
+
73
+ // Reset any existing hold-detecting interval.
74
+ this._maybeCancelPressAndHoldForTouch(touchId);
75
+
76
+ // Set the focused node ID and handle the focus event.
77
+ // Note: we can call `onFocus` with `null` IDs. The semantics of an
78
+ // `onFocus` with a `null` ID differs from that of `onBlur`. The former
79
+ // indicates that a gesture that can focus future nodes is still in
80
+ // progress, but that no node is currently focused. The latter
81
+ // indicates that the gesture has ended and nothing will be focused.
82
+ this.touchState[touchId] = {
83
+ ...this.touchState[touchId],
84
+ activeNodeId: id,
85
+ };
86
+ this.handlers.onFocus(id);
87
+
88
+ if (id) {
89
+ // Handle logic for repeating button presses.
90
+ if (this.multiPressableKeys.includes(id)) {
91
+ // Start by triggering a click, iOS style.
92
+ this.handlers.onTrigger(id);
93
+
94
+ // Set up a new hold detector for the current button.
95
+ this.touchState[touchId] = {
96
+ ...this.touchState[touchId],
97
+ pressAndHoldIntervalId: setInterval(() => {
98
+ // On every cycle, trigger the click handler.
99
+ this.handlers.onTrigger(id);
100
+ }, this.options.holdIntervalMs),
101
+ };
102
+ } else {
103
+ // Set up a new hold detector for the current button.
104
+ this.touchState[touchId] = {
105
+ ...this.touchState[touchId],
106
+ longPressTimeoutId: setTimeout(() => {
107
+ this.handlers.onLongPress(id);
108
+ this.touchState[touchId] = {
109
+ ...this.touchState[touchId],
110
+ longPressTimeoutId: null,
111
+ };
112
+ }, this.options.longPressWaitTimeMs),
113
+ };
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Clear out all active gesture information.
120
+ */
121
+ _onSwipeStart() {
122
+ for (const activeTouchId of Object.keys(this.touchState)) {
123
+ this._maybeCancelLongPressForTouch(activeTouchId);
124
+ this._maybeCancelPressAndHoldForTouch(activeTouchId);
125
+ }
126
+ this.touchState = {};
127
+ this.handlers.onBlur();
128
+ }
129
+
130
+ /**
131
+ * A function that returns the identifier of the node over which the touch
132
+ * event occurred. This is provided as a piece of lazy computation, as
133
+ * computing the DOM node for a given point is expensive, and the state
134
+ * machine won't always need that information. For example, if the user is
135
+ * swiping, then `onTouchMove` needs to be performant and doesn't care about
136
+ * the node over which the touch occurred.
137
+ *
138
+ * @typedef idComputation
139
+ * @returns {DOMNode} - the identifier of the node over which the touch
140
+ * occurred
141
+ */
142
+
143
+ /**
144
+ * Handle a touch-start event on the node with the given identifer.
145
+ *
146
+ * @param {idComputation} getId - a function that returns identifier of the
147
+ * node over which the start event occurred
148
+ * @param {number} touchId - a unique identifier associated with the touch
149
+ */
150
+ onTouchStart(getId, touchId, pageX) {
151
+ // Ignore any touch events that start mid-swipe.
152
+ if (this.swipeState) {
153
+ return;
154
+ }
155
+
156
+ if (this.touchState[touchId]) {
157
+ // It turns out we can get multiple touch starts with no
158
+ // intervening move, end, or cancel events in Android WebViews.
159
+ // TODO(benkomalo): it's not entirely clear why this happens, but
160
+ // it seems to happen with the backspace button. It may be related
161
+ // to FastClick (https://github.com/ftlabs/fastclick/issues/71)
162
+ // though I haven't verified, and it's probably good to be robust
163
+ // here anyways.
164
+ return;
165
+ }
166
+
167
+ const startingNodeId = getId();
168
+ this.touchState[touchId] = {
169
+ swipeLocked: this.swipeDisabledNodeIds.includes(startingNodeId),
170
+ startX: pageX,
171
+ };
172
+
173
+ this._onFocus(startingNodeId, touchId);
174
+ }
175
+
176
+ /**
177
+ * Handle a touch-move event on the node with the given identifer.
178
+ *
179
+ * @param {idComputation} getId - a function that returns identifier of the
180
+ * node over which the move event occurred
181
+ * @param {number} touchId - a unique identifier associated with the touch
182
+ * @param {number} pageX - the x coordinate of the touch
183
+ * @param {boolean} swipeEnabled - whether the system should allow for
184
+ * transitions into a swiping state
185
+ */
186
+ onTouchMove(getId, touchId, pageX, swipeEnabled) {
187
+ if (this.swipeState) {
188
+ // Only respect the finger that started a swipe. Any other lingering
189
+ // gestures are ignored.
190
+ if (this.swipeState.touchId === touchId) {
191
+ this.handlers.onSwipeChange(pageX - this.swipeState.startX);
192
+ }
193
+ } else if (this.touchState[touchId]) {
194
+ // It could be touch events started outside the keypad and
195
+ // moved into it; ignore them.
196
+ const {activeNodeId, startX, swipeLocked} =
197
+ this.touchState[touchId];
198
+
199
+ const dx = pageX - startX;
200
+ const shouldBeginSwiping =
201
+ swipeEnabled &&
202
+ !swipeLocked &&
203
+ Math.abs(dx) > this.options.swipeThresholdPx;
204
+
205
+ if (shouldBeginSwiping) {
206
+ this._onSwipeStart();
207
+
208
+ // Trigger the swipe.
209
+ this.swipeState = {
210
+ touchId,
211
+ startX,
212
+ };
213
+ this.handlers.onSwipeChange(pageX - this.swipeState.startX);
214
+ } else {
215
+ const id = getId();
216
+ if (id !== activeNodeId) {
217
+ this._onFocus(id, touchId);
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Handle a touch-end event on the node with the given identifer.
225
+ *
226
+ * @param {idComputation} getId - a function that returns identifier of the
227
+ * node over which the end event occurred
228
+ * @param {number} touchId - a unique identifier associated with the touch
229
+ * @param {number} pageX - the x coordinate of the touch
230
+ */
231
+ onTouchEnd(getId, touchId, pageX) {
232
+ if (this.swipeState) {
233
+ // Only respect the finger that started a swipe. Any other lingering
234
+ // gestures are ignored.
235
+ if (this.swipeState.touchId === touchId) {
236
+ this.handlers.onSwipeEnd(pageX - this.swipeState.startX);
237
+ this.swipeState = null;
238
+ }
239
+ } else if (this.touchState[touchId]) {
240
+ // It could be touch events started outside the keypad and
241
+ // moved into it; ignore them.
242
+ const {activeNodeId, pressAndHoldIntervalId} =
243
+ this.touchState[touchId];
244
+
245
+ this._cleanupTouchEvent(touchId);
246
+
247
+ const didPressAndHold = !!pressAndHoldIntervalId;
248
+ if (didPressAndHold) {
249
+ // We don't trigger a touch end if there was a press and hold,
250
+ // because the key has been triggered at least once and calling
251
+ // the onTouchEnd handler would add an extra trigger.
252
+ this.handlers.onBlur();
253
+ } else {
254
+ // Trigger a touch-end. There's no need to notify clients of a
255
+ // blur as clients are responsible for handling any cleanup in
256
+ // their touch-end handlers.
257
+ this.handlers.onTouchEnd(activeNodeId);
258
+ }
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Handle a touch-cancel event.
264
+ */
265
+ onTouchCancel(touchId) {
266
+ // If a touch is cancelled and we're swiping, end the swipe with no
267
+ // displacement.
268
+ if (this.swipeState) {
269
+ if (this.swipeState.touchId === touchId) {
270
+ this.handlers.onSwipeEnd(0);
271
+ this.swipeState = null;
272
+ }
273
+ } else if (this.touchState[touchId]) {
274
+ // Otherwise, trigger a full blur. We don't want to trigger a
275
+ // touch-up, since the cancellation means that the user probably
276
+ // didn't release over a key intentionally.
277
+ this._cleanupTouchEvent(touchId);
278
+ this.handlers.onBlur();
279
+ }
280
+ }
281
+ }
282
+
283
+ export default GestureStateMachine;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * A component that renders an icon for a symbol with the given name.
3
+ */
4
+
5
+ import {StyleSheet} from "aphrodite";
6
+ import PropTypes from "prop-types";
7
+ import * as React from "react";
8
+
9
+ import {IconTypes} from "../consts.js";
10
+
11
+ import {offBlack} from "./common-style.js";
12
+ import MathIcon from "./math-icon.js";
13
+ import {iconPropType} from "./prop-types.js";
14
+ import SvgIcon from "./svg-icon.js";
15
+ import TextIcon from "./text-icon.js";
16
+
17
+ const focusedColor = "#FFF";
18
+ const unfocusedColor = offBlack;
19
+
20
+ class Icon extends React.PureComponent {
21
+ static propTypes = {
22
+ focused: PropTypes.bool,
23
+ icon: iconPropType.isRequired,
24
+ // An Aphrodite style object, or an array of Aphrodite style objects.
25
+ // Note that custom styles will only be applied to text and math icons
26
+ // (and not SVG icons).
27
+ style: PropTypes.any,
28
+ };
29
+
30
+ render() {
31
+ const {focused, icon, style} = this.props;
32
+
33
+ const styleWithFocus = [
34
+ focused ? styles.focused : styles.unfocused,
35
+ ...(Array.isArray(style) ? style : [style]),
36
+ ];
37
+
38
+ switch (icon.type) {
39
+ case IconTypes.MATH:
40
+ return <MathIcon math={icon.data} style={styleWithFocus} />;
41
+
42
+ case IconTypes.SVG:
43
+ // TODO(charlie): Support passing style objects to `SvgIcon`.
44
+ // This will require migrating the individual icons to use
45
+ // `currentColor` and accept a `className` prop, rather than
46
+ // relying on an explicit color prop.
47
+ return (
48
+ <SvgIcon
49
+ name={icon.data}
50
+ color={focused ? focusedColor : unfocusedColor}
51
+ />
52
+ );
53
+
54
+ case IconTypes.TEXT:
55
+ return (
56
+ <TextIcon character={icon.data} style={styleWithFocus} />
57
+ );
58
+ }
59
+
60
+ throw new Error("No icon or symbol provided");
61
+ }
62
+ }
63
+
64
+ const styles = StyleSheet.create({
65
+ unfocused: {
66
+ color: unfocusedColor,
67
+ },
68
+
69
+ focused: {
70
+ color: focusedColor,
71
+ },
72
+ });
73
+
74
+ export default Icon;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * An arrow icon, used by the other navigational keys.
3
+ */
4
+ import * as React from "react";
5
+
6
+ const Arrow = (props) => {
7
+ return (
8
+ <g fill="none" fillRule="evenodd" {...props}>
9
+ <path fill="none" d="M0 0h48v48H0z" />
10
+ <path fill="none" d="M12 12h24v24H12z" />
11
+ <path
12
+ stroke="#888D93"
13
+ strokeWidth="2"
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ d="M22 18l-6 6 6 6M16 24h16"
17
+ />
18
+ </g>
19
+ );
20
+ };
21
+
22
+ export default Arrow;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * An autogenerated component that renders the BACKSPACE iconograpy in SVG.
3
+ *
4
+ * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590.
5
+ */
6
+ import * as React from "react";
7
+
8
+ const Backspace = () => {
9
+ return (
10
+ <svg width="48" height="48" viewBox="0 0 48 48">
11
+ <g fill="none" fillRule="evenodd">
12
+ <path fill="none" d="M0 0h48v48H0z" />
13
+ <path
14
+ d="M13 24l6 6h14V18H19l-6 6zm-1.414-1.414l6-6A2 2 0 0 1 19 16h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H19a2 2 0 0 1-1.414-.586l-6-6a2 2 0 0 1 0-2.828z"
15
+ fill="#888D93"
16
+ />
17
+ <path
18
+ d="M23 21l6 6M29 21l-6 6"
19
+ stroke="#888D93"
20
+ strokeWidth="2"
21
+ strokeLinecap="round"
22
+ strokeLinejoin="round"
23
+ />
24
+ </g>
25
+ </svg>
26
+ );
27
+ };
28
+
29
+ export default Backspace;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * An autogenerated component that renders the CDOT iconograpy in SVG.
3
+ *
4
+ * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590.
5
+ */
6
+ import PropTypes from "prop-types";
7
+ import * as React from "react";
8
+
9
+ class Cdot extends React.Component {
10
+ static propTypes = {
11
+ color: PropTypes.string.isRequired,
12
+ };
13
+
14
+ render() {
15
+ return (
16
+ <svg width="48" height="48" viewBox="0 0 48 48">
17
+ <g fill="none" fillRule="evenodd">
18
+ <path fill="none" d="M0 0h48v48H0z" />
19
+ <g transform="translate(12 12)">
20
+ <path fill="none" d="M0 0h24v24H0z" />
21
+ <circle fill={this.props.color} cx="12" cy="12" r="3" />
22
+ </g>
23
+ </g>
24
+ </svg>
25
+ );
26
+ }
27
+ }
28
+
29
+ export default Cdot;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * An autogenerated component that renders the COS iconograpy in SVG.
3
+ *
4
+ * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590.
5
+ */
6
+ import PropTypes from "prop-types";
7
+ import * as React from "react";
8
+
9
+ class Cos extends React.Component {
10
+ static propTypes = {
11
+ color: PropTypes.string.isRequired,
12
+ };
13
+
14
+ render() {
15
+ return (
16
+ <svg width="48" height="48" viewBox="0 0 48 48">
17
+ <g fill="none" fillRule="evenodd">
18
+ <path fill="none" d="M0 0h48v48H0z" />
19
+ <path fill="none" d="M13 12h24v24H13z" />
20
+ <path
21
+ d="M18.182 29.168c1.386 0 2.226-.602 2.674-1.232l-1.162-1.078a1.707 1.707 0 0 1-1.428.728c-1.078 0-1.834-.798-1.834-1.974s.756-1.96 1.834-1.96c.616 0 1.106.252 1.428.728l1.162-1.092c-.448-.616-1.288-1.218-2.674-1.218-2.086 0-3.584 1.47-3.584 3.542 0 2.086 1.498 3.556 3.584 3.556zm6.972 0c2.24 0 3.584-1.624 3.584-3.556 0-1.918-1.344-3.542-3.584-3.542-2.226 0-3.57 1.624-3.57 3.542 0 1.932 1.344 3.556 3.57 3.556zm0-1.582c-1.106 0-1.722-.91-1.722-1.974 0-1.05.616-1.96 1.722-1.96 1.106 0 1.736.91 1.736 1.96 0 1.064-.63 1.974-1.736 1.974zm7.336 1.582c1.876 0 2.926-.938 2.926-2.17 0-2.73-4.004-1.89-4.004-2.898 0-.378.42-.672 1.064-.672.826 0 1.596.35 2.002.784l.714-1.218c-.672-.532-1.582-.924-2.73-.924-1.778 0-2.772.994-2.772 2.128 0 2.66 4.018 1.75 4.018 2.87 0 .42-.364.728-1.134.728-.84 0-1.848-.462-2.338-.924l-.77 1.246c.714.658 1.848 1.05 3.024 1.05z"
22
+ fill={this.props.color}
23
+ />
24
+ </g>
25
+ </svg>
26
+ );
27
+ }
28
+ }
29
+
30
+ export default Cos;