@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.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/{build/math-input.css → dist/es/index.css} +0 -150
- package/dist/es/index.js +2 -0
- package/dist/es/index.js.map +1 -0
- package/dist/index.css +586 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.flow +2 -0
- package/dist/index.js.map +1 -0
- package/dist/strings.js +71 -0
- package/index.html +20 -0
- package/less/echo.less +56 -0
- package/less/main.less +5 -0
- package/less/overrides.less +129 -0
- package/less/popover.less +22 -0
- package/less/tabbar.less +6 -0
- package/package.json +38 -70
- package/src/actions/index.js +57 -0
- package/src/components/__tests__/gesture-state-machine_test.js +437 -0
- package/src/components/__tests__/node-manager_test.js +89 -0
- package/src/components/__tests__/two-page-keypad_test.js +42 -0
- package/src/components/app.js +73 -0
- package/src/components/common-style.js +47 -0
- package/src/components/compute-layout-parameters.js +157 -0
- package/src/components/corner-decal.js +56 -0
- package/src/components/echo-manager.js +160 -0
- package/src/components/empty-keypad-button.js +49 -0
- package/src/components/expression-keypad.js +323 -0
- package/src/components/fraction-keypad.js +176 -0
- package/src/components/gesture-manager.js +226 -0
- package/src/components/gesture-state-machine.js +283 -0
- package/src/components/icon.js +74 -0
- package/src/components/iconography/arrow.js +22 -0
- package/src/components/iconography/backspace.js +29 -0
- package/src/components/iconography/cdot.js +29 -0
- package/src/components/iconography/cos.js +30 -0
- package/src/components/iconography/cube-root.js +36 -0
- package/src/components/iconography/dismiss.js +25 -0
- package/src/components/iconography/divide.js +34 -0
- package/src/components/iconography/down.js +16 -0
- package/src/components/iconography/equal.js +33 -0
- package/src/components/iconography/exp-2.js +29 -0
- package/src/components/iconography/exp-3.js +29 -0
- package/src/components/iconography/exp.js +29 -0
- package/src/components/iconography/frac.js +44 -0
- package/src/components/iconography/geq.js +33 -0
- package/src/components/iconography/gt.js +33 -0
- package/src/components/iconography/index.js +45 -0
- package/src/components/iconography/jump-into-numerator.js +41 -0
- package/src/components/iconography/jump-out-base.js +30 -0
- package/src/components/iconography/jump-out-denominator.js +41 -0
- package/src/components/iconography/jump-out-exponent.js +30 -0
- package/src/components/iconography/jump-out-numerator.js +41 -0
- package/src/components/iconography/jump-out-parentheses.js +33 -0
- package/src/components/iconography/left-paren.js +33 -0
- package/src/components/iconography/left.js +16 -0
- package/src/components/iconography/leq.js +33 -0
- package/src/components/iconography/ln.js +29 -0
- package/src/components/iconography/log-n.js +29 -0
- package/src/components/iconography/log.js +29 -0
- package/src/components/iconography/lt.js +33 -0
- package/src/components/iconography/minus.js +32 -0
- package/src/components/iconography/neq.js +33 -0
- package/src/components/iconography/parens.js +33 -0
- package/src/components/iconography/percent.js +49 -0
- package/src/components/iconography/period.js +26 -0
- package/src/components/iconography/plus.js +32 -0
- package/src/components/iconography/radical.js +36 -0
- package/src/components/iconography/right-paren.js +33 -0
- package/src/components/iconography/right.js +16 -0
- package/src/components/iconography/sin.js +30 -0
- package/src/components/iconography/sqrt.js +32 -0
- package/src/components/iconography/tan.js +30 -0
- package/src/components/iconography/times.js +33 -0
- package/src/components/iconography/up.js +16 -0
- package/src/components/input/__tests__/context-tracking_test.js +177 -0
- package/src/components/input/__tests__/math-wrapper.jsx +33 -0
- package/src/components/input/__tests__/mathquill_test.js +747 -0
- package/src/components/input/cursor-contexts.js +29 -0
- package/src/components/input/cursor-handle.js +137 -0
- package/src/components/input/drag-listener.js +75 -0
- package/src/components/input/math-input.js +924 -0
- package/src/components/input/math-wrapper.js +959 -0
- package/src/components/input/scroll-into-view.js +72 -0
- package/src/components/keypad/button-assets.js +492 -0
- package/src/components/keypad/button.js +106 -0
- package/src/components/keypad/button.stories.js +27 -0
- package/src/components/keypad/index.js +64 -0
- package/src/components/keypad/keypad-page-items.js +106 -0
- package/src/components/keypad/keypad-pages.stories.js +32 -0
- package/src/components/keypad/keypad.stories.js +35 -0
- package/src/components/keypad/numeric-input-page.js +100 -0
- package/src/components/keypad/pre-algebra-page.js +98 -0
- package/src/components/keypad/trigonometry-page.js +90 -0
- package/src/components/keypad-button.js +366 -0
- package/src/components/keypad-container.js +303 -0
- package/src/components/keypad.js +154 -0
- package/src/components/many-keypad-button.js +44 -0
- package/src/components/math-icon.js +65 -0
- package/src/components/multi-symbol-grid.js +182 -0
- package/src/components/multi-symbol-popover.js +59 -0
- package/src/components/navigation-pad.js +139 -0
- package/src/components/node-manager.js +129 -0
- package/src/components/popover-manager.js +76 -0
- package/src/components/popover-state-machine.js +173 -0
- package/src/components/prop-types.js +82 -0
- package/src/components/provided-keypad.js +99 -0
- package/src/components/styles.js +38 -0
- package/src/components/svg-icon.js +25 -0
- package/src/components/tabbar/__tests__/tabbar_test.js +65 -0
- package/src/components/tabbar/icons.js +69 -0
- package/src/components/tabbar/item.js +138 -0
- package/src/components/tabbar/tabbar.js +61 -0
- package/src/components/tabbar/tabbar.stories.js +60 -0
- package/src/components/tabbar/types.js +3 -0
- package/src/components/text-icon.js +52 -0
- package/src/components/touchable-keypad-button.js +146 -0
- package/src/components/two-page-keypad.js +99 -0
- package/src/components/velocity-tracker.js +76 -0
- package/src/components/z-indexes.js +9 -0
- package/src/consts.js +74 -0
- package/src/data/key-configs.js +349 -0
- package/src/data/keys.js +72 -0
- package/src/demo.js +8 -0
- package/src/fake-react-native-web/index.js +12 -0
- package/src/fake-react-native-web/text.js +56 -0
- package/src/fake-react-native-web/view.js +91 -0
- package/src/index.js +13 -0
- package/src/native-app.js +84 -0
- package/src/store/index.js +505 -0
- package/src/utils.js +18 -0
- package/tools/svg-to-react/convert.py +111 -0
- package/tools/svg-to-react/icons/math-keypad-icon-0.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-1.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-2.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-3.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-4.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-5.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-6.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-7.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-8.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-9.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-addition.svg +34 -0
- package/tools/svg-to-react/icons/math-keypad-icon-cos.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-delete.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-dismiss.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-division.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-equals-not.svg +50 -0
- package/tools/svg-to-react/icons/math-keypad-icon-equals.svg +48 -0
- package/tools/svg-to-react/icons/math-keypad-icon-exponent-2.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-exponent-3.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-exponent.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-fraction.svg +42 -0
- package/tools/svg-to-react/icons/math-keypad-icon-greater-than.svg +46 -0
- package/tools/svg-to-react/icons/math-keypad-icon-jump-out-base.svg +44 -0
- package/tools/svg-to-react/icons/math-keypad-icon-jump-out-denominator.svg +48 -0
- package/tools/svg-to-react/icons/math-keypad-icon-jump-out-exponent.svg +44 -0
- package/tools/svg-to-react/icons/math-keypad-icon-jump-out-parentheses.svg +44 -0
- package/tools/svg-to-react/icons/math-keypad-icon-less-than.svg +46 -0
- package/tools/svg-to-react/icons/math-keypad-icon-log-10.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-log-e.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-log.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-multiplication-cross.svg +40 -0
- package/tools/svg-to-react/icons/math-keypad-icon-multiplication-dot.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-percent.svg +42 -0
- package/tools/svg-to-react/icons/math-keypad-icon-radical-2.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-radical-3.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-radical.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-radix-character.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-sin.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-subtraction.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-tan.svg +38 -0
- package/tools/svg-to-react/symbol_map.py +41 -0
- package/LICENSE.txt +0 -21
- 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);
|