@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,91 @@
1
+ // @flow
2
+
3
+ import {StyleSheet, css} from "aphrodite";
4
+ import * as React from "react";
5
+
6
+ import type {CSSProperties} from "aphrodite";
7
+
8
+ type Props = {|
9
+ ariaLabel?: string,
10
+ children: React.Node,
11
+ // The `dynamicStyle` prop is provided for animating dynamic
12
+ // properties, as creating Aphrodite StyleSheets in animation loops is
13
+ // expensive. `dynamicStyle` should be a raw style object, rather than
14
+ // a StyleSheet.
15
+ dynamicStyle?: CSSProperties,
16
+ // The `extraClassName` prop should almost never be used. It gives the
17
+ // client a way to provide an additional CSS class name, to augment
18
+ // the class name generated by Aphrodite. (Right now, it's only used to
19
+ // disable some externally-applied CSS that would otherwise be far too
20
+ // difficult to override with inline styles.)
21
+ extraClassName?: string,
22
+ numberOfLines?: number,
23
+ onClick?: () => void,
24
+ onTouchCancel?: () => void,
25
+ onTouchEnd?: () => void,
26
+ onTouchMove?: () => void,
27
+ onTouchStart?: () => void,
28
+ role?: string,
29
+ style?: CSSProperties,
30
+ |};
31
+
32
+ class View extends React.Component<Props> {
33
+ // $FlowFixMe[signature-verification-failure]
34
+ static styles = StyleSheet.create({
35
+ // From: https://github.com/necolas/react-native-web/blob/master/src/components/View/index.js
36
+ // eslint-disable-next-line react-native/no-unused-styles
37
+ initial: {
38
+ alignItems: "stretch",
39
+ borderWidth: 0,
40
+ borderStyle: "solid",
41
+ boxSizing: "border-box",
42
+ display: "flex",
43
+ flexBasis: "auto",
44
+ flexDirection: "column",
45
+ margin: 0,
46
+ padding: 0,
47
+ position: "relative",
48
+ // button and anchor reset
49
+ backgroundColor: "transparent",
50
+ color: "inherit",
51
+ font: "inherit",
52
+ textAlign: "inherit",
53
+ textDecorationLine: "none",
54
+ // list reset
55
+ listStyle: "none",
56
+ // fix flexbox bugs
57
+ maxWidth: "100%",
58
+ minHeight: 0,
59
+ minWidth: 0,
60
+ },
61
+ });
62
+
63
+ render(): React.Element<"div"> {
64
+ const className =
65
+ css(
66
+ View.styles.initial,
67
+ ...(Array.isArray(this.props.style)
68
+ ? this.props.style
69
+ : [this.props.style]),
70
+ ) +
71
+ (this.props.extraClassName ? ` ${this.props.extraClassName}` : "");
72
+
73
+ return (
74
+ <div
75
+ className={className}
76
+ style={this.props.dynamicStyle}
77
+ onClick={this.props.onClick}
78
+ onTouchCancel={this.props.onTouchCancel}
79
+ onTouchEnd={this.props.onTouchEnd}
80
+ onTouchMove={this.props.onTouchMove}
81
+ onTouchStart={this.props.onTouchStart}
82
+ aria-label={this.props.ariaLabel}
83
+ role={this.props.role}
84
+ >
85
+ {this.props.children}
86
+ </div>
87
+ );
88
+ }
89
+ }
90
+
91
+ export default View;
package/src/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A single entry-point for all of the external-facing functionality.
3
+ */
4
+
5
+ import "../less/main.less";
6
+
7
+ export {default as KeypadInput} from "./components/input/math-input.js";
8
+ export {
9
+ keypadConfigurationPropType,
10
+ keypadElementPropType,
11
+ } from "./components/prop-types.js";
12
+ export {default as Keypad} from "./components/provided-keypad.js";
13
+ export {KeypadTypes} from "./consts.js";
@@ -0,0 +1,84 @@
1
+ import * as React from "react";
2
+ import ReactDOM from "react-dom";
3
+
4
+ import KeyConfigs from "./data/key-configs.js";
5
+ import {View} from "./fake-react-native-web/index.js";
6
+
7
+ import {KeypadInput} from "./index.js";
8
+
9
+ // eslint-disable-next-line react/prop-types
10
+ const ManualInput = ({handler}) => {
11
+ return (
12
+ <div>
13
+ {Object.keys(KeyConfigs).map((k) => (
14
+ <button
15
+ style={{display: "block"}}
16
+ disabled={!handler}
17
+ onClick={() => handler(k)}
18
+ >
19
+ {k} : {KeyConfigs[k].ariaLabel}
20
+ </button>
21
+ ))}
22
+ </div>
23
+ );
24
+ };
25
+
26
+ class App extends React.Component {
27
+ state = {
28
+ active: false,
29
+ handler: null,
30
+ keypadElement: {
31
+ activate: () => this.setState({active: true}),
32
+ dismiss: () => this.setState({active: false}),
33
+ // eslint-disable-next-line no-console
34
+ configure: (config) => console.log("configure:", config),
35
+ // eslint-disable-next-line no-console
36
+ setCursor: (cursor) => console.log("Cursor:", cursor),
37
+ setKeyHandler: (handler) => this.setState({handler}),
38
+ getDOMNode: () => null,
39
+ },
40
+ value: "",
41
+ };
42
+
43
+ render() {
44
+ return (
45
+ <View>
46
+ <div
47
+ style={{
48
+ marginTop: 10,
49
+ marginLeft: 20,
50
+ marginRight: 20,
51
+ marginBottom: 40,
52
+ }}
53
+ >
54
+ <KeypadInput
55
+ value={this.state.value}
56
+ ref={(inp) => (this.inp = inp)}
57
+ keypadElement={this.state.keypadElement}
58
+ onChange={(value, cb) => this.setState({value}, cb)}
59
+ onFocus={() => this.state.keypadElement.activate()}
60
+ onBlur={() => this.state.keypadElement.dismiss()}
61
+ />
62
+ </div>
63
+ <div>
64
+ <button
65
+ style={{display: "block"}}
66
+ onClick={() => this.inp.focus()}
67
+ >
68
+ Focus
69
+ </button>
70
+ </div>
71
+ <div style={{padding: 20}}>
72
+ Handler assigned: {"" + !!this.state.handler}
73
+ <br />
74
+ Active: {"" + this.state.active}
75
+ </div>
76
+ <br />
77
+ <br />
78
+ <ManualInput handler={this.state.handler} />
79
+ </View>
80
+ );
81
+ }
82
+ }
83
+
84
+ ReactDOM.render(<App />, document.getElementById("root"));
@@ -0,0 +1,505 @@
1
+ // @flow
2
+
3
+ import * as Redux from "redux";
4
+
5
+ import {tabletCutoffPx} from "../components/common-style.js";
6
+ import {computeLayoutParameters} from "../components/compute-layout-parameters.js";
7
+ import ExpressionKeypad from "../components/expression-keypad.js";
8
+ import FractionKeypad from "../components/fraction-keypad.js";
9
+ import GestureManager from "../components/gesture-manager.js";
10
+ import * as CursorContexts from "../components/input/cursor-contexts.js";
11
+ import VelocityTracker from "../components/velocity-tracker.js";
12
+ import {
13
+ DeviceOrientations,
14
+ DeviceTypes,
15
+ EchoAnimationTypes,
16
+ KeyTypes,
17
+ KeypadTypes,
18
+ LayoutModes,
19
+ } from "../consts.js";
20
+ import KeyConfigs from "../data/key-configs.js";
21
+ import Keys from "../data/keys.js";
22
+
23
+ const keypadForType = {
24
+ [KeypadTypes.FRACTION]: FractionKeypad,
25
+ [KeypadTypes.EXPRESSION]: ExpressionKeypad,
26
+ };
27
+
28
+ export const createStore = (): $FlowFixMe => {
29
+ const initialInputState: {|
30
+ keyHandler: $FlowFixMe,
31
+ cursor: $FlowFixMe,
32
+ |} = {
33
+ keyHandler: null,
34
+ cursor: {
35
+ context: CursorContexts.NONE,
36
+ },
37
+ };
38
+
39
+ const inputReducer = function (state = initialInputState, action) {
40
+ switch (action.type) {
41
+ case "SetKeyHandler":
42
+ return {
43
+ ...state,
44
+ keyHandler: action.keyHandler,
45
+ };
46
+
47
+ case "PressKey":
48
+ const keyConfig = KeyConfigs[action.key];
49
+ if (keyConfig.type !== KeyTypes.KEYPAD_NAVIGATION) {
50
+ // This is probably an anti-pattern but it works for the
51
+ // case where we don't actually control the state but we
52
+ // still want to communicate with the other object
53
+ return {
54
+ ...state,
55
+ cursor: state.keyHandler(keyConfig.id),
56
+ };
57
+ }
58
+
59
+ // TODO(kevinb) get state from MathQuill and store it?
60
+ return state;
61
+
62
+ case "SetCursor":
63
+ return {
64
+ ...state,
65
+ cursor: action.cursor,
66
+ };
67
+
68
+ default:
69
+ return state;
70
+ }
71
+ };
72
+
73
+ const defaultKeypadType = KeypadTypes.EXPRESSION;
74
+
75
+ const initialKeypadState = {
76
+ extraKeys: ["x", "y", Keys.THETA, Keys.PI],
77
+ keypadType: defaultKeypadType,
78
+ active: false,
79
+ };
80
+
81
+ const keypadReducer = function (state = initialKeypadState, action) {
82
+ switch (action.type) {
83
+ case "DismissKeypad":
84
+ return {
85
+ ...state,
86
+ active: false,
87
+ };
88
+
89
+ case "ActivateKeypad":
90
+ return {
91
+ ...state,
92
+ active: true,
93
+ };
94
+
95
+ case "ConfigureKeypad":
96
+ return {
97
+ ...state,
98
+ // Default `extraKeys` to the empty array.
99
+ extraKeys: [],
100
+ ...action.configuration,
101
+ };
102
+
103
+ case "PressKey":
104
+ const keyConfig = KeyConfigs[action.key];
105
+ // NOTE(charlie): Our keypad system operates by triggering key
106
+ // presses with key IDs in a dumb manner, such that the keys
107
+ // don't know what they can do--instead, the store is
108
+ // responsible for interpreting key presses and triggering the
109
+ // right actions when they occur. Hence, we figure off a
110
+ // dismissal here rather than dispatching a dismiss action in
111
+ // the first place.
112
+ if (keyConfig.id === Keys.DISMISS) {
113
+ return keypadReducer(state, {type: "DismissKeypad"});
114
+ }
115
+ return state;
116
+
117
+ default:
118
+ return state;
119
+ }
120
+ };
121
+
122
+ // We default to the right-most page. This is done so-as to enforce a
123
+ // consistent orientation between the view pager layout and the flattened
124
+ // layout, where our default page appears on the far right.
125
+ const getDefaultPage = (numPages) => numPages - 1;
126
+
127
+ const initialPagerState = {
128
+ animateToPosition: false,
129
+ currentPage: getDefaultPage(keypadForType[defaultKeypadType].numPages),
130
+ // The cumulative differential in the horizontal direction for the
131
+ // current swipe.
132
+ dx: 0,
133
+ numPages: keypadForType[defaultKeypadType].numPages,
134
+ pageWidthPx: 0,
135
+ velocityTracker: new VelocityTracker(),
136
+ };
137
+
138
+ const pagerReducer = function (state = initialPagerState, action) {
139
+ switch (action.type) {
140
+ case "ConfigureKeypad":
141
+ const {keypadType} = action.configuration;
142
+ const {numPages} = keypadForType[keypadType];
143
+ return {
144
+ ...state,
145
+ numPages,
146
+ animateToPosition: false,
147
+ currentPage: getDefaultPage(numPages),
148
+ dx: 0,
149
+ };
150
+
151
+ case "SetPageSize":
152
+ return {
153
+ ...state,
154
+ pageWidthPx: action.pageWidthPx,
155
+ };
156
+
157
+ case "PressKey":
158
+ const keyConfig = KeyConfigs[action.key];
159
+
160
+ // Reset the keypad page if the user performs a math operation.
161
+ if (
162
+ keyConfig.type === KeyTypes.VALUE ||
163
+ keyConfig.type === KeyTypes.OPERATOR
164
+ ) {
165
+ return pagerReducer(state, {type: "ResetKeypadPage"});
166
+ }
167
+ return state;
168
+
169
+ case "ResetKeypadPage":
170
+ return {
171
+ ...state,
172
+ animateToPosition: true,
173
+ // We start at the right-most page.
174
+ currentPage: getDefaultPage(state.numPages),
175
+ dx: 0,
176
+ };
177
+
178
+ case "PageKeypadRight":
179
+ const nextPage = Math.min(
180
+ state.currentPage + 1,
181
+ state.numPages - 1,
182
+ );
183
+ return {
184
+ ...state,
185
+ animateToPosition: true,
186
+ currentPage: nextPage,
187
+ dx: 0,
188
+ };
189
+
190
+ case "PageKeypadLeft":
191
+ const prevPage = Math.max(state.currentPage - 1, 0);
192
+ return {
193
+ ...state,
194
+ animateToPosition: true,
195
+ currentPage: prevPage,
196
+ dx: 0,
197
+ };
198
+
199
+ case "OnSwipeChange":
200
+ state.velocityTracker.push(action.dx);
201
+
202
+ return {
203
+ ...state,
204
+ animateToPosition: false,
205
+ dx: action.dx,
206
+ };
207
+
208
+ case "OnSwipeEnd":
209
+ const {pageWidthPx, velocityTracker} = state;
210
+ const {dx} = action;
211
+ const velocity = velocityTracker.getVelocity();
212
+
213
+ // NOTE(charlie): These will need refinement. The velocity comes
214
+ // from Framer.
215
+ const minFlingVelocity = 0.1;
216
+ const minFlingDistance = 10;
217
+
218
+ const shouldPageRight =
219
+ dx < -pageWidthPx / 2 ||
220
+ (velocity < -minFlingVelocity && dx < -minFlingDistance);
221
+
222
+ const shouldPageLeft =
223
+ dx > pageWidthPx / 2 ||
224
+ (velocity > minFlingVelocity && dx > minFlingDistance);
225
+
226
+ if (shouldPageRight) {
227
+ return pagerReducer(state, {type: "PageKeypadRight"});
228
+ } else if (shouldPageLeft) {
229
+ return pagerReducer(state, {type: "PageKeypadLeft"});
230
+ }
231
+
232
+ return {
233
+ ...state,
234
+ animateToPosition: true,
235
+ dx: 0,
236
+ };
237
+
238
+ default:
239
+ return state;
240
+ }
241
+ };
242
+
243
+ const createGestureManager = (swipeEnabled) => {
244
+ return new GestureManager(
245
+ {
246
+ swipeEnabled,
247
+ },
248
+ {
249
+ onSwipeChange: (dx) => {
250
+ store.dispatch({
251
+ type: "OnSwipeChange",
252
+ dx,
253
+ });
254
+ },
255
+ onSwipeEnd: (dx) => {
256
+ store.dispatch({
257
+ type: "OnSwipeEnd",
258
+ dx,
259
+ });
260
+ },
261
+ onActiveNodesChanged: (activeNodes) => {
262
+ store.dispatch({
263
+ type: "SetActiveNodes",
264
+ activeNodes,
265
+ });
266
+ },
267
+ onClick: (key, layoutProps, inPopover) => {
268
+ store.dispatch({
269
+ type: "PressKey",
270
+ key,
271
+ ...layoutProps,
272
+ inPopover,
273
+ });
274
+ },
275
+ },
276
+ [],
277
+ [Keys.BACKSPACE, Keys.UP, Keys.RIGHT, Keys.DOWN, Keys.LEFT],
278
+ );
279
+ };
280
+
281
+ const initialGestureState = {
282
+ popover: null,
283
+ focus: null,
284
+ gestureManager: createGestureManager(
285
+ keypadForType[defaultKeypadType].numPages > 1,
286
+ ),
287
+ };
288
+
289
+ const gestureReducer = function (state = initialGestureState, action) {
290
+ switch (action.type) {
291
+ case "DismissKeypad":
292
+ // NOTE(charlie): In the past, we enforced the "gesture manager
293
+ // will not receive any events when the keypad is hidden"
294
+ // assumption by assuming that the keypad would be hidden when
295
+ // dismissed and, as such, that none of its managed DOM nodes
296
+ // would be able to receive touch events. However, on mobile
297
+ // Safari, we're seeing that some of the keys receive touch
298
+ // events even when off-screen, inexplicably. So, to guard
299
+ // against that bug and make the contract explicit, we enable
300
+ // and disable event tracking on activation and dismissal.
301
+ state.gestureManager.disableEventTracking();
302
+ return state;
303
+
304
+ case "ActivateKeypad":
305
+ state.gestureManager.enableEventTracking();
306
+ return state;
307
+
308
+ case "SetActiveNodes":
309
+ return {
310
+ ...state,
311
+ ...action.activeNodes,
312
+ };
313
+
314
+ case "ConfigureKeypad":
315
+ const {keypadType} = action.configuration;
316
+ const {numPages} = keypadForType[keypadType];
317
+ const swipeEnabled = numPages > 1;
318
+ return {
319
+ popover: null,
320
+ focus: null,
321
+ gestureManager: createGestureManager(swipeEnabled),
322
+ };
323
+
324
+ default:
325
+ return state;
326
+ }
327
+ };
328
+
329
+ // Used to generate unique animation IDs for the echo animations. The actual
330
+ // values are irrelevant as long as they are unique.
331
+ let _lastAnimationId = 0;
332
+
333
+ const initialEchoState = {
334
+ echoes: [],
335
+ };
336
+
337
+ const echoReducer = function (state = initialEchoState, action) {
338
+ switch (action.type) {
339
+ case "PressKey":
340
+ const keyConfig = KeyConfigs[action.key];
341
+
342
+ // Add in the echo animation if the user performs a math
343
+ // operation.
344
+ if (
345
+ keyConfig.type === KeyTypes.VALUE ||
346
+ keyConfig.type === KeyTypes.OPERATOR
347
+ ) {
348
+ return {
349
+ ...state,
350
+ echoes: [
351
+ ...state.echoes,
352
+ {
353
+ animationId: "" + _lastAnimationId++,
354
+ animationType: action.inPopover
355
+ ? EchoAnimationTypes.LONG_FADE_ONLY
356
+ : EchoAnimationTypes.FADE_ONLY,
357
+ borders: action.borders,
358
+ id: keyConfig.id,
359
+ initialBounds: action.initialBounds,
360
+ },
361
+ ],
362
+ };
363
+ }
364
+ return state;
365
+
366
+ case "RemoveEcho":
367
+ const remainingEchoes = state.echoes.filter((echo) => {
368
+ return echo.animationId !== action.animationId;
369
+ });
370
+ return {
371
+ ...state,
372
+ echoes: remainingEchoes,
373
+ };
374
+
375
+ default:
376
+ return state;
377
+ }
378
+ };
379
+
380
+ const initialLayoutState = {
381
+ gridDimensions: {
382
+ numRows: keypadForType[defaultKeypadType].rows,
383
+ numColumns: keypadForType[defaultKeypadType].columns,
384
+ numMaxVisibleRows: keypadForType[defaultKeypadType].maxVisibleRows,
385
+ numPages: keypadForType[defaultKeypadType].numPages,
386
+ },
387
+ buttonDimensions: {
388
+ widthPx: 48,
389
+ heightPx: 48,
390
+ },
391
+ pageDimensions: {
392
+ pageWidthPx: 0,
393
+ pageHeightPx: 0,
394
+ },
395
+ layoutMode: LayoutModes.FULLSCREEN,
396
+ paginationEnabled: false,
397
+ navigationPadEnabled: false,
398
+ };
399
+
400
+ /**
401
+ * Compute the additional layout state based on the provided page and grid
402
+ * dimensions.
403
+ */
404
+ const layoutParametersForDimensions = (pageDimensions, gridDimensions) => {
405
+ const {pageWidthPx, pageHeightPx} = pageDimensions;
406
+
407
+ // Determine the device type and orientation.
408
+ const deviceOrientation =
409
+ pageWidthPx > pageHeightPx
410
+ ? DeviceOrientations.LANDSCAPE
411
+ : DeviceOrientations.PORTRAIT;
412
+ const deviceType =
413
+ Math.min(pageWidthPx, pageHeightPx) > tabletCutoffPx
414
+ ? DeviceTypes.TABLET
415
+ : DeviceTypes.PHONE;
416
+
417
+ // Using that information, make some decisions (or assumptions)
418
+ // about the resulting layout.
419
+ const navigationPadEnabled = deviceType === DeviceTypes.TABLET;
420
+ const paginationEnabled =
421
+ deviceType === DeviceTypes.PHONE &&
422
+ deviceOrientation === DeviceOrientations.PORTRAIT;
423
+
424
+ const deviceInfo = {deviceOrientation, deviceType};
425
+ const layoutOptions = {
426
+ navigationPadEnabled,
427
+ paginationEnabled,
428
+ // HACK(charlie): It's not great that we're making assumptions about
429
+ // the toolbar (which is rendered by webapp, and should always be
430
+ // visible and anchored to the bottom of the page for phone and
431
+ // tablet exercises). But this is primarily a heuristic (the goal is
432
+ // to preserve a 'good' amount of space between the top of the
433
+ // keypad and the top of the page) so we afford to have some margin
434
+ // of error.
435
+ toolbarEnabled: true,
436
+ };
437
+
438
+ return {
439
+ ...computeLayoutParameters(
440
+ gridDimensions,
441
+ pageDimensions,
442
+ deviceInfo,
443
+ layoutOptions,
444
+ ),
445
+ // Pass along some of the layout information, so that other
446
+ // components in the heirarchy can adapt appropriately.
447
+ navigationPadEnabled,
448
+ paginationEnabled,
449
+ };
450
+ };
451
+
452
+ const layoutReducer = function (state = initialLayoutState, action) {
453
+ switch (action.type) {
454
+ case "ConfigureKeypad":
455
+ const {keypadType} = action.configuration;
456
+ const gridDimensions = {
457
+ numRows: keypadForType[keypadType].rows,
458
+ numColumns: keypadForType[keypadType].columns,
459
+ numMaxVisibleRows: keypadForType[keypadType].maxVisibleRows,
460
+ numPages: keypadForType[keypadType].numPages,
461
+ };
462
+
463
+ return {
464
+ ...state,
465
+ ...layoutParametersForDimensions(
466
+ state.pageDimensions,
467
+ gridDimensions,
468
+ ),
469
+ gridDimensions,
470
+ };
471
+
472
+ case "SetPageSize":
473
+ const {pageWidthPx, pageHeightPx} = action;
474
+ const pageDimensions = {pageWidthPx, pageHeightPx};
475
+
476
+ return {
477
+ ...state,
478
+ ...layoutParametersForDimensions(
479
+ pageDimensions,
480
+ state.gridDimensions,
481
+ ),
482
+ pageDimensions,
483
+ };
484
+
485
+ default:
486
+ return state;
487
+ }
488
+ };
489
+
490
+ const reducer = Redux.combineReducers({
491
+ input: inputReducer,
492
+ keypad: keypadReducer,
493
+ pager: pagerReducer,
494
+ gestures: gestureReducer,
495
+ echoes: echoReducer,
496
+ layout: layoutReducer,
497
+ });
498
+
499
+ // TODO(charlie): This non-inlined return is necessary so as to allow the
500
+ // gesture manager to dispatch actions on the store in its callbacks. We
501
+ // should come up with a better pattern to remove the two-way dependency.
502
+ const store = Redux.createStore(reducer);
503
+
504
+ return store;
505
+ };