@khanacademy/math-input 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/CHANGELOG.md +8 -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 +2 -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 +2 -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 +38 -70
  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 +27 -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 +99 -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 +13 -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,366 @@
1
+ /**
2
+ * A component that renders a keypad button.
3
+ */
4
+
5
+ import {StyleSheet, css} from "aphrodite";
6
+ import PropTypes from "prop-types";
7
+ import * as React from "react";
8
+ import {connect} from "react-redux";
9
+
10
+ import {KeyTypes, BorderDirections, BorderStyles} from "../consts.js";
11
+ import {View} from "../fake-react-native-web/index.js";
12
+
13
+ import {
14
+ wonderBlocksBlue,
15
+ innerBorderColor,
16
+ innerBorderStyle,
17
+ innerBorderWidthPx,
18
+ valueGrey,
19
+ operatorGrey,
20
+ controlGrey,
21
+ emptyGrey,
22
+ } from "./common-style.js";
23
+ import CornerDecal from "./corner-decal.js";
24
+ import Icon from "./icon.js";
25
+ import MultiSymbolGrid from "./multi-symbol-grid.js";
26
+ import {
27
+ bordersPropType,
28
+ iconPropType,
29
+ keyConfigPropType,
30
+ } from "./prop-types.js";
31
+
32
+ // eslint-disable-next-line react/no-unsafe
33
+ class KeypadButton extends React.PureComponent {
34
+ static propTypes = {
35
+ ariaLabel: PropTypes.string,
36
+ // The borders to display on the button. Typically, this should be set
37
+ // using one of the preset `BorderStyles` options.
38
+ borders: bordersPropType,
39
+ // Any additional keys that can be accessed by long-pressing on the
40
+ // button.
41
+ childKeys: PropTypes.arrayOf(keyConfigPropType),
42
+ // Whether the button should be rendered in a 'disabled' state, i.e.,
43
+ // without any touch feedback.
44
+ disabled: PropTypes.bool,
45
+ focused: PropTypes.bool,
46
+ heightPx: PropTypes.number.isRequired,
47
+ icon: iconPropType,
48
+ onTouchCancel: PropTypes.func,
49
+ onTouchEnd: PropTypes.func,
50
+ onTouchMove: PropTypes.func,
51
+ onTouchStart: PropTypes.func,
52
+ popoverEnabled: PropTypes.bool,
53
+ style: PropTypes.any,
54
+ type: PropTypes.oneOf(Object.keys(KeyTypes)).isRequired,
55
+ // NOTE(charlie): We may want to make this optional for phone layouts
56
+ // (and rely on Flexbox instead), since it might not be pixel perfect
57
+ // with borders and such.
58
+ widthPx: PropTypes.number.isRequired,
59
+ };
60
+
61
+ static defaultProps = {
62
+ borders: BorderStyles.ALL,
63
+ childKeys: [],
64
+ disabled: false,
65
+ focused: false,
66
+ popoverEnabled: false,
67
+ };
68
+
69
+ UNSAFE_componentWillMount() {
70
+ this.buttonSizeStyle = styleForButtonDimensions(
71
+ this.props.heightPx,
72
+ this.props.widthPx,
73
+ );
74
+ }
75
+
76
+ componentDidMount() {
77
+ this._preInjectStyles();
78
+ }
79
+
80
+ UNSAFE_componentWillUpdate(newProps, newState) {
81
+ // Only recompute the Aphrodite StyleSheet when the button height has
82
+ // changed. Though it is safe to recompute the StyleSheet (since
83
+ // they're content-addressable), it saves us a bunch of hashing and
84
+ // other work to cache it here.
85
+ if (
86
+ newProps.heightPx !== this.props.heightPx ||
87
+ newProps.widthPx !== this.props.widthPx
88
+ ) {
89
+ this.buttonSizeStyle = styleForButtonDimensions(
90
+ newProps.heightPx,
91
+ newProps.widthPx,
92
+ );
93
+
94
+ this._preInjectStyles();
95
+ }
96
+ }
97
+
98
+ _preInjectStyles = () => {
99
+ // HACK(charlie): Pre-inject all of the possible styles for the button.
100
+ // This avoids a flickering effect in the echo animation whereby the
101
+ // echoes vary in size as they animate. Note that we need to account for
102
+ // the "initial" styles that `View` will include, as these styles are
103
+ // applied to `View` components and Aphrodite will consolidate the style
104
+ // object. This method must be called whenever a property that
105
+ // influences the possible outcomes of `this._getFocusStyle` and
106
+ // `this._getButtonStyle` changes (such as `this.buttonSizeStyle`).
107
+ for (const type of Object.keys(KeyTypes)) {
108
+ css(View.styles.initial, ...this._getFocusStyle(type));
109
+
110
+ for (const borders of Object.values(BorderStyles)) {
111
+ css(
112
+ View.styles.initial,
113
+ ...this._getButtonStyle(type, borders),
114
+ );
115
+ }
116
+ }
117
+ };
118
+
119
+ _getFocusStyle = (type) => {
120
+ let focusBackgroundStyle;
121
+ if (
122
+ type === KeyTypes.INPUT_NAVIGATION ||
123
+ type === KeyTypes.KEYPAD_NAVIGATION
124
+ ) {
125
+ focusBackgroundStyle = styles.light;
126
+ } else {
127
+ focusBackgroundStyle = styles.bright;
128
+ }
129
+
130
+ return [styles.focusBox, focusBackgroundStyle];
131
+ };
132
+
133
+ _getButtonStyle = (type, borders, style) => {
134
+ // Select the appropriate style for the button.
135
+ let backgroundStyle;
136
+ switch (type) {
137
+ case KeyTypes.EMPTY:
138
+ backgroundStyle = styles.empty;
139
+ break;
140
+
141
+ case KeyTypes.MANY:
142
+ case KeyTypes.VALUE:
143
+ backgroundStyle = styles.value;
144
+ break;
145
+
146
+ case KeyTypes.OPERATOR:
147
+ backgroundStyle = styles.operator;
148
+ break;
149
+
150
+ case KeyTypes.INPUT_NAVIGATION:
151
+ case KeyTypes.KEYPAD_NAVIGATION:
152
+ backgroundStyle = styles.control;
153
+ break;
154
+
155
+ case KeyTypes.ECHO:
156
+ backgroundStyle = null;
157
+ break;
158
+ }
159
+
160
+ const borderStyle = [];
161
+ if (borders.indexOf(BorderDirections.LEFT) !== -1) {
162
+ borderStyle.push(styles.leftBorder);
163
+ }
164
+ if (borders.indexOf(BorderDirections.BOTTOM) !== -1) {
165
+ borderStyle.push(styles.bottomBorder);
166
+ }
167
+
168
+ return [
169
+ styles.buttonBase,
170
+ backgroundStyle,
171
+ ...borderStyle,
172
+ type === KeyTypes.ECHO && styles.echo,
173
+ this.buttonSizeStyle,
174
+ // React Native allows you to set the 'style' props on user defined
175
+ // components.
176
+ // See: https://facebook.github.io/react-native/docs/style.html
177
+ ...(Array.isArray(style) ? style : [style]),
178
+ ];
179
+ };
180
+
181
+ render() {
182
+ const {
183
+ ariaLabel,
184
+ borders,
185
+ childKeys,
186
+ disabled,
187
+ focused,
188
+ icon,
189
+ onTouchCancel,
190
+ onTouchEnd,
191
+ onTouchMove,
192
+ onTouchStart,
193
+ popoverEnabled,
194
+ style,
195
+ type,
196
+ } = this.props;
197
+
198
+ // We render in the focus state if the key is focused, or if it's an
199
+ // echo.
200
+ const renderFocused =
201
+ (!disabled && focused) || popoverEnabled || type === KeyTypes.ECHO;
202
+ const buttonStyle = this._getButtonStyle(type, borders, style);
203
+ const focusStyle = this._getFocusStyle(type);
204
+ const iconWrapperStyle = [
205
+ styles.iconWrapper,
206
+ disabled && styles.disabled,
207
+ ];
208
+
209
+ const eventHandlers = {
210
+ onTouchCancel,
211
+ onTouchEnd,
212
+ onTouchMove,
213
+ onTouchStart,
214
+ };
215
+
216
+ const maybeFocusBox = renderFocused && <View style={focusStyle} />;
217
+ const maybeCornerDecal = !renderFocused &&
218
+ !disabled &&
219
+ childKeys &&
220
+ childKeys.length > 0 && <CornerDecal style={styles.decalInset} />;
221
+
222
+ if (type === KeyTypes.EMPTY) {
223
+ return <View style={buttonStyle} {...eventHandlers} />;
224
+ } else if (type === KeyTypes.MANY) {
225
+ // TODO(charlie): Make the long-press interaction accessible. See
226
+ // the TODO in key-configs.js for more.
227
+ const manyButtonA11yMarkup = {
228
+ role: "button",
229
+ ariaLabel: childKeys[0].ariaLabel,
230
+ };
231
+ const icons = childKeys.map((keyConfig) => {
232
+ return keyConfig.icon;
233
+ });
234
+ return (
235
+ <View
236
+ style={buttonStyle}
237
+ {...eventHandlers}
238
+ {...manyButtonA11yMarkup}
239
+ >
240
+ {maybeFocusBox}
241
+ <View style={iconWrapperStyle}>
242
+ <MultiSymbolGrid
243
+ icons={icons}
244
+ focused={renderFocused}
245
+ />
246
+ </View>
247
+ {maybeCornerDecal}
248
+ </View>
249
+ );
250
+ } else {
251
+ const a11yMarkup = {
252
+ role: "button",
253
+ ariaLabel: ariaLabel,
254
+ };
255
+
256
+ return (
257
+ <View style={buttonStyle} {...eventHandlers} {...a11yMarkup}>
258
+ {maybeFocusBox}
259
+ <View style={iconWrapperStyle}>
260
+ <Icon icon={icon} focused={renderFocused} />
261
+ </View>
262
+ {maybeCornerDecal}
263
+ </View>
264
+ );
265
+ }
266
+ }
267
+ }
268
+
269
+ const focusInsetPx = 4;
270
+ const focusBoxZIndex = 0;
271
+
272
+ const styles = StyleSheet.create({
273
+ buttonBase: {
274
+ // HACK(benkomalo): support old style flex box in Android browsers
275
+ "-webkit-box-flex": "1",
276
+ flex: 1,
277
+ cursor: "pointer",
278
+ // Make the text unselectable
279
+ userSelect: "none",
280
+ justifyContent: "center",
281
+ alignItems: "center",
282
+ // Borders are made selectively visible.
283
+ borderColor: innerBorderColor,
284
+ borderStyle: innerBorderStyle,
285
+ boxSizing: "border-box",
286
+ },
287
+
288
+ decalInset: {
289
+ top: focusInsetPx,
290
+ right: focusInsetPx,
291
+ },
292
+
293
+ // Overrides for the echo state, where we want to render the borders for
294
+ // layout purposes, but we don't want them to be visible.
295
+ echo: {
296
+ borderColor: "transparent",
297
+ },
298
+
299
+ // Background colors and other base styles that may vary between key types.
300
+ value: {
301
+ backgroundColor: valueGrey,
302
+ },
303
+ operator: {
304
+ backgroundColor: operatorGrey,
305
+ },
306
+ control: {
307
+ backgroundColor: controlGrey,
308
+ },
309
+ empty: {
310
+ backgroundColor: emptyGrey,
311
+ cursor: "default",
312
+ },
313
+
314
+ bright: {
315
+ backgroundColor: wonderBlocksBlue,
316
+ },
317
+ light: {
318
+ backgroundColor: "rgba(33, 36, 44, 0.1)",
319
+ },
320
+
321
+ iconWrapper: {
322
+ zIndex: focusBoxZIndex + 1,
323
+ },
324
+
325
+ focusBox: {
326
+ position: "absolute",
327
+ zIndex: focusBoxZIndex,
328
+ left: focusInsetPx,
329
+ right: focusInsetPx,
330
+ bottom: focusInsetPx,
331
+ top: focusInsetPx,
332
+ borderRadius: 1,
333
+ },
334
+
335
+ disabled: {
336
+ opacity: 0.3,
337
+ },
338
+
339
+ // Styles used to render the appropriate borders. Buttons are only allowed
340
+ // to render left and bottom borders, to simplify layout.
341
+ leftBorder: {
342
+ borderLeftWidth: innerBorderWidthPx,
343
+ },
344
+ bottomBorder: {
345
+ borderBottomWidth: innerBorderWidthPx,
346
+ },
347
+ });
348
+
349
+ const styleForButtonDimensions = (heightPx, widthPx) => {
350
+ return StyleSheet.create({
351
+ // eslint-disable-next-line react-native/no-unused-styles
352
+ buttonSize: {
353
+ height: heightPx,
354
+ width: widthPx,
355
+ maxWidth: widthPx,
356
+ },
357
+ }).buttonSize;
358
+ };
359
+
360
+ const mapStateToProps = (state) => {
361
+ return state.layout.buttonDimensions;
362
+ };
363
+
364
+ export default connect(mapStateToProps, null, null, {forwardRef: true})(
365
+ KeypadButton,
366
+ );
@@ -0,0 +1,303 @@
1
+ import {StyleSheet} from "aphrodite";
2
+ import PropTypes from "prop-types";
3
+ import * as React from "react";
4
+ import {connect} from "react-redux";
5
+
6
+ import {setPageSize} from "../actions/index.js";
7
+ import {KeypadTypes, LayoutModes} from "../consts.js";
8
+ import {View} from "../fake-react-native-web/index.js";
9
+
10
+ import {
11
+ innerBorderColor,
12
+ innerBorderStyle,
13
+ innerBorderWidthPx,
14
+ compactKeypadBorderRadiusPx,
15
+ } from "./common-style.js";
16
+ import ExpressionKeypad from "./expression-keypad.js";
17
+ import FractionKeypad from "./fraction-keypad.js";
18
+ import NavigationPad from "./navigation-pad.js";
19
+ import {keyIdPropType} from "./prop-types.js";
20
+ import Styles from "./styles.js";
21
+ import * as zIndexes from "./z-indexes.js";
22
+
23
+ const {row, centered, fullWidth} = Styles;
24
+
25
+ // eslint-disable-next-line react/no-unsafe
26
+ class KeypadContainer extends React.Component {
27
+ static propTypes = {
28
+ active: PropTypes.bool,
29
+ extraKeys: PropTypes.arrayOf(keyIdPropType),
30
+ keypadType: PropTypes.oneOf(Object.keys(KeypadTypes)).isRequired,
31
+ layoutMode: PropTypes.oneOf(Object.keys(LayoutModes)).isRequired,
32
+ navigationPadEnabled: PropTypes.bool.isRequired,
33
+ onDismiss: PropTypes.func,
34
+ // A callback that should be triggered with the root React element on
35
+ // mount.
36
+ onElementMounted: PropTypes.func,
37
+ onPageSizeChange: PropTypes.func.isRequired,
38
+ style: PropTypes.any,
39
+ };
40
+
41
+ state = {
42
+ hasBeenActivated: false,
43
+ viewportWidth: "100vw",
44
+ };
45
+
46
+ UNSAFE_componentWillMount() {
47
+ if (this.props.active) {
48
+ this.setState({
49
+ hasBeenActivated: this.props.active,
50
+ });
51
+ }
52
+ }
53
+
54
+ componentDidMount() {
55
+ // Relay the initial size metrics.
56
+ this._onResize();
57
+
58
+ // And update it on resize.
59
+ window.addEventListener("resize", this._throttleResizeHandler);
60
+ window.addEventListener(
61
+ "orientationchange",
62
+ this._throttleResizeHandler,
63
+ );
64
+ }
65
+
66
+ UNSAFE_componentWillReceiveProps(nextProps) {
67
+ if (!this.state.hasBeenActivated && nextProps.active) {
68
+ this.setState({
69
+ hasBeenActivated: true,
70
+ });
71
+ }
72
+ }
73
+
74
+ componentDidUpdate(prevProps) {
75
+ if (prevProps.active && !this.props.active) {
76
+ this.props.onDismiss && this.props.onDismiss();
77
+ }
78
+ }
79
+
80
+ componentWillUnmount() {
81
+ window.removeEventListener("resize", this._throttleResizeHandler);
82
+ window.removeEventListener(
83
+ "orientationchange",
84
+ this._throttleResizeHandler,
85
+ );
86
+ }
87
+
88
+ _throttleResizeHandler = () => {
89
+ // Throttle the resize callbacks.
90
+ // https://developer.mozilla.org/en-US/docs/Web/Events/resize
91
+ if (this._resizeTimeout == null) {
92
+ this._resizeTimeout = setTimeout(() => {
93
+ this._resizeTimeout = null;
94
+
95
+ this._onResize();
96
+ }, 66);
97
+ }
98
+ };
99
+
100
+ _onResize = () => {
101
+ // Whenever the page resizes, we need to force an update, as the button
102
+ // heights and keypad width are computed based on horizontal space.
103
+ this.setState({
104
+ viewportWidth: window.innerWidth,
105
+ });
106
+
107
+ this.props.onPageSizeChange(window.innerWidth, window.innerHeight);
108
+ };
109
+
110
+ renderKeypad = () => {
111
+ const {extraKeys, keypadType, layoutMode, navigationPadEnabled} =
112
+ this.props;
113
+
114
+ const keypadProps = {
115
+ extraKeys,
116
+ // HACK(charlie): In order to properly round the corners of the
117
+ // compact keypad, we need to instruct some of our child views to
118
+ // crop themselves. At least we're colocating all the layout
119
+ // information in this component, though.
120
+ roundTopLeft:
121
+ layoutMode === LayoutModes.COMPACT && !navigationPadEnabled,
122
+ roundTopRight: layoutMode === LayoutModes.COMPACT,
123
+ };
124
+
125
+ // Select the appropriate keyboard given the type.
126
+ // TODO(charlie): In the future, we might want to move towards a
127
+ // data-driven approach to defining keyboard layouts, and have a
128
+ // generic keyboard that takes some "keyboard data" and renders it.
129
+ // However, the keyboards differ pretty heavily right now and it's not
130
+ // clear what that format would look like exactly. Plus, there aren't
131
+ // very many of them. So to keep us moving, we'll just hardcode.
132
+ switch (keypadType) {
133
+ case KeypadTypes.FRACTION:
134
+ return <FractionKeypad {...keypadProps} />;
135
+
136
+ case KeypadTypes.EXPRESSION:
137
+ return <ExpressionKeypad {...keypadProps} />;
138
+
139
+ default:
140
+ throw new Error("Invalid keypad type: " + keypadType);
141
+ }
142
+ };
143
+
144
+ render() {
145
+ const {
146
+ active,
147
+ layoutMode,
148
+ navigationPadEnabled,
149
+ onElementMounted,
150
+ style,
151
+ } = this.props;
152
+ const {hasBeenActivated} = this.state;
153
+
154
+ // NOTE(charlie): We render the transforms as pure inline styles to
155
+ // avoid an Aphrodite bug in mobile Safari.
156
+ // See: https://github.com/Khan/aphrodite/issues/68.
157
+ const dynamicStyle = {
158
+ ...(active ? inlineStyles.active : inlineStyles.hidden),
159
+ ...(!active && !hasBeenActivated ? inlineStyles.invisible : {}),
160
+ };
161
+
162
+ const keypadContainerStyle = [
163
+ row,
164
+ centered,
165
+ fullWidth,
166
+ styles.keypadContainer,
167
+ ...(Array.isArray(style) ? style : [style]),
168
+ ];
169
+
170
+ const keypadStyle = [
171
+ row,
172
+ styles.keypadBorder,
173
+ layoutMode === LayoutModes.FULLSCREEN
174
+ ? styles.fullscreen
175
+ : styles.compact,
176
+ ];
177
+
178
+ // TODO(charlie): When the keypad is shorter than the width of the
179
+ // screen, add a border on its left and right edges, and round out the
180
+ // corners.
181
+ return (
182
+ <View
183
+ style={keypadContainerStyle}
184
+ dynamicStyle={dynamicStyle}
185
+ extraClassName="keypad-container"
186
+ >
187
+ <View
188
+ style={keypadStyle}
189
+ ref={(element) => {
190
+ if (!this.hasMounted && element) {
191
+ this.hasMounted = true;
192
+ onElementMounted(element);
193
+ }
194
+ }}
195
+ >
196
+ {navigationPadEnabled && (
197
+ <NavigationPad
198
+ roundTopLeft={layoutMode === LayoutModes.COMPACT}
199
+ style={styles.navigationPadContainer}
200
+ />
201
+ )}
202
+ <View style={styles.keypadLayout}>
203
+ {this.renderKeypad()}
204
+ </View>
205
+ </View>
206
+ </View>
207
+ );
208
+ }
209
+ }
210
+
211
+ const keypadAnimationDurationMs = 300;
212
+ const borderWidthPx = 1;
213
+
214
+ const styles = StyleSheet.create({
215
+ keypadContainer: {
216
+ bottom: 0,
217
+ left: 0,
218
+ right: 0,
219
+ position: "fixed",
220
+ transition: `${keypadAnimationDurationMs}ms ease-out`,
221
+ transitionProperty: "transform",
222
+ zIndex: zIndexes.keypad,
223
+ },
224
+
225
+ keypadBorder: {
226
+ boxShadow: "0 1px 4px 0 rgba(0, 0, 0, 0.1)",
227
+ borderColor: "rgba(0, 0, 0, 0.2)",
228
+ borderStyle: "solid",
229
+ },
230
+
231
+ fullscreen: {
232
+ borderTopWidth: borderWidthPx,
233
+ },
234
+
235
+ compact: {
236
+ borderTopRightRadius: compactKeypadBorderRadiusPx,
237
+ borderTopLeftRadius: compactKeypadBorderRadiusPx,
238
+
239
+ borderTopWidth: borderWidthPx,
240
+ borderRightWidth: borderWidthPx,
241
+ borderLeftWidth: borderWidthPx,
242
+ },
243
+
244
+ navigationPadContainer: {
245
+ // Add a separator between the navigation pad and the keypad.
246
+ borderRight:
247
+ `${innerBorderWidthPx}px ${innerBorderStyle} ` +
248
+ `${innerBorderColor}`,
249
+ boxSizing: "content-box",
250
+ },
251
+
252
+ // Defer to the navigation pad, such that the navigation pad is always
253
+ // rendered at full-width, and the keypad takes up just the remaining space.
254
+ // TODO(charlie): Avoid shrinking the keys and, instead, make the keypad
255
+ // scrollable.
256
+ keypadLayout: {
257
+ flexGrow: 1,
258
+ // Avoid unitless flex-basis, per: https://philipwalton.com/articles/normalizing-cross-browser-flexbox-bugs/
259
+ flexBasis: "0%",
260
+ },
261
+ });
262
+
263
+ // Note: these don't go through an autoprefixer/aphrodite.
264
+ const inlineStyles = {
265
+ // If the keypad is yet to have ever been activated, we keep it invisible
266
+ // so as to avoid, e.g., the keypad flashing at the bottom of the page
267
+ // during the initial render.
268
+ invisible: {
269
+ visibility: "hidden",
270
+ },
271
+
272
+ hidden: {
273
+ msTransform: "translate3d(0, 100%, 0)",
274
+ WebkitTransform: "translate3d(0, 100%, 0)",
275
+ transform: "translate3d(0, 100%, 0)",
276
+ },
277
+
278
+ active: {
279
+ msTransform: "translate3d(0, 0, 0)",
280
+ WebkitTransform: "translate3d(0, 0, 0)",
281
+ transform: "translate3d(0, 0, 0)",
282
+ },
283
+ };
284
+
285
+ const mapStateToProps = (state) => {
286
+ return {
287
+ ...state.keypad,
288
+ layoutMode: state.layout.layoutMode,
289
+ navigationPadEnabled: state.layout.navigationPadEnabled,
290
+ };
291
+ };
292
+
293
+ const mapDispatchToProps = (dispatch) => {
294
+ return {
295
+ onPageSizeChange: (pageWidthPx, pageHeightPx) => {
296
+ dispatch(setPageSize(pageWidthPx, pageHeightPx));
297
+ },
298
+ };
299
+ };
300
+
301
+ export default connect(mapStateToProps, mapDispatchToProps, null, {
302
+ forwardRef: true,
303
+ })(KeypadContainer);